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
This commit is contained in:
2026-07-10 19:19:25 +01:00
parent 178a38357e
commit 7dd77d4ef4
36 changed files with 2660 additions and 27 deletions
+64
View File
@@ -0,0 +1,64 @@
use std::sync::Arc;
use axum::{Json, extract::State};
use serde::{Deserialize, Serialize};
use skald_core::skald::Skald;
use super::ApiError;
// ── GET /api/setup/status ────────────────────────────────────────────────────
//
// `needs_setup` is true when no user has ever been created. The frontend uses
// this to decide whether to show the first-run setup screen.
#[derive(Serialize)]
pub struct SetupStatus {
pub needs_setup: bool,
}
pub async fn status(State(skald): State<Arc<Skald>>) -> Result<Json<SetupStatus>, ApiError> {
let count = skald.users().count().await?;
Ok(Json(SetupStatus { needs_setup: count == 0 }))
}
// ── POST /api/setup/user — create the first (admin) user ────────────────────
#[derive(Deserialize)]
pub struct CreateUserBody {
pub username: String,
pub password: String,
#[serde(default)]
pub encrypted: bool,
}
#[derive(Serialize)]
pub struct CreateUserResult {
pub user_id: String,
}
pub async fn create_user(
State(skald): State<Arc<Skald>>,
Json(body): Json<CreateUserBody>,
) -> Result<Json<CreateUserResult>, ApiError> {
// Guard: the setup endpoint is only available before the first user exists.
let count = skald.users().count().await?;
if count > 0 {
return Err(ApiError::bad_request("setup is already complete"));
}
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, None, "admin", Some(&body.password), body.encrypted)
.await?;
Ok(Json(CreateUserResult { user_id: id }))
}