diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/channel/app.rs | 40 | ||||
| -rw-r--r-- | src/channel/repo/broadcast.rs | 112 | ||||
| -rw-r--r-- | src/channel/repo/messages.rs | 136 | ||||
| -rw-r--r-- | src/channel/repo/mod.rs | 3 | ||||
| -rw-r--r-- | src/channel/routes.rs | 10 | ||||
| -rw-r--r-- | src/events.rs | 13 | ||||
| -rw-r--r-- | src/index/app.rs | 4 | ||||
| -rw-r--r-- | src/index/routes.rs | 7 | ||||
| -rw-r--r-- | src/index/templates.rs | 2 | ||||
| -rw-r--r-- | src/lib.rs | 2 | ||||
| -rw-r--r-- | src/login/app.rs | 46 | ||||
| -rw-r--r-- | src/login/extract.rs (renamed from src/login/extract/identity_token.rs) | 2 | ||||
| -rw-r--r-- | src/login/extract/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/repo/auth.rs | 53 | ||||
| -rw-r--r-- | src/login/repo/mod.rs | 3 | ||||
| -rw-r--r-- | src/password.rs | 31 | ||||
| -rw-r--r-- | src/repo/channel.rs (renamed from src/channel/repo/channels.rs) | 0 | ||||
| -rw-r--r-- | src/repo/login/extract.rs (renamed from src/login/extract/login.rs) | 8 | ||||
| -rw-r--r-- | src/repo/login/mod.rs | 4 | ||||
| -rw-r--r-- | src/repo/login/store.rs (renamed from src/login/repo/logins.rs) | 42 | ||||
| -rw-r--r-- | src/repo/message.rs | 27 | ||||
| -rw-r--r-- | src/repo/mod.rs | 4 | ||||
| -rw-r--r-- | src/repo/token.rs (renamed from src/login/repo/tokens.rs) | 6 |
24 files changed, 299 insertions, 264 deletions
diff --git a/src/channel/app.rs b/src/channel/app.rs index 36fa552..b0b63b3 100644 --- a/src/channel/app.rs +++ b/src/channel/app.rs @@ -10,11 +10,15 @@ use sqlx::sqlite::SqlitePool; use tokio::sync::broadcast::{channel, Sender}; use tokio_stream::wrappers::BroadcastStream; -use super::repo::{ - channels::{Channel, Id as ChannelId, Provider as _}, - messages::{BroadcastMessage, Provider as _}, +use super::repo::broadcast::{self, Provider as _}; +use crate::{ + clock::DateTime, + error::BoxedError, + repo::{ + channel::{self, Channel, Provider as _}, + login::Login, + }, }; -use crate::{clock::DateTime, error::BoxedError, login::repo::logins::Login}; pub struct Channels<'a> { db: &'a SqlitePool, @@ -46,13 +50,13 @@ impl<'a> Channels<'a> { pub async fn send( &self, login: &Login, - channel: &ChannelId, + channel: &channel::Id, body: &str, sent_at: &DateTime, ) -> Result<(), BoxedError> { let mut tx = self.db.begin().await?; let message = tx - .messages() + .broadcast() .create(&login.id, channel, body, sent_at) .await?; tx.commit().await?; @@ -63,13 +67,13 @@ impl<'a> Channels<'a> { pub async fn events( &self, - channel: &ChannelId, + channel: &channel::Id, resume_at: Option<&DateTime>, - ) -> Result<impl Stream<Item = Result<BroadcastMessage, BoxedError>> + 'static, BoxedError> + ) -> Result<impl Stream<Item = Result<broadcast::Message, BoxedError>> + 'static, BoxedError> { fn skip_stale<E>( resume_at: Option<&DateTime>, - ) -> impl for<'m> FnMut(&'m BroadcastMessage) -> future::Ready<Result<bool, E>> { + ) -> impl for<'m> FnMut(&'m broadcast::Message) -> future::Ready<Result<bool, E>> { let resume_at = resume_at.cloned(); move |msg| { future::ready(Ok(match resume_at { @@ -86,7 +90,7 @@ impl<'a> Channels<'a> { .try_skip_while(skip_stale(resume_at)); let mut tx = self.db.begin().await?; - let stored_messages = tx.messages().for_replay(channel, resume_at).await?; + let stored_messages = tx.broadcast().replay(channel, resume_at).await?; tx.commit().await?; let stored_messages = stream::iter(stored_messages).map(Ok); @@ -101,7 +105,7 @@ pub struct Broadcaster { // The use of std::sync::Mutex, and not tokio::sync::Mutex, follows Tokio's // own advice: <https://tokio.rs/tokio/tutorial/shared-state>. Methods that // lock it must be sync. - senders: Arc<Mutex<HashMap<ChannelId, Sender<BroadcastMessage>>>>, + senders: Arc<Mutex<HashMap<channel::Id, Sender<broadcast::Message>>>>, } impl Broadcaster { @@ -115,7 +119,7 @@ impl Broadcaster { Ok(broadcaster) } - fn new<'i>(channels: impl IntoIterator<Item = &'i ChannelId>) -> Self { + fn new<'i>(channels: impl IntoIterator<Item = &'i channel::Id>) -> Self { let senders: HashMap<_, _> = channels .into_iter() .cloned() @@ -128,7 +132,7 @@ impl Broadcaster { } // panic: if ``channel`` is already registered. - pub fn register_channel(&self, channel: &ChannelId) { + pub fn register_channel(&self, channel: &channel::Id) { match self.senders().entry(channel.clone()) { // This ever happening indicates a serious logic error. Entry::Occupied(_) => panic!("duplicate channel registration for channel {channel}"), @@ -140,7 +144,7 @@ impl Broadcaster { // panic: if ``channel`` has not been previously registered, and was not // part of the initial set of channels. - pub fn broadcast(&self, channel: &ChannelId, message: BroadcastMessage) { + pub fn broadcast(&self, channel: &channel::Id, message: broadcast::Message) { let tx = self.sender(channel); // Per the Tokio docs, the returned error is only used to indicate that @@ -152,7 +156,7 @@ impl Broadcaster { // panic: if ``channel`` has not been previously registered, and was not // part of the initial set of channels. - pub fn listen(&self, channel: &ChannelId) -> BroadcastStream<BroadcastMessage> { + pub fn listen(&self, channel: &channel::Id) -> BroadcastStream<broadcast::Message> { let rx = self.sender(channel).subscribe(); BroadcastStream::from(rx) @@ -160,15 +164,15 @@ impl Broadcaster { // panic: if ``channel`` has not been previously registered, and was not // part of the initial set of channels. - fn sender(&self, channel: &ChannelId) -> Sender<BroadcastMessage> { + fn sender(&self, channel: &channel::Id) -> Sender<broadcast::Message> { self.senders()[channel].clone() } - fn senders(&self) -> MutexGuard<HashMap<ChannelId, Sender<BroadcastMessage>>> { + fn senders(&self) -> MutexGuard<HashMap<channel::Id, Sender<broadcast::Message>>> { self.senders.lock().unwrap() // propagate panics when mutex is poisoned } - fn make_sender() -> Sender<BroadcastMessage> { + fn make_sender() -> Sender<broadcast::Message> { // Queue depth of 16 chosen entirely arbitrarily. Don't read too much // into it. let (tx, _) = channel(16); diff --git a/src/channel/repo/broadcast.rs b/src/channel/repo/broadcast.rs new file mode 100644 index 0000000..3ca7396 --- /dev/null +++ b/src/channel/repo/broadcast.rs @@ -0,0 +1,112 @@ +use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; + +use crate::{ + clock::DateTime, + repo::{ + channel, + login::{self, Login, Logins}, + message, + }, +}; + +pub trait Provider { + fn broadcast(&mut self) -> Broadcast; +} + +impl<'c> Provider for Transaction<'c, Sqlite> { + fn broadcast(&mut self) -> Broadcast { + Broadcast(self) + } +} + +pub struct Broadcast<'t>(&'t mut SqliteConnection); + +#[derive(Clone, Debug, serde::Serialize)] +pub struct Message { + pub id: message::Id, + pub sender: Login, + pub body: String, + pub sent_at: DateTime, +} + +impl<'c> Broadcast<'c> { + pub async fn create( + &mut self, + sender: &login::Id, + channel: &channel::Id, + body: &str, + sent_at: &DateTime, + ) -> Result<Message, sqlx::Error> { + let id = message::Id::generate(); + + let sender = Logins::from(&mut *self.0).by_id(sender).await?; + + let message = sqlx::query!( + r#" + insert into message + (id, sender, channel, body, sent_at) + values ($1, $2, $3, $4, $5) + returning + id as "id: message::Id", + sender as "sender: login::Id", + body, + sent_at as "sent_at: DateTime" + "#, + id, + sender.id, + channel, + body, + sent_at, + ) + .map(|row| { + debug_assert!(row.sender == sender.id); + Message { + id: row.id, + sender: sender.clone(), + body: row.body, + sent_at: row.sent_at, + } + }) + .fetch_one(&mut *self.0) + .await?; + + Ok(message) + } + + pub async fn replay( + &mut self, + channel: &channel::Id, + resume_at: Option<&DateTime>, + ) -> Result<Vec<Message>, sqlx::Error> { + let messages = sqlx::query!( + r#" + select + message.id as "id: message::Id", + login.id as "sender_id: login::Id", + login.name as sender_name, + message.body, + message.sent_at as "sent_at: DateTime" + from message + join login on message.sender = login.id + where channel = $1 + and coalesce(sent_at > $2, true) + order by sent_at asc + "#, + channel, + resume_at, + ) + .map(|row| Message { + id: row.id, + sender: Login { + id: row.sender_id, + name: row.sender_name, + }, + body: row.body, + sent_at: row.sent_at, + }) + .fetch_all(&mut *self.0) + .await?; + + Ok(messages) + } +} diff --git a/src/channel/repo/messages.rs b/src/channel/repo/messages.rs deleted file mode 100644 index a30e6da..0000000 --- a/src/channel/repo/messages.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::fmt; - -use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; - -use super::channels::Id as ChannelId; -use crate::{ - clock::DateTime, - id::Id as BaseId, - login::repo::logins::{Id as LoginId, Login, Logins}, -}; - -pub trait Provider { - fn messages(&mut self) -> Messages; -} - -impl<'c> Provider for Transaction<'c, Sqlite> { - fn messages(&mut self) -> Messages { - Messages(self) - } -} - -pub struct Messages<'t>(&'t mut SqliteConnection); - -#[derive(Clone, Debug, serde::Serialize)] -pub struct BroadcastMessage { - pub id: Id, - pub sender: Login, - pub body: String, - pub sent_at: DateTime, -} - -impl<'c> Messages<'c> { - pub async fn create( - &mut self, - sender: &LoginId, - channel: &ChannelId, - body: &str, - sent_at: &DateTime, - ) -> Result<BroadcastMessage, sqlx::Error> { - let id = Id::generate(); - - let sender = Logins::from(&mut *self.0).by_id(sender).await?; - - let message = sqlx::query!( - r#" - insert into message - (id, sender, channel, body, sent_at) - values ($1, $2, $3, $4, $5) - returning - id as "id: Id", - sender as "sender: LoginId", - body, - sent_at as "sent_at: DateTime" - "#, - id, - sender.id, - channel, - body, - sent_at, - ) - .map(|row| { - debug_assert!(row.sender == sender.id); - BroadcastMessage { - id: row.id, - sender: sender.clone(), - body: row.body, - sent_at: row.sent_at, - } - }) - .fetch_one(&mut *self.0) - .await?; - - Ok(message) - } - - pub async fn for_replay( - &mut self, - channel: &ChannelId, - resume_at: Option<&DateTime>, - ) -> Result<Vec<BroadcastMessage>, sqlx::Error> { - let messages = sqlx::query!( - r#" - select - message.id as "id: Id", - login.id as "sender_id: LoginId", - login.name as sender_name, - message.body, - message.sent_at as "sent_at: DateTime" - from message - join login on message.sender = login.id - where channel = $1 - and coalesce(sent_at > $2, true) - order by sent_at asc - "#, - channel, - resume_at, - ) - .map(|row| BroadcastMessage { - id: row.id, - sender: Login { - id: row.sender_id, - name: row.sender_name, - }, - body: row.body, - sent_at: row.sent_at, - }) - .fetch_all(&mut *self.0) - .await?; - - Ok(messages) - } -} - -/// Stable identifier for a [Message]. Prefixed with `M`. -#[derive(Clone, Debug, Eq, Hash, PartialEq, sqlx::Type, serde::Deserialize, serde::Serialize)] -#[sqlx(transparent)] -#[serde(transparent)] -pub struct Id(BaseId); - -impl From<BaseId> for Id { - fn from(id: BaseId) -> Self { - Self(id) - } -} - -impl Id { - pub fn generate() -> Self { - BaseId::generate("M") - } -} - -impl fmt::Display for Id { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} diff --git a/src/channel/repo/mod.rs b/src/channel/repo/mod.rs index 345897d..2ed3062 100644 --- a/src/channel/repo/mod.rs +++ b/src/channel/repo/mod.rs @@ -1,2 +1 @@ -pub mod channels; -pub mod messages; +pub mod broadcast; diff --git a/src/channel/routes.rs b/src/channel/routes.rs index 3c2353b..1379153 100644 --- a/src/channel/routes.rs +++ b/src/channel/routes.rs @@ -5,8 +5,12 @@ use axum::{ Router, }; -use super::repo::channels::Id as ChannelId; -use crate::{app::App, clock::RequestedAt, error::InternalError, login::repo::logins::Login}; +use crate::{ + app::App, + clock::RequestedAt, + error::InternalError, + repo::{channel, login::Login}, +}; pub fn router() -> Router<App> { Router::new() @@ -35,7 +39,7 @@ struct SendRequest { } async fn on_send( - Path(channel): Path<ChannelId>, + Path(channel): Path<channel::Id>, RequestedAt(sent_at): RequestedAt, State(app): State<App>, login: Login, diff --git a/src/events.rs b/src/events.rs index 51f06b4..38d53fc 100644 --- a/src/events.rs +++ b/src/events.rs @@ -17,10 +17,9 @@ use futures::{ use crate::{ app::App, - channel::repo::{channels::Id as ChannelId, messages::BroadcastMessage}, - error::BoxedError, - error::InternalError, - login::repo::logins::Login, + channel::repo::broadcast, + error::{BoxedError, InternalError}, + repo::{channel, login::Login}, }; pub fn router() -> Router<App> { @@ -30,7 +29,7 @@ pub fn router() -> Router<App> { #[derive(serde::Deserialize)] struct EventsQuery { #[serde(default, rename = "channel")] - channels: Vec<ChannelId>, + channels: Vec<channel::Id>, } async fn on_events( @@ -71,7 +70,7 @@ async fn on_events( Ok(sse) } -fn to_sse_event(event: ChannelEvent<BroadcastMessage>) -> Result<sse::Event, BoxedError> { +fn to_sse_event(event: ChannelEvent<broadcast::Message>) -> Result<sse::Event, BoxedError> { let data = serde_json::to_string(&event)?; let event = sse::Event::default() .id(event @@ -85,7 +84,7 @@ fn to_sse_event(event: ChannelEvent<BroadcastMessage>) -> Result<sse::Event, Box #[derive(serde::Serialize)] struct ChannelEvent<M> { - channel: ChannelId, + channel: channel::Id, #[serde(flatten)] message: M, } diff --git a/src/index/app.rs b/src/index/app.rs index fabf35c..41b12fa 100644 --- a/src/index/app.rs +++ b/src/index/app.rs @@ -1,8 +1,8 @@ use sqlx::sqlite::SqlitePool; use crate::{ - channel::repo::channels::{Channel, Id as ChannelId, Provider as _}, error::BoxedError, + repo::channel::{self, Channel, Provider as _}, }; pub struct Index<'a> { @@ -14,7 +14,7 @@ impl<'a> Index<'a> { Self { db } } - pub async fn channel(&self, channel: ChannelId) -> Result<Channel, BoxedError> { + pub async fn channel(&self, channel: channel::Id) -> Result<Channel, BoxedError> { let mut tx = self.db.begin().await?; let channel = tx.channels().by_id(channel).await?; tx.commit().await?; diff --git a/src/index/routes.rs b/src/index/routes.rs index 2d77e5b..ef46cc1 100644 --- a/src/index/routes.rs +++ b/src/index/routes.rs @@ -9,8 +9,9 @@ use maud::Markup; use super::templates; use crate::{ - app::App, channel::repo::channels::Id as ChannelId, error::InternalError, - login::repo::logins::Login, + app::App, + error::InternalError, + repo::{channel, login::Login}, }; async fn index(State(app): State<App>, login: Option<Login>) -> Result<Markup, InternalError> { @@ -47,7 +48,7 @@ async fn js(Path(path): Path<String>) -> impl IntoResponse { async fn channel( State(app): State<App>, _: Login, - Path(channel): Path<ChannelId>, + Path(channel): Path<channel::Id>, ) -> Result<Markup, InternalError> { let channel = app.index().channel(channel).await?; Ok(templates::channel(&channel)) diff --git a/src/index/templates.rs b/src/index/templates.rs index a69c19a..d56972c 100644 --- a/src/index/templates.rs +++ b/src/index/templates.rs @@ -1,6 +1,6 @@ use maud::{html, Markup, DOCTYPE}; -use crate::{channel::repo::channels::Channel, login::repo::logins::Login}; +use crate::repo::{channel::Channel, login::Login}; pub fn authenticated<'c>(login: Login, channels: impl IntoIterator<Item = &'c Channel>) -> Markup { html! { @@ -7,3 +7,5 @@ mod events; mod id; mod index; mod login; +mod password; +mod repo; diff --git a/src/login/app.rs b/src/login/app.rs index cd65f35..c82da1a 100644 --- a/src/login/app.rs +++ b/src/login/app.rs @@ -1,13 +1,15 @@ -use argon2::Argon2; -use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; -use rand_core::OsRng; use sqlx::sqlite::SqlitePool; -use super::repo::{ - logins::{Login, Provider as _}, - tokens::Provider as _, +use super::repo::auth::Provider as _; +use crate::{ + clock::DateTime, + error::BoxedError, + password::StoredHash, + repo::{ + login::{Login, Provider as _}, + token::Provider as _, + }, }; -use crate::{clock::DateTime, error::BoxedError}; pub struct Logins<'a> { db: &'a SqlitePool, @@ -26,7 +28,7 @@ impl<'a> Logins<'a> { ) -> Result<Option<String>, BoxedError> { let mut tx = self.db.begin().await?; - let login = if let Some((login, stored_hash)) = tx.logins().for_login(name).await? { + let login = if let Some((login, stored_hash)) = tx.auth().for_name(name).await? { if stored_hash.verify(password)? { // Password verified; use the login. Some(login) @@ -75,31 +77,3 @@ impl<'a> Logins<'a> { Ok(()) } } - -#[derive(Debug, sqlx::Type)] -#[sqlx(transparent)] -pub struct StoredHash(String); - -impl StoredHash { - fn new(password: &str) -> Result<Self, password_hash::Error> { - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - let hash = argon2 - .hash_password(password.as_bytes(), &salt)? - .to_string(); - Ok(Self(hash)) - } - - fn verify(&self, password: &str) -> Result<bool, password_hash::Error> { - let hash = PasswordHash::new(&self.0)?; - - match Argon2::default().verify_password(password.as_bytes(), &hash) { - // Successful authentication, not an error - Ok(()) => Ok(true), - // Unsuccessful authentication, also not an error - Err(password_hash::errors::Error::Password) => Ok(false), - // Password validation failed for some other reason, treat as an error - Err(err) => Err(err), - } - } -} diff --git a/src/login/extract/identity_token.rs b/src/login/extract.rs index c813324..735bc22 100644 --- a/src/login/extract/identity_token.rs +++ b/src/login/extract.rs @@ -1,5 +1,3 @@ -use std::convert::Infallible; - use axum::{ extract::FromRequestParts, http::request::Parts, diff --git a/src/login/extract/mod.rs b/src/login/extract/mod.rs deleted file mode 100644 index ba943a6..0000000 --- a/src/login/extract/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod identity_token; -mod login; - -pub use self::identity_token::IdentityToken; diff --git a/src/login/mod.rs b/src/login/mod.rs index 5070301..191cce0 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -1,6 +1,6 @@ pub use self::routes::router; pub mod app; -mod extract; -pub mod repo; +pub mod extract; +mod repo; mod routes; diff --git a/src/login/repo/auth.rs b/src/login/repo/auth.rs new file mode 100644 index 0000000..78b44f0 --- /dev/null +++ b/src/login/repo/auth.rs @@ -0,0 +1,53 @@ +use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; + +use crate::{ + password::StoredHash, + repo::login::{self, Login}, +}; + +pub trait Provider { + fn auth(&mut self) -> Auth; +} + +impl<'c> Provider for Transaction<'c, Sqlite> { + fn auth(&mut self) -> Auth { + Auth(self) + } +} + +pub struct Auth<'t>(&'t mut SqliteConnection); + +impl<'t> Auth<'t> { + /// Retrieves a login by name, plus its stored password hash for + /// verification. If there's no login with the requested name, this will + /// return [None]. + pub async fn for_name( + &mut self, + name: &str, + ) -> Result<Option<(Login, StoredHash)>, sqlx::Error> { + let found = sqlx::query!( + r#" + select + id as "id: login::Id", + name, + password_hash as "password_hash: StoredHash" + from login + where name = $1 + "#, + name, + ) + .map(|rec| { + ( + Login { + id: rec.id, + name: rec.name, + }, + rec.password_hash, + ) + }) + .fetch_optional(&mut *self.0) + .await?; + + Ok(found) + } +} diff --git a/src/login/repo/mod.rs b/src/login/repo/mod.rs index 07da569..0e4a05d 100644 --- a/src/login/repo/mod.rs +++ b/src/login/repo/mod.rs @@ -1,2 +1 @@ -pub mod logins; -pub mod tokens; +pub mod auth; diff --git a/src/password.rs b/src/password.rs new file mode 100644 index 0000000..b14f728 --- /dev/null +++ b/src/password.rs @@ -0,0 +1,31 @@ +use argon2::Argon2; +use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; +use rand_core::OsRng; + +#[derive(Debug, sqlx::Type)] +#[sqlx(transparent)] +pub struct StoredHash(String); + +impl StoredHash { + pub fn new(password: &str) -> Result<Self, password_hash::Error> { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let hash = argon2 + .hash_password(password.as_bytes(), &salt)? + .to_string(); + Ok(Self(hash)) + } + + pub fn verify(&self, password: &str) -> Result<bool, password_hash::Error> { + let hash = PasswordHash::new(&self.0)?; + + match Argon2::default().verify_password(password.as_bytes(), &hash) { + // Successful authentication, not an error + Ok(()) => Ok(true), + // Unsuccessful authentication, also not an error + Err(password_hash::errors::Error::Password) => Ok(false), + // Password validation failed for some other reason, treat as an error + Err(err) => Err(err), + } + } +} diff --git a/src/channel/repo/channels.rs b/src/repo/channel.rs index ab7489c..ab7489c 100644 --- a/src/channel/repo/channels.rs +++ b/src/repo/channel.rs diff --git a/src/login/extract/login.rs b/src/repo/login/extract.rs index 8b5bb41..a068bc0 100644 --- a/src/login/extract/login.rs +++ b/src/repo/login/extract.rs @@ -4,12 +4,8 @@ use axum::{ response::{IntoResponse, Response}, }; -use crate::{ - app::App, - clock::RequestedAt, - error::InternalError, - login::{extract::IdentityToken, repo::logins::Login}, -}; +use super::Login; +use crate::{app::App, clock::RequestedAt, error::InternalError, login::extract::IdentityToken}; #[async_trait::async_trait] impl FromRequestParts<App> for Login { diff --git a/src/repo/login/mod.rs b/src/repo/login/mod.rs new file mode 100644 index 0000000..e23a7b7 --- /dev/null +++ b/src/repo/login/mod.rs @@ -0,0 +1,4 @@ +mod extract; +mod store; + +pub use self::store::{Id, Login, Logins, Provider}; diff --git a/src/login/repo/logins.rs b/src/repo/login/store.rs index 11ae50f..24dd744 100644 --- a/src/login/repo/logins.rs +++ b/src/repo/login/store.rs @@ -1,7 +1,6 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; -use crate::id::Id as BaseId; -use crate::login::app::StoredHash; +use crate::{id::Id as BaseId, password::StoredHash}; pub trait Provider { fn logins(&mut self) -> Logins; @@ -15,8 +14,10 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Logins<'t>(&'t mut SqliteConnection); -// This also implements FromRequestParts (see `src/login/extract/login.rs`). As -// a result, it can be used as an extractor. +// This also implements FromRequestParts (see `./extract.rs`). As a result, it +// can be used as an extractor for endpoints that want to require login, or for +// endpoints that need to behave differently depending on whether the client is +// or is not logged in. #[derive(Clone, Debug, serde::Serialize)] pub struct Login { pub id: Id, @@ -71,39 +72,6 @@ impl<'c> Logins<'c> { Ok(login) } - - /// Retrieves a login by name, plus its stored password hash for - /// verification. If there's no login with the requested name, this will - /// return [None]. - pub async fn for_login( - &mut self, - name: &str, - ) -> Result<Option<(Login, StoredHash)>, sqlx::Error> { - let found = sqlx::query!( - r#" - select - id as "id: Id", - name, - password_hash as "password_hash: StoredHash" - from login - where name = $1 - "#, - name, - ) - .map(|rec| { - ( - Login { - id: rec.id, - name: rec.name, - }, - rec.password_hash, - ) - }) - .fetch_optional(&mut *self.0) - .await?; - - Ok(found) - } } impl<'t> From<&'t mut SqliteConnection> for Logins<'t> { diff --git a/src/repo/message.rs b/src/repo/message.rs new file mode 100644 index 0000000..e331a4e --- /dev/null +++ b/src/repo/message.rs @@ -0,0 +1,27 @@ +use std::fmt; + +use crate::id::Id as BaseId; + +/// Stable identifier for a [Message]. Prefixed with `M`. +#[derive(Clone, Debug, Eq, Hash, PartialEq, sqlx::Type, serde::Deserialize, serde::Serialize)] +#[sqlx(transparent)] +#[serde(transparent)] +pub struct Id(BaseId); + +impl From<BaseId> for Id { + fn from(id: BaseId) -> Self { + Self(id) + } +} + +impl Id { + pub fn generate() -> Self { + BaseId::generate("M") + } +} + +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs new file mode 100644 index 0000000..d8995a3 --- /dev/null +++ b/src/repo/mod.rs @@ -0,0 +1,4 @@ +pub mod channel; +pub mod login; +pub mod message; +pub mod token; diff --git a/src/login/repo/tokens.rs b/src/repo/token.rs index ec95f6a..e7eb273 100644 --- a/src/login/repo/tokens.rs +++ b/src/repo/token.rs @@ -2,7 +2,7 @@ use chrono::TimeDelta; use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use uuid::Uuid; -use super::logins::{Id as LoginId, Login}; +use super::login::{self, Login}; use crate::clock::DateTime; pub trait Provider { @@ -22,7 +22,7 @@ impl<'c> Tokens<'c> { /// be used to control expiry, until the token is actually used. pub async fn issue( &mut self, - login: &LoginId, + login: &login::Id, issued_at: DateTime, ) -> Result<String, sqlx::Error> { let secret = Uuid::new_v4().to_string(); @@ -109,7 +109,7 @@ impl<'c> Tokens<'c> { Login, r#" select - login.id as "id: LoginId", + login.id as "id: login::Id", name from login join token on login.id = token.login |
