//! /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 { 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, #[schema(value_type = String, example = "admin")] pub role: Option, pub is_active: Option, } #[derive(Deserialize, ToSchema)] pub struct PasswordRequest { pub password: String, } #[utoipa::path(get, path = "/api/users", tag = "users", security(("bearer" = [])), responses((status = 200, body = Vec), (status = 401), (status = 403)))] async fn list(State(state): State, _: AdminUser) -> Result>, 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, _: AdminUser, Json(req): Json, ) -> Result<(StatusCode, Json), 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, _: AdminUser, Path(id): Path, ) -> Result, 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, _: AdminUser, Path(id): Path, Json(req): Json, ) -> Result, 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, _: AdminUser, Path(id): Path, Json(req): Json, ) -> Result { state.users.reset_password(id, &req.password).await?; Ok(StatusCode::NO_CONTENT) }