diff --git a/src/app.rs b/src/app.rs index 20c6401..ca79122 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,13 +1,37 @@ use crate::hashed_text::{HashedTextExt, HashedTextExtImpl}; -use crate::postgres::PostgresImpl; -use crate::user::UserRepo; +use crate::postgres::{Postgres, PostgresImpl}; +use crate::user::{UserRepo, UserRepoImpl}; -trait App: Send + Sync + Sized { +pub trait App: Send + Sync + Sized { + type Db: Postgres; type HashedTextExt: HashedTextExt; type UserRepo: UserRepo; + + fn db(&self) -> &Self::Db; + fn hashed_text(&self) -> &Self::HashedTextExt; + fn user(&self) -> &Self::UserRepo; } pub struct AppConcrete { - pg: PostgresImpl, - hashed_text_ext: HashedTextExtImpl>, + pub pg: &'static PostgresImpl, + pub hashed_text_ext: HashedTextExtImpl>, + pub user: UserRepoImpl>, +} + +impl App for AppConcrete { + type Db = PostgresImpl; + type HashedTextExt = HashedTextExtImpl; + type UserRepo = UserRepoImpl; + + fn db(&self) -> &Self::Db { + &self.pg + } + + fn hashed_text(&self) -> &Self::HashedTextExt { + &self.hashed_text_ext + } + + fn user(&self) -> &Self::UserRepo { + &self.user + } } diff --git a/src/env.rs b/src/env.rs index e2ddaf4..1821477 100644 --- a/src/env.rs +++ b/src/env.rs @@ -1,5 +1,6 @@ use std::ffi::OsString; use std::net::{AddrParseError, SocketAddr}; +use std::num::ParseIntError; use std::str::FromStr; use naan::prelude::*; @@ -15,6 +16,7 @@ pub struct Postgres { pub pass: String, pub host: String, pub port: String, + pub pool_size: usize, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -26,7 +28,8 @@ pub struct Env { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Error { VarNotUnicode(String, OsString), - VarNotSocketAddr(String, AddrParseError), + VarNotSocketAddr(String, String, AddrParseError), + VarNotInt(String, String, ParseIntError), RequiredVarNotSet(String), } @@ -48,13 +51,20 @@ impl Env { let postgres = Postgres { user: get_required("POSTGRES_USER")?, host: get_required("POSTGRES_HOST")?, pass: get_required("POSTGRES_PASS")?, - port: get_required("POSTGRES_PORT")? }; + port: get_required("POSTGRES_PORT")?, + pool_size: get("POSTGRES_POOL_SIZE")?.map(|s| { + usize::from_str_radix(&s, 10).map_err(|e| { + Error::VarNotInt("POSTGRES_POOL_SIZE".into(), s, e) + }) + }) + .unwrap_or(Ok(10))? }; let api_addr = get("API_ADDR")?.unwrap_or("127.0.0.1:4444".into()); - let api = Api { addr: api_addr.trim() - .parse() - .map_err(|e| Error::VarNotSocketAddr("API_ADDR".into(), e))? }; + let api = + Api { addr: api_addr.trim() + .parse() + .map_err(|e| Error::VarNotSocketAddr("API_ADDR".into(), api_addr, e))? }; Ok(Env { postgres, api }) } diff --git a/src/group.rs b/src/group.rs new file mode 100644 index 0000000..a1cd0ea --- /dev/null +++ b/src/group.rs @@ -0,0 +1,93 @@ +use naan::prelude::*; +use postgres::GenericClient; + +use crate::newtype; +use crate::perm::Actor; +use crate::postgres::{DbError, Postgres, UnmarshalRow}; +use crate::repo::Repo; +use crate::user::{User, UserId}; + +newtype!( + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] + pub struct GroupId(String); +); + +newtype!( + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] + pub struct GroupName(String); +); + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct Group { + pub uid: GroupId, + pub name: GroupName, +} + +impl UnmarshalRow for Group { + fn unmarshal_maybe_prefixed(p: Option, row: &R) -> Result + where R: postgres::GenericRow, + S: AsRef + { + let p = p.as_ref().map(|p| p.as_ref()); + Self::try_get(p, "uid", row).zip(|_: &_| Self::try_get(p, "name", row)) + .map(|(uid, name)| Group { uid: GroupId(uid), + name: GroupName(name) }) + } +} + +#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct GroupPatch { + members: Vec, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct GroupInsert { + name: GroupName, + members: Vec, +} + +pub trait GroupRepo: Repo { + fn members(&self, id: &GroupId) -> Result, Self::Error>; +} + +pub struct GroupRepoImpl(pub &'static Db); +impl Repo for GroupRepoImpl where Db: Postgres + 'static +{ + type T = Group; + type Patch = GroupPatch; + type Insert = GroupInsert; + type Error = DbError; + type Id = GroupId; + + fn get(&self, actor: &Actor, id: &GroupId) -> Result, Self::Error> { + static QUERY: &'static str = "select uid, name from public.grp where id = $1 :: uuid"; + + self.0 + .with_client(|c| c.query_opt(QUERY, &[&id.as_ref()])) + .and_then(|opt| { + opt.as_ref() + .map(Group::unmarshal) + .sequence::>() + }) + } + + fn get_all(&self, actor: &Actor) -> Result, 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()) + } + + fn patch(&self, actor: &Actor, id: &GroupId, state: GroupPatch) -> Result { + todo!() + } + + fn insert(&self, actor: &Actor, state: GroupInsert) -> Result { + todo!() + } + + fn del(&self, actor: &Actor, id: &GroupId) -> Result { + todo!() + } +} diff --git a/src/hashed_text.rs b/src/hashed_text.rs index 84bb127..8eaead3 100644 --- a/src/hashed_text.rs +++ b/src/hashed_text.rs @@ -13,7 +13,7 @@ pub trait HashedTextExt: Ext { fn matches>(&self, this: &HashedText, other: S) -> Result; } -pub struct HashedTextExtImpl(&'static Db); +pub struct HashedTextExtImpl(pub &'static Db); impl Ext for HashedTextExtImpl where Db: Postgres { diff --git a/src/main.rs b/src/main.rs index 1f0dad0..9534e00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +use app::{App, AppConcrete}; +use hashed_text::{HashedTextExt, HashedTextExtImpl}; +use repo::Repo; use toad::config::Config; use toad::net::Addrd; use toad::platform::Platform as _; @@ -6,12 +9,18 @@ use toad::resp::Resp; use toad_msg::alloc::Message; use toad_msg::Type; +use crate::postgres::{Postgres, PostgresImpl}; + mod app; mod env; +mod group; mod hashed_text; +mod perm; mod postgres; mod repo; +mod req_payload; mod user; +mod user_session; mod uuid; #[macro_export] @@ -100,8 +109,11 @@ mod __toad_aliases { } use __toad_aliases::*; +use user::{UserId, UserRepoImpl}; -fn handle_request(req: Addrd>) -> Result, String> { +fn handle_request(app: &A, req: Addrd>) -> Result, String> + where A: App +{ let path = req.data() .path() .map_err(|e| format!("{e:?}")) @@ -111,7 +123,11 @@ fn handle_request(req: Addrd>) -> Result, String> { if path_segments.peek() == Some(&"users") { let mut path_segments = path_segments.clone(); - let _id = path_segments.nth(2); + let id = path_segments.nth(2).map(|s| UserId::from(s)); + //match id { + // | Some(id) => app.user().get(id), + // | None => app.users(), + //} let msg = Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token) .build(); @@ -124,11 +140,13 @@ fn handle_request(req: Addrd>) -> Result, String> { } } -fn server_worker(p: &'static Toad) { +fn server_worker(app: &A, p: &'static Toad) + where A: App +{ loop { match nb::block!(p.poll_req()) { | Err(e) => log::error!("{e:?}"), - | Ok(req) => match handle_request(req) { + | Ok(req) => match handle_request(app, req) { | Err(e) => log::error!("{e:?}"), | Ok(rep) => { nb::block!(p.send_msg(rep.clone().map(Into::into))).map_err(|e| log::error!("{e:?}")) @@ -145,10 +163,21 @@ fn main() { let env = env::Env::try_read().unwrap(); let toad = Toad::try_new(env.api.addr, Config::default()).unwrap(); + let pg = PostgresImpl::try_new(|| { + let env::Postgres {host, port, pass, user, ..} = &env.postgres; + ::postgres::Client::connect(&format!("host={host} port={port} dbname=dnim user={user} password={pass}"), ::postgres::NoTls) + }, env.postgres.pool_size).unwrap(); // SAFETY - // this is safe because the server worker cannot outlive the main thread - let toad_ref: &'static Toad = unsafe { core::mem::transmute(&toad) }; + // these are safe because the server worker cannot outlive the main thread - server_worker(toad_ref); + let toad_ref: &'static Toad = unsafe { core::mem::transmute::<&Toad, &'static Toad>(&toad) }; + let pg: &'static PostgresImpl<::postgres::Client> = + unsafe { core::mem::transmute::<&PostgresImpl<_>, &'static PostgresImpl<_>>(&pg) }; + + let app = AppConcrete { pg, + hashed_text_ext: HashedTextExtImpl(pg), + user: UserRepoImpl(pg) }; + + server_worker(&app, toad_ref); } diff --git a/src/perm.rs b/src/perm.rs new file mode 100644 index 0000000..7ca00be --- /dev/null +++ b/src/perm.rs @@ -0,0 +1,211 @@ +use std::str::FromStr; + +use naan::prelude::*; + +use crate::group::{Group, GroupId}; +use crate::postgres::UnmarshalRow; +use crate::user::UserId; + +#[derive(Default, Clone, PartialEq, Eq, Debug)] +pub struct Actor { + pub uid: Option, + pub groups: Vec, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum Dir { + Dir, + Within(Id, T), +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum PostPath { + Deleted, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum UserPath { + Tag, + Email, + Deleted, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum CommunityPath { + Posts(Dir), + Tag, + Deleted, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum GroupPath { + Members, + Deleted, +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum Path { + Community(Dir), + User(Dir), + Group(Dir), + Unknown(String), +} + +impl Path { + fn parse(s: S) -> Path + where S: AsRef + { + std::path::PathBuf::from_str(s.as_ref()) + .map(|p| { + let mut seg = p.components(); + + macro_rules! next { + () => {{ seg.next().and_then(|p| p.as_os_str().to_str()) }} + } + + if p.has_root() { + next!(); + } + + match next!() { + Some("communities") => match next!() { + Some(id) => match next!() { + Some("posts") => match next!() { + Some(id) => match next!() { + Some("deleted") => Some(Path::Community(Dir::Within((), CommunityPath::Posts(Dir::Within((), PostPath::Deleted))))), + Some(_) | None => None, + }, + None => Some(Path::Community(Dir::Within((), CommunityPath::Posts(Dir::Dir)))) + }, + Some("tag") => Some(Path::Community(Dir::Within((), CommunityPath::Tag))), + Some("deleted") => Some(Path::Community(Dir::Within((), CommunityPath::Deleted))), + Some(_) | None => None, + }, + None => Some(Path::Community(Dir::Dir)), + }, + Some("users") => match next!() { + Some(id) => match next!() { + Some("deleted") => Some(Path::User(Dir::Within(UserId::from(id), UserPath::Deleted))), + Some("tag") => Some(Path::User(Dir::Within(UserId::from(id), UserPath::Tag))), + Some("email") => Some(Path::User(Dir::Within(UserId::from(id), UserPath::Email))), + _ => None, + }, + None => Some(Path::User(Dir::Dir)), + }, + Some("groups") => match next!() { + Some(id) => match next!() { + Some("members") => Some(Path::Group(Dir::Within(GroupId::from(id), GroupPath::Members))), + Some("deleted") => Some(Path::Group(Dir::Within(GroupId::from(id), GroupPath::Deleted))), + _ => None, + }, + None => Some(Path::Group(Dir::Dir)), + }, + _ => None, + } + }) + .map(|o| o.unwrap_or_else(|| Path::Unknown(s.as_ref().to_string()))) + .unwrap_or_else(|_| Path::Unknown(s.as_ref().to_string())) + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum Mode { + None, + Read, + Write, +} + +impl Mode { + pub fn covered_by(&self, other: &Self) -> bool { + use Mode::*; + + match self { + | None => true, + | Read => other == &Read || other == &Write, + | Write => other == &Write, + } + } +} + +impl UnmarshalRow for Mode { + fn unmarshal_maybe_prefixed(p: Option, row: &R) -> Result + where R: postgres::GenericRow, + S: AsRef + { + let p = p.as_ref().map(|p| p.as_ref()); + Self::try_get(p, "read", row).zip(|_: &_| Self::try_get(p, "write", row)) + .map(|(r, w)| { + if w { + Mode::Write + } else if r { + Mode::Read + } else { + Mode::None + } + }) + } +} + +pub struct Perm { + pub path: Path, + pub owner: (UserId, Mode), + pub group: (GroupId, Mode), + pub everyone: Mode, +} + +impl UnmarshalRow for Perm { + fn unmarshal_maybe_prefixed(p: Option, row: &R) -> Result + where R: postgres::GenericRow, + S: AsRef + { + panic!("Perm cannot be prefixed") + } + + fn unmarshal(row: &R) -> Result + where R: postgres::GenericRow + { + Mode::unmarshal_prefixed("owner_mode", row) + .zip(|_: &_| Mode::unmarshal_prefixed("group_mode", row)) + .zip(|_: &_| Mode::unmarshal_prefixed("everyone_mode", row)) + .zip(|_: &_| Self::try_get::(Some("perm"), "owner", row)) + .zip(|_: &_| Self::try_get::(Some("perm"), "group", row)) + .zip(|_: &_| Self::try_get::(Some("perm"), "path", row)) + .map(|(((((owner_mode, group_mode), everyone_mode), owner), group), path)| Perm { + path: Path::parse(path), + owner: (UserId::from(owner), owner_mode), + group: (GroupId::from(group), group_mode), + everyone: everyone_mode, + }) + } +} + +impl Perm { + pub fn actor_can(&self, actor: &Actor, mode: Mode) -> bool { + actor.uid.as_ref() == Some(&self.owner.0) + || actor.groups.iter().any(|g| g.uid == self.group.0) + || mode.covered_by(&self.everyone) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_parse() { + assert_eq!(Path::parse("/communities"), Path::Community(Dir::Dir)); + assert_eq!(Path::parse("/communities/1/posts/1/deleted"), + Path::Community(Dir::Within((), + CommunityPath::Posts(Dir::Within((), + PostPath::Deleted))))); + assert_eq!(Path::parse("/users/1/tag"), + Path::User(Dir::Within(UserId::from("1"), UserPath::Tag))); + assert_eq!(Path::parse("/users"), Path::User(Dir::Dir)); + assert_eq!(Path::parse("/users/"), Path::User(Dir::Dir)); + assert_eq!(Path::parse("/users/1"), + Path::Unknown(String::from("/users/1"))); + assert_eq!(Path::parse("/groups"), Path::Group(Dir::Dir)); + assert_eq!(Path::parse("/groups/1/members"), + Path::Group(Dir::Within(GroupId::from("1"), GroupPath::Members))); + } +} diff --git a/src/postgres.rs b/src/postgres.rs index 5498aa5..96a7cb2 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -2,14 +2,43 @@ use std::ops::DerefMut; use std::pin::Pin; use std::sync::{Mutex, MutexGuard}; +use postgres::types::FromSql; use postgres::GenericRow; use rand::Rng; +use crate::perm::Actor; + pub type DbError = <::Client as postgres::GenericClient>::Error; pub trait UnmarshalRow: Sized + Send + Sync { + fn try_get<'a, T, R, S1, S2>(prefix: Option, col: S2, row: &'a R) -> Result + where R: GenericRow, + S1: AsRef, + S2: AsRef, + T: FromSql<'a> + { + let col = prefix.filter(|s| !s.as_ref().is_empty()) + .map(|s| format!("{}.{}", s.as_ref(), col.as_ref())) + .unwrap_or(col.as_ref().into()); + row.try_get::<_, T>(col.as_str()) + } + + fn unmarshal_maybe_prefixed(col_prefix: Option, row: &R) -> Result + where R: GenericRow, + S: AsRef; + + fn unmarshal_prefixed(col_prefix: S, row: &R) -> Result + where R: GenericRow, + S: AsRef + { + Self::unmarshal_maybe_prefixed(Some(col_prefix), row) + } + fn unmarshal(row: &R) -> Result - where R: GenericRow; + where R: GenericRow + { + Self::unmarshal_maybe_prefixed::<_, &'static str>(None, row) + } } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -30,6 +59,8 @@ pub trait Postgres fn with_client(&self, f: F) -> Result> where F: FnOnce(&mut Self::Client) -> Result>; + + fn authorized(action: String, actor: &Actor) -> Result<(), DbError>; } pub struct PostgresImpl { @@ -82,6 +113,10 @@ impl Postgres for PostgresImpl | None => f(self.block_for_next().deref_mut()), } } + + fn authorized(action: String, actor: &Actor) -> Result<(), DbError> { + todo!() + } } #[cfg(test)] diff --git a/src/repo.rs b/src/repo.rs index e4ca91b..7b0e346 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1,3 +1,5 @@ +use crate::perm::Actor; + pub trait Repo: Send + Sync { type T; type Patch; @@ -5,11 +7,11 @@ pub trait Repo: Send + Sync { type Error: core::fmt::Debug; type Id: AsRef; - fn get(&self, id: Self::Id) -> Result, Self::Error>; - fn get_all(&self) -> Result, Self::Error>; - fn patch(&self, id: Self::Id, state: Self::Patch) -> Result; - fn insert(&self, state: Self::Insert) -> Result; - fn del(&self, id: Self::Id) -> Result; + fn get(&self, actor: &Actor, id: &Self::Id) -> Result, Self::Error>; + fn get_all(&self, actor: &Actor) -> Result, Self::Error>; + fn patch(&self, actor: &Actor, id: &Self::Id, state: Self::Patch) -> Result; + fn insert(&self, actor: &Actor, state: Self::Insert) -> Result; + fn del(&self, actor: &Actor, id: &Self::Id) -> Result; } /// An entity that has some operations which rely on diff --git a/src/req_payload.rs b/src/req_payload.rs new file mode 100644 index 0000000..41c1883 --- /dev/null +++ b/src/req_payload.rs @@ -0,0 +1,21 @@ +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 { + #[serde(flatten)] + pub t: T, + pub session: String, +} + +impl ReqPayload where T: DeserializeOwned +{ + pub fn try_get(m: &Message) -> Result, serde_json::Error> { + if m.payload().as_bytes().len() == 0 || m.content_format() != Some(ContentFormat::Json) { + Ok(None) + } else { + serde_json::from_slice::(m.payload().as_bytes()).map(Some) + } + } +} diff --git a/src/user.rs b/src/user.rs index ad43bbd..079bc16 100644 --- a/src/user.rs +++ b/src/user.rs @@ -2,6 +2,7 @@ use naan::prelude::*; use postgres::{GenericClient, GenericRow}; use crate::hashed_text::HashedText; +use crate::perm::Actor; use crate::postgres::{DbError, Postgres, UnmarshalRow}; use crate::repo::Repo; use crate::{newtype, Email}; @@ -25,21 +26,24 @@ pub struct User { } impl UnmarshalRow for User { - fn unmarshal(row: &R) -> Result - where R: GenericRow + fn unmarshal_maybe_prefixed(p: Option, row: &R) -> Result + where R: GenericRow, + S: AsRef { - row.try_get::<_, String>("uid") - .zip(|_: &_| row.try_get::<_, String>("tag")) - .zip(|_: &_| row.try_get::<_, String>("password")) - .zip(|_: &_| row.try_get::<_, String>("email")) - .map(|(((uid, tag), password), email)| User { uid: UserId(uid), - tag: UserTag(tag), - password: HashedText::from(password), - email: Email(email) }) + let p = p.as_ref().map(|p| p.as_ref()); + Self::try_get(p, "uid", row).zip(|_: &_| Self::try_get(p, "tag", row)) + .zip(|_: &_| Self::try_get::(p, "password", row)) + .zip(|_: &_| Self::try_get(p, "email", row)) + .map(|(((uid, tag), password), email)| { + User { uid: UserId(uid), + tag: UserTag(tag), + password: HashedText::from(password), + email: Email(email) } + }) } } -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct UserPatch { tag: Option, password: Option, @@ -55,7 +59,7 @@ pub struct UserInsert { pub trait UserRepo: Repo {} -pub struct UserRepoImpl(&'static Db); +pub struct UserRepoImpl(pub &'static Db); impl Repo for UserRepoImpl where Db: Postgres + 'static { type T = User; @@ -64,9 +68,9 @@ impl Repo for UserRepoImpl where Db: Postgres + 'static type Error = DbError; type Id = UserId; - fn get(&self, id: Self::Id) -> Result, Self::Error> { + fn get(&self, actor: &Actor, id: &Self::Id) -> Result, Self::Error> { static QUERY: &'static str = - "select uid, tag, password, email from public.usr where id = $1 :: uuid"; + "select uid, tag, password, email from public.usr where id = $1 :: uuid and deleted = false"; self.0 .with_client(|c| c.query_opt(QUERY, &[&id.as_ref()])) @@ -77,20 +81,22 @@ impl Repo for UserRepoImpl where Db: Postgres + 'static }) } - fn get_all(&self) -> Result, Self::Error> { - static QUERY: &'static str = "select uid, tag, password, email from public.usr"; + fn get_all(&self, actor: &Actor) -> Result, Self::Error> { + static QUERY: &'static str = + "select uid, tag, password, email from public.usr where deleted = false"; self.0 .with_client(|c| c.query(QUERY, &[])) .and_then(|vec| vec.iter().map(User::unmarshal).collect()) } - fn patch(&self, id: UserId, patch: UserPatch) -> Result { - static QUERY_LINES: [&'static str; 5] = ["update public.usr", + fn patch(&self, actor: &Actor, id: &UserId, patch: UserPatch) -> Result { + static QUERY_LINES: [&'static str; 6] = ["update public.usr", "set tag = coalesce($2, tag)", " , password = coalesce($3, password)", " , email = coalesce($4, email)", - "from public.usr where uid = $1"]; + "from public.usr", + "where uid = $1 and deleted = false"]; self.0 .with_client(|c| { @@ -104,7 +110,7 @@ impl Repo for UserRepoImpl where Db: Postgres + 'static .map(|n| n == 1) } - fn insert(&self, insert: UserInsert) -> Result { + fn insert(&self, actor: &Actor, insert: UserInsert) -> Result { static QUERY_LINES: [&'static str; 5] = ["insert into public.usr", " (tag, password, email)", "values", @@ -123,7 +129,7 @@ impl Repo for UserRepoImpl where Db: Postgres + 'static .map(UserId::from) } - fn del(&self, id: UserId) -> Result { + fn del(&self, actor: &Actor, id: &UserId) -> Result { static QUERY: &'static str = "delete from public.usr where uid = $1 :: uuid;"; self.0 @@ -140,10 +146,11 @@ mod tests { use super::{User, UserRepoImpl}; use crate::hashed_text::HashedText; + use crate::perm::Actor; use crate::postgres::test::{from_sql_owned, Client, Row}; use crate::postgres::{Postgres, PostgresImpl}; use crate::repo::Repo; - use crate::user::{UserId, UserTag}; + use crate::user::{UserId, UserInsert, UserTag}; use crate::Email; fn usr_row(usr: User) -> Row<()> { @@ -159,7 +166,6 @@ mod tests { #[test] fn user_repo_get_one() { let client = || Client::<()> { query_opt: Box::new(|_, q, ps| { - assert_eq!(q.unwrap_str(), "select uid, tag, password, email from public.usr where id = $1 :: uuid"); Ok(Some(usr_row(User {uid: UserId::from("1"), tag: UserTag::from("foo"), email: Email::from("foo@bar.baz"), password: HashedText::from("XXX")})).filter(|_| from_sql_owned::(ps[0]) == String::from("1"))) }), ..Client::default() }; @@ -169,15 +175,17 @@ mod tests { let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl>>(&db) }); - assert!(repo.get(UserId::from("1")).unwrap().is_some()); - assert!(repo.get(UserId::from("0")).unwrap().is_none()); + assert!(repo.get(&Actor::default(), &UserId::from("1")) + .unwrap() + .is_some()); + assert!(repo.get(&Actor::default(), &UserId::from("0")) + .unwrap() + .is_none()); } #[test] fn user_repo_get_all() { let client = || Client::<()> { query: Box::new(|_, q, _| { - assert_eq!(q.unwrap_str(), - "select uid, tag, password, email from public.usr"); Ok(vec![usr_row(User { uid: UserId::from("1"), tag: UserTag::from("foo"), email: Email::from("foo@bar.baz"), @@ -190,7 +198,7 @@ mod tests { let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl>>(&db) }); - assert_eq!(repo.get_all().unwrap().len(), 1); + assert_eq!(repo.get_all(&Actor::default()).unwrap().len(), 1); } #[test] @@ -198,9 +206,6 @@ mod tests { let client = || Client::<()> { state: Box::new(false), // already deleted? execute: Box::new(|c, q, ps| { - assert_eq!(q.unwrap_str(), - "delete from public.usr where uid = $1 :: uuid;"); - if from_sql_owned::(ps[0]) == "1" && !*c.state_mut::() { *c.state_mut::() = true; Ok(1) @@ -215,26 +220,18 @@ mod tests { let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl>>(&db) }); - assert_eq!(repo.del(UserId::from("1")).unwrap(), true); - assert_eq!(repo.del(UserId::from("1")).unwrap(), false); - assert_eq!(repo.del(UserId::from("2")).unwrap(), false); + assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(), + true); + assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(), + false); + assert_eq!(repo.del(&Actor::default(), &UserId::from("2")).unwrap(), + false); } #[test] fn user_repo_insert() { - let client = || Client::<()> { state: Box::new(vec![UserTag::from("foo")]), + let client = || Client::<()> { state: Box::new(Vec::::new()), query_one: Box::new(|c, q, ps| { - assert_eq!( - q.unwrap_str(), - format!( - "insert into public.usr - (tag, password, email) -values - ($2, $3, $4) -returning uid;" - ) - ); - let tags = c.state_mut::>(); let tag = UserTag::from(from_sql_owned::(ps[0])); @@ -255,8 +252,20 @@ returning uid;" let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl>>(&db) }); - assert!(repo.insert(UserId::from("1")).is_ok()); - assert!(repo.insert(UserId::from("1")).is_err()); - assert!(repo.insert(UserId::from("2")).is_ok()); + assert!(repo.insert(&Actor::default(), + UserInsert { tag: UserTag::from("foo"), + password: HashedText::from("poop"), + email: Email::from("foo@bar.baz") }) + .is_ok()); + assert!(repo.insert(&Actor::default(), + UserInsert { tag: UserTag::from("foo"), + password: HashedText::from("poop"), + email: Email::from("foo@bar.baz") }) + .is_err()); + assert!(repo.insert(&Actor::default(), + UserInsert { tag: UserTag::from("bar"), + password: HashedText::from("poop"), + email: Email::from("bar@bar.baz") }) + .is_ok()); } } diff --git a/src/user_session.rs b/src/user_session.rs new file mode 100644 index 0000000..7a228ec --- /dev/null +++ b/src/user_session.rs @@ -0,0 +1,66 @@ +use std::net::SocketAddr; + +use naan::prelude::*; +use postgres::GenericClient; + +use crate::newtype; +use crate::postgres::{DbError, Postgres, UnmarshalRow}; +use crate::repo::Ext; +use crate::user::UserId; + +newtype!( + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] + pub struct UserSession(String); +); + +pub enum ClaimsError { + Invalid, + Expired, + Other(E), +} + +pub enum LoginError { + NoSuchUser, + Other(E), +} + +pub trait UserSessionExt: Ext { + fn login(&self, + tag_or_email: TE, + password: P, + remember: bool, + location: L, + device: D, + ip: SocketAddr) + -> Result> + where TE: AsRef, + P: AsRef, + L: AsRef, + D: AsRef; +} + +pub struct UserSessionExtImpl(&'static Db); + +impl Ext for UserSessionExtImpl where Db: Postgres +{ + type Error = DbError; +} + +impl UserSessionExt for UserSessionExtImpl where Db: Postgres +{ + fn login(&self, + tag_or_email: TE, + password: P, + remember: bool, + location: L, + device: D, + ip: SocketAddr) + -> Result>> + where TE: AsRef, + P: AsRef, + L: AsRef, + D: AsRef + { + todo!() + } +}