From 38ac83aef9667f1a4fe86e03e53565376081179f Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 17:47:30 -0400 Subject: Convert `Logins` into a freestanding component. --- src/login/handlers/login/mod.rs | 14 +++++++++----- src/login/handlers/login/test.rs | 8 ++++---- src/login/handlers/password/mod.rs | 8 +++----- src/login/handlers/password/test.rs | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) (limited to 'src/login/handlers') diff --git a/src/login/handlers/login/mod.rs b/src/login/handlers/login/mod.rs index 6591984..2ce8a67 100644 --- a/src/login/handlers/login/mod.rs +++ b/src/login/handlers/login/mod.rs @@ -5,21 +5,25 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::Internal, login::app, name::Name, - password::Password, token::extract::IdentityCookie, + clock::RequestedAt, + empty::Empty, + error::Internal, + login::{app, app::Logins}, + name::Name, + password::Password, + token::extract::IdentityCookie, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(logins): State, RequestedAt(now): RequestedAt, identity: IdentityCookie, Json(request): Json, ) -> Result<(IdentityCookie, Empty), Error> { - let secret = app - .logins() + let secret = logins .with_password(&request.name, &request.password, &now) .await .map_err(Error)?; diff --git a/src/login/handlers/login/test.rs b/src/login/handlers/login/test.rs index f3911d0..7bb56b6 100644 --- a/src/login/handlers/login/test.rs +++ b/src/login/handlers/login/test.rs @@ -22,7 +22,7 @@ async fn correct_credentials() { password, }; let (identity, Empty) = - super::handler(State(app.clone()), logged_in_at, identity, Json(request)) + super::handler(State(app.logins()), logged_in_at, identity, Json(request)) .await .expect("logged in with valid credentials"); @@ -52,7 +52,7 @@ async fn invalid_name() { password, }; let super::Error(error) = - super::handler(State(app.clone()), logged_in_at, identity, Json(request)) + super::handler(State(app.logins()), logged_in_at, identity, Json(request)) .await .expect_err("logged in with an incorrect password fails"); @@ -77,7 +77,7 @@ async fn incorrect_password() { password: fixtures::user::propose_password(), }; let super::Error(error) = - super::handler(State(app.clone()), logged_in_at, identity, Json(request)) + super::handler(State(app.logins()), logged_in_at, identity, Json(request)) .await .expect_err("logged in with an incorrect password"); @@ -98,7 +98,7 @@ async fn token_expires() { let logged_in_at = fixtures::ancient(); let identity = fixtures::cookie::not_logged_in(); let request = super::Request { name, password }; - let (identity, _) = super::handler(State(app.clone()), logged_in_at, identity, Json(request)) + let (identity, _) = super::handler(State(app.logins()), logged_in_at, identity, Json(request)) .await .expect("logged in with valid credentials"); let secret = identity.secret().expect("logged in with valid credentials"); diff --git a/src/login/handlers/password/mod.rs b/src/login/handlers/password/mod.rs index 94c7fb4..8b82605 100644 --- a/src/login/handlers/password/mod.rs +++ b/src/login/handlers/password/mod.rs @@ -5,11 +5,10 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::Internal, - login::app, + login::{app, app::Logins}, password::Password, token::extract::{Identity, IdentityCookie}, }; @@ -18,14 +17,13 @@ use crate::{ mod test; pub async fn handler( - State(app): State, + State(logins): State, RequestedAt(now): RequestedAt, identity: Identity, cookie: IdentityCookie, Json(request): Json, ) -> Result<(IdentityCookie, Empty), Error> { - let secret = app - .logins() + let secret = logins .change_password(&identity.login, &request.password, &request.to, &now) .await .map_err(Error)?; diff --git a/src/login/handlers/password/test.rs b/src/login/handlers/password/test.rs index ba2f28f..61d5b5a 100644 --- a/src/login/handlers/password/test.rs +++ b/src/login/handlers/password/test.rs @@ -21,7 +21,7 @@ async fn password_change() { to: to.clone(), }; let (new_cookie, Empty) = super::handler( - State(app.clone()), + State(app.logins()), fixtures::now(), identity.clone(), cookie.clone(), -- cgit v1.2.3 From be21b088f0d1b591cbd8dcfed1e06f2742a524d0 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 18:23:34 -0400 Subject: Convert the `Tokens` component into a freestanding struct. As with the `Setup` component, I've generalized the associated middleware across anything that can provide a `Tokens`, where possible. --- src/app.rs | 10 ++++++++-- src/login/handlers/logout/mod.rs | 9 ++++----- src/login/handlers/logout/test.rs | 25 +++++++++++++++++-------- src/test/verify/identity.rs | 26 +++++++++++++++++++------- src/test/verify/token.rs | 29 ++++++++++++++++++----------- src/token/app.rs | 10 +++++----- src/token/extract/identity.rs | 24 +++++++++++++++++------- 7 files changed, 88 insertions(+), 45 deletions(-) (limited to 'src/login/handlers') diff --git a/src/app.rs b/src/app.rs index 202d542..6f6e8ba 100644 --- a/src/app.rs +++ b/src/app.rs @@ -62,8 +62,8 @@ impl App { Setup::new(self.db.clone(), self.events.clone()) } - pub const fn tokens(&self) -> Tokens<'_> { - Tokens::new(&self.db, &self.token_events) + pub fn tokens(&self) -> Tokens { + Tokens::new(self.db.clone(), self.token_events.clone()) } #[cfg(test)] @@ -107,3 +107,9 @@ impl FromRef for Setup { app.setup() } } + +impl FromRef for Tokens { + fn from_ref(app: &App) -> Self { + app.tokens() + } +} diff --git a/src/login/handlers/logout/mod.rs b/src/login/handlers/logout/mod.rs index 73efe73..ce4cb1a 100644 --- a/src/login/handlers/logout/mod.rs +++ b/src/login/handlers/logout/mod.rs @@ -4,25 +4,24 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::{Internal, Unauthorized}, - token::{app, extract::IdentityCookie}, + token::{app, app::Tokens, extract::IdentityCookie}, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(tokens): State, RequestedAt(now): RequestedAt, identity: IdentityCookie, Json(_): Json, ) -> Result<(IdentityCookie, Empty), Error> { if let Some(secret) = identity.secret() { - let identity = app.tokens().validate(&secret, &now).await?; - app.tokens().logout(&identity.token).await?; + let identity = tokens.validate(&secret, &now).await?; + tokens.logout(&identity.token).await?; } let identity = identity.clear(); diff --git a/src/login/handlers/logout/test.rs b/src/login/handlers/logout/test.rs index e7b7dd4..18744ed 100644 --- a/src/login/handlers/logout/test.rs +++ b/src/login/handlers/logout/test.rs @@ -18,7 +18,7 @@ async fn successful() { // Call the endpoint let (response_identity, Empty) = super::handler( - State(app.clone()), + State(app.tokens()), fixtures::now(), identity.clone(), Json::default(), @@ -42,9 +42,14 @@ async fn no_identity() { // Call the endpoint let identity = fixtures::cookie::not_logged_in(); - let (identity, Empty) = super::handler(State(app), fixtures::now(), identity, Json::default()) - .await - .expect("logged out with no token succeeds"); + let (identity, Empty) = super::handler( + State(app.tokens()), + fixtures::now(), + identity, + Json::default(), + ) + .await + .expect("logged out with no token succeeds"); // Verify the return value's basic structure @@ -60,10 +65,14 @@ async fn invalid_token() { // Call the endpoint let identity = fixtures::cookie::fictitious(); - let super::Error(error) = - super::handler(State(app), fixtures::now(), identity, Json::default()) - .await - .expect_err("logged out with an invalid token fails"); + let super::Error(error) = super::handler( + State(app.tokens()), + fixtures::now(), + identity, + Json::default(), + ) + .await + .expect_err("logged out with an invalid token fails"); // Verify the return value's basic structure diff --git a/src/test/verify/identity.rs b/src/test/verify/identity.rs index 8e2d36e..fba2a4d 100644 --- a/src/test/verify/identity.rs +++ b/src/test/verify/identity.rs @@ -1,31 +1,43 @@ +use axum::extract::FromRef; + use crate::{ - app::App, login::Login, name::Name, test::{fixtures, verify}, - token::{app::ValidateError, extract::IdentityCookie}, + token::{ + app::{Tokens, ValidateError}, + extract::IdentityCookie, + }, }; -pub async fn valid_for_name(app: &App, identity: &IdentityCookie, name: &Name) { +pub async fn valid_for_name(app: &App, identity: &IdentityCookie, name: &Name) +where + Tokens: FromRef, +{ let secret = identity .secret() .expect("identity cookie must be set to be valid"); verify::token::valid_for_name(app, &secret, name).await; } -pub async fn valid_for_login(app: &App, identity: &IdentityCookie, login: &Login) { +pub async fn valid_for_login(app: &App, identity: &IdentityCookie, login: &Login) +where + Tokens: FromRef, +{ let secret = identity .secret() .expect("identity cookie must be set to be valid"); verify::token::valid_for_login(app, &secret, login).await; } -pub async fn invalid(app: &App, identity: &IdentityCookie) { +pub async fn invalid(app: &App, identity: &IdentityCookie) +where + Tokens: FromRef, +{ let secret = identity .secret() .expect("identity cookie must be set to be invalid"); - let validate_err = app - .tokens() + let validate_err = Tokens::from_ref(app) .validate(&secret, &fixtures::now()) .await .expect_err("identity cookie secret must be invalid"); diff --git a/src/test/verify/token.rs b/src/test/verify/token.rs index adc4397..1b61a19 100644 --- a/src/test/verify/token.rs +++ b/src/test/verify/token.rs @@ -1,32 +1,39 @@ +use axum::extract::FromRef; + use crate::{ - app::App, login::Login, name::Name, test::fixtures, - token::{Secret, app}, + token::{Secret, app, app::Tokens}, }; -pub async fn valid_for_name(app: &App, secret: &Secret, name: &Name) { - let identity = app - .tokens() +pub async fn valid_for_name(app: &App, secret: &Secret, name: &Name) +where + Tokens: FromRef, +{ + let identity = Tokens::from_ref(app) .validate(secret, &fixtures::now()) .await .expect("provided secret is valid"); assert_eq!(name, &identity.login.name); } -pub async fn valid_for_login(app: &App, secret: &Secret, login: &Login) { - let identity = app - .tokens() +pub async fn valid_for_login(app: &App, secret: &Secret, login: &Login) +where + Tokens: FromRef, +{ + let identity = Tokens::from_ref(app) .validate(secret, &fixtures::now()) .await .expect("provided secret is valid"); assert_eq!(login, &identity.login); } -pub async fn invalid(app: &App, secret: &Secret) { - let error = app - .tokens() +pub async fn invalid(app: &App, secret: &Secret) +where + Tokens: FromRef, +{ + let error = Tokens::from_ref(app) .validate(secret, &fixtures::now()) .await .expect_err("provided secret is invalid"); diff --git a/src/token/app.rs b/src/token/app.rs index 1d68f32..332473d 100644 --- a/src/token/app.rs +++ b/src/token/app.rs @@ -12,13 +12,13 @@ use super::{ }; use crate::{clock::DateTime, db::NotFound as _, name}; -pub struct Tokens<'a> { - db: &'a SqlitePool, - token_events: &'a Broadcaster, +pub struct Tokens { + db: SqlitePool, + token_events: Broadcaster, } -impl<'a> Tokens<'a> { - pub const fn new(db: &'a SqlitePool, token_events: &'a Broadcaster) -> Self { +impl Tokens { + pub const fn new(db: SqlitePool, token_events: Broadcaster) -> Self { Self { db, token_events } } diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs index bee4e31..5c004ef 100644 --- a/src/token/extract/identity.rs +++ b/src/token/extract/identity.rs @@ -1,16 +1,18 @@ use axum::{ - extract::{FromRequestParts, OptionalFromRequestParts, State}, + extract::{FromRef, FromRequestParts, OptionalFromRequestParts, State}, http::request::Parts, response::{IntoResponse, Response}, }; use super::IdentityCookie; use crate::{ - app::App, clock::RequestedAt, error::{Internal, Unauthorized}, login::Login, - token::{Token, app::ValidateError}, + token::{ + Token, + app::{Tokens, ValidateError}, + }, }; #[derive(Clone, Debug)] @@ -19,7 +21,11 @@ pub struct Identity { pub login: Login, } -impl FromRequestParts for Identity { +impl FromRequestParts for Identity +where + Tokens: FromRef, + App: Send + Sync, +{ type Rejection = LoginError; async fn from_request_parts(parts: &mut Parts, state: &App) -> Result { @@ -28,8 +34,8 @@ impl FromRequestParts for Identity { let secret = cookie.secret().ok_or(LoginError::Unauthorized)?; - let app = State::::from_request_parts(parts, state).await?; - app.tokens() + let tokens = State::::from_request_parts(parts, state).await?; + tokens .validate(&secret, &used_at) .await .map_err(|err| match err { @@ -39,7 +45,11 @@ impl FromRequestParts for Identity { } } -impl OptionalFromRequestParts for Identity { +impl OptionalFromRequestParts for Identity +where + Tokens: FromRef, + App: Send + Sync, +{ type Rejection = LoginError; async fn from_request_parts( -- cgit v1.2.3