feature/authentication service #9

Merged
GW_MC merged 19 commits from feature/authentication into master 2025-12-19 12:24:49 +08:00
5 changed files with 218 additions and 4 deletions
Showing only changes of commit 08b1a055a4 - Show all commits

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

@@ -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: {