use axum::{ Router, extract::State, routing::{delete, get, post, put}, }; use axum_extra::extract::Form; use cache_bust::asset; use maud::{Markup, html}; use serde::Deserialize; use serde_json::json; use short_uuid::ShortUuid; use super::Layout; use crate::models::user::{AuthSession, Credentials}; use crate::{AppError, AppState}; pub fn router() -> Router { Router::new() .route("/settings", get(self::get::settings)) .route("/settings/ics_path", post(self::post::ics_path)) .route("/settings/ics_path", delete(self::delete::ics_path)) .route("/password", put(self::put::password)) } fn calendar_link(path: Option) -> Markup { if let Some(path) = path { html! { #cal-link x-data=(json!({ "path": path })) hx-target="this" hx-swap="outerHTML" { a x-bind:href="window.location.origin + '/cal/' + path" { span x-text="window.location.origin + '/cal/'" {} span { (path) } } p { "Warning: These actions unrecoverably change your calendar's URL." } button hx-post="/settings/ics_path" { "Regenerate path" } button hx-delete="/settings/ics_path" { "Destroy calendar" } } } } else { html! { #cal-link hx-target="this" hx-swap="outerHTML" { div { "Birthdays calendar is disabled." } button hx-post="/settings/ics_path" { "Enable calendar" } } } } } mod get { use super::*; pub async fn settings( auth_session: AuthSession, State(state): State, layout: Layout, ) -> Result { let pool = &state.db(&auth_session.user.unwrap()).pool; let ics_path: (Option,) = sqlx::query_as("select ics_path from settings") .fetch_one(pool) .await?; let ics_path: Option = ics_path.0; Ok(layout.render( Some(vec![asset!("settings.css")]), html! { h2 { "Birthdays Calendar URL" } (calendar_link(ics_path)) h2 { "Change Password" } form x-data="{ old_p: '', new_p: '', confirm: '' }" hx-put="/password" hx-on::after-request="if(event.detail.successful) { this.reset(); setTimeout(() => window.location.reload(), 5000); }" hx-target="this" hx-target-error="this" hx-swap="beforeend" { label for="old" { "Current password:" } input id="old" name="current" x-model="old_p" type="password"; label for="new" { "New password:" } input id="new" name="new_password" x-model="new_p" type="password"; label for="confirm" { "Confirm:" } input id="confirm" x-model="confirm" type="password"; button type="submit" x-bind:disabled="!(new_p.length && new_p === confirm)" { "Submit" } .error x-show="new_p.length && confirm.length && new_p !== confirm" { "Passwords do not match" } } }, )) } } mod post { use super::*; pub async fn ics_path( auth_session: AuthSession, State(state): State, ) -> Result { let user = auth_session.user.unwrap(); let pool = &state.db(&user).pool; let ics_path = format!("{}-{}.ics", &user.username, ShortUuid::generate()); sqlx::query!("update settings set ics_path=$1", ics_path) .execute(pool) .await?; Ok(calendar_link(Some(ics_path))) } } mod delete { use super::*; pub async fn ics_path( auth_session: AuthSession, State(state): State, ) -> Result { let pool = &state.db(&auth_session.user.unwrap()).pool; sqlx::query!("update settings set ics_path=null") .execute(pool) .await?; Ok(calendar_link(None)) } } mod put { use super::*; #[derive(Deserialize)] pub struct PassChange { current: String, new_password: String, } pub async fn password( auth_session: AuthSession, Form(payload): Form, ) -> Result { let username = auth_session.user.as_ref().unwrap().username.clone(); tracing::debug!("Resetting password for {}...", username); let current_creds = Credentials { username: username.clone(), password: payload.current, }; let new_creds = Credentials { username: username, password: payload.new_password, }; match auth_session.authenticate(current_creds).await { Err(_) => Ok(html! { .error { "Server error; could not verify authentication." } }), Ok(None) => Ok(html! { .error { "Current password is incorrect." } }), Ok(Some(_)) => { auth_session.backend.set_password(new_creds).await?; Ok(html! { .msg { "Password changed successfully. Redirecting to login page after 5 seconds..." } }) } } } }