fix: update toad-msg, add /groups

This commit is contained in:
Orion Kindel 2023-07-21 00:07:13 -05:00
parent bf07ba9fa2
commit dc27262328
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
12 changed files with 478 additions and 76 deletions

View File

@ -1,7 +1,8 @@
use std::sync::Mutex;
use crate::env::Env;
use crate::model::{GroupRepoImpl,
use crate::model::{GroupRepo,
GroupRepoImpl,
HashedTextExt,
HashedTextExtImpl,
UserRepo,
@ -14,6 +15,7 @@ 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;
@ -23,6 +25,7 @@ pub trait App: Send + Sync + Sized {
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;
@ -34,6 +37,7 @@ 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>,
}
@ -42,6 +46,7 @@ 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>>;
@ -66,6 +71,10 @@ impl App for AppConcrete {
self.user
}
fn group(&self) -> &Self::GroupRepo {
self.group
}
fn enqueue_shutdown(&self) {
*self.should_shutdown.lock().unwrap() = true;
}

View File

@ -141,13 +141,14 @@ 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::groups::groups,
route::user_sessions::user_sessions,
route::debug::debug];
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<()>>)
p.map(serde_json::from_slice::<ReqPayload<Option<()>>>)
.sequence::<hkt::ResultOk<_>>()
})
.map_err(ToE::to_e)?
@ -157,7 +158,10 @@ fn handle_request<A>(app: &A, req: Addrd<Req<ToadT>>) -> Addrd<Message>
.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()
};
@ -248,6 +252,7 @@ fn main() {
let app = AppConcrete { pg,
hashed_text,
group: group_repo,
user_session,
user,
env,

View File

@ -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)
}
}

View File

@ -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));

View File

@ -216,12 +216,11 @@ 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 = (($1 :: text) :: human_uuid.huid) and deleted = false"];
let paths =
@ -262,7 +261,7 @@ impl<Db> Insert for UserRepoImpl<Db> where Db: Postgres + 'static
static QUERY_LINES: [&'static str; 5] = ["insert into public.usr",
" (tag, password, email)",
"values",
" ($1 :: text, $2 :: text, $3 :: text)",
" ($1 :: text, hash_text($2), $3 :: text)",
"returning (uid :: text);"];
if !self.0

View File

@ -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

View File

@ -93,8 +93,14 @@ pub trait Postgres
.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)
},
})
}
@ -111,9 +117,9 @@ 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}'")
}
});
@ -121,12 +127,12 @@ pub trait Postgres
" , p.owner_group_mode :: text",
" , p.everyone_mode :: text",
" , p.path",
" , ou.uid as owner_user",
" , og.uid as owner_group",
" , 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 in {set}",].into_iter()
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, &[]))?

View File

@ -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)
}
}
}

196
src/route/groups.rs Normal file
View 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))
}

View File

@ -1,3 +1,4 @@
pub mod debug;
pub mod groups;
pub mod user_sessions;
pub mod users;

View File

@ -20,7 +20,7 @@ pub fn user_sessions<A>(app: &A, _: &Actor, req: Addrd<&Message>) -> Result<Opti
.ok_or_else(|| {
LoginError::<()>::MalformedPayload.into_e()
})?;
let p = serde_json::from_slice(p).map_err(ToE::to_e)?;
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 =

View File

@ -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};
@ -21,7 +19,7 @@ pub struct SingleUserLinks {
impl<'a> From<&'a UserId> for SingleUserLinks {
fn from(uid: &'a UserId) -> Self {
Self { groups: format!("groups?filter[user]={uid}"),
Self { groups: format!("groups?filter[contains_user]={uid}"),
login: "user_sessions".to_string() }
}
}