diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2024-10-02 00:41:25 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2024-10-02 00:41:38 -0400 |
| commit | 357116366c1307bedaac6a3dfe9c5ed8e0e0c210 (patch) | |
| tree | d701378187d8b0f99d524991925e8348c6cab0d6 /src | |
| parent | f878f0b5eaa44e8ee8d67cbfd706926ff2119113 (diff) | |
First pass on reorganizing the backend.
This is primarily renames and repackagings.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.rs | 2 | ||||
| -rw-r--r-- | src/channel/app.rs | 9 | ||||
| -rw-r--r-- | src/channel/mod.rs | 14 | ||||
| -rw-r--r-- | src/channel/routes.rs | 6 | ||||
| -rw-r--r-- | src/channel/routes/test/on_create.rs | 2 | ||||
| -rw-r--r-- | src/channel/routes/test/on_send.rs | 2 | ||||
| -rw-r--r-- | src/cli.rs | 4 | ||||
| -rw-r--r-- | src/event/app.rs (renamed from src/events/app.rs) | 9 | ||||
| -rw-r--r-- | src/event/broadcaster.rs (renamed from src/events/broadcaster.rs) | 2 | ||||
| -rw-r--r-- | src/event/extract.rs (renamed from src/events/extract.rs) | 0 | ||||
| -rw-r--r-- | src/event/mod.rs (renamed from src/events/mod.rs) | 3 | ||||
| -rw-r--r-- | src/event/repo/message.rs (renamed from src/events/repo/message.rs) | 8 | ||||
| -rw-r--r-- | src/event/repo/mod.rs (renamed from src/events/repo/mod.rs) | 0 | ||||
| -rw-r--r-- | src/event/routes.rs (renamed from src/events/routes.rs) | 5 | ||||
| -rw-r--r-- | src/event/routes/test.rs (renamed from src/events/routes/test.rs) | 2 | ||||
| -rw-r--r-- | src/event/sequence.rs | 24 | ||||
| -rw-r--r-- | src/event/types.rs (renamed from src/events/types.rs) | 7 | ||||
| -rw-r--r-- | src/lib.rs | 4 | ||||
| -rw-r--r-- | src/login/app.rs | 17 | ||||
| -rw-r--r-- | src/login/extract.rs | 181 | ||||
| -rw-r--r-- | src/login/mod.rs | 18 | ||||
| -rw-r--r-- | src/login/password.rs (renamed from src/password.rs) | 0 | ||||
| -rw-r--r-- | src/login/repo/auth.rs | 2 | ||||
| -rw-r--r-- | src/login/routes.rs | 6 | ||||
| -rw-r--r-- | src/login/token/mod.rs | 3 | ||||
| -rw-r--r-- | src/login/types.rs | 2 | ||||
| -rw-r--r-- | src/message/mod.rs | 6 | ||||
| -rw-r--r-- | src/repo/channel.rs | 15 | ||||
| -rw-r--r-- | src/repo/login.rs (renamed from src/repo/login/store.rs) | 15 | ||||
| -rw-r--r-- | src/repo/login/extract.rs | 15 | ||||
| -rw-r--r-- | src/repo/login/mod.rs | 4 | ||||
| -rw-r--r-- | src/repo/message.rs | 7 | ||||
| -rw-r--r-- | src/repo/mod.rs | 1 | ||||
| -rw-r--r-- | src/repo/sequence.rs | 27 | ||||
| -rw-r--r-- | src/repo/token.rs | 10 | ||||
| -rw-r--r-- | src/test/fixtures/channel.rs | 2 | ||||
| -rw-r--r-- | src/test/fixtures/filter.rs | 2 | ||||
| -rw-r--r-- | src/test/fixtures/identity.rs | 9 | ||||
| -rw-r--r-- | src/test/fixtures/login.rs | 3 | ||||
| -rw-r--r-- | src/test/fixtures/message.rs | 7 | ||||
| -rw-r--r-- | src/token/extract/identity.rs | 75 | ||||
| -rw-r--r-- | src/token/extract/identity_token.rs | 94 | ||||
| -rw-r--r-- | src/token/extract/mod.rs | 4 | ||||
| -rw-r--r-- | src/token/id.rs (renamed from src/login/token/id.rs) | 0 | ||||
| -rw-r--r-- | src/token/mod.rs | 5 | ||||
| -rw-r--r-- | src/token/secret.rs | 27 |
46 files changed, 333 insertions, 327 deletions
@@ -2,7 +2,7 @@ use sqlx::sqlite::SqlitePool; use crate::{ channel::app::Channels, - events::{app::Events, broadcaster::Broadcaster as EventBroadcaster}, + event::{app::Events, broadcaster::Broadcaster as EventBroadcaster}, login::{app::Logins, broadcaster::Broadcaster as LoginBroadcaster}, }; diff --git a/src/channel/app.rs b/src/channel/app.rs index d89e733..1422651 100644 --- a/src/channel/app.rs +++ b/src/channel/app.rs @@ -2,12 +2,11 @@ use chrono::TimeDelta; use sqlx::sqlite::SqlitePool; use crate::{ + channel::Channel, clock::DateTime, - events::{broadcaster::Broadcaster, types::ChannelEvent}, - repo::{ - channel::{Channel, Provider as _}, - sequence::{Provider as _, Sequence}, - }, + event::Sequence, + event::{broadcaster::Broadcaster, types::ChannelEvent}, + repo::{channel::Provider as _, sequence::Provider as _}, }; pub struct Channels<'a> { diff --git a/src/channel/mod.rs b/src/channel/mod.rs index 3115e98..02d0ed4 100644 --- a/src/channel/mod.rs +++ b/src/channel/mod.rs @@ -1,7 +1,17 @@ +use crate::{clock::DateTime, event::Sequence}; + pub mod app; mod id; mod routes; -pub use self::routes::router; +pub use self::{id::Id, routes::router}; -pub use self::id::Id; +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct Channel { + pub id: Id, + pub name: String, + #[serde(skip)] + pub created_at: DateTime, + #[serde(skip)] + pub created_sequence: Sequence, +} diff --git a/src/channel/routes.rs b/src/channel/routes.rs index 72d6195..5d8b61e 100644 --- a/src/channel/routes.rs +++ b/src/channel/routes.rs @@ -10,11 +10,11 @@ use axum_extra::extract::Query; use super::app; use crate::{ app::App, - channel, + channel::{self, Channel}, clock::RequestedAt, error::Internal, - events::app::EventsError, - repo::{channel::Channel, login::Login, sequence::Sequence}, + event::{app::EventsError, Sequence}, + login::Login, }; #[cfg(test)] diff --git a/src/channel/routes/test/on_create.rs b/src/channel/routes/test/on_create.rs index 72980ac..9988932 100644 --- a/src/channel/routes/test/on_create.rs +++ b/src/channel/routes/test/on_create.rs @@ -3,7 +3,7 @@ use futures::stream::StreamExt as _; use crate::{ channel::{app, routes}, - events::types, + event::types, test::fixtures::{self, future::Immediately as _}, }; diff --git a/src/channel/routes/test/on_send.rs b/src/channel/routes/test/on_send.rs index 987784d..6f844cd 100644 --- a/src/channel/routes/test/on_send.rs +++ b/src/channel/routes/test/on_send.rs @@ -4,7 +4,7 @@ use futures::stream::StreamExt; use crate::{ channel, channel::routes, - events::{app, types}, + event::{app, types}, test::fixtures::{self, future::Immediately as _}, }; @@ -10,7 +10,7 @@ use clap::Parser; use sqlx::sqlite::SqlitePool; use tokio::net; -use crate::{app::App, channel, clock, events, expire, login, repo::pool}; +use crate::{app::App, channel, clock, event, expire, login, repo::pool}; /// Command-line entry point for running the `hi` server. /// @@ -105,7 +105,7 @@ impl Args { } fn routers() -> Router<App> { - [channel::router(), events::router(), login::router()] + [channel::router(), event::router(), login::router()] .into_iter() .fold(Router::default(), Router::merge) } diff --git a/src/events/app.rs b/src/event/app.rs index 1fa2f70..b5f2ecc 100644 --- a/src/events/app.rs +++ b/src/event/app.rs @@ -14,12 +14,9 @@ use super::{ use crate::{ channel, clock::DateTime, - repo::{ - channel::Provider as _, - error::NotFound as _, - login::Login, - sequence::{Provider as _, Sequence}, - }, + event::Sequence, + login::Login, + repo::{channel::Provider as _, error::NotFound as _, sequence::Provider as _}, }; pub struct Events<'a> { diff --git a/src/events/broadcaster.rs b/src/event/broadcaster.rs index 6b664cb..92f631f 100644 --- a/src/events/broadcaster.rs +++ b/src/event/broadcaster.rs @@ -1,3 +1,3 @@ -use crate::{broadcast, events::types}; +use crate::{broadcast, event::types}; pub type Broadcaster = broadcast::Broadcaster<types::ChannelEvent>; diff --git a/src/events/extract.rs b/src/event/extract.rs index e3021e2..e3021e2 100644 --- a/src/events/extract.rs +++ b/src/event/extract.rs diff --git a/src/events/mod.rs b/src/event/mod.rs index 711ae64..7ad3f9c 100644 --- a/src/events/mod.rs +++ b/src/event/mod.rs @@ -3,6 +3,7 @@ pub mod broadcaster; mod extract; pub mod repo; mod routes; +mod sequence; pub mod types; -pub use self::routes::router; +pub use self::{routes::router, sequence::Sequence}; diff --git a/src/events/repo/message.rs b/src/event/repo/message.rs index 00c24b1..f051fec 100644 --- a/src/events/repo/message.rs +++ b/src/event/repo/message.rs @@ -1,11 +1,11 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use crate::{ - channel, + channel::{self, Channel}, clock::DateTime, - events::types, - login, message, - repo::{channel::Channel, login::Login, message::Message, sequence::Sequence}, + event::{types, Sequence}, + login::{self, Login}, + message::{self, Message}, }; pub trait Provider { diff --git a/src/events/repo/mod.rs b/src/event/repo/mod.rs index e216a50..e216a50 100644 --- a/src/events/repo/mod.rs +++ b/src/event/repo/mod.rs diff --git a/src/events/routes.rs b/src/event/routes.rs index d81c7fb..77761ca 100644 --- a/src/events/routes.rs +++ b/src/event/routes.rs @@ -14,8 +14,9 @@ use super::{extract::LastEventId, types}; use crate::{ app::App, error::{Internal, Unauthorized}, - login::{app::ValidateError, extract::Identity}, - repo::sequence::Sequence, + event::Sequence, + login::app::ValidateError, + token::extract::Identity, }; #[cfg(test)] diff --git a/src/events/routes/test.rs b/src/event/routes/test.rs index 11f01b8..9a3b12a 100644 --- a/src/events/routes/test.rs +++ b/src/event/routes/test.rs @@ -6,7 +6,7 @@ use futures::{ }; use crate::{ - events::routes, + event::routes, test::fixtures::{self, future::Immediately as _}, }; diff --git a/src/event/sequence.rs b/src/event/sequence.rs new file mode 100644 index 0000000..9ebddd7 --- /dev/null +++ b/src/event/sequence.rs @@ -0,0 +1,24 @@ +use std::fmt; + +#[derive( + Clone, + Copy, + Debug, + Eq, + Ord, + PartialEq, + PartialOrd, + serde::Deserialize, + serde::Serialize, + sqlx::Type, +)] +#[serde(transparent)] +#[sqlx(transparent)] +pub struct Sequence(i64); + +impl fmt::Display for Sequence { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self(value) = self; + value.fmt(f) + } +} diff --git a/src/events/types.rs b/src/event/types.rs index 762b6e5..cd7dea6 100644 --- a/src/events/types.rs +++ b/src/event/types.rs @@ -1,8 +1,9 @@ use crate::{ - channel, + channel::{self, Channel}, clock::DateTime, - message, - repo::{channel::Channel, login::Login, message::Message, sequence::Sequence}, + event::Sequence, + login::Login, + message::{self, Message}, }; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] @@ -8,12 +8,12 @@ mod channel; pub mod cli; mod clock; mod error; -mod events; +mod event; mod expire; mod id; mod login; mod message; -mod password; mod repo; #[cfg(test)] mod test; +mod token; diff --git a/src/login/app.rs b/src/login/app.rs index 8ea0a91..60475af 100644 --- a/src/login/app.rs +++ b/src/login/app.rs @@ -6,18 +6,15 @@ use futures::{ }; use sqlx::sqlite::SqlitePool; -use super::{ - broadcaster::Broadcaster, extract::IdentitySecret, repo::auth::Provider as _, token, types, -}; +use super::{broadcaster::Broadcaster, repo::auth::Provider as _, types, Login}; use crate::{ clock::DateTime, - password::Password, + event::Sequence, + login::Password, repo::{ - error::NotFound as _, - login::{Login, Provider as _}, - sequence::{Provider as _, Sequence}, - token::Provider as _, + error::NotFound as _, login::Provider as _, sequence::Provider as _, token::Provider as _, }, + token::{self, Secret}, }; pub struct Logins<'a> { @@ -43,7 +40,7 @@ impl<'a> Logins<'a> { name: &str, password: &Password, login_at: &DateTime, - ) -> Result<IdentitySecret, LoginError> { + ) -> Result<Secret, LoginError> { let mut tx = self.db.begin().await?; let login = if let Some((login, stored_hash)) = tx.auth().for_name(name).await? { @@ -78,7 +75,7 @@ impl<'a> Logins<'a> { pub async fn validate( &self, - secret: &IdentitySecret, + secret: &Secret, used_at: &DateTime, ) -> Result<(token::Id, Login), ValidateError> { let mut tx = self.db.begin().await?; diff --git a/src/login/extract.rs b/src/login/extract.rs index 39dd9e4..c2d97f2 100644 --- a/src/login/extract.rs +++ b/src/login/extract.rs @@ -1,182 +1,15 @@ -use std::fmt; +use axum::{extract::FromRequestParts, http::request::Parts}; -use axum::{ - extract::{FromRequestParts, State}, - http::request::Parts, - response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, -}; -use axum_extra::extract::cookie::{Cookie, CookieJar}; - -use crate::{ - app::App, - clock::RequestedAt, - error::{Internal, Unauthorized}, - login::{app::ValidateError, token}, - repo::login::Login, -}; - -// The usage pattern here - receive the extractor as an argument, return it in -// the response - is heavily modelled after CookieJar's own intended usage. -#[derive(Clone)] -pub struct IdentityToken { - cookies: CookieJar, -} - -impl fmt::Debug for IdentityToken { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("IdentityToken") - .field( - "identity", - &self.cookies.get(IDENTITY_COOKIE).map(|_| "********"), - ) - .finish() - } -} - -impl IdentityToken { - // Creates a new, unpopulated identity token store. - #[cfg(test)] - pub fn new() -> Self { - Self { - cookies: CookieJar::new(), - } - } - - // Get the identity secret sent in the request, if any. If the identity - // was not sent, or if it has previously been [clear]ed, then this will - // return [None]. If the identity has previously been [set], then this - // will return that secret, regardless of what the request originally - // included. - pub fn secret(&self) -> Option<IdentitySecret> { - self.cookies - .get(IDENTITY_COOKIE) - .map(Cookie::value) - .map(IdentitySecret::from) - } - - // Positively set the identity secret, and ensure that it will be sent - // back to the client when this extractor is included in a response. - pub fn set(self, secret: impl Into<IdentitySecret>) -> Self { - let IdentitySecret(secret) = secret.into(); - let identity_cookie = Cookie::build((IDENTITY_COOKIE, secret)) - .http_only(true) - .path("/api/") - .permanent() - .build(); - - Self { - cookies: self.cookies.add(identity_cookie), - } - } - - // Remove the identity secret and ensure that it will be cleared when this - // extractor is included in a response. - pub fn clear(self) -> Self { - Self { - cookies: self.cookies.remove(IDENTITY_COOKIE), - } - } -} - -const IDENTITY_COOKIE: &str = "identity"; - -#[async_trait::async_trait] -impl<S> FromRequestParts<S> for IdentityToken -where - S: Send + Sync, -{ - type Rejection = <CookieJar as FromRequestParts<S>>::Rejection; - - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { - let cookies = CookieJar::from_request_parts(parts, state).await?; - Ok(Self { cookies }) - } -} - -impl IntoResponseParts for IdentityToken { - type Error = <CookieJar as IntoResponseParts>::Error; - - fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> { - let Self { cookies } = self; - cookies.into_response_parts(res) - } -} - -#[derive(sqlx::Type)] -#[sqlx(transparent)] -pub struct IdentitySecret(String); - -impl fmt::Debug for IdentitySecret { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("IdentityToken").field(&"********").finish() - } -} - -impl<S> From<S> for IdentitySecret -where - S: Into<String>, -{ - fn from(value: S) -> Self { - Self(value.into()) - } -} - -#[derive(Clone, Debug)] -pub struct Identity { - pub token: token::Id, - pub login: Login, -} +use super::Login; +use crate::{app::App, token::extract::Identity}; #[async_trait::async_trait] -impl FromRequestParts<App> for Identity { - type Rejection = LoginError<Internal>; +impl FromRequestParts<App> for Login { + type Rejection = <Identity as FromRequestParts<App>>::Rejection; async fn from_request_parts(parts: &mut Parts, state: &App) -> Result<Self, Self::Rejection> { - // After Rust 1.82 (and #[feature(min_exhaustive_patterns)] lands on - // stable), the following can be replaced: - // - // ``` - // let Ok(identity_token) = IdentityToken::from_request_parts( - // parts, - // state, - // ).await; - // ``` - let identity_token = IdentityToken::from_request_parts(parts, state).await?; - let RequestedAt(used_at) = RequestedAt::from_request_parts(parts, state).await?; - - let secret = identity_token.secret().ok_or(LoginError::Unauthorized)?; - - let app = State::<App>::from_request_parts(parts, state).await?; - match app.logins().validate(&secret, &used_at).await { - Ok((token, login)) => Ok(Identity { token, login }), - Err(ValidateError::InvalidToken) => Err(LoginError::Unauthorized), - Err(other) => Err(other.into()), - } - } -} - -pub enum LoginError<E> { - Failure(E), - Unauthorized, -} - -impl<E> IntoResponse for LoginError<E> -where - E: IntoResponse, -{ - fn into_response(self) -> Response { - match self { - Self::Unauthorized => Unauthorized.into_response(), - Self::Failure(e) => e.into_response(), - } - } -} + let identity = Identity::from_request_parts(parts, state).await?; -impl<E> From<E> for LoginError<Internal> -where - E: Into<Internal>, -{ - fn from(err: E) -> Self { - Self::Failure(err.into()) + Ok(identity.login) } } diff --git a/src/login/mod.rs b/src/login/mod.rs index 0430f4b..91c1821 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -2,10 +2,22 @@ pub mod app; pub mod broadcaster; pub mod extract; mod id; +pub mod password; mod repo; mod routes; -pub mod token; pub mod types; -pub use self::id::Id; -pub use self::routes::router; +pub use self::{id::Id, password::Password, routes::router}; + +// 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, Eq, PartialEq, serde::Serialize)] +pub struct Login { + pub id: Id, + pub name: String, + // The omission of the hashed password is deliberate, to minimize the + // chance that it ends up tangled up in debug output or in some other chunk + // of logic elsewhere. +} diff --git a/src/password.rs b/src/login/password.rs index da3930f..da3930f 100644 --- a/src/password.rs +++ b/src/login/password.rs diff --git a/src/login/repo/auth.rs b/src/login/repo/auth.rs index 9816c5c..b299697 100644 --- a/src/login/repo/auth.rs +++ b/src/login/repo/auth.rs @@ -1,6 +1,6 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; -use crate::{login, password::StoredHash, repo::login::Login}; +use crate::login::{self, password::StoredHash, Login}; pub trait Provider { fn auth(&mut self) -> Auth; diff --git a/src/login/routes.rs b/src/login/routes.rs index ef75871..b571bd5 100644 --- a/src/login/routes.rs +++ b/src/login/routes.rs @@ -10,11 +10,11 @@ use crate::{ app::App, clock::RequestedAt, error::{Internal, Unauthorized}, - password::Password, - repo::login::Login, + login::{Login, Password}, }; -use super::{app, extract::IdentityToken}; +use super::app; +use crate::token::extract::IdentityToken; #[cfg(test)] mod test; diff --git a/src/login/token/mod.rs b/src/login/token/mod.rs deleted file mode 100644 index d563a88..0000000 --- a/src/login/token/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod id; - -pub use self::id::Id; diff --git a/src/login/types.rs b/src/login/types.rs index a210977..d53d436 100644 --- a/src/login/types.rs +++ b/src/login/types.rs @@ -1,4 +1,4 @@ -use crate::login::token; +use crate::token; #[derive(Clone, Debug)] pub struct TokenRevoked { diff --git a/src/message/mod.rs b/src/message/mod.rs index d563a88..9a9bf14 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -1,3 +1,9 @@ mod id; pub use self::id::Id; + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct Message { + pub id: Id, + pub body: String, +} diff --git a/src/repo/channel.rs b/src/repo/channel.rs index 9f1d930..18cd81f 100644 --- a/src/repo/channel.rs +++ b/src/repo/channel.rs @@ -1,10 +1,9 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; -use super::sequence::Sequence; use crate::{ - channel::Id, + channel::{Channel, Id}, clock::DateTime, - events::types::{self}, + event::{types, Sequence}, }; pub trait Provider { @@ -19,16 +18,6 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Channels<'t>(&'t mut SqliteConnection); -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -pub struct Channel { - pub id: Id, - pub name: String, - #[serde(skip)] - pub created_at: DateTime, - #[serde(skip)] - pub created_sequence: Sequence, -} - impl<'c> Channels<'c> { pub async fn create( &mut self, diff --git a/src/repo/login/store.rs b/src/repo/login.rs index 47d1a7c..d1a02c4 100644 --- a/src/repo/login/store.rs +++ b/src/repo/login.rs @@ -1,6 +1,6 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; -use crate::{login::Id, password::StoredHash}; +use crate::login::{password::StoredHash, Id, Login}; pub trait Provider { fn logins(&mut self) -> Logins; @@ -14,19 +14,6 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Logins<'t>(&'t mut SqliteConnection); -// 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, Eq, PartialEq, serde::Serialize)] -pub struct Login { - pub id: Id, - pub name: String, - // The omission of the hashed password is deliberate, to minimize the - // chance that it ends up tangled up in debug output or in some other chunk - // of logic elsewhere. -} - impl<'c> Logins<'c> { pub async fn create( &mut self, diff --git a/src/repo/login/extract.rs b/src/repo/login/extract.rs deleted file mode 100644 index ab61106..0000000 --- a/src/repo/login/extract.rs +++ /dev/null @@ -1,15 +0,0 @@ -use axum::{extract::FromRequestParts, http::request::Parts}; - -use super::Login; -use crate::{app::App, login::extract::Identity}; - -#[async_trait::async_trait] -impl FromRequestParts<App> for Login { - type Rejection = <Identity as FromRequestParts<App>>::Rejection; - - async fn from_request_parts(parts: &mut Parts, state: &App) -> Result<Self, Self::Rejection> { - let identity = Identity::from_request_parts(parts, state).await?; - - Ok(identity.login) - } -} diff --git a/src/repo/login/mod.rs b/src/repo/login/mod.rs deleted file mode 100644 index 4ff7a96..0000000 --- a/src/repo/login/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod extract; -mod store; - -pub use self::store::{Login, Provider}; diff --git a/src/repo/message.rs b/src/repo/message.rs deleted file mode 100644 index acde3ea..0000000 --- a/src/repo/message.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::message::Id; - -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -pub struct Message { - pub id: Id, - pub body: String, -} diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 8f271f4..69ad82c 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,7 +1,6 @@ pub mod channel; pub mod error; pub mod login; -pub mod message; pub mod pool; pub mod sequence; pub mod token; diff --git a/src/repo/sequence.rs b/src/repo/sequence.rs index c47b41c..c985869 100644 --- a/src/repo/sequence.rs +++ b/src/repo/sequence.rs @@ -1,7 +1,7 @@ -use std::fmt; - use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use crate::event::Sequence; + pub trait Provider { fn sequence(&mut self) -> Sequences; } @@ -42,26 +42,3 @@ impl<'c> Sequences<'c> { Ok(next) } } - -#[derive( - Clone, - Copy, - Debug, - Eq, - Ord, - PartialEq, - PartialOrd, - serde::Deserialize, - serde::Serialize, - sqlx::Type, -)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct Sequence(i64); - -impl fmt::Display for Sequence { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self(value) = self; - value.fmt(f) - } -} diff --git a/src/repo/token.rs b/src/repo/token.rs index 79e5c54..5f64dac 100644 --- a/src/repo/token.rs +++ b/src/repo/token.rs @@ -1,10 +1,10 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use uuid::Uuid; -use super::login::Login; use crate::{ clock::DateTime, - login::{self, extract::IdentitySecret, token::Id}, + login::{self, Login}, + token::{Id, Secret}, }; pub trait Provider { @@ -26,7 +26,7 @@ impl<'c> Tokens<'c> { &mut self, login: &Login, issued_at: &DateTime, - ) -> Result<IdentitySecret, sqlx::Error> { + ) -> Result<Secret, sqlx::Error> { let id = Id::generate(); let secret = Uuid::new_v4().to_string(); @@ -35,7 +35,7 @@ impl<'c> Tokens<'c> { insert into token (id, secret, login, issued_at, last_used_at) values ($1, $2, $3, $4, $4) - returning secret as "secret!: IdentitySecret" + returning secret as "secret!: Secret" "#, id, secret, @@ -103,7 +103,7 @@ impl<'c> Tokens<'c> { // timestamp will be set to `used_at`. pub async fn validate( &mut self, - secret: &IdentitySecret, + secret: &Secret, used_at: &DateTime, ) -> Result<(Id, Login), sqlx::Error> { // I would use `update … returning` to do this in one query, but diff --git a/src/test/fixtures/channel.rs b/src/test/fixtures/channel.rs index 8744470..b678717 100644 --- a/src/test/fixtures/channel.rs +++ b/src/test/fixtures/channel.rs @@ -4,7 +4,7 @@ use faker_rand::{ }; use rand; -use crate::{app::App, clock::RequestedAt, repo::channel::Channel}; +use crate::{app::App, channel::Channel, clock::RequestedAt}; pub async fn create(app: &App, created_at: &RequestedAt) -> Channel { let name = propose(); diff --git a/src/test/fixtures/filter.rs b/src/test/fixtures/filter.rs index c31fa58..d1939a5 100644 --- a/src/test/fixtures/filter.rs +++ b/src/test/fixtures/filter.rs @@ -1,6 +1,6 @@ use futures::future; -use crate::events::types; +use crate::event::types; pub fn messages() -> impl FnMut(&types::ChannelEvent) -> future::Ready<bool> { |event| future::ready(matches!(event.data, types::ChannelEventData::Message(_))) diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs index 633fb8a..9e8e403 100644 --- a/src/test/fixtures/identity.rs +++ b/src/test/fixtures/identity.rs @@ -3,8 +3,11 @@ use uuid::Uuid; use crate::{ app::App, clock::RequestedAt, - login::extract::{Identity, IdentitySecret, IdentityToken}, - password::Password, + login::Password, + token::{ + extract::{Identity, IdentityToken}, + Secret, + }, }; pub fn not_logged_in() -> IdentityToken { @@ -38,7 +41,7 @@ pub async fn identity(app: &App, login: &(String, Password), issued_at: &Request from_token(app, &secret, issued_at).await } -pub fn secret(identity: &IdentityToken) -> IdentitySecret { +pub fn secret(identity: &IdentityToken) -> Secret { identity.secret().expect("identity contained a secret") } diff --git a/src/test/fixtures/login.rs b/src/test/fixtures/login.rs index d6a321b..00c2789 100644 --- a/src/test/fixtures/login.rs +++ b/src/test/fixtures/login.rs @@ -3,8 +3,7 @@ use uuid::Uuid; use crate::{ app::App, - password::Password, - repo::login::{self, Login}, + login::{self, Login, Password}, }; pub async fn create_with_password(app: &App) -> (String, Password) { diff --git a/src/test/fixtures/message.rs b/src/test/fixtures/message.rs index bfca8cd..fd50887 100644 --- a/src/test/fixtures/message.rs +++ b/src/test/fixtures/message.rs @@ -1,11 +1,6 @@ use faker_rand::lorem::Paragraphs; -use crate::{ - app::App, - clock::RequestedAt, - events::types, - repo::{channel::Channel, login::Login}, -}; +use crate::{app::App, channel::Channel, clock::RequestedAt, event::types, login::Login}; pub async fn send( app: &App, diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs new file mode 100644 index 0000000..42c7c60 --- /dev/null +++ b/src/token/extract/identity.rs @@ -0,0 +1,75 @@ +use axum::{ + extract::{FromRequestParts, State}, + http::request::Parts, + response::{IntoResponse, Response}, +}; + +use super::IdentityToken; + +use crate::{ + app::App, + clock::RequestedAt, + error::{Internal, Unauthorized}, + login::{app::ValidateError, Login}, + token, +}; + +#[derive(Clone, Debug)] +pub struct Identity { + pub token: token::Id, + pub login: Login, +} + +#[async_trait::async_trait] +impl FromRequestParts<App> for Identity { + type Rejection = LoginError<Internal>; + + async fn from_request_parts(parts: &mut Parts, state: &App) -> Result<Self, Self::Rejection> { + // After Rust 1.82 (and #[feature(min_exhaustive_patterns)] lands on + // stable), the following can be replaced: + // + // ``` + // let Ok(identity_token) = IdentityToken::from_request_parts( + // parts, + // state, + // ).await; + // ``` + let identity_token = IdentityToken::from_request_parts(parts, state).await?; + let RequestedAt(used_at) = RequestedAt::from_request_parts(parts, state).await?; + + let secret = identity_token.secret().ok_or(LoginError::Unauthorized)?; + + let app = State::<App>::from_request_parts(parts, state).await?; + match app.logins().validate(&secret, &used_at).await { + Ok((token, login)) => Ok(Identity { token, login }), + Err(ValidateError::InvalidToken) => Err(LoginError::Unauthorized), + Err(other) => Err(other.into()), + } + } +} + +pub enum LoginError<E> { + Failure(E), + Unauthorized, +} + +impl<E> IntoResponse for LoginError<E> +where + E: IntoResponse, +{ + fn into_response(self) -> Response { + match self { + Self::Unauthorized => Unauthorized.into_response(), + Self::Failure(e) => e.into_response(), + } + } +} + +impl<E> From<E> for LoginError<Internal> +where + E: Into<Internal>, +{ + fn from(err: E) -> Self { + Self::Failure(err.into()) + } +} diff --git a/src/token/extract/identity_token.rs b/src/token/extract/identity_token.rs new file mode 100644 index 0000000..0a47a43 --- /dev/null +++ b/src/token/extract/identity_token.rs @@ -0,0 +1,94 @@ +use std::fmt; + +use axum::{ + extract::FromRequestParts, + http::request::Parts, + response::{IntoResponseParts, ResponseParts}, +}; +use axum_extra::extract::cookie::{Cookie, CookieJar}; + +use crate::token::Secret; + +// The usage pattern here - receive the extractor as an argument, return it in +// the response - is heavily modelled after CookieJar's own intended usage. +#[derive(Clone)] +pub struct IdentityToken { + cookies: CookieJar, +} + +impl fmt::Debug for IdentityToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IdentityToken") + .field("identity", &self.secret()) + .finish() + } +} + +impl IdentityToken { + // Creates a new, unpopulated identity token store. + #[cfg(test)] + pub fn new() -> Self { + Self { + cookies: CookieJar::new(), + } + } + + // Get the identity secret sent in the request, if any. If the identity + // was not sent, or if it has previously been [clear]ed, then this will + // return [None]. If the identity has previously been [set], then this + // will return that secret, regardless of what the request originally + // included. + pub fn secret(&self) -> Option<Secret> { + self.cookies + .get(IDENTITY_COOKIE) + .map(Cookie::value) + .map(Secret::from) + } + + // Positively set the identity secret, and ensure that it will be sent + // back to the client when this extractor is included in a response. + pub fn set(self, secret: impl Into<Secret>) -> Self { + let secret = secret.into().reveal(); + let identity_cookie = Cookie::build((IDENTITY_COOKIE, secret)) + .http_only(true) + .path("/api/") + .permanent() + .build(); + + Self { + cookies: self.cookies.add(identity_cookie), + } + } + + // Remove the identity secret and ensure that it will be cleared when this + // extractor is included in a response. + pub fn clear(self) -> Self { + Self { + cookies: self.cookies.remove(IDENTITY_COOKIE), + } + } +} + +const IDENTITY_COOKIE: &str = "identity"; + +#[async_trait::async_trait] +impl<S> FromRequestParts<S> for IdentityToken +where + S: Send + Sync, +{ + type Rejection = <CookieJar as FromRequestParts<S>>::Rejection; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { + let cookies = CookieJar::from_request_parts(parts, state).await?; + Ok(Self { cookies }) + } +} + +impl IntoResponseParts for IdentityToken { + type Error = <CookieJar as IntoResponseParts>::Error; + + fn into_response_parts(self, res: ResponseParts) -> Result<ResponseParts, Self::Error> { + let Self { cookies } = self; + cookies.into_response_parts(res) + } +} diff --git a/src/token/extract/mod.rs b/src/token/extract/mod.rs new file mode 100644 index 0000000..b4800ae --- /dev/null +++ b/src/token/extract/mod.rs @@ -0,0 +1,4 @@ +mod identity; +mod identity_token; + +pub use self::{identity::Identity, identity_token::IdentityToken}; diff --git a/src/login/token/id.rs b/src/token/id.rs index 9ef063c..9ef063c 100644 --- a/src/login/token/id.rs +++ b/src/token/id.rs diff --git a/src/token/mod.rs b/src/token/mod.rs new file mode 100644 index 0000000..c98b8c2 --- /dev/null +++ b/src/token/mod.rs @@ -0,0 +1,5 @@ +pub mod extract; +mod id; +mod secret; + +pub use self::{id::Id, secret::Secret}; diff --git a/src/token/secret.rs b/src/token/secret.rs new file mode 100644 index 0000000..28c93bb --- /dev/null +++ b/src/token/secret.rs @@ -0,0 +1,27 @@ +use std::fmt; + +#[derive(sqlx::Type)] +#[sqlx(transparent)] +pub struct Secret(String); + +impl Secret { + pub fn reveal(self) -> String { + let Self(secret) = self; + secret + } +} + +impl fmt::Debug for Secret { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("IdentityToken").field(&"********").finish() + } +} + +impl<S> From<S> for Secret +where + S: Into<String>, +{ + fn from(value: S) -> Self { + Self(value.into()) + } +} |
