diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2024-10-16 20:14:33 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2024-10-16 20:14:33 -0400 |
| commit | ea74daca4809e4008dd8d01039db9fff3be659d9 (patch) | |
| tree | 5972cabf646e8d5e635e9e2a176bff56c178461a /src/login | |
| parent | 56e16e29db55dae84549229d24b971f8bcf7da21 (diff) | |
Organizational pass on endpoints and routes.
Diffstat (limited to 'src/login')
| -rw-r--r-- | src/login/app.rs | 4 | ||||
| -rw-r--r-- | src/login/routes.rs | 97 | ||||
| -rw-r--r-- | src/login/routes/login/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/routes/login/post.rs | 51 | ||||
| -rw-r--r-- | src/login/routes/login/test.rs (renamed from src/login/routes/test/login.rs) | 23 | ||||
| -rw-r--r-- | src/login/routes/logout/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/routes/logout/post.rs | 47 | ||||
| -rw-r--r-- | src/login/routes/logout/test.rs (renamed from src/login/routes/test/logout.rs) | 41 | ||||
| -rw-r--r-- | src/login/routes/mod.rs | 12 | ||||
| -rw-r--r-- | src/login/routes/test/mod.rs | 2 |
10 files changed, 144 insertions, 141 deletions
diff --git a/src/login/app.rs b/src/login/app.rs index bb1419b..b6f7e1c 100644 --- a/src/login/app.rs +++ b/src/login/app.rs @@ -39,6 +39,6 @@ impl<'a> Logins<'a> { #[derive(Debug, thiserror::Error)] #[error(transparent)] pub enum CreateError { - DatabaseError(#[from] sqlx::Error), - PasswordHashError(#[from] password_hash::Error), + Database(#[from] sqlx::Error), + PasswordHash(#[from] password_hash::Error), } diff --git a/src/login/routes.rs b/src/login/routes.rs deleted file mode 100644 index 6579ae6..0000000 --- a/src/login/routes.rs +++ /dev/null @@ -1,97 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, - response::{IntoResponse, Response}, - routing::post, - Router, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::{Internal, Unauthorized}, - login::Password, - token::{app, extract::IdentityToken}, -}; - -#[cfg(test)] -mod test; - -pub fn router() -> Router<App> { - Router::new() - .route("/api/auth/login", post(on_login)) - .route("/api/auth/logout", post(on_logout)) -} - -#[derive(serde::Deserialize)] -struct LoginRequest { - name: String, - password: Password, -} - -async fn on_login( - State(app): State<App>, - RequestedAt(now): RequestedAt, - identity: IdentityToken, - Json(request): Json<LoginRequest>, -) -> Result<(IdentityToken, StatusCode), LoginError> { - let token = app - .tokens() - .login(&request.name, &request.password, &now) - .await - .map_err(LoginError)?; - let identity = identity.set(token); - Ok((identity, StatusCode::NO_CONTENT)) -} - -#[derive(Debug)] -struct LoginError(app::LoginError); - -impl IntoResponse for LoginError { - fn into_response(self) -> Response { - let Self(error) = self; - match error { - app::LoginError::Rejected => { - // not error::Unauthorized due to differing messaging - (StatusCode::UNAUTHORIZED, "invalid name or password").into_response() - } - other => Internal::from(other).into_response(), - } - } -} - -#[derive(serde::Deserialize)] -struct LogoutRequest {} - -async fn on_logout( - State(app): State<App>, - RequestedAt(now): RequestedAt, - identity: IdentityToken, - // This forces the only valid request to be `{}`, and not the infinite - // variation allowed when there's no body extractor. - Json(LogoutRequest {}): Json<LogoutRequest>, -) -> Result<(IdentityToken, StatusCode), LogoutError> { - if let Some(secret) = identity.secret() { - let (token, _) = app.tokens().validate(&secret, &now).await?; - app.tokens().logout(&token).await?; - } - - let identity = identity.clear(); - Ok((identity, StatusCode::NO_CONTENT)) -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -enum LogoutError { - ValidateError(#[from] app::ValidateError), - DatabaseError(#[from] sqlx::Error), -} - -impl IntoResponse for LogoutError { - fn into_response(self) -> Response { - match self { - Self::ValidateError(app::ValidateError::InvalidToken) => Unauthorized.into_response(), - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/login/routes/login/mod.rs b/src/login/routes/login/mod.rs new file mode 100644 index 0000000..36b384e --- /dev/null +++ b/src/login/routes/login/mod.rs @@ -0,0 +1,4 @@ +pub mod post; + +#[cfg(test)] +mod test; diff --git a/src/login/routes/login/post.rs b/src/login/routes/login/post.rs new file mode 100644 index 0000000..e33acad --- /dev/null +++ b/src/login/routes/login/post.rs @@ -0,0 +1,51 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + clock::RequestedAt, + error::Internal, + login::Password, + token::{app, extract::IdentityToken}, +}; + +pub async fn handler( + State(app): State<App>, + RequestedAt(now): RequestedAt, + identity: IdentityToken, + Json(request): Json<Request>, +) -> Result<(IdentityToken, StatusCode), Error> { + let token = app + .tokens() + .login(&request.name, &request.password, &now) + .await + .map_err(Error)?; + let identity = identity.set(token); + Ok((identity, StatusCode::NO_CONTENT)) +} + +#[derive(serde::Deserialize)] +pub struct Request { + pub name: String, + pub password: Password, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] pub app::LoginError); + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let Self(error) = self; + match error { + app::LoginError::Rejected => { + // not error::Unauthorized due to differing messaging + (StatusCode::UNAUTHORIZED, "invalid name or password").into_response() + } + other => Internal::from(other).into_response(), + } + } +} diff --git a/src/login/routes/test/login.rs b/src/login/routes/login/test.rs index 68c92de..d431612 100644 --- a/src/login/routes/test/login.rs +++ b/src/login/routes/login/test.rs @@ -3,7 +3,8 @@ use axum::{ http::StatusCode, }; -use crate::{login::routes, test::fixtures, token::app}; +use super::post; +use crate::{test::fixtures, token::app}; #[tokio::test] async fn correct_credentials() { @@ -16,12 +17,12 @@ async fn correct_credentials() { let identity = fixtures::identity::not_logged_in(); let logged_in_at = fixtures::now(); - let request = routes::LoginRequest { + let request = post::Request { name: name.clone(), password, }; let (identity, status) = - routes::on_login(State(app.clone()), logged_in_at, identity, Json(request)) + post::handler(State(app.clone()), logged_in_at, identity, Json(request)) .await .expect("logged in with valid credentials"); @@ -53,12 +54,12 @@ async fn invalid_name() { let identity = fixtures::identity::not_logged_in(); let logged_in_at = fixtures::now(); let (name, password) = fixtures::login::propose(); - let request = routes::LoginRequest { + let request = post::Request { name: name.clone(), password, }; - let routes::LoginError(error) = - routes::on_login(State(app.clone()), logged_in_at, identity, Json(request)) + let post::Error(error) = + post::handler(State(app.clone()), logged_in_at, identity, Json(request)) .await .expect_err("logged in with an incorrect password"); @@ -78,12 +79,12 @@ async fn incorrect_password() { let logged_in_at = fixtures::now(); let identity = fixtures::identity::not_logged_in(); - let request = routes::LoginRequest { + let request = post::Request { name: login.name, password: fixtures::login::propose_password(), }; - let routes::LoginError(error) = - routes::on_login(State(app.clone()), logged_in_at, identity, Json(request)) + let post::Error(error) = + post::handler(State(app.clone()), logged_in_at, identity, Json(request)) .await .expect_err("logged in with an incorrect password"); @@ -103,8 +104,8 @@ async fn token_expires() { let logged_in_at = fixtures::ancient(); let identity = fixtures::identity::not_logged_in(); - let request = routes::LoginRequest { name, password }; - let (identity, _) = routes::on_login(State(app.clone()), logged_in_at, identity, Json(request)) + let request = post::Request { name, password }; + let (identity, _) = post::handler(State(app.clone()), 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/routes/logout/mod.rs b/src/login/routes/logout/mod.rs new file mode 100644 index 0000000..36b384e --- /dev/null +++ b/src/login/routes/logout/mod.rs @@ -0,0 +1,4 @@ +pub mod post; + +#[cfg(test)] +mod test; diff --git a/src/login/routes/logout/post.rs b/src/login/routes/logout/post.rs new file mode 100644 index 0000000..6b7a62a --- /dev/null +++ b/src/login/routes/logout/post.rs @@ -0,0 +1,47 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + clock::RequestedAt, + error::{Internal, Unauthorized}, + token::{app, extract::IdentityToken}, +}; + +pub async fn handler( + State(app): State<App>, + RequestedAt(now): RequestedAt, + identity: IdentityToken, + Json(_): Json<Request>, +) -> Result<(IdentityToken, StatusCode), Error> { + if let Some(secret) = identity.secret() { + let (token, _) = app.tokens().validate(&secret, &now).await?; + app.tokens().logout(&token).await?; + } + + let identity = identity.clear(); + Ok((identity, StatusCode::NO_CONTENT)) +} + +// This forces the only valid request to be `{}`, and not the infinite +// variation allowed when there's no body extractor. +#[derive(Default, serde::Deserialize)] +pub struct Request {} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] pub app::ValidateError); + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let Self(error) = self; + #[allow(clippy::match_wildcard_for_single_variants)] + match error { + app::ValidateError::InvalidToken => Unauthorized.into_response(), + other => Internal::from(other).into_response(), + } + } +} diff --git a/src/login/routes/test/logout.rs b/src/login/routes/logout/test.rs index 611829e..0e70e4c 100644 --- a/src/login/routes/test/logout.rs +++ b/src/login/routes/logout/test.rs @@ -3,7 +3,8 @@ use axum::{ http::StatusCode, }; -use crate::{login::routes, test::fixtures, token::app}; +use super::post; +use crate::{test::fixtures, token::app}; #[tokio::test] async fn successful() { @@ -17,11 +18,11 @@ async fn successful() { // Call the endpoint - let (response_identity, response_status) = routes::on_logout( + let (response_identity, response_status) = post::handler( State(app.clone()), fixtures::now(), identity.clone(), - Json(routes::LogoutRequest {}), + Json::default(), ) .await .expect("logged out with a valid token"); @@ -38,12 +39,7 @@ async fn successful() { .validate(&secret, &now) .await .expect_err("secret is invalid"); - match error { - app::ValidateError::InvalidToken => (), // should be invalid - other @ app::ValidateError::DatabaseError(_) => { - panic!("expected ValidateError::InvalidToken, got {other:#}") - } - } + assert!(matches!(error, app::ValidateError::InvalidToken)); } #[tokio::test] @@ -55,14 +51,9 @@ async fn no_identity() { // Call the endpoint let identity = fixtures::identity::not_logged_in(); - let (identity, status) = routes::on_logout( - State(app), - fixtures::now(), - identity, - Json(routes::LogoutRequest {}), - ) - .await - .expect("logged out with no token"); + let (identity, status) = post::handler(State(app), fixtures::now(), identity, Json::default()) + .await + .expect("logged out with no token"); // Verify the return value's basic structure @@ -79,19 +70,11 @@ async fn invalid_token() { // Call the endpoint let identity = fixtures::identity::fictitious(); - let error = routes::on_logout( - State(app), - fixtures::now(), - identity, - Json(routes::LogoutRequest {}), - ) - .await - .expect_err("logged out with an invalid token"); + let post::Error(error) = post::handler(State(app), fixtures::now(), identity, Json::default()) + .await + .expect_err("logged out with an invalid token"); // Verify the return value's basic structure - assert!(matches!( - error, - routes::LogoutError::ValidateError(app::ValidateError::InvalidToken) - )); + assert!(matches!(error, app::ValidateError::InvalidToken)); } diff --git a/src/login/routes/mod.rs b/src/login/routes/mod.rs new file mode 100644 index 0000000..8cb8852 --- /dev/null +++ b/src/login/routes/mod.rs @@ -0,0 +1,12 @@ +use axum::{routing::post, Router}; + +use crate::app::App; + +mod login; +mod logout; + +pub fn router() -> Router<App> { + Router::new() + .route("/api/auth/login", post(login::post::handler)) + .route("/api/auth/logout", post(logout::post::handler)) +} diff --git a/src/login/routes/test/mod.rs b/src/login/routes/test/mod.rs deleted file mode 100644 index 90522c4..0000000 --- a/src/login/routes/test/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod login; -mod logout; |
