From 08b1a055a40fe1fd0371fd038bcf902fc1c5bc67 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Thu, 18 Dec 2025 22:10:50 +0800 Subject: [PATCH] feat: add admin user initialization endpoint with request handling --- apps/api/src/routes/api/auth.rs | 4 +- apps/api/src/routes/api/auth/init_admin.rs | 143 ++++++++++++++++++ apps/api/src/routes/api/openapi.rs | 10 +- apps/api/swagger.json | 54 +++++++ .../app/generated/api-client/api-client.ts | 11 ++ 5 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 apps/api/src/routes/api/auth/init_admin.rs diff --git a/apps/api/src/routes/api/auth.rs b/apps/api/src/routes/api/auth.rs index 98d87d9..2242a44 100644 --- a/apps/api/src/routes/api/auth.rs +++ b/apps/api/src/routes/api/auth.rs @@ -1,3 +1,4 @@ +pub mod init_admin; pub mod login; use std::sync::Arc; @@ -11,6 +12,7 @@ use crate::routes::AppState; pub fn get_basic_auth_router(state: Arc) -> Router { Router::new() - .route("/login", post(login::login)) + .route("/auth/login", post(login::login)) + .route("/auth/init_admin", post(init_admin::init_admin)) .with_state(state) } diff --git a/apps/api/src/routes/api/auth/init_admin.rs b/apps/api/src/routes/api/auth/init_admin.rs new file mode 100644 index 0000000..2c15054 --- /dev/null +++ b/apps/api/src/routes/api/auth/init_admin.rs @@ -0,0 +1,143 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use database::generated::entities::user; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, TransactionTrait}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, from_value}; +use tracing::{debug, error, info, warn}; + +use crate::{ + helpers::constants::ADMIN_INIT_SECRET_KEY, + routes::{AppState, api::openapi::tag::AUTH_TAG}, + services::auth::user::NewUser, +}; + +/// Login request payload +#[derive(Serialize, Deserialize, utoipa::ToSchema)] +pub struct AdminInitRequest { + username: String, + password: String, + // The secret key required to initialize the admin user + setup_secret: String, +} + +/// Initializes the admin user +/// +/// Initializes the admin user if no admin user exists and the correct setup secret is provided. +#[utoipa::path( + post, + path = "/api/auth/init_admin", + request_body = AdminInitRequest, + responses( + (status = 200, description = "Admin user initialized successfully"), + (status = 400, description = "Invalid request payload"), + (status = 401, description = "Unauthorized: Admin user already exists or invalid setup secret"), + (status = 500, description = "Internal server error"), + ), + tag = AUTH_TAG, + )] +pub async fn init_admin( + State(state): State>, + Json(payload): Json, +) -> Response { + if user::Entity::find() + .filter(user::Column::IsAdmin.eq(true)) + .filter(user::Column::IsActive.eq(true)) + .one(state.database_connection.as_ref()) + .await + .map_err(|err| { + error!("Failed to query for existing admin user: {}", err); + StatusCode::INTERNAL_SERVER_ERROR + }) + .unwrap_or(None) + .is_some() + { + warn!("Admin user already exists. Skipping admin initialization."); + return (StatusCode::UNAUTHORIZED).into_response(); + } + + let init_request: AdminInitRequest = match from_value(payload) { + Ok(req) => req, + Err(e) => { + warn!("Invalid login request: {}", e); + return (StatusCode::BAD_REQUEST).into_response(); + } + }; + + let admin_secret = match state + .service + .settings + .get_setting(ADMIN_INIT_SECRET_KEY) + .await + { + Ok(secret) => secret, + Err(e) => { + error!( + "Failed to retrieve admin initialization secret. Invalid internal state?: {}", + e + ); + return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); + } + }; + + if init_request.setup_secret != admin_secret { + info!("{},{}", init_request.setup_secret, admin_secret); + warn!("Invalid admin initialization secret provided."); + return (StatusCode::UNAUTHORIZED).into_response(); + } + + let mut tx = match state.database_connection.begin().await { + Ok(tx) => tx, + Err(e) => { + error!("Failed to start transaction: {}", e); + return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); + } + }; + + let user = match state + .service + .user + .create_user( + NewUser { + username: init_request.username, + is_admin: true, + }, + Some(&mut tx), + ) + .await + { + Ok(user) => user, + Err(e) => { + error!("Failed to initialize admin user: {}", e); + return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); + } + }; + + debug!("Created admin user with ID: {}", user.id); + match state + .service + .auth_state + .strategy + .password + .create_identity(user.id, &init_request.password, Some(&mut tx)) + .await + { + Ok(_) => {} + Err(e) => { + error!("Failed to create admin user identity: {}", e); + return (StatusCode::INTERNAL_SERVER_ERROR).into_response(); + } + }; + + tx.commit().await.unwrap_or_else(|e| { + error!("Failed to commit transaction: {}", e); + }); + + (StatusCode::OK).into_response() +} diff --git a/apps/api/src/routes/api/openapi.rs b/apps/api/src/routes/api/openapi.rs index b8ae85b..d7c4d9d 100644 --- a/apps/api/src/routes/api/openapi.rs +++ b/apps/api/src/routes/api/openapi.rs @@ -8,11 +8,15 @@ pub mod tag { #[openapi( paths( crate::routes::api::health::info::get_health_info, - crate::routes::api::auth::login::login + // Authentication paths + crate::routes::api::auth::login::login, + crate::routes::api::auth::init_admin::init_admin, ), components( - schemas(crate::routes::api::health::info::HealthInfo), // Register any schemas used in your paths - schemas(crate::routes::api::auth::login::LoginRequest) + schemas(crate::routes::api::health::info::HealthInfo), + // Authentication schemas + schemas(crate::routes::api::auth::login::LoginRequest), + schemas(crate::routes::api::auth::init_admin::AdminInitRequest), ), tags( (name = tag::HEALTH_TAG, description = "Health information API"), diff --git a/apps/api/swagger.json b/apps/api/swagger.json index 71bd824..6bc6036 100644 --- a/apps/api/swagger.json +++ b/apps/api/swagger.json @@ -9,6 +9,40 @@ "version": "0.1.0" }, "paths": { + "/api/auth/init_admin": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Initializes the admin user", + "description": "Initializes the admin user if no admin user exists and the correct setup secret is provided.", + "operationId": "init_admin", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminInitRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Admin user initialized successfully" + }, + "400": { + "description": "Invalid request payload" + }, + "401": { + "description": "Unauthorized: Admin user already exists or invalid setup secret" + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/api/auth/login": { "post": { "tags": [ @@ -75,6 +109,26 @@ }, "components": { "schemas": { + "AdminInitRequest": { + "type": "object", + "description": "Login request payload", + "required": [ + "username", + "password", + "setup_secret" + ], + "properties": { + "password": { + "type": "string" + }, + "setup_secret": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "HealthInfo": { "type": "object", "description": "System health information", diff --git a/apps/frontend/app/generated/api-client/api-client.ts b/apps/frontend/app/generated/api-client/api-client.ts index 4f648e5..aba754f 100644 --- a/apps/frontend/app/generated/api-client/api-client.ts +++ b/apps/frontend/app/generated/api-client/api-client.ts @@ -1,5 +1,6 @@ export namespace Schemas { // + export type AdminInitRequest = { password: string; setup_secret: string; username: string }; export type HealthInfo = { errors?: (Array | null) | undefined; status: string; @@ -14,6 +15,15 @@ export namespace Schemas { export namespace Endpoints { // + export type post_Init_admin = { + method: "POST"; + path: "/api/auth/init_admin"; + requestFormat: "json"; + parameters: { + body: Schemas.AdminInitRequest; + }; + responses: { 200: unknown; 400: unknown; 401: unknown; 500: unknown }; + }; export type post_Login = { method: "POST"; path: "/api/auth/login"; @@ -37,6 +47,7 @@ export namespace Endpoints { // export type EndpointByMethod = { post: { + "/api/auth/init_admin": Endpoints.post_Init_admin; "/api/auth/login": Endpoints.post_Login; }; get: {