3 Commits

Author SHA1 Message Date
GW_MC
86fb222d18 added serving openapi options 2025-12-18 22:19:16 +08:00
GW_MC
08b1a055a4 feat: add admin user initialization endpoint with request handling 2025-12-18 22:10:50 +08:00
GW_MC
8f2193bed2 Fix invalid query for settings and users 2025-12-18 22:10:10 +08:00
11 changed files with 255 additions and 10 deletions

View File

@@ -87,7 +87,21 @@ pub async fn start_server() {
// build the axum app and run the server...
info!("Starting application...");
let app: Router = routes::get_root_router(Arc::new(get_app_state(&db_connection, &settings)));
let mut app: Router =
routes::get_root_router(Arc::new(get_app_state(&db_connection, &settings)));
if settings.server.serve_openapi {
info!("Enabling OpenAPI documentation endpoint at /openapi.json");
app = app.route(
"/openapi.json",
axum::routing::get(|| async {
use utoipa::OpenApi;
let doc = routes::ApiDoc::openapi();
doc.to_pretty_json()
.expect("Failed to serialize OpenAPI doc to JSON")
}),
);
}
let address = format!("{}:{}", settings.server.address, settings.server.port);
info!("Starting server at http://{}", address);

View File

@@ -3,6 +3,7 @@ pub(crate) const LOGGING_UTC_KEY: &str = "LOGGING.UTC";
//
pub(crate) const SERVER_ADDRESS_KEY: &str = "SERVER.ADDRESS";
pub(crate) const SERVER_PORT_KEY: &str = "SERVER.PORT";
pub(crate) const SERVER_SERVE_OPENAPI_KEY: &str = "SERVER.SERVE_OPENAPI";
//
pub(crate) const DATABASE_URL_KEY: &str = "DATABASE.URL";
pub(crate) const DATABASE_MAX_CONNECTIONS_KEY: &str = "DATABASE.MAX_CONNECTIONS";

View File

@@ -3,6 +3,8 @@ use std::net::IpAddr;
use config::{Config, ConfigError};
use tracing::warn;
use crate::configs::key::SERVER_SERVE_OPENAPI_KEY;
use super::{
FromConfig,
key::{SERVER_ADDRESS_KEY, SERVER_PORT_KEY},
@@ -12,6 +14,7 @@ use super::{
pub struct ServerSettings {
pub address: IpAddr,
pub port: u16,
pub serve_openapi: bool,
}
impl FromConfig for ServerSettings {
@@ -43,6 +46,17 @@ impl FromConfig for ServerSettings {
);
DEFAULT_PORT
}) as u16,
serve_openapi: _config
.get_bool(SERVER_SERVE_OPENAPI_KEY)
.unwrap_or_else(|err| {
const DEFAULT_SERVE_OPENAPI: bool = false;
warn!(
"{} not set or invalid in configuration, defaulting to {}. Error: {}",
SERVER_SERVE_OPENAPI_KEY, DEFAULT_SERVE_OPENAPI, err
);
DEFAULT_SERVE_OPENAPI
}),
})
}

View File

@@ -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<AppState>) -> 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)
}

View File

@@ -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<Arc<AppState>>,
Json(payload): Json<Value>,
) -> 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()
}

View File

@@ -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"),

View File

@@ -108,6 +108,7 @@ impl PasswordStrategy {
.to_string();
let new_identity = user_identity::ActiveModel {
id: sea_orm::ActiveValue::Set(Uuid::new_v4()),
user_id: sea_orm::ActiveValue::Set(user_id),
provider: sea_orm::ActiveValue::Set(PASSWORD_PROVIDER.to_string()),
password_hash: sea_orm::ActiveValue::Set(Some(password_hash)),

View File

@@ -163,7 +163,7 @@ impl UserService for UserServiceImpl {
tx: Option<&mut DatabaseTransaction>,
) -> Result<User, ServiceError> {
let user_active_model = UserActiveModel {
id: ActiveValue::NotSet,
id: ActiveValue::Set(Uuid::new_v4()),
name: ActiveValue::Set(user.username),
is_admin: ActiveValue::Set(user.is_admin),
is_active: ActiveValue::Set(true),

View File

@@ -77,10 +77,11 @@ impl SettingsStore for SettingsService {
Ok(None) => {
handle_not_found(key.to_string(), value).await?;
}
Ok(Some(mut record)) => {
record.value = value;
record
.into_active_model()
Ok(Some(record)) => {
let mut record_active_model = record.into_active_model();
record_active_model.value = ActiveValue::Set(value);
record_active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
record_active_model
.update(&*self.connection)
.await
.map_err(ServiceError::from)?;

View File

@@ -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",

View File

@@ -1,5 +1,6 @@
export namespace Schemas {
// <Schemas>
export type AdminInitRequest = { password: string; setup_secret: string; username: string };
export type HealthInfo = {
errors?: (Array<string> | null) | undefined;
status: string;
@@ -14,6 +15,15 @@ export namespace Schemas {
export namespace Endpoints {
// <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 {
// <EndpointByMethod>
export type EndpointByMethod = {
post: {
"/api/auth/init_admin": Endpoints.post_Init_admin;
"/api/auth/login": Endpoints.post_Login;
};
get: {