use axum::{ extract::{Json, State}, http::StatusCode, response::{IntoResponse, Response}, routing::{get, post}, Router, }; use crate::{ app::App, clock::RequestedAt, error::{Internal, Unauthorized}, password::Password, repo::login::Login, }; use super::{app, extract::IdentityToken}; #[cfg(test)] mod test; pub fn router() -> Router { Router::new() .route("/api/boot", get(boot)) .route("/api/auth/login", post(on_login)) .route("/api/auth/logout", post(on_logout)) } async fn boot(State(app): State, login: Login) -> Result { let resume_point = app.logins().boot_point().await?; Ok(Boot { login, resume_point: resume_point.to_string(), }) } #[derive(serde::Serialize)] struct Boot { login: Login, resume_point: String, } impl IntoResponse for Boot { fn into_response(self) -> Response { Json(self).into_response() } } #[derive(serde::Deserialize)] struct LoginRequest { name: String, password: Password, } async fn on_login( State(app): State, RequestedAt(now): RequestedAt, identity: IdentityToken, Json(request): Json, ) -> Result<(IdentityToken, StatusCode), LoginError> { let token = app .logins() .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, 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, ) -> Result<(IdentityToken, StatusCode), LogoutError> { if let Some(secret) = identity.secret() { let (token, _) = app.logins().validate(&secret, &now).await?; app.logins().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(), } } }