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...
|
// 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);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
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(
|
#[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"),
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user