143 lines
3.9 KiB
Rust
143 lines
3.9 KiB
Rust
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<User>,
|
|
pub error: Option<&'a str>,
|
|
}
|
|
|
|
pub fn router() -> Router<Arc<AppState>> {
|
|
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<Arc<AppState>>,
|
|
Extension(current_user): Extension<User>,
|
|
) -> Result<Response, AppError> {
|
|
render_dashboard(&state, ¤t_user, None).await
|
|
}
|
|
|
|
async fn render_dashboard(
|
|
state: &AppState,
|
|
current_user: &User,
|
|
error: Option<&str>,
|
|
) -> Result<Response, AppError> {
|
|
let users: Vec<User> = 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<String>,
|
|
pub role: String,
|
|
}
|
|
|
|
pub async fn add_user(
|
|
State(state): State<Arc<AppState>>,
|
|
Extension(current_user): Extension<User>,
|
|
Form(payload): Form<AddUserPayload>,
|
|
) -> Result<Response, AppError> {
|
|
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<Arc<AppState>>,
|
|
Extension(current_user): Extension<User>,
|
|
Path(id): Path<i64>,
|
|
) -> Result<Response, AppError> {
|
|
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<Arc<AppState>>,
|
|
Extension(current_user): Extension<User>,
|
|
Path(id): Path<i64>,
|
|
Form(payload): Form<ChangePasswordPayload>,
|
|
) -> Result<Response, AppError> {
|
|
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())
|
|
}
|