Files
Infrastruktur-Monitoring-Sy…/backend/crates/api/src/users.rs
Dennis Nemec f1136fdf3d
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled
WP-01: authentication, user management and application shell
Argon2id password hashing, JWT access tokens with rotating HttpOnly refresh
cookies and reuse detection, login rate limiting, auth audit log, bootstrap
admin, admin-only user CRUD and password reset, OpenAPI spec. Vue login page,
auth store with automatic token refresh, route guards, sidebar shell with
toasts and placeholder pages, user management page.

All tests green: 40 backend, 12 Vitest, 4 Playwright.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 21:37:14 +02:00

115 lines
3.5 KiB
Rust

//! /api/users: admin-only user management.
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use domain::user::{NewUser, Role, UserUpdate};
use serde::Deserialize;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::auth::UserDto;
use crate::error::ApiError;
use crate::extract::AdminUser;
use crate::AppState;
pub fn router() -> Router<AppState> {
Router::new()
.route("/", get(list).post(create))
.route("/{id}", get(get_one).patch(update))
.route("/{id}/password", post(reset_password))
}
#[derive(Deserialize, ToSchema)]
pub struct CreateUserRequest {
pub email: String,
pub display_name: String,
pub password: String,
#[schema(value_type = String, example = "admin")]
pub role: Role,
}
#[derive(Deserialize, ToSchema)]
pub struct UpdateUserRequest {
pub display_name: Option<String>,
#[schema(value_type = String, example = "admin")]
pub role: Option<Role>,
pub is_active: Option<bool>,
}
#[derive(Deserialize, ToSchema)]
pub struct PasswordRequest {
pub password: String,
}
#[utoipa::path(get, path = "/api/users", tag = "users", security(("bearer" = [])),
responses((status = 200, body = Vec<UserDto>), (status = 401), (status = 403)))]
async fn list(State(state): State<AppState>, _: AdminUser) -> Result<Json<Vec<UserDto>>, ApiError> {
Ok(Json(
state
.users
.list()
.await?
.into_iter()
.map(Into::into)
.collect(),
))
}
#[utoipa::path(post, path = "/api/users", tag = "users", security(("bearer" = [])), request_body = CreateUserRequest,
responses((status = 201, body = UserDto), (status = 409), (status = 422)))]
async fn create(
State(state): State<AppState>,
_: AdminUser,
Json(req): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<UserDto>), ApiError> {
let user = state
.users
.create(NewUser {
email: req.email,
display_name: req.display_name,
password: req.password,
role: req.role,
})
.await?;
Ok((StatusCode::CREATED, Json(user.into())))
}
#[utoipa::path(get, path = "/api/users/{id}", tag = "users", security(("bearer" = [])),
responses((status = 200, body = UserDto), (status = 404)))]
async fn get_one(
State(state): State<AppState>,
_: AdminUser,
Path(id): Path<Uuid>,
) -> Result<Json<UserDto>, ApiError> {
Ok(Json(state.users.get(id).await?.into()))
}
#[utoipa::path(patch, path = "/api/users/{id}", tag = "users", security(("bearer" = [])), request_body = UpdateUserRequest,
responses((status = 200, body = UserDto), (status = 404), (status = 409)))]
async fn update(
State(state): State<AppState>,
_: AdminUser,
Path(id): Path<Uuid>,
Json(req): Json<UpdateUserRequest>,
) -> Result<Json<UserDto>, ApiError> {
let update = UserUpdate {
display_name: req.display_name,
role: req.role,
is_active: req.is_active,
};
Ok(Json(state.users.update(id, update).await?.into()))
}
#[utoipa::path(post, path = "/api/users/{id}/password", tag = "users", security(("bearer" = [])), request_body = PasswordRequest,
responses((status = 204), (status = 404), (status = 422)))]
async fn reset_password(
State(state): State<AppState>,
_: AdminUser,
Path(id): Path<Uuid>,
Json(req): Json<PasswordRequest>,
) -> Result<StatusCode, ApiError> {
state.users.reset_password(id, &req.password).await?;
Ok(StatusCode::NO_CONTENT)
}