feat: lots of cool stuff!
i forgot to commmit
This commit is contained in:
parent
50f416d402
commit
46eaa49586
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -40,6 +40,7 @@ dependencies = [
|
||||
"simple_logger",
|
||||
"toad",
|
||||
"toad-msg",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1140,6 +1141,12 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
@ -15,3 +15,4 @@ log = "0.4"
|
||||
postgres = {path = "./postgres/postgres"}
|
||||
rand = "0.8"
|
||||
naan = "0.1.32"
|
||||
uuid = "1.4"
|
||||
|
36
src/app.rs
36
src/app.rs
@ -1,37 +1,55 @@
|
||||
use crate::hashed_text::{HashedTextExt, HashedTextExtImpl};
|
||||
use crate::model::{GroupRepoImpl,
|
||||
HashedTextExt,
|
||||
HashedTextExtImpl,
|
||||
UserRepo,
|
||||
UserRepoImpl,
|
||||
UserSessionExt,
|
||||
UserSessionExtImpl};
|
||||
use crate::postgres::{Postgres, PostgresImpl};
|
||||
use crate::user::{UserRepo, UserRepoImpl};
|
||||
|
||||
pub trait App: Send + Sync + Sized {
|
||||
type Db: Postgres;
|
||||
type HashedTextExt: HashedTextExt;
|
||||
|
||||
type UserRepo: UserRepo;
|
||||
|
||||
type HashedTextExt: HashedTextExt;
|
||||
type UserSessionExt: UserSessionExt;
|
||||
|
||||
fn db(&self) -> &Self::Db;
|
||||
fn hashed_text(&self) -> &Self::HashedTextExt;
|
||||
fn user_session(&self) -> &Self::UserSessionExt;
|
||||
fn user(&self) -> &Self::UserRepo;
|
||||
}
|
||||
|
||||
pub struct AppConcrete {
|
||||
pub pg: &'static PostgresImpl<postgres::Client>,
|
||||
pub hashed_text_ext: HashedTextExtImpl<PostgresImpl<postgres::Client>>,
|
||||
pub user: UserRepoImpl<PostgresImpl<postgres::Client>>,
|
||||
pub hashed_text: &'static HashedTextExtImpl<PostgresImpl<postgres::Client>>,
|
||||
pub user_session: &'static UserSessionExtImpl<PostgresImpl<postgres::Client>,
|
||||
GroupRepoImpl<PostgresImpl<postgres::Client>>>,
|
||||
pub user: &'static UserRepoImpl<PostgresImpl<postgres::Client>>,
|
||||
}
|
||||
|
||||
impl App for AppConcrete {
|
||||
type Db = PostgresImpl<postgres::Client>;
|
||||
type HashedTextExt = HashedTextExtImpl<Self::Db>;
|
||||
|
||||
type UserRepo = UserRepoImpl<Self::Db>;
|
||||
|
||||
type HashedTextExt = HashedTextExtImpl<Self::Db>;
|
||||
type UserSessionExt = UserSessionExtImpl<Self::Db, GroupRepoImpl<Self::Db>>;
|
||||
|
||||
fn db(&self) -> &Self::Db {
|
||||
&self.pg
|
||||
self.pg
|
||||
}
|
||||
|
||||
fn hashed_text(&self) -> &Self::HashedTextExt {
|
||||
&self.hashed_text_ext
|
||||
self.hashed_text
|
||||
}
|
||||
|
||||
fn user_session(&self) -> &Self::UserSessionExt {
|
||||
self.user_session
|
||||
}
|
||||
|
||||
fn user(&self) -> &Self::UserRepo {
|
||||
&self.user
|
||||
self.user
|
||||
}
|
||||
}
|
||||
|
176
src/main.rs
176
src/main.rs
@ -1,32 +1,39 @@
|
||||
use std::ptr::hash;
|
||||
|
||||
use app::{App, AppConcrete};
|
||||
use hashed_text::{HashedTextExt, HashedTextExtImpl};
|
||||
use repo::Repo;
|
||||
use model::{HashedTextExt,
|
||||
HashedTextExtImpl,
|
||||
UserId,
|
||||
UserRepoImpl,
|
||||
UserSession,
|
||||
UserSessionExt,
|
||||
UserSessionExtImpl};
|
||||
use naan::prelude::*;
|
||||
use repo::{Page, ReadMany, ReadOne, Repo};
|
||||
use req_payload::ReqPayload;
|
||||
use toad::config::Config;
|
||||
use toad::net::Addrd;
|
||||
use toad::platform::Platform as _;
|
||||
use toad::req::Req;
|
||||
use toad::resp::Resp;
|
||||
use toad::resp::{code, Resp};
|
||||
use toad_msg::alloc::Message;
|
||||
use toad_msg::Type;
|
||||
use toad_msg::{Code, MessageBuilder, Type};
|
||||
|
||||
use crate::model::GroupRepoImpl;
|
||||
use crate::postgres::{Postgres, PostgresImpl};
|
||||
|
||||
mod app;
|
||||
mod env;
|
||||
mod group;
|
||||
mod hashed_text;
|
||||
mod perm;
|
||||
mod model;
|
||||
mod postgres;
|
||||
mod rep_payload;
|
||||
mod repo;
|
||||
mod req_payload;
|
||||
mod user;
|
||||
mod user_session;
|
||||
mod uuid;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! newtype {
|
||||
(
|
||||
$(#[derive($($der:ident),+)])?
|
||||
$(#[derive($($der:path),+)])?
|
||||
$(#[doc = $doc:literal])*
|
||||
pub struct $name:ident(String);
|
||||
) => {
|
||||
@ -40,9 +47,9 @@ macro_rules! newtype {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for $name {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
impl core::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +70,17 @@ macro_rules! newtype {
|
||||
s.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ::postgres::types::FromSql<'a> for $name {
|
||||
fn from_sql(ty: &::postgres::types::Type, raw: &'a [u8]) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
|
||||
let s = <&str as ::postgres::types::FromSql>::from_sql(ty, raw)?;
|
||||
Ok($name::from(s))
|
||||
}
|
||||
|
||||
fn accepts(ty: &::postgres::types::Type) -> bool {
|
||||
<&str as ::postgres::types::FromSql>::accepts(ty)
|
||||
}
|
||||
}
|
||||
};
|
||||
(pub struct $name:ident($inner:ty);) => {
|
||||
pub struct $name($inner);
|
||||
@ -82,7 +100,7 @@ macro_rules! newtype {
|
||||
}
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Email(String);
|
||||
);
|
||||
|
||||
@ -109,35 +127,66 @@ mod __toad_aliases {
|
||||
}
|
||||
|
||||
use __toad_aliases::*;
|
||||
use user::{UserId, UserRepoImpl};
|
||||
|
||||
fn handle_request<A>(app: &A, req: Addrd<Req<ToadT>>) -> Result<Addrd<Message>, String>
|
||||
fn handle_request<A>(app: &A, req: Addrd<Req<ToadT>>) -> Addrd<Message>
|
||||
where A: App
|
||||
{
|
||||
let path = req.data()
|
||||
.path()
|
||||
.map_err(|e| format!("{e:?}"))
|
||||
.unwrap_or(None)
|
||||
.unwrap_or("");
|
||||
let mut path_segments = path.split('/').peekable();
|
||||
let body = || {
|
||||
let mut msg =
|
||||
Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token);
|
||||
|
||||
if path_segments.peek() == Some(&"users") {
|
||||
let mut path_segments = path_segments.clone();
|
||||
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();
|
||||
Ok(Addrd(msg, req.addr()))
|
||||
} else {
|
||||
let msg =
|
||||
Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token)
|
||||
.build();
|
||||
Ok(Addrd(msg, req.addr()))
|
||||
}
|
||||
let path = req.data().path().unwrap_or(None).unwrap_or("");
|
||||
|
||||
let mut path_segments = path.split('/').peekable();
|
||||
|
||||
let actor = Ok(req.data().payload()).and_then(serde_json::from_slice::<Option<ReqPayload<()>>>)
|
||||
.map_err(|e| Box::new(e) as _)?
|
||||
.and_then(|r| r.session)
|
||||
.map(|s| app.user_session().touch(UserSession::from(s)))
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
.map_err(|e| Box::new(e) as _)?
|
||||
.unwrap_or_default();
|
||||
|
||||
if path_segments.peek() == Some(&"users") {
|
||||
let mut path_segments = path_segments.clone();
|
||||
|
||||
if req.data().msg().code == Code::GET {
|
||||
let id = path_segments.nth(2).map(UserId::from);
|
||||
match id {
|
||||
| Some(id) => {
|
||||
let user = app.user().get(&actor, &id).map_err(|e| Box::new(e) as _)?;
|
||||
msg = msg.payload(serde_json::to_vec(&user).map_err(|e| Box::new(e) as _)?);
|
||||
},
|
||||
| None => {
|
||||
let payload =
|
||||
Ok(req.data().payload()).and_then(serde_json::from_slice::<Option<ReqPayload<_>>>)
|
||||
.map_err(|e| Box::new(e) as _)?
|
||||
.ok_or_else(|| {
|
||||
String::from("\"limit\" and \"after\" must be provided")
|
||||
})
|
||||
.map_err(|e| Box::new(e) as _)?;
|
||||
|
||||
let users = app.user()
|
||||
.get_all(&actor, payload.t)
|
||||
.map_err(|e| Box::new(e) as _)?;
|
||||
|
||||
let bytes = serde_json::to_vec(&users).map_err(|e| Box::new(e) as _)?;
|
||||
msg = msg.payload(bytes);
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
msg = msg.code(toad::resp::code::NOT_FOUND);
|
||||
}
|
||||
|
||||
Ok(Addrd(msg.build(), req.addr()))
|
||||
};
|
||||
|
||||
body().map_err(|e: Box<dyn core::fmt::Debug>| log::error!("{e:?}"))
|
||||
.unwrap_or_else(|_| {
|
||||
Addrd(Message::builder(Type::Ack, code::INTERNAL_SERVER_ERROR).build(),
|
||||
req.addr())
|
||||
})
|
||||
}
|
||||
|
||||
fn server_worker<A>(app: &A, p: &'static Toad)
|
||||
@ -146,38 +195,57 @@ fn server_worker<A>(app: &A, p: &'static Toad)
|
||||
loop {
|
||||
match nb::block!(p.poll_req()) {
|
||||
| Err(e) => log::error!("{e:?}"),
|
||||
| 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:?}"))
|
||||
.ok();
|
||||
},
|
||||
| Ok(req) => {
|
||||
let rep = handle_request(app, req);
|
||||
nb::block!(p.send_msg(rep.clone().map(Into::into))).map_err(|e| log::error!("{e:?}"))
|
||||
.ok();
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn assert_static<'a, T>(t: &'a T) -> &'static T {
|
||||
// SAFETY
|
||||
// this will always be safe because the referenced data cannot outlive the main thread
|
||||
unsafe { core::mem::transmute::<&T, &'static T>(t) }
|
||||
}
|
||||
|
||||
simple_logger::init().unwrap();
|
||||
|
||||
let env = env::Env::try_read().unwrap();
|
||||
|
||||
let toad = Toad::try_new(env.api.addr, Config::default()).unwrap();
|
||||
let toad = &toad;
|
||||
let toad = assert_static(toad);
|
||||
|
||||
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();
|
||||
let pg = &pg;
|
||||
let pg = assert_static(pg);
|
||||
|
||||
// SAFETY
|
||||
// these are safe because the server worker cannot outlive the main thread
|
||||
let group_repo = GroupRepoImpl(pg);
|
||||
let group_repo = &group_repo;
|
||||
let group_repo = assert_static(group_repo);
|
||||
|
||||
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 hashed_text = HashedTextExtImpl(pg);
|
||||
let hashed_text = &hashed_text;
|
||||
let hashed_text = assert_static(hashed_text);
|
||||
|
||||
let user_session = UserSessionExtImpl(pg, group_repo);
|
||||
let user_session = &user_session;
|
||||
let user_session = assert_static(user_session);
|
||||
|
||||
let user = UserRepoImpl(pg);
|
||||
let user = &user;
|
||||
let user = assert_static(user);
|
||||
|
||||
let app = AppConcrete { pg,
|
||||
hashed_text_ext: HashedTextExtImpl(pg),
|
||||
user: UserRepoImpl(pg) };
|
||||
hashed_text,
|
||||
user_session,
|
||||
user };
|
||||
|
||||
server_worker(&app, toad_ref);
|
||||
server_worker(&app, toad);
|
||||
}
|
||||
|
6
src/model/community.rs
Normal file
6
src/model/community.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use crate::newtype;
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct CommunityId(String);
|
||||
);
|
@ -1,11 +1,10 @@
|
||||
use naan::prelude::*;
|
||||
use postgres::GenericClient;
|
||||
|
||||
use crate::model::{Actor, User, UserId};
|
||||
use crate::newtype;
|
||||
use crate::perm::Actor;
|
||||
use crate::postgres::{try_get, DbError, Postgres, UnmarshalRow};
|
||||
use crate::repo::Repo;
|
||||
use crate::user::{User, UserId};
|
||||
use crate::repo::{Del, Insert, Page, Patch, ReadMany, ReadOne, Repo};
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
@ -46,19 +45,40 @@ pub struct GroupInsert {
|
||||
members: Vec<UserId>,
|
||||
}
|
||||
|
||||
pub trait GroupRepo: Repo<T = Group, Patch = GroupPatch, Id = GroupId> {
|
||||
pub trait GroupRepo:
|
||||
Repo<T = Group, Id = GroupId>
|
||||
+ 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>;
|
||||
}
|
||||
|
||||
pub struct GroupRepoImpl<Db: 'static>(pub &'static Db);
|
||||
|
||||
impl<Db> GroupRepo for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
fn members(&self, id: &GroupId) -> Result<Vec<UserId>, Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn groups(&self, id: &UserId) -> Result<Vec<GroupId>, Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> Repo for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
type T = Group;
|
||||
type Patch = GroupPatch;
|
||||
type Insert = GroupInsert;
|
||||
type Error = 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";
|
||||
|
||||
@ -70,8 +90,11 @@ impl<Db> Repo for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_all(&self, actor: &Actor) -> Result<Vec<Group>, Self::Error> {
|
||||
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
|
||||
@ -79,14 +102,31 @@ impl<Db> Repo for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
.and_then(|vec| vec.iter().map(Group::unmarshal).collect())
|
||||
}
|
||||
|
||||
fn page_valid(&self, page: &Page<Self::Id>) -> bool {
|
||||
page.limit < 100
|
||||
}
|
||||
}
|
||||
|
||||
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!()
|
||||
}
|
||||
}
|
||||
|
||||
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!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> Del for GroupRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
fn del(&self, actor: &Actor, id: &GroupId) -> Result<bool, Self::Error> {
|
||||
todo!()
|
||||
}
|
14
src/model/mod.rs
Normal file
14
src/model/mod.rs
Normal file
@ -0,0 +1,14 @@
|
||||
pub mod community;
|
||||
pub mod group;
|
||||
pub mod hashed_text;
|
||||
pub mod perm;
|
||||
pub mod thread;
|
||||
pub mod user;
|
||||
pub mod user_session;
|
||||
pub use community::*;
|
||||
pub use group::*;
|
||||
pub use hashed_text::*;
|
||||
pub use perm::*;
|
||||
pub use thread::*;
|
||||
pub use user::*;
|
||||
pub use user_session::*;
|
330
src/model/perm.rs
Normal file
330
src/model/perm.rs
Normal file
@ -0,0 +1,330 @@
|
||||
use std::convert::Infallible;
|
||||
use std::str::FromStr;
|
||||
|
||||
use naan::prelude::*;
|
||||
use postgres::types::{FromSql, Type};
|
||||
|
||||
use super::community::CommunityId;
|
||||
use super::group::{Group, GroupId};
|
||||
use super::thread::ThreadId;
|
||||
use super::user::UserId;
|
||||
use crate::postgres::{try_get, UnmarshalRow};
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Actor {
|
||||
pub uid: Option<UserId>,
|
||||
pub groups: Vec<GroupId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub enum Dir<T, Id> {
|
||||
Dir,
|
||||
Within(Id, T),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub enum ThreadsPath {
|
||||
Deleted,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ThreadsPath {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
| ThreadsPath::Deleted => write!(f, "deleted"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub enum UsersPath {
|
||||
Tag,
|
||||
Email,
|
||||
Deleted,
|
||||
Password,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for UsersPath {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
| UsersPath::Tag => write!(f, "tag"),
|
||||
| UsersPath::Email => write!(f, "email"),
|
||||
| UsersPath::Deleted => write!(f, "deleted"),
|
||||
| UsersPath::Password => write!(f, "password"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub enum CommunitiesPath {
|
||||
Threads(Dir<ThreadsPath, ThreadId>),
|
||||
Tag,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CommunitiesPath {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
| CommunitiesPath::Threads(Dir::Dir) => write!(f, "threads/"),
|
||||
| CommunitiesPath::Threads(Dir::Within(id, p)) => write!(f, "threads/{id}/{p}"),
|
||||
| CommunitiesPath::Tag => write!(f, "tag"),
|
||||
| CommunitiesPath::Deleted => write!(f, "deleted"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub enum GroupsPath {
|
||||
Members,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for GroupsPath {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
| GroupsPath::Members => write!(f, "members"),
|
||||
| GroupsPath::Deleted => write!(f, "deleted"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub enum Path {
|
||||
Community(Dir<CommunitiesPath, CommunityId>),
|
||||
User(Dir<UsersPath, UserId>),
|
||||
Group(Dir<GroupsPath, GroupId>),
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Path {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
| Path::Community(Dir::Dir) => write!(f, "/communities/"),
|
||||
| Path::User(Dir::Dir) => write!(f, "/users/"),
|
||||
| Path::Group(Dir::Dir) => write!(f, "/groups/"),
|
||||
| Path::Community(Dir::Within(id, p)) => write!(f, "/communities/{id}/{p}"),
|
||||
| Path::User(Dir::Within(id, p)) => write!(f, "/users/{id}/{p}"),
|
||||
| Path::Group(Dir::Within(id, p)) => write!(f, "/groups/{id}/{p}"),
|
||||
| Path::Unknown(s) => write!(f, "{s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn parse<S>(s: S) -> Path
|
||||
where S: AsRef<str>
|
||||
{
|
||||
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(cid) => {
|
||||
let comm = |p| Path::Community(Dir::Within(CommunityId::from(cid), p));
|
||||
|
||||
match next!() {
|
||||
| Some("threads") => match next!() {
|
||||
| Some(tid) => {
|
||||
let thread =
|
||||
|p| comm(CommunitiesPath::Threads(Dir::Within(ThreadId::from(tid), p)));
|
||||
|
||||
match next!() {
|
||||
| Some("deleted") => Some(thread(ThreadsPath::Deleted)),
|
||||
| _ => None,
|
||||
}
|
||||
},
|
||||
| None => Some(comm(CommunitiesPath::Threads(Dir::Dir))),
|
||||
},
|
||||
| Some("tag") => Some(comm(CommunitiesPath::Tag)),
|
||||
| Some("deleted") => Some(comm(CommunitiesPath::Deleted)),
|
||||
| _ => None,
|
||||
}
|
||||
},
|
||||
| None => Some(Path::Community(Dir::Dir)),
|
||||
},
|
||||
| Some("users") => match next!() {
|
||||
| Some(id) => {
|
||||
let usr = |p| Path::User(Dir::Within(UserId::from(id), p));
|
||||
match next!() {
|
||||
| Some("deleted") => Some(usr(UsersPath::Deleted)),
|
||||
| Some("tag") => Some(usr(UsersPath::Tag)),
|
||||
| Some("email") => Some(usr(UsersPath::Email)),
|
||||
| Some("password") => Some(usr(UsersPath::Password)),
|
||||
| _ => None,
|
||||
}
|
||||
},
|
||||
| None => Some(Path::User(Dir::Dir)),
|
||||
},
|
||||
| Some("groups") => match next!() {
|
||||
| Some(id) => {
|
||||
let grp = |p| Path::Group(Dir::Within(GroupId::from(id), p));
|
||||
match next!() {
|
||||
| Some("members") => Some(grp(GroupsPath::Members)),
|
||||
| Some("deleted") => Some(grp(GroupsPath::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 FromStr for Mode {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
| "r" => Mode::Read,
|
||||
| "w" => Mode::Write,
|
||||
| _ => Mode::None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromSql<'a> for Mode {
|
||||
fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
|
||||
let s = <&str as FromSql>::from_sql(ty, raw)?;
|
||||
Mode::from_str(s).map_err(|_| unreachable!())
|
||||
}
|
||||
|
||||
fn accepts(ty: &Type) -> bool {
|
||||
<&str as FromSql>::accepts(ty)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
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<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
|
||||
where R: postgres::GenericRow,
|
||||
S: AsRef<str>
|
||||
{
|
||||
let mode_owner_usr = try_get!(p, "owner_user_mode", row)?;
|
||||
let mode_owner_grp = try_get!(p, "owner_group_mode", row)?;
|
||||
let mode_everyone = try_get!(p, "everyone_mode", row)?;
|
||||
let owner_usr = try_get!(p, "owner_user", row)?;
|
||||
let owner_grp = try_get!(p, "owner_group", row)?;
|
||||
let path = try_get!(<String> p, "path", row)?;
|
||||
|
||||
Ok(Perm { path: Path::parse(path),
|
||||
owner: (owner_usr, mode_owner_usr),
|
||||
group: (owner_grp, mode_owner_grp),
|
||||
everyone: mode_everyone })
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use postgres::types::Type;
|
||||
|
||||
use super::*;
|
||||
use crate::model::GroupName;
|
||||
use crate::postgres::test::Row;
|
||||
|
||||
#[test]
|
||||
fn path_parse() {
|
||||
assert_eq!(Path::parse("/communities"), Path::Community(Dir::Dir));
|
||||
assert_eq!(Path::parse("/communities/c1/threads/t1/deleted"),
|
||||
Path::Community(Dir::Within(CommunityId::from("c1"),
|
||||
CommunitiesPath::Threads(Dir::Within(ThreadId::from("t1"),
|
||||
ThreadsPath::Deleted)))));
|
||||
assert_eq!(Path::parse("/users/1/tag"),
|
||||
Path::User(Dir::Within(UserId::from("1"), UsersPath::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"), GroupsPath::Members)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perm_unmarshal() {
|
||||
let row = Row::<()>::new(vec![("owner_user_mode", Type::TEXT, Box::new("w")),
|
||||
("owner_group_mode", Type::TEXT, Box::new("w")),
|
||||
("everyone_mode", Type::TEXT, Box::new("r")),
|
||||
("owner_user", Type::TEXT, Box::new("u1")),
|
||||
("owner_group", Type::TEXT, Box::new("g1")),
|
||||
("path", Type::TEXT, Box::new("/groups/")),]);
|
||||
|
||||
let perm = Perm::unmarshal(&row).unwrap();
|
||||
assert_eq!(perm,
|
||||
Perm { everyone: Mode::Read,
|
||||
group: (GroupId::from("g1"), Mode::Write),
|
||||
owner: (UserId::from("u1"), Mode::Write),
|
||||
path: Path::Group(Dir::Dir) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actor_can() {
|
||||
let all_groups = Perm { everyone: Mode::Write,
|
||||
group: (GroupId::from("g1"), Mode::Write),
|
||||
owner: (UserId::from("u1"), Mode::Write),
|
||||
path: Path::Group(Dir::Dir) };
|
||||
|
||||
let group_admin_members = Perm { everyone: Mode::Read,
|
||||
group: (GroupId::from("g1"), Mode::Write),
|
||||
owner: (UserId::from("u1"), Mode::Write),
|
||||
path: Path::Group(Dir::Within(GroupId::from("g1"),
|
||||
GroupsPath::Members)) };
|
||||
|
||||
let admin = Actor { uid: Some(UserId::from("u1")),
|
||||
groups: vec![GroupId::from("g1")] };
|
||||
|
||||
let nonadmin = Actor { uid: Some(UserId::from("u2")),
|
||||
groups: vec![GroupId::from("g2")] };
|
||||
|
||||
assert!(all_groups.actor_can(&admin, Mode::Write));
|
||||
assert!(all_groups.actor_can(&nonadmin, Mode::Write));
|
||||
assert!(group_admin_members.actor_can(&admin, Mode::Write));
|
||||
assert!(!group_admin_members.actor_can(&nonadmin, Mode::Write));
|
||||
}
|
||||
}
|
6
src/model/thread.rs
Normal file
6
src/model/thread.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use crate::newtype;
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct ThreadId(String);
|
||||
);
|
458
src/model/user.rs
Normal file
458
src/model/user.rs
Normal file
@ -0,0 +1,458 @@
|
||||
use naan::prelude::*;
|
||||
use postgres::{GenericClient, GenericRow};
|
||||
|
||||
use super::{Actor, HashedText, Mode, Path};
|
||||
use crate::postgres::{try_get, DbError, Postgres, UnmarshalRow};
|
||||
use crate::repo::{Del, Insert, Page, Patch, ReadMany, ReadOne, Repo};
|
||||
use crate::{newtype, Email};
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UserId(String);
|
||||
);
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UserTag {
|
||||
pub tag: String,
|
||||
pub discrim: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct User {
|
||||
pub uid: UserId,
|
||||
pub tag: UserTag,
|
||||
pub email: Email,
|
||||
}
|
||||
|
||||
impl UnmarshalRow for User {
|
||||
fn unmarshal_maybe_prefixed<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
|
||||
where R: GenericRow,
|
||||
S: AsRef<str>
|
||||
{
|
||||
let p = p.as_ref().map(|p| p.as_ref());
|
||||
try_get!(<UserId> p, "uid", row).zip(|_: &_| try_get!(<String> p, "tag", row))
|
||||
.zip(|_: &_| try_get!(<i32> p, "discrim", row))
|
||||
.zip(|_: &_| try_get!(<String> p, "email", row))
|
||||
.map(|(((uid, tag), discrim), email)| {
|
||||
User { uid,
|
||||
tag: UserTag { tag,
|
||||
discrim: if discrim < 0 {
|
||||
0
|
||||
} else {
|
||||
discrim as u32
|
||||
} },
|
||||
email: Email(email) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct UserPatch {
|
||||
pub tag: Option<UserTag>,
|
||||
pub password: Option<HashedText>,
|
||||
pub email: Option<Email>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct UserInsert {
|
||||
pub tag: UserTag,
|
||||
pub password: HashedText,
|
||||
pub email: Email,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum UserRepoError<E> {
|
||||
Unauthorized,
|
||||
PageInvalid,
|
||||
Other(E),
|
||||
}
|
||||
|
||||
pub trait UserRepo:
|
||||
Repo<T = User, Id = UserId>
|
||||
+ Patch<Patch = UserPatch>
|
||||
+ Insert<Insert = UserInsert>
|
||||
+ ReadOne
|
||||
+ ReadMany
|
||||
+ Del
|
||||
{
|
||||
}
|
||||
|
||||
pub struct UserRepoImpl<Db: 'static>(pub &'static Db);
|
||||
|
||||
impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
type T = User;
|
||||
type Error = UserRepoError<DbError<Db>>;
|
||||
type Id = UserId;
|
||||
}
|
||||
|
||||
impl<Db> ReadOne for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
fn get(&self, actor: &Actor, id: &Self::Id) -> Result<Option<Self::T>, Self::Error> {
|
||||
use UserRepoError::*;
|
||||
|
||||
static QUERY_LINES: [&'static str; 3] =
|
||||
["select uid, tag, email",
|
||||
"from public.usr",
|
||||
"where uid = human_uuid.huid_of_string($1) and deleted = false"];
|
||||
|
||||
let paths = vec![format!("/users/{id}/tag"), format!("/users/{id}/email")].into_iter()
|
||||
.map(Path::parse)
|
||||
.collect::<Vec<_>>();
|
||||
if !self.0
|
||||
.authorized_all(&paths, Mode::Read, actor)
|
||||
.map_err(Other)?
|
||||
{
|
||||
return Err(Unauthorized);
|
||||
}
|
||||
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
c.query_opt(&QUERY_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&id.as_ref()])
|
||||
})
|
||||
.and_then(|opt| {
|
||||
opt.as_ref()
|
||||
.map(User::unmarshal)
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
})
|
||||
.map_err(Other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> ReadMany for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
fn page_valid(&self, page: &Page<Self::Id>) -> bool {
|
||||
page.limit <= 100
|
||||
}
|
||||
|
||||
fn get_all(&self, actor: &Actor, page: Page<UserId>) -> Result<Vec<Self::T>, Self::Error> {
|
||||
use UserRepoError::*;
|
||||
|
||||
if !self.page_valid(&page) {
|
||||
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",
|
||||
"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"];
|
||||
|
||||
let usrs: Vec<User> =
|
||||
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(User::unmarshal).collect())
|
||||
.map_err(Other)?;
|
||||
|
||||
let paths = usrs.iter()
|
||||
.map(|User { uid, .. }| {
|
||||
vec![format!("/users/{uid}/tag"), format!("/users/{uid}/email")].into_iter()
|
||||
.map(Path::parse)
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !self.0
|
||||
.authorized_all(&paths, Mode::Read, actor)
|
||||
.map_err(Other)?
|
||||
{
|
||||
Err(Unauthorized)
|
||||
} else {
|
||||
Ok(usrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> Patch for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
type Patch = UserPatch;
|
||||
|
||||
fn patch(&self, actor: &Actor, id: &UserId, patch: UserPatch) -> Result<bool, Self::Error> {
|
||||
use UserRepoError::*;
|
||||
|
||||
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 = human_uuid.huid_of_string($1) and deleted = false"];
|
||||
|
||||
let paths = vec!["/users/{id}/tag",
|
||||
"/users/{id}/email",
|
||||
"/users/{id}/password"].into_iter()
|
||||
.map(Path::parse)
|
||||
.collect::<Vec<_>>();
|
||||
if !self.0
|
||||
.authorized_all(&paths, Mode::Write, actor)
|
||||
.map_err(Other)?
|
||||
{
|
||||
return Err(Unauthorized);
|
||||
}
|
||||
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
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.password.as_ref().map(|h| h.as_ref()),
|
||||
&patch.email.as_ref().map(|e| e.as_ref())])
|
||||
})
|
||||
.map(|n| n == 1)
|
||||
.map_err(Other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> Insert for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
type Insert = UserInsert;
|
||||
|
||||
fn insert(&self, actor: &Actor, insert: UserInsert) -> Result<UserId, Self::Error> {
|
||||
use UserRepoError::*;
|
||||
|
||||
static QUERY_LINES: [&'static str; 5] = ["insert into public.usr",
|
||||
" (tag, password, email)",
|
||||
"values",
|
||||
" ($2, $3, $4)",
|
||||
"returning human_uuid.huid_to_string(uid);"];
|
||||
|
||||
if !self.0
|
||||
.authorized(&Path::parse("/users/"), Mode::Write, actor)
|
||||
.map_err(Other)?
|
||||
{
|
||||
return Err(Unauthorized);
|
||||
}
|
||||
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
c.query_one(&QUERY_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&insert.tag.tag,
|
||||
&insert.password.as_ref(),
|
||||
&insert.email.as_ref()])
|
||||
})
|
||||
.and_then(|r| r.try_get::<_, String>(0))
|
||||
.map(UserId::from)
|
||||
.map_err(Other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> Del for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
fn del(&self, actor: &Actor, id: &UserId) -> Result<bool, Self::Error> {
|
||||
use UserRepoError::*;
|
||||
|
||||
static QUERY: &'static str = "delete from public.usr where uid = $1 :: uuid;";
|
||||
|
||||
if !self.0
|
||||
.authorized(&Path::parse(format!("/users/{id}/deleted")),
|
||||
Mode::Write,
|
||||
actor)
|
||||
.map_err(Other)?
|
||||
{
|
||||
return Err(Unauthorized);
|
||||
}
|
||||
|
||||
self.0
|
||||
.with_client(|c| c.execute(QUERY, &[&id.as_ref()]))
|
||||
.map(|n| n == 1)
|
||||
.map_err(Other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> UserRepo for UserRepoImpl<Db> where Db: Postgres + 'static {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use postgres::types::Type;
|
||||
|
||||
use super::{User, UserRepoImpl};
|
||||
use crate::model::{Actor,
|
||||
Dir,
|
||||
GroupId,
|
||||
HashedText,
|
||||
Mode,
|
||||
Path,
|
||||
Perm,
|
||||
UserId,
|
||||
UserInsert,
|
||||
UserTag};
|
||||
use crate::postgres::test::{from_sql_owned, Client, Postgres, Row};
|
||||
use crate::postgres::Postgres as _;
|
||||
use crate::repo::{Del, Insert, Page, ReadMany, ReadOne, Repo};
|
||||
use crate::Email;
|
||||
|
||||
fn usr_row(usr: User) -> Row<()> {
|
||||
Row::new(vec![("uid", Type::TEXT, Box::new(usr.uid.to_string())),
|
||||
("email", Type::TEXT, Box::new(usr.email.to_string())),
|
||||
("discrim", Type::INT4, Box::new(0)),
|
||||
("tag", Type::TEXT, Box::new(usr.tag.tag))])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_repo_get_one() {
|
||||
let client = || Client::<()> { query_opt: Box::new(|_, q, ps| {
|
||||
Ok(Some(usr_row(User {uid: UserId::from("1"), tag: UserTag {tag: "foo".into(), discrim: 0}, email: Email::from("foo@bar.baz")})).filter(|_| from_sql_owned::<String>(ps[0]) == String::from("1")))
|
||||
}),
|
||||
..Client::default() };
|
||||
|
||||
let mut db = Postgres::try_new(|| Ok(client()), 1).unwrap();
|
||||
db.perms.push(Perm { path: Path::parse("/users/1/tag"),
|
||||
everyone: Mode::Write,
|
||||
owner: (UserId::from("root"), Mode::Write),
|
||||
group: (GroupId::from("admins"), Mode::Write) });
|
||||
db.perms.push(Perm { path: Path::parse("/users/1/email"),
|
||||
everyone: Mode::Write,
|
||||
owner: (UserId::from("root"), Mode::Write),
|
||||
group: (GroupId::from("admins"), Mode::Write) });
|
||||
db.perms.push(Perm { path: Path::parse("/users/0/tag"),
|
||||
everyone: Mode::Write,
|
||||
owner: (UserId::from("root"), Mode::Write),
|
||||
group: (GroupId::from("admins"), Mode::Write) });
|
||||
db.perms.push(Perm { path: Path::parse("/users/0/email"),
|
||||
everyone: Mode::Write,
|
||||
owner: (UserId::from("root"), Mode::Write),
|
||||
group: (GroupId::from("admins"), Mode::Write) });
|
||||
|
||||
let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static Postgres<()>>(&db) });
|
||||
|
||||
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, _| {
|
||||
Ok(vec![usr_row(User { uid: UserId::from("1"),
|
||||
tag: UserTag { tag:
|
||||
"foo".into(),
|
||||
discrim: 0 },
|
||||
email: Email::from("foo@bar.baz") })])
|
||||
}),
|
||||
..Client::default() };
|
||||
|
||||
let mut db = Postgres::try_new(|| Ok(client()), 1).unwrap();
|
||||
db.perms.push(Perm { path: Path::parse("/users/"),
|
||||
everyone: Mode::Write,
|
||||
owner: (UserId::from("root"), Mode::Write),
|
||||
group: (GroupId::from("admins"), Mode::Write) });
|
||||
db.perms.push(Perm { path: Path::parse("/users/1/tag"),
|
||||
everyone: Mode::Write,
|
||||
owner: (UserId::from("root"), Mode::Write),
|
||||
group: (GroupId::from("admins"), Mode::Write) });
|
||||
db.perms.push(Perm { path: Path::parse("/users/1/email"),
|
||||
everyone: Mode::Write,
|
||||
owner: (UserId::from("root"), Mode::Write),
|
||||
group: (GroupId::from("admins"), Mode::Write) });
|
||||
|
||||
let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static Postgres<()>>(&db) });
|
||||
|
||||
assert_eq!(repo.get_all(&Actor::default(),
|
||||
Page { limit: 100,
|
||||
after: None })
|
||||
.unwrap()
|
||||
.len(),
|
||||
1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_repo_del() {
|
||||
let client =
|
||||
|| Client::<()> { state: Box::new(false), // already deleted?
|
||||
execute: Box::new(|c, q, ps| {
|
||||
if from_sql_owned::<String>(ps[0]) == "1" && !*c.state_mut::<bool>() {
|
||||
*c.state_mut::<bool>() = true;
|
||||
Ok(1)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}),
|
||||
..Client::default() };
|
||||
|
||||
let mut db = Postgres::try_new(|| Ok(client()), 1).unwrap();
|
||||
db.perms.push(Perm { path: Path::parse("/users/1/deleted"),
|
||||
everyone: Mode::Write,
|
||||
owner: (UserId::from("root"), Mode::Write),
|
||||
group: (GroupId::from("admins"), Mode::Write) });
|
||||
|
||||
let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static Postgres<()>>(&db) });
|
||||
|
||||
assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(),
|
||||
true);
|
||||
assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(),
|
||||
false);
|
||||
assert!(repo.del(&Actor::default(), &UserId::from("2")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_repo_insert() {
|
||||
let client =
|
||||
|| Client::<()> { state: Box::new(Vec::<UserTag>::new()),
|
||||
query_one: Box::new(|c, q, ps| {
|
||||
let tags = c.state_mut::<Vec<UserTag>>();
|
||||
|
||||
let tag = UserTag { tag: from_sql_owned::<String>(ps[0]),
|
||||
discrim: 0 };
|
||||
|
||||
if tags.contains(&tag) {
|
||||
Err(())
|
||||
} else {
|
||||
tags.push(tag);
|
||||
Ok(Row::new(vec![("", Type::TEXT, Box::new(tags.len().to_string()))]))
|
||||
}
|
||||
}),
|
||||
..Client::default() };
|
||||
|
||||
let mut db = Postgres::try_new(|| Ok(client()), 1).unwrap();
|
||||
db.perms.push(Perm { path: Path::User(Dir::Dir),
|
||||
everyone: Mode::Write,
|
||||
owner: (UserId::from("root"), Mode::Write),
|
||||
group: (GroupId::from("admins"), Mode::Write) });
|
||||
|
||||
let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static Postgres<()>>(&db) });
|
||||
|
||||
assert!(repo.insert(&Actor::default(),
|
||||
UserInsert { tag: UserTag { tag: "foo".into(),
|
||||
discrim: 0 },
|
||||
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 },
|
||||
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 },
|
||||
password: HashedText::from("poop"),
|
||||
email: Email::from("bar@bar.baz") })
|
||||
.is_ok());
|
||||
}
|
||||
}
|
122
src/model/user_session.rs
Normal file
122
src/model/user_session.rs
Normal file
@ -0,0 +1,122 @@
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use naan::prelude::*;
|
||||
use postgres::{GenericClient, GenericRow};
|
||||
|
||||
use super::{Actor, GroupRepo, User};
|
||||
use crate::newtype;
|
||||
use crate::postgres::{DbError, Error, Postgres, UnmarshalRow};
|
||||
use crate::repo::Ext;
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct UserSession(String);
|
||||
);
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LoginOpts {
|
||||
pub remember: bool,
|
||||
pub location: Option<String>,
|
||||
pub device: Option<String>,
|
||||
pub ip: Option<IpAddr>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LoginError<E> {
|
||||
NoSuchUser,
|
||||
Other(E),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ValidateError<E> {
|
||||
Expired,
|
||||
Invalid,
|
||||
Other(E),
|
||||
}
|
||||
|
||||
pub trait UserSessionExt: Ext {
|
||||
fn touch(&self, session: UserSession) -> Result<Actor, ValidateError<Self::Error>>;
|
||||
|
||||
fn login<TE, P>(&self,
|
||||
tag_or_email: TE,
|
||||
password: P,
|
||||
opts: LoginOpts)
|
||||
-> Result<UserSession, LoginError<Self::Error>>
|
||||
where TE: AsRef<str>,
|
||||
P: AsRef<str>;
|
||||
}
|
||||
|
||||
pub struct UserSessionExtImpl<Db: 'static, Group: 'static>(pub &'static Db, pub &'static Group);
|
||||
|
||||
impl<Db, G> Ext for UserSessionExtImpl<Db, G>
|
||||
where Db: Postgres,
|
||||
G: GroupRepo
|
||||
{
|
||||
type Error = DbError<Db>;
|
||||
}
|
||||
|
||||
impl<Db, G> UserSessionExt for UserSessionExtImpl<Db, G>
|
||||
where Db: Postgres,
|
||||
G: GroupRepo<Error = Self::Error>
|
||||
{
|
||||
fn touch(&self, session: UserSession) -> Result<Actor, ValidateError<Self::Error>> {
|
||||
let query = "select public.usr_session_touch(public.usr_session_key_of_string($1))";
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
c.query_one(query, &[&session.as_ref()])
|
||||
.and_then(|r| User::unmarshal(&r))
|
||||
})
|
||||
.map_err(|e| {
|
||||
if e.message() == Some("usr_session_expired".to_string()) {
|
||||
ValidateError::Expired
|
||||
} else if e.message() == Some("usr_session_invalid".to_string()) {
|
||||
ValidateError::Invalid
|
||||
} else {
|
||||
ValidateError::Other(e)
|
||||
}
|
||||
})
|
||||
.zip(|u: &User| self.1.groups(&u.uid).map_err(ValidateError::Other))
|
||||
.map(|(u, gs)| Actor { uid: Some(u.uid),
|
||||
groups: gs })
|
||||
}
|
||||
|
||||
fn login<TE, P>(&self,
|
||||
tag_or_email: TE,
|
||||
password: P,
|
||||
opts: LoginOpts)
|
||||
-> Result<UserSession, LoginError<DbError<Db>>>
|
||||
where TE: AsRef<str>,
|
||||
P: AsRef<str>
|
||||
{
|
||||
const QUERY_LINES: [&'static str; 9] =
|
||||
["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",
|
||||
" ) s"];
|
||||
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
c.query_one(&QUERY_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&tag_or_email.as_ref(),
|
||||
&password.as_ref(),
|
||||
&opts.remember,
|
||||
&opts.location,
|
||||
&opts.device,
|
||||
&opts.ip])
|
||||
.and_then(|r| r.try_get::<_, UserSession>(0))
|
||||
})
|
||||
.map_err(|e: Db::Error| {
|
||||
if e.message() == Some("incorrect_password".to_string()) {
|
||||
LoginError::NoSuchUser
|
||||
} else {
|
||||
LoginError::Other(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
263
src/perm.rs
263
src/perm.rs
@ -1,263 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use naan::prelude::*;
|
||||
|
||||
use crate::group::{Group, GroupId};
|
||||
use crate::postgres::{try_get, UnmarshalRow};
|
||||
use crate::user::UserId;
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Actor {
|
||||
pub uid: Option<UserId>,
|
||||
pub groups: Vec<Group>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub enum Dir<T, Id> {
|
||||
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<PostPath, ()>),
|
||||
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<CommunityPath, ()>),
|
||||
User(Dir<UserPath, UserId>),
|
||||
Group(Dir<GroupPath, GroupId>),
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl Path {
|
||||
fn parse<S>(s: S) -> Path
|
||||
where S: AsRef<str>
|
||||
{
|
||||
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<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
|
||||
where R: postgres::GenericRow,
|
||||
S: AsRef<str>
|
||||
{
|
||||
let p = p.as_ref().map(|p| p.as_ref());
|
||||
try_get!(p, "read", row).zip(|_: &_| try_get!(p, "write", row))
|
||||
.map(|(r, w)| {
|
||||
if w {
|
||||
Mode::Write
|
||||
} else if r {
|
||||
Mode::Read
|
||||
} else {
|
||||
Mode::None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
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<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
|
||||
where R: postgres::GenericRow,
|
||||
S: AsRef<str>
|
||||
{
|
||||
panic!("Perm cannot be prefixed")
|
||||
}
|
||||
|
||||
fn unmarshal<R>(row: &R) -> Result<Self, R::Error>
|
||||
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(|_: &_| try_get!(<String> Some("perm"), "owner", row))
|
||||
.zip(|_: &_| try_get!(<String> Some("perm"), "group", row))
|
||||
.zip(|_: &_| try_get!(<String> 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 {
|
||||
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.uid == self.group.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use postgres::types::Type;
|
||||
|
||||
use super::*;
|
||||
use crate::group::GroupName;
|
||||
use crate::postgres::test::Row;
|
||||
|
||||
#[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)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perm_unmarshal() {
|
||||
let row = Row::<()>::new(vec![("owner_mode.read", Type::BOOL, Box::new(true)),
|
||||
("owner_mode.write", Type::BOOL, Box::new(true)),
|
||||
("group_mode.read", Type::BOOL, Box::new(true)),
|
||||
("group_mode.write", Type::BOOL, Box::new(true)),
|
||||
("everyone_mode.read", Type::BOOL, Box::new(true)),
|
||||
("everyone_mode.write", Type::BOOL, Box::new(false)),
|
||||
("perm.owner", Type::TEXT, Box::new("u1")),
|
||||
("perm.group", Type::TEXT, Box::new("g1")),
|
||||
("perm.path", Type::TEXT, Box::new("/groups/")),]);
|
||||
|
||||
let perm = Perm::unmarshal(&row).unwrap();
|
||||
assert_eq!(perm,
|
||||
Perm { everyone: Mode::Read,
|
||||
group: (GroupId::from("g1"), Mode::Write),
|
||||
owner: (UserId::from("u1"), Mode::Write),
|
||||
path: Path::Group(Dir::Dir) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actor_can() {
|
||||
let all_groups = Perm { everyone: Mode::Write,
|
||||
group: (GroupId::from("g1"), Mode::Write),
|
||||
owner: (UserId::from("u1"), Mode::Write),
|
||||
path: Path::Group(Dir::Dir) };
|
||||
|
||||
let group_admin_members = Perm { everyone: Mode::Read,
|
||||
group: (GroupId::from("g1"), Mode::Write),
|
||||
owner: (UserId::from("u1"), Mode::Write),
|
||||
path: Path::Group(Dir::Within(GroupId::from("g1"),
|
||||
GroupPath::Members)) };
|
||||
|
||||
let admin = Actor { uid: Some(UserId::from("u1")),
|
||||
groups: vec![Group { uid: GroupId::from("g1"),
|
||||
name: GroupName::from("admins") }] };
|
||||
|
||||
let nonadmin = Actor { uid: Some(UserId::from("u2")),
|
||||
groups: vec![Group { uid: GroupId::from("g2"),
|
||||
name: GroupName::from("foo") }] };
|
||||
|
||||
assert!(all_groups.actor_can(&admin, Mode::Write));
|
||||
assert!(all_groups.actor_can(&nonadmin, Mode::Write));
|
||||
assert!(group_admin_members.actor_can(&admin, Mode::Write));
|
||||
assert!(!group_admin_members.actor_can(&nonadmin, Mode::Write));
|
||||
}
|
||||
}
|
138
src/postgres.rs
138
src/postgres.rs
@ -3,18 +3,35 @@ use std::pin::Pin;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use postgres::types::FromSql;
|
||||
use postgres::GenericRow;
|
||||
use postgres::{GenericClient, GenericRow};
|
||||
use rand::Rng;
|
||||
|
||||
use crate::perm::Actor;
|
||||
use crate::model::{Actor, Mode, Path, Perm};
|
||||
|
||||
pub type DbError<Pg> = <<Pg as Postgres>::Client as postgres::GenericClient>::Error;
|
||||
|
||||
pub trait Error {
|
||||
fn message(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl Error for postgres::Error {
|
||||
fn message(&self) -> Option<String> {
|
||||
self.as_db_error().map(|db| db.message().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for () {
|
||||
fn message(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! try_get {
|
||||
(<$t:ty> $p:expr, $col:expr, $row:expr) => {{
|
||||
let col = $p.filter(|s| !str::is_empty(s.as_ref()))
|
||||
.map(|s| format!("{}.{}", s, $col))
|
||||
.unwrap_or($col.to_string());
|
||||
let col = $p.as_ref()
|
||||
.filter(|s| !str::is_empty(s.as_ref()))
|
||||
.map(|s| format!("{}.{}", AsRef::<str>::as_ref(s), $col))
|
||||
.unwrap_or($col.to_string());
|
||||
$row.try_get::<_, $t>(col.as_str())
|
||||
}};
|
||||
($p:expr, $col:expr, $row:expr) => {{
|
||||
@ -53,7 +70,8 @@ pub struct ConnectionParams {
|
||||
pub trait Postgres
|
||||
where Self: Send + Sync + Sized + 'static
|
||||
{
|
||||
type Client: postgres::GenericClient;
|
||||
type Client: postgres::GenericClient<Error = Self::Error>;
|
||||
type Error: Error + core::fmt::Debug;
|
||||
|
||||
fn try_new<F>(connect: F, pool_size: usize) -> Result<Self, DbError<Self>>
|
||||
where F: Fn() -> Result<Self::Client, DbError<Self>>;
|
||||
@ -61,7 +79,43 @@ pub trait Postgres
|
||||
fn with_client<F, R>(&self, f: F) -> Result<R, DbError<Self>>
|
||||
where F: FnOnce(&mut Self::Client) -> Result<R, DbError<Self>>;
|
||||
|
||||
fn authorized(action: String, actor: &Actor) -> Result<(), 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()]))?;
|
||||
Ok(match got_perm {
|
||||
| None => false,
|
||||
| Some(perm) => Perm::unmarshal(&perm)?.actor_can(actor, wants_to),
|
||||
})
|
||||
}
|
||||
|
||||
fn authorized_all(&self,
|
||||
paths: &[Path],
|
||||
wants_to: Mode,
|
||||
actor: &Actor)
|
||||
-> Result<bool, DbError<Self>> {
|
||||
if paths.iter().any(|p| matches!(p, Path::Unknown(_))) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let set = paths.iter()
|
||||
.map(Path::to_string)
|
||||
.fold(String::new(), |b, a| {
|
||||
if b.is_empty() {
|
||||
a.to_string()
|
||||
} else {
|
||||
format!("{b}, {a}")
|
||||
}
|
||||
});
|
||||
let q = format!("select * from public.perm where path in {set}");
|
||||
|
||||
let pass = self.with_client(|c| c.query(&q, &[]))?
|
||||
.iter()
|
||||
.map(Perm::unmarshal)
|
||||
.collect::<Result<Vec<Perm>, _>>()?
|
||||
.iter()
|
||||
.all(|p| p.actor_can(actor, wants_to));
|
||||
Ok(pass)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostgresImpl<C> {
|
||||
@ -82,9 +136,11 @@ impl<C> PostgresImpl<C> {
|
||||
|
||||
impl<C> Postgres for PostgresImpl<C>
|
||||
where Self: Send + Sync,
|
||||
C: 'static + postgres::GenericClient
|
||||
C: 'static + postgres::GenericClient,
|
||||
C::Error: Error
|
||||
{
|
||||
type Client = C;
|
||||
type Error = C::Error;
|
||||
|
||||
fn try_new<F>(connect: F, pool_size: usize) -> Result<Self, C::Error>
|
||||
where F: Fn() -> Result<C, C::Error>
|
||||
@ -114,22 +170,19 @@ impl<C> Postgres for PostgresImpl<C>
|
||||
| None => f(self.block_for_next().deref_mut()),
|
||||
}
|
||||
}
|
||||
|
||||
fn authorized(action: String, actor: &Actor) -> Result<(), DbError<Self>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use std::any::Any;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub fn from_sql_owned<T>(to: &dyn ToSql) -> T
|
||||
where T: 'static + for<'a> FromSql<'a> + postgres::types::Typed
|
||||
{
|
||||
let mut buf = BytesMut::with_capacity(128);
|
||||
let mut buf = BytesMut::with_capacity(256);
|
||||
to.to_sql(&T::TYPE, &mut buf).unwrap();
|
||||
T::from_sql(&T::TYPE, &buf).unwrap()
|
||||
}
|
||||
@ -202,7 +255,8 @@ pub mod test {
|
||||
use postgres::types::{FromSql, ToSql, Type};
|
||||
use postgres::{Column, GenericClient, GenericRow};
|
||||
|
||||
use super::{Postgres, PostgresImpl};
|
||||
use super::{Postgres as _, PostgresImpl};
|
||||
use crate::model::{Actor, Mode, Path, Perm};
|
||||
|
||||
pub struct Row<E> {
|
||||
pub columns: Vec<Column>,
|
||||
@ -217,7 +271,13 @@ pub mod test {
|
||||
.collect(),
|
||||
values: cols.into_iter()
|
||||
.map(|(_, ty, val)| {
|
||||
let mut bs = BytesMut::with_capacity(128);
|
||||
let mut bs = if ty == Type::INT2 {
|
||||
BytesMut::with_capacity(2)
|
||||
} else if ty == Type::INT4 {
|
||||
BytesMut::with_capacity(4)
|
||||
} else {
|
||||
BytesMut::with_capacity(128)
|
||||
};
|
||||
val.to_sql(&ty, &mut bs).unwrap();
|
||||
bs
|
||||
})
|
||||
@ -257,6 +317,54 @@ pub mod test {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Postgres<E> {
|
||||
client: Mutex<Client<E>>,
|
||||
pub perms: Vec<Perm>,
|
||||
}
|
||||
|
||||
unsafe impl<E> Send for Postgres<E> {}
|
||||
unsafe impl<E> Sync for Postgres<E> {}
|
||||
|
||||
impl<E> super::Postgres for Postgres<E> where E: core::fmt::Debug + 'static + super::Error
|
||||
{
|
||||
type Client = Client<E>;
|
||||
type Error = E;
|
||||
|
||||
fn try_new<F>(connect: F, _: usize) -> Result<Self, super::DbError<Self>>
|
||||
where F: Fn() -> Result<Self::Client, super::DbError<Self>>
|
||||
{
|
||||
Ok(Self { client: Mutex::new(connect()?),
|
||||
perms: vec![] })
|
||||
}
|
||||
|
||||
fn with_client<F, R>(&self, f: F) -> Result<R, super::DbError<Self>>
|
||||
where F: FnOnce(&mut Self::Client) -> Result<R, super::DbError<Self>>
|
||||
{
|
||||
f(self.client.lock().unwrap().deref_mut())
|
||||
}
|
||||
|
||||
fn authorized(&self,
|
||||
path: &Path,
|
||||
wants_to: Mode,
|
||||
actor: &Actor)
|
||||
-> Result<bool, super::DbError<Self>> {
|
||||
Ok(self.perms
|
||||
.iter()
|
||||
.find(|p| &p.path == path)
|
||||
.map(|p| p.actor_can(actor, wants_to))
|
||||
.unwrap_or(false))
|
||||
}
|
||||
|
||||
fn authorized_all(&self,
|
||||
paths: &[Path],
|
||||
wants_to: Mode,
|
||||
actor: &Actor)
|
||||
-> Result<bool, super::DbError<Self>> {
|
||||
Ok(paths.iter()
|
||||
.all(|p| self.authorized(p, wants_to, actor).unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
pub struct Client<E> {
|
||||
pub __phantom: PhantomData<E>,
|
||||
|
8
src/rep_payload.rs
Normal file
8
src/rep_payload.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct RepPayload<T> {
|
||||
#[serde(flatten)]
|
||||
pub t: T,
|
||||
pub links: HashMap<String, String>,
|
||||
}
|
33
src/repo.rs
33
src/repo.rs
@ -1,19 +1,44 @@
|
||||
use crate::perm::Actor;
|
||||
use crate::model::Actor;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Page<Id> {
|
||||
pub limit: u32,
|
||||
pub after: Option<Id>,
|
||||
}
|
||||
|
||||
pub trait Repo: Send + Sync {
|
||||
type T;
|
||||
type Patch;
|
||||
type Insert;
|
||||
type Error: core::fmt::Debug;
|
||||
type Id: AsRef<str>;
|
||||
}
|
||||
|
||||
pub trait ReadOne: Send + Sync + Repo {
|
||||
fn get(&self, actor: &Actor, id: &Self::Id) -> Result<Option<Self::T>, Self::Error>;
|
||||
fn get_all(&self, actor: &Actor) -> Result<Vec<Self::T>, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait ReadMany: Send + Sync + Repo {
|
||||
fn page_valid(&self, page: &Page<Self::Id>) -> bool;
|
||||
fn get_all(&self, actor: &Actor, page: Page<Self::Id>) -> Result<Vec<Self::T>, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait Patch: Send + Sync + Repo {
|
||||
type Patch;
|
||||
fn patch(&self, actor: &Actor, id: &Self::Id, state: Self::Patch) -> Result<bool, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait Insert: Send + Sync + Repo {
|
||||
type Insert;
|
||||
fn insert(&self, actor: &Actor, state: Self::Insert) -> Result<Self::Id, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait Del: Send + Sync + Repo {
|
||||
fn del(&self, actor: &Actor, id: &Self::Id) -> Result<bool, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait Dump: Send + Sync + Repo {
|
||||
fn dump(&self, actor: &Actor) -> Result<Vec<Self::T>, Self::Error>;
|
||||
}
|
||||
|
||||
/// An entity that has some operations which rely on
|
||||
/// an external service
|
||||
pub trait Ext: Send + Sync {
|
||||
|
@ -6,7 +6,7 @@ use toad_msg::{ContentFormat, MessageOptions};
|
||||
pub struct ReqPayload<T> {
|
||||
#[serde(flatten)]
|
||||
pub t: T,
|
||||
pub session: String,
|
||||
pub session: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> ReqPayload<T> where T: DeserializeOwned
|
||||
|
267
src/user.rs
267
src/user.rs
@ -1,267 +0,0 @@
|
||||
use naan::prelude::*;
|
||||
use postgres::{GenericClient, GenericRow};
|
||||
|
||||
use crate::hashed_text::HashedText;
|
||||
use crate::perm::Actor;
|
||||
use crate::postgres::{try_get, DbError, Postgres, UnmarshalRow};
|
||||
use crate::repo::Repo;
|
||||
use crate::{newtype, Email};
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct UserId(String);
|
||||
);
|
||||
|
||||
newtype!(
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct UserTag(String);
|
||||
);
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct User {
|
||||
uid: UserId,
|
||||
tag: UserTag,
|
||||
password: HashedText,
|
||||
email: Email,
|
||||
}
|
||||
|
||||
impl UnmarshalRow for User {
|
||||
fn unmarshal_maybe_prefixed<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
|
||||
where R: GenericRow,
|
||||
S: AsRef<str>
|
||||
{
|
||||
let p = p.as_ref().map(|p| p.as_ref());
|
||||
try_get!(p, "uid", row).zip(|_: &_| try_get!(p, "tag", row))
|
||||
.zip(|_: &_| try_get!(<String> p, "password", row))
|
||||
.zip(|_: &_| 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(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct UserPatch {
|
||||
tag: Option<UserTag>,
|
||||
password: Option<HashedText>,
|
||||
email: Option<Email>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct UserInsert {
|
||||
tag: UserTag,
|
||||
password: HashedText,
|
||||
email: Email,
|
||||
}
|
||||
|
||||
pub trait UserRepo: Repo<T = User, Patch = UserPatch, Id = UserId> {}
|
||||
|
||||
pub struct UserRepoImpl<Db: 'static>(pub &'static Db);
|
||||
impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
{
|
||||
type T = User;
|
||||
type Patch = UserPatch;
|
||||
type Insert = UserInsert;
|
||||
type Error = DbError<Db>;
|
||||
type Id = UserId;
|
||||
|
||||
fn get(&self, actor: &Actor, id: &Self::Id) -> Result<Option<Self::T>, Self::Error> {
|
||||
static QUERY: &'static str =
|
||||
"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()]))
|
||||
.and_then(|opt| {
|
||||
opt.as_ref()
|
||||
.map(User::unmarshal)
|
||||
.sequence::<hkt::ResultOk<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
fn get_all(&self, actor: &Actor) -> Result<Vec<Self::T>, 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, actor: &Actor, id: &UserId, patch: UserPatch) -> Result<bool, Self::Error> {
|
||||
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 and deleted = false"];
|
||||
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
c.execute(&QUERY_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&id.as_ref(),
|
||||
&patch.tag.as_ref().map(|t| t.as_ref()),
|
||||
&patch.password.as_ref().map(|h| h.as_ref()),
|
||||
&patch.email.as_ref().map(|e| e.as_ref())])
|
||||
})
|
||||
.map(|n| n == 1)
|
||||
}
|
||||
|
||||
fn insert(&self, actor: &Actor, insert: UserInsert) -> Result<UserId, Self::Error> {
|
||||
static QUERY_LINES: [&'static str; 5] = ["insert into public.usr",
|
||||
" (tag, password, email)",
|
||||
"values",
|
||||
" ($2, $3, $4)",
|
||||
"returning uid;"];
|
||||
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
c.query_one(&QUERY_LINES.iter()
|
||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||
&[&insert.tag.as_ref(),
|
||||
&insert.password.as_ref(),
|
||||
&insert.email.as_ref()])
|
||||
})
|
||||
.and_then(|r| r.try_get::<_, String>(0))
|
||||
.map(UserId::from)
|
||||
}
|
||||
|
||||
fn del(&self, actor: &Actor, id: &UserId) -> Result<bool, Self::Error> {
|
||||
static QUERY: &'static str = "delete from public.usr where uid = $1 :: uuid;";
|
||||
|
||||
self.0
|
||||
.with_client(|c| c.execute(QUERY, &[&id.as_ref()]))
|
||||
.map(|n| n == 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Db> UserRepo for UserRepoImpl<Db> where Db: Postgres + 'static {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use postgres::types::Type;
|
||||
|
||||
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, UserInsert, UserTag};
|
||||
use crate::Email;
|
||||
|
||||
fn usr_row(usr: User) -> Row<()> {
|
||||
Row::new(vec![("uid", Type::UUID, Box::new(usr.uid.to_string())),
|
||||
("email", Type::TEXT, Box::new(usr.email.to_string())),
|
||||
("password", Type::TEXT, Box::new(usr.password.to_string())),
|
||||
("tag", Type::TEXT, Box::new(usr.tag.to_string()))])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_repo_get_one() {
|
||||
let client = || Client::<()> { query_opt: Box::new(|_, q, ps| {
|
||||
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::<String>(ps[0]) == String::from("1")))
|
||||
}),
|
||||
..Client::default() };
|
||||
|
||||
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
|
||||
|
||||
let repo =
|
||||
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
|
||||
|
||||
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, _| {
|
||||
Ok(vec![usr_row(User { uid: UserId::from("1"),
|
||||
tag: UserTag::from("foo"),
|
||||
email: Email::from("foo@bar.baz"),
|
||||
password: HashedText::from("XXX") })])
|
||||
}),
|
||||
..Client::default() };
|
||||
|
||||
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
|
||||
|
||||
let repo =
|
||||
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
|
||||
|
||||
assert_eq!(repo.get_all(&Actor::default()).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_repo_del() {
|
||||
let client =
|
||||
|| Client::<()> { state: Box::new(false), // already deleted?
|
||||
execute: Box::new(|c, q, ps| {
|
||||
if from_sql_owned::<String>(ps[0]) == "1" && !*c.state_mut::<bool>() {
|
||||
*c.state_mut::<bool>() = true;
|
||||
Ok(1)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}),
|
||||
..Client::default() };
|
||||
|
||||
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
|
||||
|
||||
let repo =
|
||||
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
|
||||
|
||||
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>::new()),
|
||||
query_one: Box::new(|c, q, ps| {
|
||||
let tags = c.state_mut::<Vec<UserTag>>();
|
||||
|
||||
let tag = UserTag::from(from_sql_owned::<String>(ps[0]));
|
||||
|
||||
if tags.contains(&tag) {
|
||||
Err(())
|
||||
} else {
|
||||
tags.push(tag);
|
||||
Ok(Row::new(vec![("", Type::TEXT, Box::new(tags.len().to_string()))]))
|
||||
}
|
||||
}),
|
||||
..Client::default() };
|
||||
|
||||
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
|
||||
|
||||
let repo =
|
||||
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
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<E> {
|
||||
Invalid,
|
||||
Expired,
|
||||
Other(E),
|
||||
}
|
||||
|
||||
pub enum LoginError<E> {
|
||||
NoSuchUser,
|
||||
Other(E),
|
||||
}
|
||||
|
||||
pub trait UserSessionExt: Ext {
|
||||
fn login<TE, P, L, D>(&self,
|
||||
tag_or_email: TE,
|
||||
password: P,
|
||||
remember: bool,
|
||||
location: L,
|
||||
device: D,
|
||||
ip: SocketAddr)
|
||||
-> Result<UserSession, LoginError<Self::Error>>
|
||||
where TE: AsRef<str>,
|
||||
P: AsRef<str>,
|
||||
L: AsRef<str>,
|
||||
D: AsRef<str>;
|
||||
}
|
||||
|
||||
pub struct UserSessionExtImpl<Db: 'static>(&'static Db);
|
||||
|
||||
impl<Db> Ext for UserSessionExtImpl<Db> where Db: Postgres
|
||||
{
|
||||
type Error = DbError<Db>;
|
||||
}
|
||||
|
||||
impl<Db> UserSessionExt for UserSessionExtImpl<Db> where Db: Postgres
|
||||
{
|
||||
fn login<TE, P, L, D>(&self,
|
||||
tag_or_email: TE,
|
||||
password: P,
|
||||
remember: bool,
|
||||
location: L,
|
||||
device: D,
|
||||
ip: SocketAddr)
|
||||
-> Result<UserSession, LoginError<DbError<Db>>>
|
||||
where TE: AsRef<str>,
|
||||
P: AsRef<str>,
|
||||
L: AsRef<str>,
|
||||
D: AsRef<str>
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
|
Loading…
Reference in New Issue
Block a user