use axum::{ extract::{Json, State}, http::StatusCode, response::{IntoResponse, Response}, routing::{get, post}, Router, }; use futures::stream::{self, StreamExt as _, TryStreamExt as _}; use crate::{ app::App, channel::Channel, clock::RequestedAt, error::{Internal, Unauthorized}, event::Instant, login::{Login, Password}, message::{self, Message}, token::{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?; let channels = app.channels().all(resume_point.into()).await?; let channels = stream::iter(channels) .then(|channel| async { app.messages() .in_channel(&channel.id, resume_point.into()) .await .map(|messages| BootChannel::new(channel, messages)) }) .try_collect() .await?; Ok(Boot { login, resume_point: resume_point.to_string(), channels, }) } #[derive(serde::Serialize)] struct Boot { login: Login, resume_point: String, channels: Vec, } #[derive(serde::Serialize)] struct BootChannel { #[serde(flatten)] channel: Channel, messages: Vec, } impl BootChannel { fn new(channel: Channel, messages: impl IntoIterator) -> Self { Self { channel, messages: messages.into_iter().map(BootMessage::from).collect(), } } } #[derive(serde::Serialize)] struct BootMessage { #[serde(flatten)] sent: Instant, sender: Login, message: BootMessageBody, } impl From for BootMessage { fn from(message: Message) -> Self { let Message { sent, channel: _, sender, id, body, } = message; Self { sent, sender, message: BootMessageBody { id, body }, } } } #[derive(serde::Serialize)] struct BootMessageBody { id: message::Id, body: 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 .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, 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.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(), } } }