Compare commits
4 Commits
34ebfaddbc
...
81fbf8281f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81fbf8281f | ||
|
|
8875122e1b | ||
|
|
65d40c1e83 | ||
|
|
d2b842d933 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4353,11 +4353,13 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clap",
|
||||||
"config",
|
"config",
|
||||||
"database",
|
"database",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"migration",
|
"migration",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
"once_cell",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ sea-orm = { workspace = true }
|
|||||||
include_dir = { version = "0.7.4" }
|
include_dir = { version = "0.7.4" }
|
||||||
mime_guess = { version = "2.0.5" }
|
mime_guess = { version = "2.0.5" }
|
||||||
utoipa = { version = "5.4.0", features = ["macros", "axum_extras", "chrono", "decimal", "uuid", "time", "openapi_extensions"] }
|
utoipa = { version = "5.4.0", features = ["macros", "axum_extras", "chrono", "decimal", "uuid", "time", "openapi_extensions"] }
|
||||||
|
clap = { version = "4.5.53" }
|
||||||
|
once_cell = { version = "1.21.3" }
|
||||||
|
|||||||
49
apps/api/src/cmd.rs
Normal file
49
apps/api/src/cmd.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
mod generate_openapi;
|
||||||
|
mod start_server;
|
||||||
|
|
||||||
|
pub use start_server::start_server;
|
||||||
|
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::{future::Future, process::exit};
|
||||||
|
|
||||||
|
use clap::{ArgMatches, Command};
|
||||||
|
|
||||||
|
pub struct CliCommand {
|
||||||
|
pub command: Command,
|
||||||
|
pub action: fn(&clap::ArgMatches) -> Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static CLI_COMMANDS: once_cell::sync::Lazy<
|
||||||
|
[CliCommand; 2 /* Update this count when adding new commands */],
|
||||||
|
> =
|
||||||
|
once_cell::sync::Lazy::new(|| {
|
||||||
|
[
|
||||||
|
// Add new commands here
|
||||||
|
generate_openapi::get_cli_command(),
|
||||||
|
start_server::get_cli_command(),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn get_command() -> Command {
|
||||||
|
let mut c = Command::new("cmd");
|
||||||
|
|
||||||
|
for cmd in CLI_COMMANDS.iter() {
|
||||||
|
c = c.subcommand(cmd.command.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(matches: &ArgMatches, help_msg: &str) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||||
|
if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() {
|
||||||
|
for cmd in CLI_COMMANDS.iter() {
|
||||||
|
if cmd.command.get_name() == subcommand_name {
|
||||||
|
return (cmd.action)(subcommand_matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("Error: No valid subcommand provided.");
|
||||||
|
eprintln!("{}", help_msg);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
44
apps/api/src/cmd/generate_openapi.rs
Normal file
44
apps/api/src/cmd/generate_openapi.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use clap::{Arg, Command};
|
||||||
|
use tracing::info;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
use crate::{cmd::CliCommand, log, routes::ApiDoc};
|
||||||
|
|
||||||
|
pub fn get_cli_command() -> CliCommand {
|
||||||
|
CliCommand {
|
||||||
|
command: command(),
|
||||||
|
action,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command() -> Command {
|
||||||
|
Command::new("generate:openapi")
|
||||||
|
.arg(
|
||||||
|
Arg::new("output_path")
|
||||||
|
.short('o')
|
||||||
|
.long("output-path")
|
||||||
|
.value_name("PATH")
|
||||||
|
.help("Path to output the generated OpenAPI documentation")
|
||||||
|
.required(true),
|
||||||
|
)
|
||||||
|
.about("Generate OpenAPI documentation")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(
|
||||||
|
_matches: &clap::ArgMatches,
|
||||||
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
|
||||||
|
let output_path = _matches.get_one::<String>("output_path");
|
||||||
|
let output_path = output_path.unwrap().to_string();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
tracing::subscriber::with_default(log::make_temporary_subscriber(), || {
|
||||||
|
info!("Generating OpenAPI documentation...");
|
||||||
|
let doc = ApiDoc::openapi();
|
||||||
|
let json = doc
|
||||||
|
.to_pretty_json()
|
||||||
|
.expect("Failed to serialize OpenAPI doc to JSON");
|
||||||
|
std::fs::write(&output_path, json).expect("Failed to write OpenAPI doc to file");
|
||||||
|
info!("OpenAPI documentation generated at {}", output_path);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
142
apps/api/src/cmd/start_server.rs
Normal file
142
apps/api/src/cmd/start_server.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
use clap::Command;
|
||||||
|
use database::get_connection;
|
||||||
|
use sea_orm::ConnectOptions;
|
||||||
|
use tracing::{debug, info};
|
||||||
|
use tracing_subscriber::fmt::format::{DefaultFields, Format};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cmd::CliCommand,
|
||||||
|
configs::{ProgramSettings, get_program_settings, logging::LoggingSettings},
|
||||||
|
log,
|
||||||
|
routes::{self, AppService, AppState},
|
||||||
|
services::settings::SettingsService,
|
||||||
|
tasks,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn get_cli_command() -> CliCommand {
|
||||||
|
CliCommand {
|
||||||
|
command: command(),
|
||||||
|
action,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command() -> Command {
|
||||||
|
Command::new("start").about("Start the server")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(
|
||||||
|
_matches: &clap::ArgMatches,
|
||||||
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
|
||||||
|
Box::pin(async move {})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_server() {
|
||||||
|
let settings = tracing::subscriber::with_default(
|
||||||
|
log::make_temporary_subscriber(),
|
||||||
|
|| -> ProgramSettings {
|
||||||
|
debug!("Temporary subscriber installed.");
|
||||||
|
info!("Reading configuration...");
|
||||||
|
let settings = get_program_settings();
|
||||||
|
info!("Configuration read successfully.");
|
||||||
|
debug!("Resetting global subscriber...");
|
||||||
|
|
||||||
|
let subscriber = get_global_tracing_subscriber_builder(&settings.logging).finish();
|
||||||
|
tracing::subscriber::set_global_default(subscriber)
|
||||||
|
.expect("Failed to set global default subscriber");
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Global subscriber set with logging level: {:?}",
|
||||||
|
settings.logging.level
|
||||||
|
);
|
||||||
|
|
||||||
|
settings
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tasks::startup::run_startup_tasks(&settings)
|
||||||
|
.await
|
||||||
|
.expect("Failed to run startup tasks");
|
||||||
|
|
||||||
|
// setup database connection pool
|
||||||
|
info!("Establishing database connection...");
|
||||||
|
debug!("Database URL: {}", settings.database.url);
|
||||||
|
|
||||||
|
let db_options = |options: &mut ConnectOptions| {
|
||||||
|
options.max_connections(settings.database.max_connections);
|
||||||
|
};
|
||||||
|
|
||||||
|
let db_connection = Arc::new(
|
||||||
|
get_connection(&settings.database.url, Some(db_options))
|
||||||
|
.await
|
||||||
|
.expect("Failed to establish database connection"),
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("Database connection established.");
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
|
||||||
|
let address = format!("{}:{}", settings.server.address, settings.server.port);
|
||||||
|
info!("Starting server at http://{}", address);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(address)
|
||||||
|
.await
|
||||||
|
.expect("Failed to bind to address");
|
||||||
|
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.expect("Failed to run the server");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_global_tracing_subscriber_builder(
|
||||||
|
settings: &LoggingSettings,
|
||||||
|
) -> tracing_subscriber::fmt::SubscriberBuilder<
|
||||||
|
DefaultFields,
|
||||||
|
Format<tracing_subscriber::fmt::format::Full, BoxedTimer>,
|
||||||
|
> {
|
||||||
|
// After configuration is read, install the global subscriber
|
||||||
|
let builder = tracing_subscriber::fmt()
|
||||||
|
.with_max_level(settings.level)
|
||||||
|
.with_target(false)
|
||||||
|
.with_level(true);
|
||||||
|
|
||||||
|
if settings.utc {
|
||||||
|
builder.with_timer(BoxedTimer(Box::new(
|
||||||
|
tracing_subscriber::fmt::time::UtcTime::rfc_3339(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
builder.with_timer(BoxedTimer(Box::new(
|
||||||
|
tracing_subscriber::fmt::time::ChronoLocal::rfc_3339(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_app_state(db_connection: &Arc<sea_orm::DatabaseConnection>) -> AppState {
|
||||||
|
AppState {
|
||||||
|
database_connection: db_connection.clone(),
|
||||||
|
service: Arc::new(AppService {
|
||||||
|
settings: Arc::new(SettingsService::new(db_connection.clone())),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A small wrapper that holds a boxed `FormatTime` trait object and itself
|
||||||
|
// implements `FormatTime`, allowing us to use it as a concrete type with
|
||||||
|
// `builder.with_timer` while still picking the concrete timer implementation
|
||||||
|
// at runtime.
|
||||||
|
// wrapper type to hold boxed timers and implement the `FormatTime` trait for
|
||||||
|
// a concrete type so `with_timer` may be called once outside the conditional.
|
||||||
|
struct BoxedTimer(Box<dyn tracing_subscriber::fmt::time::FormatTime + Send + Sync + 'static>);
|
||||||
|
|
||||||
|
impl tracing_subscriber::fmt::time::FormatTime for BoxedTimer {
|
||||||
|
fn format_time(
|
||||||
|
&self,
|
||||||
|
w: &mut tracing_subscriber::fmt::format::Writer<'_>,
|
||||||
|
) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
self.0.format_time(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
apps/api/src/log.rs
Normal file
7
apps/api/src/log.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub fn make_temporary_subscriber() -> tracing_subscriber::fmt::Subscriber {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::DEBUG)
|
||||||
|
.with_target(false)
|
||||||
|
.with_level(true)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
@@ -1,136 +1,39 @@
|
|||||||
|
mod cmd;
|
||||||
mod configs;
|
mod configs;
|
||||||
mod errors;
|
mod errors;
|
||||||
|
mod log;
|
||||||
mod middlewares;
|
mod middlewares;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod services;
|
mod services;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use axum::Router;
|
|
||||||
use database::get_connection;
|
|
||||||
use sea_orm::ConnectOptions;
|
|
||||||
use tracing::{debug, info};
|
|
||||||
use tracing_subscriber::fmt::format::{DefaultFields, Format};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
configs::{ProgramSettings, get_program_settings, logging::LoggingSettings},
|
|
||||||
routes::{AppService, AppState},
|
|
||||||
services::settings::SettingsService,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Temporary subscriber for initial logging during configuration reading
|
// If there are command-line arguments, treat it as a CLI command
|
||||||
let make_temporary_subscriber = || {
|
if std::env::args().len() > 1 {
|
||||||
tracing_subscriber::fmt()
|
tracing::subscriber::with_default(log::make_temporary_subscriber(), || {
|
||||||
.with_max_level(tracing::Level::DEBUG)
|
use clap::error::ErrorKind;
|
||||||
.with_target(false)
|
//
|
||||||
.with_level(true)
|
let mut command = cmd::get_command();
|
||||||
.finish()
|
let help_output = format!("{}", command.render_help());
|
||||||
};
|
let matches = command
|
||||||
|
.try_get_matches()
|
||||||
let settings =
|
.unwrap_or_else(|err| match err.kind() {
|
||||||
tracing::subscriber::with_default(make_temporary_subscriber(), || -> ProgramSettings {
|
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
|
||||||
debug!("Temporary subscriber installed.");
|
err.print().expect("Error writing Error");
|
||||||
info!("Reading configuration...");
|
std::process::exit(0);
|
||||||
let settings = get_program_settings();
|
}
|
||||||
info!("Configuration read successfully.");
|
_ => {
|
||||||
debug!("Resetting global subscriber...");
|
err.print().expect("Error writing Error");
|
||||||
|
std::process::exit(1);
|
||||||
let subscriber = get_global_tracing_subscriber_builder(&settings.logging).finish();
|
}
|
||||||
tracing::subscriber::set_global_default(subscriber)
|
|
||||||
.expect("Failed to set global default subscriber");
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Global subscriber set with logging level: {:?}",
|
|
||||||
settings.logging.level
|
|
||||||
);
|
|
||||||
|
|
||||||
settings
|
|
||||||
});
|
});
|
||||||
|
cmd::execute(&matches, &help_output)
|
||||||
tasks::startup::run_startup_tasks(&settings)
|
})
|
||||||
.await
|
.await;
|
||||||
.expect("Failed to run startup tasks");
|
return;
|
||||||
|
|
||||||
// setup database connection pool
|
|
||||||
info!("Establishing database connection...");
|
|
||||||
debug!("Database URL: {}", settings.database.url);
|
|
||||||
|
|
||||||
let db_options = |options: &mut ConnectOptions| {
|
|
||||||
options.max_connections(settings.database.max_connections);
|
|
||||||
};
|
|
||||||
|
|
||||||
let db_connection = Arc::new(
|
|
||||||
get_connection(&settings.database.url, Some(db_options))
|
|
||||||
.await
|
|
||||||
.expect("Failed to establish database connection"),
|
|
||||||
);
|
|
||||||
|
|
||||||
info!("Database connection established.");
|
|
||||||
|
|
||||||
// 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)));
|
|
||||||
|
|
||||||
let address = format!("{}:{}", settings.server.address, settings.server.port);
|
|
||||||
info!("Starting server at http://{}", address);
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(address)
|
|
||||||
.await
|
|
||||||
.expect("Failed to bind to address");
|
|
||||||
|
|
||||||
axum::serve(listener, app)
|
|
||||||
.await
|
|
||||||
.expect("Failed to run the server");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_global_tracing_subscriber_builder(
|
// No command-line arguments, start the server normally
|
||||||
settings: &LoggingSettings,
|
cmd::start_server().await;
|
||||||
) -> tracing_subscriber::fmt::SubscriberBuilder<
|
|
||||||
DefaultFields,
|
|
||||||
Format<tracing_subscriber::fmt::format::Full, BoxedTimer>,
|
|
||||||
> {
|
|
||||||
// After configuration is read, install the global subscriber
|
|
||||||
let builder = tracing_subscriber::fmt()
|
|
||||||
.with_max_level(settings.level)
|
|
||||||
.with_target(false)
|
|
||||||
.with_level(true);
|
|
||||||
|
|
||||||
if settings.utc {
|
|
||||||
builder.with_timer(BoxedTimer(Box::new(
|
|
||||||
tracing_subscriber::fmt::time::UtcTime::rfc_3339(),
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
builder.with_timer(BoxedTimer(Box::new(
|
|
||||||
tracing_subscriber::fmt::time::ChronoLocal::rfc_3339(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_app_state(db_connection: &Arc<sea_orm::DatabaseConnection>) -> AppState {
|
|
||||||
AppState {
|
|
||||||
database_connection: db_connection.clone(),
|
|
||||||
service: Arc::new(AppService {
|
|
||||||
settings: Arc::new(SettingsService::new(db_connection.clone())),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A small wrapper that holds a boxed `FormatTime` trait object and itself
|
|
||||||
// implements `FormatTime`, allowing us to use it as a concrete type with
|
|
||||||
// `builder.with_timer` while still picking the concrete timer implementation
|
|
||||||
// at runtime.
|
|
||||||
// wrapper type to hold boxed timers and implement the `FormatTime` trait for
|
|
||||||
// a concrete type so `with_timer` may be called once outside the conditional.
|
|
||||||
struct BoxedTimer(Box<dyn tracing_subscriber::fmt::time::FormatTime + Send + Sync + 'static>);
|
|
||||||
|
|
||||||
impl tracing_subscriber::fmt::time::FormatTime for BoxedTimer {
|
|
||||||
fn format_time(
|
|
||||||
&self,
|
|
||||||
w: &mut tracing_subscriber::fmt::format::Writer<'_>,
|
|
||||||
) -> std::result::Result<(), std::fmt::Error> {
|
|
||||||
self.0.format_time(w)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,27 @@ use axum::{Json, extract::State, http::StatusCode};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::routes::api::health::state::HealthState;
|
use crate::routes::api::{health::state::HealthState, openapi::tag::HEALTH_TAG};
|
||||||
|
|
||||||
const STATUS_HEALTHY: &str = "healthy";
|
const STATUS_HEALTHY: &str = "healthy";
|
||||||
const STATUS_UNHEALTHY: &str = "unhealthy";
|
const STATUS_UNHEALTHY: &str = "unhealthy";
|
||||||
|
|
||||||
|
/// System health information
|
||||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct HealthInfo {
|
pub struct HealthInfo {
|
||||||
|
/// Health status: "healthy" or "unhealthy"
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
/// Application version
|
||||||
pub version: String,
|
pub version: String,
|
||||||
// RFC 3339 formatted timestamp
|
/// RFC 3339 formatted timestamp
|
||||||
pub up_since: DateTime<Utc>,
|
pub up_since: DateTime<Utc>,
|
||||||
|
/// List of error messages if unhealthy
|
||||||
pub errors: Option<Vec<String>>,
|
pub errors: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Health check endpoint
|
||||||
|
///
|
||||||
|
/// Returns the health status, version, uptime, and any errors if unhealthy.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/health/info",
|
path = "/api/health/info",
|
||||||
@@ -25,9 +32,7 @@ pub struct HealthInfo {
|
|||||||
(status = 200, description = "Health information retrieved successfully", body = HealthInfo),
|
(status = 200, description = "Health information retrieved successfully", body = HealthInfo),
|
||||||
(status = NOT_FOUND, description = "Health information not found")
|
(status = NOT_FOUND, description = "Health information not found")
|
||||||
),
|
),
|
||||||
params(
|
tag = HEALTH_TAG,
|
||||||
("id" = u64, Path, description = "Pet database id to get Pet for"),
|
|
||||||
)
|
|
||||||
)]
|
)]
|
||||||
pub async fn get_health_info(
|
pub async fn get_health_info(
|
||||||
State(state): State<Arc<HealthState>>,
|
State(state): State<Arc<HealthState>>,
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
pub mod tag {
|
||||||
|
/// Health tag constant
|
||||||
|
pub const HEALTH_TAG: &str = "Health";
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(utoipa::OpenApi)]
|
#[derive(utoipa::OpenApi)]
|
||||||
#[openapi(paths(crate::routes::api::health::info::get_health_info))]
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::routes::api::health::info::get_health_info
|
||||||
|
),
|
||||||
|
components(
|
||||||
|
schemas(crate::routes::api::health::info::HealthInfo) // Register any schemas used in your paths
|
||||||
|
),
|
||||||
|
tags(
|
||||||
|
(name = tag::HEALTH_TAG, description = "Health information API")
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub struct ApiDoc;
|
pub struct ApiDoc;
|
||||||
|
|||||||
82
apps/api/swagger.json
Normal file
82
apps/api/swagger.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.1.0",
|
||||||
|
"info": {
|
||||||
|
"title": "yet-another-nginx-proxy-manager",
|
||||||
|
"description": "",
|
||||||
|
"license": {
|
||||||
|
"name": ""
|
||||||
|
},
|
||||||
|
"version": "0.1.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/api/health/info": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Health"
|
||||||
|
],
|
||||||
|
"summary": "Health check endpoint",
|
||||||
|
"description": "Returns the health status, version, uptime, and any errors if unhealthy.",
|
||||||
|
"operationId": "get_health_info",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Health information retrieved successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HealthInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Health information not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"HealthInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "System health information",
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"version",
|
||||||
|
"up_since"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"errors": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "List of error messages if unhealthy"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Health status: \"healthy\" or \"unhealthy\""
|
||||||
|
},
|
||||||
|
"up_since": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"description": "RFC 3339 formatted timestamp"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Application version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Health",
|
||||||
|
"description": "Health information API"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
justfile
5
justfile
@@ -41,6 +41,11 @@ generate-entity:
|
|||||||
# delegate to cli
|
# delegate to cli
|
||||||
just cli db:migrate_and_generate --output-path ../../public/database/src/generated/entities
|
just cli db:migrate_and_generate --output-path ../../public/database/src/generated/entities
|
||||||
|
|
||||||
|
generate-openapi:
|
||||||
|
# delegate to cli
|
||||||
|
cd apps/api && \
|
||||||
|
cargo run -- generate:openapi --output-path ./swagger.json
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
# build frontend assets
|
# build frontend assets
|
||||||
cd apps/frontend && \
|
cd apps/frontend && \
|
||||||
|
|||||||
Reference in New Issue
Block a user