use axum::extract::State; use axum::http::HeaderMap; use axum::{ Form, Router, extract::Query, response::{IntoResponse, Redirect}, routing::{get, post}, }; use maud::{DOCTYPE, html}; use serde::Deserialize; use crate::models::user::{AuthSession, Credentials}; use crate::{AppError, AppState}; #[derive(Deserialize, Debug)] struct NextUrl { next: Option, } pub fn router() -> Router { Router::new() .route("/login", post(self::post::login)) .route("/login", get(self::get::login)) .route("/logout", get(self::get::logout)) } mod post { use super::*; #[axum::debug_handler] pub async fn login( mut auth_session: AuthSession, State(mut state): State, Query(NextUrl { next }): Query, Form(creds): Form, ) -> Result { let mut headers = HeaderMap::new(); let user = match auth_session.authenticate(creds.clone()).await { Ok(Some(user)) => user, Ok(None) => { return Err(AppError(anyhow::Error::msg( "Username and password do not match", ))); } Err(_) => return Err(AppError(anyhow::Error::msg("Internal server error"))), }; if auth_session.login(&user).await.is_err() { return Err(AppError(anyhow::Error::msg("Server error during login"))); } state.init(&user).await?; if let Some(url) = next { headers.insert("HX-Redirect", url.parse()?); } else { headers.insert("HX-Redirect", "/".parse()?); } Ok((headers, "ok")) } } mod get { use super::*; pub async fn login(Query(NextUrl { next }): Query) -> impl IntoResponse { let post_url = format!( "/login{}", next.map_or("".to_string(), |n| format!("?next={}", n)) ); html! { (DOCTYPE) html { head { meta name="viewport" content="width=device-width"; script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" {} script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" {} script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js" defer {} link rel="stylesheet" type="text/css" href="/static/index.css"; link rel="stylesheet" type="text/css" href="/static/login.css"; title { "Mascarpone" } } body hx-ext="response-targets" { h1 { "Mascarpone" } form hx-post=(post_url) hx-target-error="#error" x-data="{ user: '', pass: '' }" { label for="username" { "Username" } input name="username" #username autofocus x-model="user"; label for="password" { "Password" } input name="password" #password type="password" x-model="pass"; input type="submit" value="login" x-bind:disabled="!(user.length && pass.length)"; #error {} } } } } } pub async fn logout( mut auth_session: AuthSession, State(mut state): State, ) -> Result { let user = auth_session.user.clone(); auth_session.logout().await?; if let Some(user) = user { state.remove(&user); } Ok(Redirect::to("/login").into_response()) } }