From ae7ef10b67737f08a86743f687bca30a12e9e17a Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 12 Jul 2026 19:38:03 +0200 Subject: first stable --- src/routes/accounts.rs | 236 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 src/routes/accounts.rs (limited to 'src/routes/accounts.rs') diff --git a/src/routes/accounts.rs b/src/routes/accounts.rs new file mode 100644 index 0000000..89774f8 --- /dev/null +++ b/src/routes/accounts.rs @@ -0,0 +1,236 @@ +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 +} -- cgit v1.2.3