use askama::Template; use axum::{ extract::{Extension, Form, Path, State}, response::{IntoResponse, Redirect, Response}, routing::{get, post}, Router, }; use bcrypt::{hash, DEFAULT_COST}; use serde::Deserialize; use std::sync::Arc; use crate::models::{Role, User}; use crate::utils::HtmlTemplate; use crate::AppState; use crate::error::AppError; use anyhow::Context; #[derive(Template)] #[template(path = "dashboard.html")] pub struct DashboardTemplate<'a> { pub current_user: &'a User, pub users: Vec, pub error: Option<&'a str>, } pub fn router() -> Router> { Router::new() .route("/", get(dashboard)) .route("/users/add", post(add_user)) .route("/users/delete/{id}", post(delete_user)) .route("/users/password/{id}", post(change_password)) } pub async fn dashboard( State(state): State>, Extension(current_user): Extension, ) -> Result { render_dashboard(&state, ¤t_user, None).await } async fn render_dashboard( state: &AppState, current_user: &User, error: Option<&str>, ) -> Result { let users: Vec = sqlx::query_as("SELECT * FROM users") .fetch_all(&state.db) .await .context("Failed to fetch users from database in render_dashboard")?; Ok(HtmlTemplate(DashboardTemplate { current_user, users, error, }) .into_response()) } #[derive(Deserialize)] pub struct AddUserPayload { pub username: String, pub password: Option, pub role: String, } pub async fn add_user( State(state): State>, Extension(current_user): Extension, Form(payload): Form, ) -> Result { if current_user.role() != Role::Admin { return render_dashboard(&state, ¤t_user, Some("Permission denied")).await; } let password = payload.password.unwrap_or_else(|| "password".to_string()); let hashed = hash(&password, DEFAULT_COST).context("Failed to hash password")?; let role = if payload.role == "admin" { "admin" } else { "readonly" }; sqlx::query("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)") .bind(&payload.username) .bind(&hashed) .bind(role) .execute(&state.db) .await .context("Failed to insert new user into database")?; Ok(Redirect::to("/__dungeon").into_response()) } pub async fn delete_user( State(state): State>, Extension(current_user): Extension, Path(id): Path, ) -> Result { if current_user.role() != Role::Admin { return render_dashboard(&state, ¤t_user, Some("Permission denied")).await; } if current_user.id == id { return render_dashboard(&state, ¤t_user, Some("Cannot delete yourself")).await; } sqlx::query("DELETE FROM users WHERE id = ?") .bind(id) .execute(&state.db) .await .context(format!("Failed to delete user with id {}", id))?; Ok(Redirect::to("/__dungeon").into_response()) } #[derive(Deserialize)] pub struct ChangePasswordPayload { pub password: String, } pub async fn change_password( State(state): State>, Extension(current_user): Extension, Path(id): Path, Form(payload): Form, ) -> Result { if current_user.role() != Role::Admin { return render_dashboard(&state, ¤t_user, Some("Permission denied")).await; } let hashed = hash(&payload.password, DEFAULT_COST).context("Failed to hash password")?; sqlx::query("UPDATE users SET password_hash = ? WHERE id = ?") .bind(&hashed) .bind(id) .execute(&state.db) .await .context(format!("Failed to update password for user with id {}", id))?; Ok(Redirect::to("/__dungeon").into_response()) }