diff --git a/Cargo.lock b/Cargo.lock index ecbe02e..6c7e2d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 05c58b3..ae5c3a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"]} diff --git a/src/app.rs b/src/app.rs index b63137c..e8318f0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,6 @@ +use std::sync::Mutex; + +use crate::env::Env; use crate::model::{GroupRepoImpl, HashedTextExt, HashedTextExtImpl, @@ -15,10 +18,14 @@ pub trait App: Send + Sync + Sized { 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 enqueue_shutdown(&self); + fn should_shutdown(&self) -> bool; } pub struct AppConcrete { @@ -27,6 +34,8 @@ pub struct AppConcrete { pub user_session: &'static UserSessionExtImpl, GroupRepoImpl>>, pub user: &'static UserRepoImpl>, + pub env: Env, + pub should_shutdown: Mutex, } impl App for AppConcrete { @@ -37,6 +46,10 @@ impl App for AppConcrete { type HashedTextExt = HashedTextExtImpl; type UserSessionExt = UserSessionExtImpl>; + fn env(&self) -> &Env { + &self.env + } + fn db(&self) -> &Self::Db { self.pg } @@ -52,4 +65,12 @@ impl App for AppConcrete { fn user(&self) -> &Self::UserRepo { self.user } + + fn enqueue_shutdown(&self) { + *self.should_shutdown.lock().unwrap() = true; + } + + fn should_shutdown(&self) -> bool { + *self.should_shutdown.lock().unwrap() + } } diff --git a/src/env.rs b/src/env.rs index 1821477..847846f 100644 --- a/src/env.rs +++ b/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, + } }) } } diff --git a/src/main.rs b/src/main.rs index 4c3c1c2..ab9ffe4 100644 --- a/src/main.rs +++ b/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,15 +140,22 @@ fn handle_request(app: &A, req: Addrd>) -> Addrd { let body = || { let routes: Vec) -> Result, E>> = - vec![route::users::users, route::user_sessions::user_sessions]; + vec![route::users::users, + route::user_sessions::user_sessions, + route::debug::debug]; - let actor = Ok(req.data().payload()).and_then(serde_json::from_slice::>>) - .map_err(ToE::to_e)? - .and_then(|r| r.session) - .map(|s| app.user_session().touch(UserSession::from(s))) - .sequence::>() - .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::>) + .sequence::>() + }) + .map_err(ToE::to_e)? + .and_then(|r| r.session) + .map(|s| app.user_session().touch(UserSession::from(s))) + .sequence::>() + .map_err(ToE::to_e)? + .unwrap_or_default(); let not_found = || { Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token) @@ -171,7 +180,8 @@ fn handle_request(app: &A, req: Addrd>) -> Addrd 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 +192,11 @@ fn server_worker(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 +215,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(); @@ -234,7 +249,9 @@ fn main() { let app = AppConcrete { pg, hashed_text, user_session, - user }; + user, + env, + should_shutdown: Mutex::new(false) }; server_worker(&app, toad); } diff --git a/src/model/user.rs b/src/model/user.rs index e9ed346..36835cf 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -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, + pub tag: Option, + pub tag_discrim: Option, pub password: Option, pub email: Option, } #[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 ReadOne for UserRepoImpl 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 ReadMany for UserRepoImpl 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 = self.0 @@ -216,7 +222,7 @@ impl Patch for UserRepoImpl where Db: Postgres + 'static " , 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 +243,7 @@ impl Patch for UserRepoImpl 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 +262,8 @@ impl Insert for UserRepoImpl 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, $2 :: text, $3 :: text)", + "returning (uid :: text);"]; if !self.0 .authorized(&Path::parse("/users/"), Mode::Write, actor) @@ -270,7 +276,7 @@ impl Insert for UserRepoImpl 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 +465,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()); diff --git a/src/postgres.rs b/src/postgres.rs index d82b3a6..b593ffa 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -80,8 +80,18 @@ pub trait Postgres where F: FnOnce(&mut Self::Client) -> Result>; fn authorized(&self, path: &Path, wants_to: Mode, actor: &Actor) -> Result> { - 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), @@ -106,7 +116,18 @@ pub trait Postgres 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 as owner_user", + " , og.uid 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() + .fold(String::new(), |b, a| format!("{b}\n{a}")); let pass = self.with_client(|c| c.query(&q, &[]))? .iter() diff --git a/src/rep_payload.rs b/src/rep_payload.rs index 70b6f6f..c6c7853 100644 --- a/src/rep_payload.rs +++ b/src/rep_payload.rs @@ -1,9 +1,6 @@ -use std::collections::HashMap; - #[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] -pub struct RepPayload { +pub struct RepPayload { #[serde(flatten)] pub t: T, - #[serde(skip_serializing_if = "HashMap::is_empty")] - pub links: HashMap, + pub links: L, } diff --git a/src/route/debug.rs b/src/route/debug.rs new file mode 100644 index 0000000..4240b6f --- /dev/null +++ b/src/route/debug.rs @@ -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(app: &A, _: &Actor, req: Addrd<&Message>) -> Result, 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), + }) +} diff --git a/src/route/mod.rs b/src/route/mod.rs index 5db9fe2..7d8cae4 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -1,2 +1,3 @@ +pub mod debug; pub mod user_sessions; pub mod users; diff --git a/src/route/user_sessions.rs b/src/route/user_sessions.rs index d96fc1c..5adb841 100644 --- a/src/route/user_sessions.rs +++ b/src/route/user_sessions.rs @@ -7,29 +7,30 @@ use crate::app::App; use crate::err::{ToE, E}; use crate::model::{Actor, LoginError, LoginPayload, UserSessionExt}; -pub fn user_sessions(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result, E> +pub fn user_sessions(app: &A, _: &Actor, req: Addrd<&Message>) -> Result, 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 = 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), + }) } diff --git a/src/route/users.rs b/src/route/users.rs index fb26b24..8a1d16d 100644 --- a/src/route/users.rs +++ b/src/route/users.rs @@ -13,20 +13,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[user]={uid}"), + login: "user_sessions".to_string() } + } +} + pub fn users(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result, 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(app: &A, @@ -37,13 +52,19 @@ pub fn get_user(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::>() + .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(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result, E> @@ -71,6 +92,11 @@ pub fn get_users(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result, 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 +116,9 @@ pub fn post_user(app: &A, actor: &Actor, req: Addrd<&Message>) -> Result