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... // build the axum app and run the server...
info!("Starting application..."); 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); let address = format!("{}:{}", settings.server.address, settings.server.port);
info!("Starting server at http://{}", address); 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_ADDRESS_KEY: &str = "SERVER.ADDRESS";
pub(crate) const SERVER_PORT_KEY: &str = "SERVER.PORT"; 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_URL_KEY: &str = "DATABASE.URL";
pub(crate) const DATABASE_MAX_CONNECTIONS_KEY: &str = "DATABASE.MAX_CONNECTIONS"; 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 config::{Config, ConfigError};
use tracing::warn; use tracing::warn;
use crate::configs::key::SERVER_SERVE_OPENAPI_KEY;
use super::{ use super::{
FromConfig, FromConfig,
key::{SERVER_ADDRESS_KEY, SERVER_PORT_KEY}, key::{SERVER_ADDRESS_KEY, SERVER_PORT_KEY},
@@ -12,6 +14,7 @@ use super::{
pub struct ServerSettings { pub struct ServerSettings {
pub address: IpAddr, pub address: IpAddr,
pub port: u16, pub port: u16,
pub serve_openapi: bool,
} }
impl FromConfig for ServerSettings { impl FromConfig for ServerSettings {
@@ -43,6 +46,17 @@ impl FromConfig for ServerSettings {
); );
DEFAULT_PORT DEFAULT_PORT
}) as u16, }) 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; pub mod login;
use std::sync::Arc; use std::sync::Arc;
@@ -11,6 +12,7 @@ use crate::routes::AppState;
pub fn get_basic_auth_router(state: Arc<AppState>) -> Router { pub fn get_basic_auth_router(state: Arc<AppState>) -> Router {
Router::new() 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) .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( #[openapi(
paths( paths(
crate::routes::api::health::info::get_health_info, 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( components(
schemas(crate::routes::api::health::info::HealthInfo), // Register any schemas used in your paths schemas(crate::routes::api::health::info::HealthInfo),
schemas(crate::routes::api::auth::login::LoginRequest) // Authentication schemas
schemas(crate::routes::api::auth::login::LoginRequest),
schemas(crate::routes::api::auth::init_admin::AdminInitRequest),
), ),
tags( tags(
(name = tag::HEALTH_TAG, description = "Health information API"), (name = tag::HEALTH_TAG, description = "Health information API"),

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,40 @@
"version": "0.1.0" "version": "0.1.0"
}, },
"paths": { "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": { "/api/auth/login": {
"post": { "post": {
"tags": [ "tags": [
@@ -75,6 +109,26 @@
}, },
"components": { "components": {
"schemas": { "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": { "HealthInfo": {
"type": "object", "type": "object",
"description": "System health information", "description": "System health information",

View File

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