use axum::{ extract::{Form, State}, http::StatusCode, response::{IntoResponse, Redirect, Response}, routing::post, Router, }; use crate::{app::App, clock::RequestedAt, error::InternalError}; use super::{app, extract::IdentityToken}; pub fn router() -> Router { Router::new() .route("/login", post(on_login)) .route("/logout", post(on_logout)) } #[derive(serde::Deserialize)] struct LoginRequest { name: String, password: String, } async fn on_login( State(app): State, RequestedAt(now): RequestedAt, identity: IdentityToken, Form(form): Form, ) -> Result { let token = app .logins() .login(&form.name, &form.password, now) .await .map_err(LoginError)?; let identity = identity.set(&token); Ok(LoginSuccess(identity)) } struct LoginSuccess(IdentityToken); impl IntoResponse for LoginSuccess { fn into_response(self) -> Response { let Self(identity) = self; (identity, Redirect::to("/")).into_response() } } 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() } app::LoginError::DatabaseError(error) => InternalError::from(error).into_response(), app::LoginError::PasswordHashError(error) => InternalError::from(error).into_response(), } } } async fn on_logout( State(app): State, identity: IdentityToken, ) -> Result { if let Some(secret) = identity.secret() { app.logins().logout(secret).await?; } let identity = identity.clear(); Ok((identity, Redirect::to("/"))) }