Compare commits
2 Commits
74701aa0c5
...
dc27262328
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc27262328
|
||
|
|
bf07ba9fa2
|
39
Cargo.lock
generated
39
Cargo.lock
generated
@@ -211,6 +211,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.28"
|
||||
@@ -310,6 +319,16 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.6"
|
||||
@@ -907,9 +926,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toad"
|
||||
version = "1.0.0-beta.6"
|
||||
version = "1.0.0-beta.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b305d4763b0236558486735374e796a028567a8d6f6a4c94c07096b73573ba0a"
|
||||
checksum = "6b4d56ca31b3b83e311136e8e425dc62b8190ed72e2677485431a419493828f2"
|
||||
dependencies = [
|
||||
"embedded-time",
|
||||
"log",
|
||||
@@ -1001,9 +1020,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toad-msg"
|
||||
version = "1.0.0-beta.2"
|
||||
version = "1.0.0-beta.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63f5ff0cb4b95ec5eb83db517a2301d6a5a5917d94124140dff2fafa283a41f5"
|
||||
checksum = "219f76dce7b054dc71ff7dff5409b450f821b2bdeccf67d95d3577aa029fe737"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"tinyvec",
|
||||
@@ -1013,6 +1032,7 @@ dependencies = [
|
||||
"toad-len",
|
||||
"toad-macros",
|
||||
"toad-map",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1141,6 +1161,17 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.0"
|
||||
|
||||
@@ -5,8 +5,8 @@ edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
toad = "1.0.0-beta.6"
|
||||
toad-msg = "1.0.0-beta.2"
|
||||
toad = "1.0.0-beta.8"
|
||||
toad-msg = "1.0.0-beta.5"
|
||||
simple_logger = "4.2"
|
||||
nb = "1.1.0"
|
||||
serde = {version = "1", features = ["derive"]}
|
||||
|
||||
32
src/app.rs
32
src/app.rs
@@ -1,4 +1,8 @@
|
||||
use crate::model::{GroupRepoImpl,
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::env::Env;
|
||||
use crate::model::{GroupRepo,
|
||||
GroupRepoImpl,
|
||||
HashedTextExt,
|
||||
HashedTextExtImpl,
|
||||
UserRepo,
|
||||
@@ -11,14 +15,20 @@ pub trait App: Send + Sync + Sized {
|
||||
type Db: Postgres;
|
||||
|
||||
type UserRepo: UserRepo<DbError<Self::Db>>;
|
||||
type GroupRepo: GroupRepo<DbError<Self::Db>>;
|
||||
|
||||
type HashedTextExt: HashedTextExt;
|
||||
type UserSessionExt: UserSessionExt;
|
||||
|
||||
fn env(&self) -> &Env;
|
||||
fn db(&self) -> &Self::Db;
|
||||
fn hashed_text(&self) -> &Self::HashedTextExt;
|
||||
fn user_session(&self) -> &Self::UserSessionExt;
|
||||
fn user(&self) -> &Self::UserRepo;
|
||||
fn group(&self) -> &Self::GroupRepo;
|
||||
|
||||
fn enqueue_shutdown(&self);
|
||||
fn should_shutdown(&self) -> bool;
|
||||
}
|
||||
|
||||
pub struct AppConcrete {
|
||||
@@ -27,16 +37,24 @@ pub struct AppConcrete {
|
||||
pub user_session: &'static UserSessionExtImpl<PostgresImpl<postgres::Client>,
|
||||
GroupRepoImpl<PostgresImpl<postgres::Client>>>,
|
||||
pub user: &'static UserRepoImpl<PostgresImpl<postgres::Client>>,
|
||||
pub group: &'static GroupRepoImpl<PostgresImpl<postgres::Client>>,
|
||||
pub env: Env,
|
||||
pub should_shutdown: Mutex<bool>,
|
||||
}
|
||||
|
||||
impl App for AppConcrete {
|
||||
type Db = PostgresImpl<postgres::Client>;
|
||||
|
||||
type UserRepo = UserRepoImpl<Self::Db>;
|
||||
type GroupRepo = GroupRepoImpl<Self::Db>;
|
||||
|
||||
type HashedTextExt = HashedTextExtImpl<Self::Db>;
|
||||
type UserSessionExt = UserSessionExtImpl<Self::Db, GroupRepoImpl<Self::Db>>;
|
||||
|
||||
fn env(&self) -> &Env {
|
||||
&self.env
|
||||
}
|
||||
|
||||
fn db(&self) -> &Self::Db {
|
||||
self.pg
|
||||
}
|
||||
@@ -52,4 +70,16 @@ impl App for AppConcrete {
|
||||
fn user(&self) -> &Self::UserRepo {
|
||||
self.user
|
||||
}
|
||||
|
||||
fn group(&self) -> &Self::GroupRepo {
|
||||
self.group
|
||||
}
|
||||
|
||||
fn enqueue_shutdown(&self) {
|
||||
*self.should_shutdown.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
fn should_shutdown(&self) -> bool {
|
||||
*self.should_shutdown.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
18
src/env.rs
18
src/env.rs
@@ -1,10 +1,15 @@
|
||||
use std::ffi::OsString;
|
||||
use std::net::{AddrParseError, SocketAddr};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
|
||||
use naan::prelude::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Environment {
|
||||
Debug,
|
||||
Release,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Api {
|
||||
pub addr: SocketAddr,
|
||||
@@ -23,6 +28,7 @@ pub struct Postgres {
|
||||
pub struct Env {
|
||||
pub postgres: Postgres,
|
||||
pub api: Api,
|
||||
pub environ: Environment,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -66,6 +72,14 @@ impl Env {
|
||||
.parse()
|
||||
.map_err(|e| Error::VarNotSocketAddr("API_ADDR".into(), api_addr, e))? };
|
||||
|
||||
Ok(Env { postgres, api })
|
||||
Ok(Env { postgres,
|
||||
api,
|
||||
environ: match get("ENVIRON")?.unwrap_or("release".into())
|
||||
.to_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
| "debug" => Environment::Debug,
|
||||
| _ => Environment::Release,
|
||||
} })
|
||||
}
|
||||
}
|
||||
|
||||
44
src/main.rs
44
src/main.rs
@@ -1,5 +1,6 @@
|
||||
use std::path::Path;
|
||||
use std::ptr::hash;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use app::{App, AppConcrete};
|
||||
use err::ToE;
|
||||
@@ -20,6 +21,7 @@ use toad::platform::Platform as _;
|
||||
use toad::req::Req;
|
||||
use toad::resp::{code, Resp};
|
||||
use toad_msg::alloc::Message;
|
||||
use toad_msg::repeat::PATH;
|
||||
use toad_msg::{Code, ContentFormat, MessageBuilder, MessageOptions, Type};
|
||||
|
||||
use crate::err::E;
|
||||
@@ -138,17 +140,28 @@ fn handle_request<A>(app: &A, req: Addrd<Req<ToadT>>) -> Addrd<Message>
|
||||
{
|
||||
let body = || {
|
||||
let routes: Vec<fn(&A, &Actor, Addrd<&Message>) -> Result<Option<Message>, E>> =
|
||||
vec![route::users::users, route::user_sessions::user_sessions];
|
||||
vec![route::users::users,
|
||||
route::groups::groups,
|
||||
route::user_sessions::user_sessions,
|
||||
route::debug::debug];
|
||||
|
||||
let actor = Ok(req.data().payload()).and_then(serde_json::from_slice::<Option<ReqPayload<()>>>)
|
||||
.map_err(ToE::to_e)?
|
||||
.and_then(|r| r.session)
|
||||
.map(|s| app.user_session().touch(UserSession::from(s)))
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
.map_err(ToE::to_e)?
|
||||
.unwrap_or_default();
|
||||
let actor =
|
||||
Ok(Some(req.data().payload())).map(|p| p.filter(|p| !p.is_empty()))
|
||||
.and_then(|p| {
|
||||
p.map(serde_json::from_slice::<ReqPayload<Option<()>>>)
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
})
|
||||
.map_err(ToE::to_e)?
|
||||
.and_then(|r| r.session)
|
||||
.map(|s| app.user_session().touch(UserSession::from(s)))
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
.map_err(ToE::to_e)?
|
||||
.unwrap_or_default();
|
||||
|
||||
let actor = dbg!(actor);
|
||||
|
||||
let not_found = || {
|
||||
log::info!("{:?} {} not found", req.data().msg().code, req.data().msg().path_string().unwrap());
|
||||
Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token)
|
||||
.build()
|
||||
};
|
||||
@@ -171,7 +184,8 @@ fn handle_request<A>(app: &A, req: Addrd<Req<ToadT>>) -> Addrd<Message>
|
||||
e
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
Addrd(Message::builder(Type::Ack, e.code).content_format(ContentFormat::Json)
|
||||
Addrd(Message::builder(Type::Ack, e.code).token(req.data().msg().token)
|
||||
.content_format(ContentFormat::Json)
|
||||
.payload(serde_json::to_vec(&e).unwrap())
|
||||
.build(),
|
||||
req.addr())
|
||||
@@ -182,6 +196,11 @@ fn server_worker<A>(app: &A, p: &'static Toad)
|
||||
where A: App
|
||||
{
|
||||
loop {
|
||||
if app.should_shutdown() {
|
||||
log::info!("shutting down");
|
||||
break;
|
||||
}
|
||||
|
||||
match nb::block!(p.poll_req()) {
|
||||
| Err(e) => log::error!("{e:?}"),
|
||||
| Ok(req) => {
|
||||
@@ -200,7 +219,7 @@ fn main() {
|
||||
unsafe { core::mem::transmute::<&T, &'static T>(t) }
|
||||
}
|
||||
|
||||
simple_logger::init().unwrap();
|
||||
simple_logger::init_with_level(log::Level::Debug).unwrap();
|
||||
|
||||
let env = env::Env::try_read().unwrap();
|
||||
|
||||
@@ -233,8 +252,11 @@ fn main() {
|
||||
|
||||
let app = AppConcrete { pg,
|
||||
hashed_text,
|
||||
group: group_repo,
|
||||
user_session,
|
||||
user };
|
||||
user,
|
||||
env,
|
||||
should_shutdown: Mutex::new(false) };
|
||||
|
||||
server_worker(&app, toad);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
use naan::prelude::*;
|
||||
use postgres::GenericClient;
|
||||
use std::convert::identity;
|
||||
|
||||
use crate::model::{Actor, User, UserId};
|
||||
use naan::prelude::*;
|
||||
use postgres::{GenericClient, GenericRow};
|
||||
use toad::resp::code::BAD_REQUEST;
|
||||
|
||||
use crate::err::ToE;
|
||||
use crate::model::{Actor, Mode, Path, User, UserId};
|
||||
use crate::newtype;
|
||||
use crate::postgres::{try_get, DbError, Postgres, UnmarshalRow};
|
||||
use crate::repo::{Del, Insert, Page, Patch, ReadMany, ReadOne, Repo};
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct GroupId(String);
|
||||
);
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct GroupName(String);
|
||||
);
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct Group {
|
||||
pub uid: GroupId,
|
||||
pub name: GroupName,
|
||||
@@ -28,59 +32,139 @@ impl UnmarshalRow for Group {
|
||||
S: AsRef<str>
|
||||
{
|
||||
let p = p.as_ref().map(|p| p.as_ref());
|
||||
try_get!(p, "uid", row).zip(|_: &_| try_get!(p, "name", row))
|
||||
try_get!(p, "uid", row).zip(|_: &_| try_get!(p, "tag", row))
|
||||
.map(|(uid, name)| Group { uid: GroupId(uid),
|
||||
name: GroupName(name) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
#[derive(serde::Serialize,
|
||||
serde::Deserialize,
|
||||
Default,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Debug)]
|
||||
pub struct GroupPatch {
|
||||
members: Vec<UserId>,
|
||||
name: Option<GroupName>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct GroupInsert {
|
||||
name: GroupName,
|
||||
members: Vec<UserId>,
|
||||
}
|
||||
|
||||
pub trait GroupRepo:
|
||||
Repo<T = Group, Id = GroupId>
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum GroupRepoError<E> {
|
||||
Unauthorized,
|
||||
PageInvalid,
|
||||
Other(E),
|
||||
}
|
||||
|
||||
impl<E> GroupRepoError<E> where E: 'static + core::fmt::Debug
|
||||
{
|
||||
pub fn into_e(self) -> crate::err::E {
|
||||
match self {
|
||||
Self::PageInvalid =>
|
||||
crate::err::E::new()
|
||||
.code(BAD_REQUEST)
|
||||
.error("bad_request".into())
|
||||
.explain("bad pagination for `GET /groups`".into())
|
||||
.hint("{\"limit\": <int less than or equal to 100>, \"after\": <group_id | null>}".into()),
|
||||
Self::Unauthorized => crate::err::E::unauthorized(),
|
||||
Self::Other(e) => e.to_e(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GroupRepo<E>:
|
||||
Repo<T = Group, Id = GroupId, Error = GroupRepoError<E>>
|
||||
+ ReadOne
|
||||
+ Patch<Patch = GroupPatch>
|
||||
+ Insert<Insert = GroupInsert>
|
||||
+ Del
|
||||
+ ReadMany
|
||||
{
|
||||
fn members(&self, id: &GroupId) -> Result<Vec<UserId>, Self::Error>;
|
||||
fn groups(&self, id: &UserId) -> Result<Vec<GroupId>, Self::Error>;
|
||||
fn members(&self, actor: &Actor, id: &GroupId) -> Result<Vec<UserId>, Self::Error>;
|
||||
fn user_groups(&self, actor: &Actor, id: &UserId) -> Result<Vec<Group>, Self::Error>;
|
||||
}
|
||||
|
||||
pub struct GroupRepoImpl<Db: 'static>(pub &'static Db);
|
||||
|
||||
impl<Db> GroupRepo for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
impl<Db> GroupRepo<DbError<Db>> for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
fn members(&self, id: &GroupId) -> Result<Vec<UserId>, Self::Error> {
|
||||
todo!()
|
||||
fn user_groups(&self, actor: &Actor, id: &UserId) -> Result<Vec<Group>, Self::Error> {
|
||||
static QUERY: &'static str = "select g.tag :: text, g.uid :: text from public.usr_groups(($1 :: text) :: human_uuid.huid) as g";
|
||||
|
||||
let grps = self.0
|
||||
.with_client(|c| c.query(QUERY, &[&id.as_ref()]))
|
||||
.and_then(|rows| {
|
||||
rows.iter()
|
||||
.map(Group::unmarshal)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
})
|
||||
.map_err(GroupRepoError::Other)?;
|
||||
|
||||
let paths = grps.iter().map(|g| Path::parse(format!("/groups/{}/members", g.uid))).collect::<Vec<_>>();
|
||||
|
||||
if !self.0
|
||||
.authorized_all(&paths,
|
||||
Mode::Read,
|
||||
actor)
|
||||
.map_err(GroupRepoError::Other)?
|
||||
{
|
||||
return Err(GroupRepoError::Unauthorized);
|
||||
}
|
||||
|
||||
Ok(grps)
|
||||
}
|
||||
|
||||
fn groups(&self, id: &UserId) -> Result<Vec<GroupId>, Self::Error> {
|
||||
todo!()
|
||||
fn members(&self, actor: &Actor, id: &GroupId) -> Result<Vec<UserId>, Self::Error> {
|
||||
static QUERY: &'static str = "select u.uid from public.grp_members($1 :: human_uuid.huid)";
|
||||
|
||||
if !self.0
|
||||
.authorized(&Path::parse(format!("/groups/{id}/members")),
|
||||
Mode::Read,
|
||||
actor)
|
||||
.map_err(GroupRepoError::Other)?
|
||||
{
|
||||
return Err(GroupRepoError::Unauthorized);
|
||||
}
|
||||
|
||||
self.0
|
||||
.with_client(|c| c.query(QUERY, &[&id.as_ref()]))
|
||||
.and_then(|rows| {
|
||||
rows.into_iter()
|
||||
.map(|row| row.try_get::<_, String>("u.uid").map(UserId::from))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
})
|
||||
.map_err(GroupRepoError::Other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> Repo for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
type T = Group;
|
||||
type Error = DbError<Db>;
|
||||
type Error = GroupRepoError<DbError<Db>>;
|
||||
type Id = GroupId;
|
||||
}
|
||||
|
||||
impl<Db> ReadOne for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
fn get(&self, actor: &Actor, id: &GroupId) -> Result<Option<Group>, Self::Error> {
|
||||
static QUERY: &'static str = "select uid, name from public.grp where id = $1 :: uuid";
|
||||
static QUERY: &'static str = "select uid, tag from public.grp where id = $1 :: text";
|
||||
|
||||
let paths = vec![format!("/groups/{id}/name")].into_iter()
|
||||
.map(Path::parse)
|
||||
.collect::<Vec<_>>();
|
||||
if !self.0
|
||||
.authorized_all(&paths, Mode::Read, actor)
|
||||
.map_err(GroupRepoError::Other)?
|
||||
{
|
||||
return Err(GroupRepoError::Unauthorized);
|
||||
}
|
||||
|
||||
self.0
|
||||
.with_client(|c| c.query_opt(QUERY, &[&id.as_ref()]))
|
||||
@@ -89,6 +173,7 @@ impl<Db> ReadOne for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
.map(Group::unmarshal)
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
})
|
||||
.map_err(GroupRepoError::Other)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +182,61 @@ impl<Db> ReadMany for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
fn get_all(&self, actor: &Actor, page: Page<GroupId>) -> Result<Vec<Group>, Self::Error> {
|
||||
static QUERY: &'static str = "select uid, tag, password, email from public.grp";
|
||||
|
||||
self.0
|
||||
.with_client(|c| c.query(QUERY, &[]))
|
||||
.and_then(|vec| vec.iter().map(Group::unmarshal).collect())
|
||||
if !self.page_valid(&page) {
|
||||
return Err(GroupRepoError::PageInvalid);
|
||||
}
|
||||
|
||||
static QUERY_AFTER_LINES: [&'static str; 7] =
|
||||
["with after as (select id from public.grp where uid = (($1 :: text) :: human_uuid.huid))",
|
||||
"select uid :: text",
|
||||
" , tag :: text",
|
||||
"from public.grp",
|
||||
"where id > after.id and deleted = false",
|
||||
"order by id asc",
|
||||
"limit $2"];
|
||||
|
||||
static QUERY_FIRST_LINES: [&'static str; 6] = ["select uid :: text",
|
||||
" , tag :: text",
|
||||
"from public.grp",
|
||||
"deleted = false",
|
||||
"order by id asc",
|
||||
"limit $1"];
|
||||
|
||||
let grps: Vec<Group> =
|
||||
self.0
|
||||
.with_client(|c| match page.after {
|
||||
| Some(after) => {
|
||||
c.query(&QUERY_AFTER_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&after.as_ref(), &page.limit])
|
||||
},
|
||||
| None => c.query(&QUERY_FIRST_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&page.limit]),
|
||||
})
|
||||
.and_then(|vec| vec.iter().map(Group::unmarshal).collect())
|
||||
.map_err(GroupRepoError::Other)?;
|
||||
|
||||
let paths = grps.iter()
|
||||
.map(|Group { uid, .. }| {
|
||||
vec![format!("/groups/{uid}/name")].into_iter()
|
||||
.map(Path::parse)
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !self.0
|
||||
.authorized_all(&paths, Mode::Read, actor)
|
||||
.map_err(GroupRepoError::Other)?
|
||||
{
|
||||
Err(GroupRepoError::Unauthorized)
|
||||
} else {
|
||||
Ok(grps)
|
||||
}
|
||||
}
|
||||
|
||||
fn page_valid(&self, page: &Page<Self::Id>) -> bool {
|
||||
page.limit < 100
|
||||
page.limit <= 100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +245,31 @@ impl<Db> Patch for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
type Patch = GroupPatch;
|
||||
|
||||
fn patch(&self, actor: &Actor, id: &GroupId, state: GroupPatch) -> Result<bool, Self::Error> {
|
||||
todo!()
|
||||
static QUERY_LINES: [&'static str; 4] = ["update public.grp",
|
||||
"set tag = coalesce(($2 :: text), tag)",
|
||||
"where uid = (($1 :: text) :: human_uuid.huid)",
|
||||
" and deleted = false"];
|
||||
|
||||
let paths =
|
||||
vec![Some(format!("/groups/{id}/name")).filter(|_| state.name.is_some())].into_iter()
|
||||
.filter_map(identity)
|
||||
.map(Path::parse)
|
||||
.collect::<Vec<_>>();
|
||||
if !self.0
|
||||
.authorized_all(&paths, Mode::Write, actor)
|
||||
.map_err(GroupRepoError::Other)?
|
||||
{
|
||||
return Err(GroupRepoError::Unauthorized);
|
||||
}
|
||||
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
c.execute(&QUERY_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&id.as_ref(), &state.name.as_ref().map(|n| n.as_ref())])
|
||||
})
|
||||
.map(|n| n == 1)
|
||||
.map_err(GroupRepoError::Other)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,14 +277,49 @@ impl<Db> Insert for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
type Insert = GroupInsert;
|
||||
|
||||
fn insert(&self, actor: &Actor, state: GroupInsert) -> Result<GroupId, Self::Error> {
|
||||
todo!()
|
||||
fn insert(&self, actor: &Actor, insert: GroupInsert) -> Result<GroupId, Self::Error> {
|
||||
static QUERY_LINES: [&'static str; 5] = ["insert into public.grp",
|
||||
" (tag)",
|
||||
"values",
|
||||
" ($1 :: text)",
|
||||
"returning (uid :: text);"];
|
||||
|
||||
if !self.0
|
||||
.authorized(&Path::parse("/groups/"), Mode::Write, actor)
|
||||
.map_err(GroupRepoError::Other)?
|
||||
{
|
||||
return Err(GroupRepoError::Unauthorized);
|
||||
}
|
||||
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
c.query_one(&QUERY_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&insert.name.as_ref()])
|
||||
})
|
||||
.and_then(|r| r.try_get::<_, String>(0))
|
||||
.map(GroupId::from)
|
||||
.map_err(GroupRepoError::Other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> Del for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
fn del(&self, actor: &Actor, id: &GroupId) -> Result<bool, Self::Error> {
|
||||
todo!()
|
||||
static QUERY: &'static str = "delete from public.grp where uid = $1 :: human_uuid.huid;";
|
||||
|
||||
if !self.0
|
||||
.authorized(&Path::parse(format!("/groups/{id}/deleted")),
|
||||
Mode::Write,
|
||||
actor)
|
||||
.map_err(GroupRepoError::Other)?
|
||||
{
|
||||
return Err(GroupRepoError::Unauthorized);
|
||||
}
|
||||
|
||||
self.0
|
||||
.with_client(|c| c.execute(QUERY, &[&id.as_ref()]))
|
||||
.map(|n| n == 1)
|
||||
.map_err(GroupRepoError::Other)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::postgres::{try_get, UnmarshalRow};
|
||||
#[derive(Default, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Actor {
|
||||
pub uid: Option<UserId>,
|
||||
pub groups: Vec<GroupId>,
|
||||
pub groups: Vec<Group>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
@@ -256,7 +256,7 @@ impl Perm {
|
||||
pub fn actor_can(&self, actor: &Actor, mode: Mode) -> bool {
|
||||
mode.covered_by(&self.everyone)
|
||||
|| actor.uid.as_ref() == Some(&self.owner.0) && mode.covered_by(&self.owner.1)
|
||||
|| mode.covered_by(&self.group.1) && actor.groups.iter().any(|g| g == &self.group.0)
|
||||
|| mode.covered_by(&self.group.1) && actor.groups.iter().any(|g| g.uid == self.group.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,10 +317,12 @@ mod tests {
|
||||
GroupsPath::Members)) };
|
||||
|
||||
let admin = Actor { uid: Some(UserId::from("u1")),
|
||||
groups: vec![GroupId::from("g1")] };
|
||||
groups: vec![Group { uid: GroupId::from("g1"),
|
||||
name: GroupName::from("foo") }] };
|
||||
|
||||
let nonadmin = Actor { uid: Some(UserId::from("u2")),
|
||||
groups: vec![GroupId::from("g2")] };
|
||||
groups: vec![Group { uid: GroupId::from("g2"),
|
||||
name: GroupName::from("bar") }] };
|
||||
|
||||
assert!(all_groups.actor_can(&admin, Mode::Write));
|
||||
assert!(all_groups.actor_can(&nonadmin, Mode::Write));
|
||||
|
||||
@@ -52,14 +52,15 @@ impl UnmarshalRow for User {
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UserPatch {
|
||||
pub tag: Option<UserTag>,
|
||||
pub tag: Option<String>,
|
||||
pub tag_discrim: Option<u32>,
|
||||
pub password: Option<HashedText>,
|
||||
pub email: Option<Email>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UserInsert {
|
||||
pub tag: UserTag,
|
||||
pub tag: String,
|
||||
pub password: HashedText,
|
||||
pub email: Email,
|
||||
}
|
||||
@@ -112,9 +113,9 @@ impl<Db> ReadOne for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
use UserRepoError::*;
|
||||
|
||||
static QUERY_LINES: [&'static str; 3] =
|
||||
["select uid, tag, email",
|
||||
["select uid, tag, discrim, email",
|
||||
"from public.usr",
|
||||
"where uid = human_uuid.huid_of_string($1) and deleted = false"];
|
||||
"where uid = (($1 :: text) :: human_uuid.huid) and deleted = false"];
|
||||
|
||||
let paths = vec![format!("/users/{id}/tag"), format!("/users/{id}/email")].into_iter()
|
||||
.map(Path::parse)
|
||||
@@ -154,20 +155,25 @@ impl<Db> ReadMany for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
return Err(PageInvalid);
|
||||
}
|
||||
|
||||
static QUERY_AFTER_LINES: [&'static str; 6] =
|
||||
["with after as (select id from public.usr where uid = human_uuid.huid_of_string($1))",
|
||||
"select human_uuid.huid_to_string(uid), tag, email",
|
||||
static QUERY_AFTER_LINES: [&'static str; 9] =
|
||||
["with after as (select id from public.usr where uid = (($1 :: text) :: human_uuid.huid))",
|
||||
"select uid :: text",
|
||||
" , tag :: text",
|
||||
" , discrim",
|
||||
" , email",
|
||||
"from public.usr",
|
||||
"where id > after.id and deleted = false",
|
||||
"order by id asc",
|
||||
"limit $2"];
|
||||
|
||||
static QUERY_FIRST_LINES: [&'static str; 5] =
|
||||
["select human_uuid.huid_to_string(uid), tag, email",
|
||||
"from public.usr",
|
||||
"where id > after.id and deleted = false",
|
||||
"order by id asc",
|
||||
"limit $1"];
|
||||
static QUERY_FIRST_LINES: [&'static str; 8] = ["select uid :: text",
|
||||
" , tag :: text",
|
||||
" , discrim",
|
||||
" , email",
|
||||
"from public.usr",
|
||||
"deleted = false",
|
||||
"order by id asc",
|
||||
"limit $1"];
|
||||
|
||||
let usrs: Vec<User> =
|
||||
self.0
|
||||
@@ -210,13 +216,12 @@ impl<Db> Patch for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
fn patch(&self, actor: &Actor, id: &UserId, patch: UserPatch) -> Result<bool, Self::Error> {
|
||||
use UserRepoError::*;
|
||||
|
||||
static QUERY_LINES: [&'static str; 6] =
|
||||
static QUERY_LINES: [&'static str; 5] =
|
||||
["update public.usr",
|
||||
"set tag = coalesce($2, tag)",
|
||||
" , password = coalesce($3, password)",
|
||||
" , email = coalesce($4, email)",
|
||||
"from public.usr",
|
||||
"where uid = human_uuid.huid_of_string($1) and deleted = false"];
|
||||
"where uid = (($1 :: text) :: human_uuid.huid) and deleted = false"];
|
||||
|
||||
let paths =
|
||||
vec![Some("/users/{id}/tag").filter(|_| patch.tag.is_some()),
|
||||
@@ -237,7 +242,7 @@ impl<Db> Patch for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
c.execute(&QUERY_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&id.as_ref(),
|
||||
&patch.tag.as_ref().map(|t| &t.tag),
|
||||
&patch.tag.as_ref(),
|
||||
&patch.password.as_ref().map(|h| h.as_ref()),
|
||||
&patch.email.as_ref().map(|e| e.as_ref())])
|
||||
})
|
||||
@@ -256,8 +261,8 @@ impl<Db> Insert for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
static QUERY_LINES: [&'static str; 5] = ["insert into public.usr",
|
||||
" (tag, password, email)",
|
||||
"values",
|
||||
" ($2, $3, $4)",
|
||||
"returning human_uuid.huid_to_string(uid);"];
|
||||
" ($1 :: text, hash_text($2), $3 :: text)",
|
||||
"returning (uid :: text);"];
|
||||
|
||||
if !self.0
|
||||
.authorized(&Path::parse("/users/"), Mode::Write, actor)
|
||||
@@ -270,7 +275,7 @@ impl<Db> Insert for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
.with_client(|c| {
|
||||
c.query_one(&QUERY_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&insert.tag.tag,
|
||||
&[&insert.tag,
|
||||
&insert.password.as_ref(),
|
||||
&insert.email.as_ref()])
|
||||
})
|
||||
@@ -459,20 +464,17 @@ mod tests {
|
||||
let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static Postgres<()>>(&db) });
|
||||
|
||||
assert!(repo.insert(&Actor::default(),
|
||||
UserInsert { tag: UserTag { tag: "foo".into(),
|
||||
discrim: 0 },
|
||||
UserInsert { tag: "foo".into(),
|
||||
password: HashedText::from("poop"),
|
||||
email: Email::from("foo@bar.baz") })
|
||||
.is_ok());
|
||||
assert!(repo.insert(&Actor::default(),
|
||||
UserInsert { tag: UserTag { tag: "foo".into(),
|
||||
discrim: 0 },
|
||||
UserInsert { tag: "foo".into(),
|
||||
password: HashedText::from("poop"),
|
||||
email: Email::from("foo@bar.baz") })
|
||||
.is_err());
|
||||
assert!(repo.insert(&Actor::default(),
|
||||
UserInsert { tag: UserTag { tag: "bar".into(),
|
||||
discrim: 0 },
|
||||
UserInsert { tag: "bar".into(),
|
||||
password: HashedText::from("poop"),
|
||||
email: Email::from("bar@bar.baz") })
|
||||
.is_ok());
|
||||
|
||||
@@ -4,7 +4,7 @@ use naan::prelude::*;
|
||||
use postgres::{GenericClient, GenericRow};
|
||||
use toad::resp::code::BAD_REQUEST;
|
||||
|
||||
use super::{Actor, GroupRepo, User};
|
||||
use super::{Actor, GroupRepo, GroupRepoError, User};
|
||||
use crate::err::ToE;
|
||||
use crate::newtype;
|
||||
use crate::postgres::{DbError, Error, Postgres, UnmarshalRow};
|
||||
@@ -37,6 +37,7 @@ impl<E> LoginError<E> where E: core::fmt::Debug + 'static
|
||||
pub enum ValidateError<E> {
|
||||
Expired,
|
||||
Invalid,
|
||||
GroupRepo(GroupRepoError<E>),
|
||||
Other(E),
|
||||
}
|
||||
|
||||
@@ -62,20 +63,22 @@ pub struct UserSessionExtImpl<Db: 'static, Group: 'static>(pub &'static Db, pub
|
||||
|
||||
impl<Db, G> Ext for UserSessionExtImpl<Db, G>
|
||||
where Db: Postgres,
|
||||
G: GroupRepo
|
||||
G: GroupRepo<DbError<Db>>
|
||||
{
|
||||
type Error = DbError<Db>;
|
||||
}
|
||||
|
||||
impl<Db, G> UserSessionExt for UserSessionExtImpl<Db, G>
|
||||
where Db: Postgres,
|
||||
G: GroupRepo<Error = Self::Error>
|
||||
G: GroupRepo<DbError<Db>>
|
||||
{
|
||||
fn touch(&self, session: UserSession) -> Result<Actor, ValidateError<Self::Error>> {
|
||||
let query = "select public.usr_session_touch(public.usr_session_key_of_string($1))";
|
||||
let cols = "u.uid :: text, u.discrim, u.tag :: text, u.email :: text";
|
||||
let expr = "public.usr_session_touch(public.usr_session_key_of_string($1))";
|
||||
let query = format!("select {cols} from {expr} as u");
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
c.query_one(query, &[&session.as_ref()])
|
||||
c.query_one(&query, &[&session.as_ref()])
|
||||
.and_then(|r| User::unmarshal(&r))
|
||||
})
|
||||
.map_err(|e| {
|
||||
@@ -87,7 +90,13 @@ impl<Db, G> UserSessionExt for UserSessionExtImpl<Db, G>
|
||||
ValidateError::Other(e)
|
||||
}
|
||||
})
|
||||
.zip(|u: &User| self.1.groups(&u.uid).map_err(ValidateError::Other))
|
||||
.zip(|u: &User| {
|
||||
self.1
|
||||
.user_groups(&Actor { uid: Some(u.uid.clone()),
|
||||
groups: vec![] },
|
||||
&u.uid)
|
||||
.map_err(ValidateError::GroupRepo)
|
||||
})
|
||||
.map(|(u, gs)| Actor { uid: Some(u.uid),
|
||||
groups: gs })
|
||||
}
|
||||
@@ -101,11 +110,11 @@ impl<Db, G> UserSessionExt for UserSessionExtImpl<Db, G>
|
||||
["select public.usr_session_key_to_string(s)",
|
||||
" from public.usr_session_login",
|
||||
" ( tag_or_email => public.usr_tag_or_email_of_string($1)",
|
||||
" , password => public.hashed_text_of_string($2)",
|
||||
" , remember => $3",
|
||||
" , location => $4",
|
||||
" , device => $5",
|
||||
" , ip => $6",
|
||||
" , password => $2 :: text",
|
||||
" , remember => $3 :: boolean",
|
||||
" , location => $4 :: text",
|
||||
" , device => ($5 :: text) :: public.usr_session_device",
|
||||
" , ip => ($6 :: text) :: inet",
|
||||
" ) s"];
|
||||
|
||||
self.0
|
||||
|
||||
@@ -80,11 +80,27 @@ pub trait Postgres
|
||||
where F: FnOnce(&mut Self::Client) -> Result<R, DbError<Self>>;
|
||||
|
||||
fn authorized(&self, path: &Path, wants_to: Mode, actor: &Actor) -> Result<bool, DbError<Self>> {
|
||||
const Q: &'static str = "select * from public.perm where path = $1";
|
||||
let got_perm = self.with_client(|c| c.query_opt(Q, &[&path.to_string()]))?;
|
||||
let q = vec!["select p.owner_user_mode :: text",
|
||||
" , p.owner_group_mode :: text",
|
||||
" , p.everyone_mode :: text",
|
||||
" , p.path",
|
||||
" , ou.uid :: text as owner_user",
|
||||
" , og.uid :: text as owner_group",
|
||||
"from public.perm p",
|
||||
"inner join public.grp og on og.id = p.owner_group",
|
||||
"inner join public.usr ou on ou.id = p.owner_user",
|
||||
"where path = $1 :: text",].into_iter()
|
||||
.fold(String::new(), |b, a| format!("{b}\n{a}"));
|
||||
let got_perm = self.with_client(|c| c.query_opt(&q, &[&path.to_string()]))?;
|
||||
Ok(match got_perm {
|
||||
| None => false,
|
||||
| Some(perm) => Perm::unmarshal(&perm)?.actor_can(actor, wants_to),
|
||||
| None => {
|
||||
log::debug!("no such perm: {}", path);
|
||||
false
|
||||
},
|
||||
| Some(perm) => {
|
||||
log::debug!("checking {:?} against perm {}", actor, path);
|
||||
Perm::unmarshal(&perm)?.actor_can(actor, wants_to)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,12 +117,23 @@ pub trait Postgres
|
||||
.map(Path::to_string)
|
||||
.fold(String::new(), |b, a| {
|
||||
if b.is_empty() {
|
||||
a.to_string()
|
||||
format!("'{a}'")
|
||||
} else {
|
||||
format!("{b}, {a}")
|
||||
format!("{b}, '{a}'")
|
||||
}
|
||||
});
|
||||
let q = format!("select * from public.perm where path in {set}");
|
||||
|
||||
let q = vec!["select p.owner_user_mode :: text",
|
||||
" , p.owner_group_mode :: text",
|
||||
" , p.everyone_mode :: text",
|
||||
" , p.path",
|
||||
" , ou.uid :: text as owner_user",
|
||||
" , og.uid :: text as owner_group",
|
||||
"from public.perm p",
|
||||
"inner join public.grp og on og.id = p.owner_group",
|
||||
"inner join public.usr ou on ou.id = p.owner_user",
|
||||
format!("where path in ({set})").as_str(),].into_iter()
|
||||
.fold(String::new(), |b, a| format!("{b}\n{a}"));
|
||||
|
||||
let pass = self.with_client(|c| c.query(&q, &[]))?
|
||||
.iter()
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct RepPayload<T> {
|
||||
pub struct RepPayload<T, L> {
|
||||
#[serde(flatten)]
|
||||
pub t: T,
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
pub links: HashMap<String, String>,
|
||||
pub links: L,
|
||||
}
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use toad_msg::alloc::Message;
|
||||
use toad_msg::{ContentFormat, MessageOptions};
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ReqPayload<T> {
|
||||
#[serde(flatten)]
|
||||
pub t: T,
|
||||
pub session: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> ReqPayload<T> where T: DeserializeOwned
|
||||
{
|
||||
pub fn try_get(m: &Message) -> Result<Option<Self>, serde_json::Error> {
|
||||
if m.payload().as_bytes().len() == 0 || m.content_format() != Some(ContentFormat::Json) {
|
||||
Ok(None)
|
||||
} else {
|
||||
serde_json::from_slice::<Self>(m.payload().as_bytes()).map(Some)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
src/route/debug.rs
Normal file
32
src/route/debug.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use toad::net::Addrd;
|
||||
use toad::resp::code::CREATED;
|
||||
use toad_msg::alloc::Message;
|
||||
use toad_msg::{Code, ContentFormat, MessageOptions, Type};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::env::Environment;
|
||||
use crate::err::{ToE, E};
|
||||
use crate::model::{Actor, LoginError, LoginPayload, UserSessionExt};
|
||||
|
||||
pub fn debug<A>(app: &A, _: &Actor, req: Addrd<&Message>) -> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
let path_segments: Vec<&str> = req.data().path_segments().map_err(ToE::to_e)?;
|
||||
|
||||
Ok(Some(req.data().code)).map(|o| {
|
||||
o.filter(|_| {
|
||||
app.env().environ == Environment::Debug
|
||||
&& path_segments.get(0) == Some(&"debug")
|
||||
})
|
||||
})
|
||||
.and_then(|code| match (path_segments.get(1), code) {
|
||||
| (Some(&"shutdown"), Some(Code::POST)) => {
|
||||
log::info!("received shutdown request");
|
||||
app.enqueue_shutdown();
|
||||
Ok(Some(Message::builder(Type::Ack, CREATED).token(req.data()
|
||||
.token)
|
||||
.build()))
|
||||
},
|
||||
| _ => Ok(None),
|
||||
})
|
||||
}
|
||||
196
src/route/groups.rs
Normal file
196
src/route/groups.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use naan::prelude::*;
|
||||
use toad::net::Addrd;
|
||||
use toad::resp::code::{self, BAD_REQUEST, CHANGED, CONTENT, CREATED};
|
||||
use toad_msg::alloc::Message;
|
||||
use toad_msg::{Code, ContentFormat, MessageOptions, Type};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::err::{ToE, E};
|
||||
use crate::model::{Actor, GroupId, GroupInsert, GroupPatch, GroupRepo, GroupRepoError, UserId};
|
||||
use crate::rep_payload::RepPayload;
|
||||
use crate::repo::{Del, Insert, Page, Patch, ReadMany, ReadOne};
|
||||
use crate::req_payload::ReqPayload;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
pub struct SingleGroupLinks {
|
||||
users: String,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a GroupId> for SingleGroupLinks {
|
||||
fn from(uid: &'a GroupId) -> Self {
|
||||
Self { users: format!("users?filter[group_member]={uid}") }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn groups<A>(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
let path_segments: Vec<&str> = dbg!(req.data().path_segments().map_err(ToE::to_e)?);
|
||||
|
||||
Ok(Some(req.data().code)).map(|o| o.filter(|_| path_segments.get(0) == Some(&"groups")))
|
||||
.and_then(|code| {
|
||||
match (code, path_segments.get(1).map(|id| GroupId::from(*id))) {
|
||||
| (Some(Code::GET), Some(uid)) => get_group(app, actor, uid, req),
|
||||
| (Some(Code::GET), None) => get_groups(app, actor, req),
|
||||
| (Some(Code::PUT), Some(uid)) => put_group(app, actor, uid, req),
|
||||
| (Some(Code::POST), None) => post_group(app, actor, req),
|
||||
| (Some(Code::DELETE), Some(uid)) => del_group(app, actor, uid, req),
|
||||
| _ => Ok(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_group<A>(app: &A,
|
||||
actor: &Actor,
|
||||
id: GroupId,
|
||||
req: Addrd<&Message>)
|
||||
-> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
let user = app.group()
|
||||
.get(&actor, &id)
|
||||
.map_err(GroupRepoError::into_e)?;
|
||||
|
||||
user.map(|u| RepPayload { links: SingleGroupLinks::from(&u.uid),
|
||||
t: u })
|
||||
.map(|r| serde_json::to_vec(&r).map_err(ToE::to_e))
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
.map(|o| {
|
||||
o.map(|r| {
|
||||
Message::builder(Type::Ack, CONTENT).token(req.data().token)
|
||||
.content_format(ContentFormat::Json)
|
||||
.payload(r)
|
||||
.build()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_groups<A>(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
let qs = req.data().query::<Vec<_>>().map_err(ToE::to_e)?;
|
||||
let filter_user_kv = qs.iter().find(|(k, _)| k == &"filter[contains_user]");
|
||||
|
||||
let get_by_user = |u: UserId| {
|
||||
app.group()
|
||||
.user_groups(&actor, &u)
|
||||
.map_err(GroupRepoError::into_e)
|
||||
};
|
||||
|
||||
let get_paginated = || {
|
||||
let payload: ReqPayload<Page<GroupId>> =
|
||||
Some(req.data().payload().as_bytes()).filter(|bs| !bs.is_empty())
|
||||
.map(serde_json::from_slice)
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
.map_err(ToE::to_e)?
|
||||
.ok_or_else(|| {
|
||||
GroupRepoError::<()>::PageInvalid.into_e()
|
||||
})
|
||||
.map_err(ToE::to_e)?;
|
||||
|
||||
app.group()
|
||||
.get_all(&actor, payload.t)
|
||||
.map_err(GroupRepoError::into_e)
|
||||
};
|
||||
|
||||
let grps = match filter_user_kv {
|
||||
| Some((_, Some(usr_uid))) => get_by_user(UserId::from(*usr_uid)),
|
||||
| Some((_, None)) => {
|
||||
let e = crate::err::E::new().code(BAD_REQUEST)
|
||||
.error("bad_request".into())
|
||||
.explain("bad filter criteria for `GET /groups`".into())
|
||||
.hint("/groups?filter[contains_user]=abc".into());
|
||||
|
||||
return Err(e);
|
||||
},
|
||||
| None => get_paginated(),
|
||||
}?;
|
||||
|
||||
let bytes = serde_json::to_vec(&grps).map_err(ToE::to_e)?;
|
||||
Ok(Some(Message::builder(Type::Ack, CONTENT).token(req.data().token)
|
||||
.content_format(ContentFormat::Json)
|
||||
.payload(bytes)
|
||||
.build()))
|
||||
}
|
||||
|
||||
pub fn post_group<A>(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
struct Rep {
|
||||
uid: GroupId,
|
||||
}
|
||||
|
||||
let insert_required = || {
|
||||
E::new().code(BAD_REQUEST)
|
||||
.error("bad_request".into())
|
||||
.explain("bad payload for `POST /users`".into())
|
||||
.hint("{\"tag\": <string>, \"email\": <string>, \"password\": <string>}".into())
|
||||
};
|
||||
|
||||
let payload: ReqPayload<GroupInsert> =
|
||||
Some(req.data().payload().as_bytes()).filter(|bs| !bs.is_empty())
|
||||
.map(serde_json::from_slice)
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
.map_err(ToE::to_e)?
|
||||
.ok_or_else(insert_required)
|
||||
.map_err(ToE::to_e)?;
|
||||
|
||||
let uid = app.group()
|
||||
.insert(&actor, payload.t)
|
||||
.map_err(GroupRepoError::into_e)?;
|
||||
|
||||
let rep = RepPayload { links: SingleGroupLinks::from(&uid),
|
||||
t: Rep { uid } };
|
||||
let bytes = serde_json::to_vec(&rep).map_err(ToE::to_e)?;
|
||||
|
||||
Ok(Some(Message::builder(Type::Ack, CREATED).token(req.data().token)
|
||||
.content_format(ContentFormat::Json)
|
||||
.payload(bytes)
|
||||
.build()))
|
||||
}
|
||||
|
||||
pub fn put_group<A>(app: &A,
|
||||
actor: &Actor,
|
||||
uid: GroupId,
|
||||
req: Addrd<&Message>)
|
||||
-> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
let patch_required = || {
|
||||
E::new().code(BAD_REQUEST)
|
||||
.error("bad_request".into())
|
||||
.explain(format!("bad payload for `PUT /users/{uid}`"))
|
||||
.hint("{\"tag\": <string?>, \"email\": <string?>, \"password\": <string?>}".into())
|
||||
};
|
||||
|
||||
let payload: ReqPayload<GroupPatch> =
|
||||
Some(req.data().payload().as_bytes()).filter(|bs| !bs.is_empty())
|
||||
.map(serde_json::from_slice)
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
.map_err(ToE::to_e)?
|
||||
.ok_or_else(patch_required)
|
||||
.map_err(ToE::to_e)?;
|
||||
|
||||
let patched = app.group()
|
||||
.patch(&actor, &uid, payload.t)
|
||||
.map_err(GroupRepoError::into_e)?;
|
||||
|
||||
Ok(Some(Message::builder(Type::Ack, CHANGED).token(req.data().token)
|
||||
.build()).filter(|_| patched))
|
||||
}
|
||||
|
||||
pub fn del_group<A>(app: &A,
|
||||
actor: &Actor,
|
||||
id: GroupId,
|
||||
req: Addrd<&Message>)
|
||||
-> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
let deleted = app.group()
|
||||
.del(&actor, &id)
|
||||
.map_err(GroupRepoError::into_e)?;
|
||||
|
||||
Ok(Some(Message::builder(Type::Ack, code::DELETED).token(req.data().token)
|
||||
.build()).filter(|_| deleted))
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
pub mod debug;
|
||||
pub mod groups;
|
||||
pub mod user_sessions;
|
||||
pub mod users;
|
||||
|
||||
@@ -7,29 +7,30 @@ use crate::app::App;
|
||||
use crate::err::{ToE, E};
|
||||
use crate::model::{Actor, LoginError, LoginPayload, UserSessionExt};
|
||||
|
||||
pub fn user_sessions<A>(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result<Option<Message>, E>
|
||||
pub fn user_sessions<A>(app: &A, _: &Actor, req: Addrd<&Message>) -> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
let path_segments: Vec<&str> = req.data().path().map_err(ToE::to_e)?;
|
||||
let path_segments: Vec<&str> = req.data().path_segments().map_err(ToE::to_e)?;
|
||||
|
||||
Ok(None).map(|o| o.filter(|_: &()| path_segments.get(0) == Some(&"user_sessions")))
|
||||
.and_then(|_| match req.data().code {
|
||||
| Code::POST => {
|
||||
let p =
|
||||
Ok(Some(req.data().code)).map(|o| o.filter(|_| path_segments.get(0) == Some(&"user_sessions")))
|
||||
.and_then(|code| match code {
|
||||
| Some(Code::POST) => {
|
||||
let p =
|
||||
Some(req.data().payload.as_bytes()).filter(|b| !b.is_empty())
|
||||
.ok_or_else(|| {
|
||||
LoginError::<()>::MalformedPayload.into_e()
|
||||
})?;
|
||||
let p = serde_json::from_slice(p).map_err(ToE::to_e)?;
|
||||
let session = app.user_session().login(p).map_err(LoginError::into_e)?;
|
||||
let bytes =
|
||||
let p: LoginPayload = serde_json::from_slice(p).map_err(ToE::to_e)?;
|
||||
let session =
|
||||
app.user_session().login(p).map_err(LoginError::into_e)?;
|
||||
let bytes =
|
||||
serde_json::to_vec(&serde_json::json!({ "session": session })).map_err(ToE::to_e)?;
|
||||
|
||||
Ok(Some(Message::builder(Type::Ack, CREATED).token(req.data().token)
|
||||
Ok(Some(Message::builder(Type::Ack, CREATED).token(req.data().token)
|
||||
.payload(bytes)
|
||||
.content_format(ContentFormat::Json)
|
||||
.build()))
|
||||
},
|
||||
| _ => Ok(None),
|
||||
})
|
||||
},
|
||||
| _ => Ok(None),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use naan::prelude::*;
|
||||
use toad::net::Addrd;
|
||||
use toad::resp::code::{self, BAD_REQUEST, CHANGED, CONTENT, CREATED};
|
||||
@@ -13,20 +11,35 @@ use crate::rep_payload::RepPayload;
|
||||
use crate::repo::{Del, Insert, Page, Patch, ReadMany, ReadOne};
|
||||
use crate::req_payload::ReqPayload;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
pub struct SingleUserLinks {
|
||||
groups: String,
|
||||
login: String,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a UserId> for SingleUserLinks {
|
||||
fn from(uid: &'a UserId) -> Self {
|
||||
Self { groups: format!("groups?filter[contains_user]={uid}"),
|
||||
login: "user_sessions".to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn users<A>(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
let path_segments: Vec<&str> = req.data().path().map_err(ToE::to_e)?;
|
||||
let path_segments: Vec<&str> = req.data().path_segments().map_err(ToE::to_e)?;
|
||||
|
||||
Ok(None).map(|o| o.filter(|_: &()| path_segments.get(0) == Some(&"users")))
|
||||
.and_then(|_| match (req.data().code, path_segments.get(1).map(|id| UserId::from(*id))) {
|
||||
| (Code::GET, Some(uid)) => get_user(app, actor, uid, req),
|
||||
| (Code::GET, None) => get_users(app, actor, req),
|
||||
| (Code::PUT, Some(uid)) => put_user(app, actor, uid, req),
|
||||
| (Code::POST, None) => post_user(app, actor, req),
|
||||
| (Code::DELETE, Some(uid)) => del_user(app, actor, uid, req),
|
||||
| _ => Ok(None),
|
||||
})
|
||||
Ok(Some(req.data().code)).map(|o| o.filter(|_| path_segments.get(0) == Some(&"users")))
|
||||
.and_then(|code| {
|
||||
match (code, path_segments.get(1).map(|id| UserId::from(*id))) {
|
||||
| (Some(Code::GET), Some(uid)) => get_user(app, actor, uid, req),
|
||||
| (Some(Code::GET), None) => get_users(app, actor, req),
|
||||
| (Some(Code::PUT), Some(uid)) => put_user(app, actor, uid, req),
|
||||
| (Some(Code::POST), None) => post_user(app, actor, req),
|
||||
| (Some(Code::DELETE), Some(uid)) => del_user(app, actor, uid, req),
|
||||
| _ => Ok(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_user<A>(app: &A,
|
||||
@@ -37,13 +50,19 @@ pub fn get_user<A>(app: &A,
|
||||
where A: App
|
||||
{
|
||||
let user = app.user().get(&actor, &id).map_err(UserRepoError::into_e)?;
|
||||
let rep = RepPayload { t: user,
|
||||
links: HashMap::from([("login".into(), "user_sessions/".into())]) };
|
||||
|
||||
Ok(Some(Message::builder(Type::Ack, CONTENT).token(req.data().token)
|
||||
.content_format(ContentFormat::Json)
|
||||
.payload(serde_json::to_vec(&rep).map_err(ToE::to_e)?)
|
||||
.build()))
|
||||
user.map(|u| RepPayload { links: SingleUserLinks::from(&u.uid),
|
||||
t: u })
|
||||
.map(|r| serde_json::to_vec(&r).map_err(ToE::to_e))
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
.map(|o| {
|
||||
o.map(|r| {
|
||||
Message::builder(Type::Ack, CONTENT).token(req.data().token)
|
||||
.content_format(ContentFormat::Json)
|
||||
.payload(r)
|
||||
.build()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_users<A>(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result<Option<Message>, E>
|
||||
@@ -71,6 +90,11 @@ pub fn get_users<A>(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result<Opti
|
||||
pub fn post_user<A>(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result<Option<Message>, E>
|
||||
where A: App
|
||||
{
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
struct Rep {
|
||||
uid: UserId,
|
||||
}
|
||||
|
||||
let insert_required = || {
|
||||
E::new().code(BAD_REQUEST)
|
||||
.error("bad_request".into())
|
||||
@@ -90,7 +114,9 @@ pub fn post_user<A>(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result<Opti
|
||||
.insert(&actor, payload.t)
|
||||
.map_err(UserRepoError::into_e)?;
|
||||
|
||||
let bytes = serde_json::to_vec(&serde_json::json!({"uid": uid.to_string()})).map_err(ToE::to_e)?;
|
||||
let rep = RepPayload { links: SingleUserLinks::from(&uid),
|
||||
t: Rep { uid } };
|
||||
let bytes = serde_json::to_vec(&rep).map_err(ToE::to_e)?;
|
||||
|
||||
Ok(Some(Message::builder(Type::Ack, CREATED).token(req.data().token)
|
||||
.content_format(ContentFormat::Json)
|
||||
|
||||
Reference in New Issue
Block a user