use axum::{ extract::{Json, State}, http::StatusCode, response::{IntoResponse, Response}, routing::{get, post}, Router, }; use crate::{app::App, clock::RequestedAt, error::Internal, 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(login: Login) -> Boot { Boot { login } } #[derive(serde::Serialize)] struct Boot { login: Login, } impl IntoResponse for Boot { fn into_response(self) -> Response { Json(self).into_response() } } #[derive(serde::Deserialize)] struct LoginRequest { name: String, password: String, } 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 => { (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, 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() { app.logins().logout(secret).await.map_err(LogoutError)?; } let identity = identity.clear(); Ok((identity, StatusCode::NO_CONTENT)) } #[derive(Debug)] struct LogoutError(app::ValidateError); impl IntoResponse for LogoutError { fn into_response(self) -> Response { let Self(error) = self; match error { error @ app::ValidateError::InvalidToken => { (StatusCode::UNAUTHORIZED, error.to_string()).into_response() } other => Internal::from(other).into_response(), } } }