Compare commits
3 Commits
ed4a091d6e
...
86fb222d18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86fb222d18 | ||
|
|
08b1a055a4 | ||
|
|
8f2193bed2 |
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
143
apps/api/src/routes/api/auth/init_admin.rs
Normal file
143
apps/api/src/routes/api/auth/init_admin.rs
Normal 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()
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user