use askama::Template; use axum::{ Form, Router, extract::State, response::{Html, Redirect}, routing::{get, post}, }; use rusqlite::params; use serde::Deserialize; use crate::auth::{CurrentUser, hash_password}; use crate::routes::AppState; #[derive(Template)] #[template(path = "accounts/manage.html")] struct AccountManagementTemplate { username: String, is_superuser: bool, password_change_required: bool, accounts: Vec, message: Option, } struct AccountRow { id: i64, username: String, is_superuser: bool, } #[derive(Debug, Deserialize)] pub struct CreateAccountForm { pub username: String, pub password: String, pub is_superuser: Option, } #[derive(Debug, Deserialize)] pub struct DeleteAccountForm { pub id: i64, } #[derive(Debug, Deserialize)] pub struct ChangePasswordForm { pub current_password: String, pub new_password: String, } #[derive(Debug, Deserialize)] pub struct SetPasswordForm { pub id: i64, pub new_password: String, } pub fn router() -> Router { Router::new() .route("/accounts", get(manage_accounts)) .route("/accounts/create", post(create_account)) .route("/accounts/delete", post(delete_account)) .route("/accounts/set-password", post(set_password)) .route( "/accounts/change-own-password", post(change_own_password_from_accounts), ) } pub async fn change_password( State(state): State, CurrentUser(account): CurrentUser, Form(form): Form, ) -> Result { let conn = state.db.conn(); let hash: String = conn .query_row( "SELECT password FROM accounts WHERE id = ?1", params![account.id], |row| row.get(0), ) .map_err(|_| Redirect::to("/login"))?; if !crate::auth::verify_password(&form.current_password, &hash).unwrap_or(false) { drop(conn); return Ok(Redirect::to("/change-password")); } let new_hash = hash_password(&form.new_password).map_err(|_| Redirect::to("/login"))?; conn.execute( "UPDATE accounts SET password = ?1, password_change_required = 0 WHERE id = ?2", params![new_hash, account.id], ) .map_err(|_| Redirect::to("/login"))?; conn.execute( "DELETE FROM sessions WHERE account_id = ?1", params![account.id], ) .ok(); drop(conn); Ok(Redirect::to("/login")) } async fn manage_accounts( State(state): State, CurrentUser(account): CurrentUser, ) -> Result, Redirect> { if !account.is_superuser { return Err(Redirect::to("/")); } let conn = state.db.conn(); let mut stmt = conn .prepare("SELECT id, username, is_superuser FROM accounts ORDER BY username") .map_err(|_| Redirect::to("/login"))?; let accounts: Vec = stmt .query_map([], |row| { Ok(AccountRow { id: row.get(0)?, username: row.get(1)?, is_superuser: row.get::<_, i32>(2)? != 0, }) }) .map_err(|_| Redirect::to("/login"))? .filter_map(|r| r.ok()) .collect(); drop(stmt); drop(conn); let template = AccountManagementTemplate { username: account.username, is_superuser: account.is_superuser, password_change_required: account.password_change_required, accounts, message: None, }; Ok(Html(template.render().unwrap_or_default())) } async fn create_account( State(state): State, CurrentUser(account): CurrentUser, Form(form): Form, ) -> Result { if !account.is_superuser { return Ok(Redirect::to("/")); } let hash = hash_password(&form.password).map_err(|_| Redirect::to("/accounts"))?; let is_superuser = form.is_superuser.as_deref() == Some("on"); let conn = state.db.conn(); let _ = conn.execute( "INSERT INTO accounts (username, password, is_superuser) VALUES (?1, ?2, ?3)", params![form.username, hash, is_superuser], ); drop(conn); Ok(Redirect::to("/accounts")) } async fn delete_account( State(state): State, CurrentUser(account): CurrentUser, Form(form): Form, ) -> Result { if !account.is_superuser || account.id == form.id { return Ok(Redirect::to("/accounts")); } let conn = state.db.conn(); let superuser_count: i32 = conn .query_row( "SELECT COUNT(*) FROM accounts WHERE is_superuser = 1 AND id != ?1", params![form.id], |row| row.get(0), ) .unwrap_or(0); let is_superuser: bool = conn .query_row( "SELECT is_superuser FROM accounts WHERE id = ?1", params![form.id], |row| row.get::<_, i32>(0).map(|v| v != 0), ) .unwrap_or(false); if is_superuser && superuser_count == 0 { drop(conn); return Ok(Redirect::to("/accounts")); } let _ = conn.execute("DELETE FROM accounts WHERE id = ?1", params![form.id]); drop(conn); Ok(Redirect::to("/accounts")) } async fn set_password( State(state): State, CurrentUser(account): CurrentUser, Form(form): Form, ) -> Result { if !account.is_superuser { return Ok(Redirect::to("/")); } let new_hash = hash_password(&form.new_password).map_err(|_| Redirect::to("/accounts"))?; let conn = state.db.conn(); conn.execute( "UPDATE accounts SET password = ?1, password_change_required = 0 WHERE id = ?2", params![new_hash, form.id], ) .ok(); // Invalidate sessions for the target account conn.execute( "DELETE FROM sessions WHERE account_id = ?1", params![form.id], ) .ok(); drop(conn); Ok(Redirect::to("/accounts")) } async fn change_own_password_from_accounts( State(state): State, CurrentUser(account): CurrentUser, Form(form): Form, ) -> Result { change_password(State(state), CurrentUser(account), Form(form)).await }