mascarpone/src/web/auth.rs
2025-12-01 15:23:56 -06:00

115 lines
3.7 KiB
Rust

use axum::extract::State;
use axum::http::HeaderMap;
use axum::{
Form, Router,
extract::Query,
response::{IntoResponse, Redirect},
routing::{get, post},
};
use cache_bust::asset;
use maud::{DOCTYPE, html};
use serde::Deserialize;
use crate::models::user::{AuthSession, Credentials};
use crate::{AppError, AppState};
#[derive(Deserialize, Debug)]
struct NextUrl {
next: Option<String>,
}
pub fn router() -> Router<AppState> {
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<AppState>,
Query(NextUrl { next }): Query<NextUrl>,
Form(creds): Form<Credentials>,
) -> Result<impl IntoResponse, AppError> {
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<NextUrl>) -> 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=(format!("/static/{}", asset!("index.css")));
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("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<AppState>,
) -> Result<impl IntoResponse, AppError> {
let user = auth_session.user.clone();
auth_session.logout().await?;
if let Some(user) = user {
state.remove(&user);
}
Ok(Redirect::to("/login").into_response())
}
}