Files
Skald-Circle/src/frontend/api/users_mgmt.rs
T
dguiducci 7dd77d4ef4 feat(auth): login, roles, user mgmt, setup wizard, and session guard
- New skald-setup crate: interactive first-run wizard that creates the
  admin user, prompts for encryption choice and password
- Auth system: session-based login/logout with cookie, guard middleware
- Roles API: CRUD for data-driven roles, seeded on first boot
- Users management API: create, list, edit, delete users
- Setup state API: check if first admin has been created
- Frontend: login-page, setup-page, users-page, roles-page, profile-page
  components with corresponding CSS
- Topbar: avatar dropdown with profile link and logout
- Sidebar: nav entries for Users and Roles (admin only)
- Page shell CSS: layout support for the new pages
- build.sh: builds both skald and skald-setup binaries
- run.sh: runs skald-setup before the server loop
- CLAUDE.md: updated workspace layout and build/run docs
2026-07-10 19:19:25 +01:00

121 lines
4.1 KiB
Rust

use std::sync::Arc;
use axum::{Json, extract::{Path, State}};
use serde::{Deserialize, Serialize};
use skald_core::db::users::UserSummary;
use skald_core::skald::Skald;
use super::ApiError;
// ── GET /api/users ───────────────────────────────────────────────────────────
pub async fn list(State(skald): State<Arc<Skald>>) -> Result<Json<Vec<UserSummary>>, ApiError> {
let users = skald.users().list().await?;
Ok(Json(users))
}
// ── POST /api/users ──────────────────────────────────────────────────────────
#[derive(Deserialize)]
pub struct CreateUserBody {
pub username: String,
pub display_name: Option<String>,
pub role_id: String,
pub password: String,
#[serde(default)]
pub encrypted: bool,
}
#[derive(Serialize)]
pub struct CreatedUser {
pub id: String,
}
pub async fn create(
State(skald): State<Arc<Skald>>,
Json(body): Json<CreateUserBody>,
) -> Result<Json<CreatedUser>, ApiError> {
let username = body.username.trim();
if username.is_empty() {
return Err(ApiError::bad_request("username must not be empty"));
}
if body.password.is_empty() {
return Err(ApiError::bad_request("password must not be empty"));
}
let id = skald
.users()
.register_user(username, body.display_name.as_deref(), &body.role_id, Some(&body.password), body.encrypted)
.await?;
Ok(Json(CreatedUser { id }))
}
// ── PUT /api/users/:id ───────────────────────────────────────────────────────
#[derive(Deserialize)]
pub struct UpdateUserBody {
pub username: String,
pub display_name: Option<String>,
pub role_id: String,
#[serde(default)]
pub active: bool,
}
pub async fn update(
State(skald): State<Arc<Skald>>,
Path(id): Path<String>,
Json(body): Json<UpdateUserBody>,
) -> Result<Json<serde_json::Value>, ApiError> {
let username = body.username.trim();
if username.is_empty() {
return Err(ApiError::bad_request("username must not be empty"));
}
skald_core::db::users::update_profile(
skald.db(),
&id,
username,
body.display_name.as_deref(),
&body.role_id,
)
.await?;
// active is separate because it's a boolean flip
skald_core::db::users::set_active(skald.db(), &id, body.active).await?;
Ok(Json(serde_json::json!({ "ok": true })))
}
// ── DELETE /api/users/:id ────────────────────────────────────────────────────
pub async fn delete(
State(skald): State<Arc<Skald>>,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, ApiError> {
skald.users().delete_user(&id).await?;
Ok(Json(serde_json::json!({ "ok": true })))
}
// ── POST /api/users/:id/password ─────────────────────────────────────────────
#[derive(Deserialize)]
pub struct ResetPasswordBody {
pub password: String,
}
pub async fn reset_password(
State(skald): State<Arc<Skald>>,
Path(id): Path<String>,
Json(body): Json<ResetPasswordBody>,
) -> Result<Json<serde_json::Value>, ApiError> {
if body.password.is_empty() {
return Err(ApiError::bad_request("password must not be empty"));
}
// change_password with no old password and a new one resets it. For
// encrypted users the admin must supply the old password; this endpoint
// is for cleartext users or for admin-initiated resets of cleartext
// accounts. For encrypted users, the user should change their own password.
skald.users().change_password(&id, None, Some(&body.password)).await
.map_err(|e| ApiError::bad_request(e.to_string()))?;
Ok(Json(serde_json::json!({ "ok": true })))
}