4c98dd93ad
- Added an enterprise-grade, request-scoped AuditLogger extractor in Axum. - Configured MongoDB persistence for structured, replayable audit logs (capturing timestamp, user, action, type, payload snapshot, client IP with proxy header support, and User-Agent). - Created a live Administrator console at /auth/audit to filter and inspect log events. - Re-architected documentation by moving Design Wiki pages out of /components into a dedicated /docs route. - Published logging architecture documentation at /docs/logging.
156 lines
4.3 KiB
Rust
156 lines
4.3 KiB
Rust
mod common;
|
|
mod auth;
|
|
mod tasks;
|
|
mod audit;
|
|
mod developers;
|
|
mod main_view;
|
|
mod docs;
|
|
|
|
use axum::{extract::FromRef, Router};
|
|
use std::net::SocketAddr;
|
|
use tracing::info;
|
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
|
|
|
use crate::common::config::Config;
|
|
use crate::common::database::connect_db;
|
|
use crate::auth::repository::MongoUserRepository;
|
|
use crate::tasks::repository::MongoTaskRepository;
|
|
use crate::developers::repository::MongoDeveloperRepository;
|
|
use crate::audit::repository::MongoAuditRepository;
|
|
|
|
#[derive(Clone)]
|
|
struct AppState {
|
|
config: Config,
|
|
db: mongodb::Database,
|
|
user_repo: MongoUserRepository,
|
|
task_repo: MongoTaskRepository,
|
|
dev_repo: MongoDeveloperRepository,
|
|
audit_repo: MongoAuditRepository,
|
|
}
|
|
|
|
impl FromRef<AppState> for Config {
|
|
fn from_ref(state: &AppState) -> Self {
|
|
state.config.clone()
|
|
}
|
|
}
|
|
|
|
impl FromRef<AppState> for mongodb::Database {
|
|
fn from_ref(state: &AppState) -> Self {
|
|
state.db.clone()
|
|
}
|
|
}
|
|
|
|
impl FromRef<AppState> for MongoUserRepository {
|
|
fn from_ref(state: &AppState) -> Self {
|
|
state.user_repo.clone()
|
|
}
|
|
}
|
|
|
|
impl FromRef<AppState> for MongoTaskRepository {
|
|
fn from_ref(state: &AppState) -> Self {
|
|
state.task_repo.clone()
|
|
}
|
|
}
|
|
|
|
impl FromRef<AppState> for MongoDeveloperRepository {
|
|
fn from_ref(state: &AppState) -> Self {
|
|
state.dev_repo.clone()
|
|
}
|
|
}
|
|
|
|
impl FromRef<AppState> for MongoAuditRepository {
|
|
fn from_ref(state: &AppState) -> Self {
|
|
state.audit_repo.clone()
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
// 1. Initialize logging
|
|
tracing_subscriber::registry()
|
|
.with(tracing_subscriber::fmt::layer())
|
|
.init();
|
|
|
|
info!("Starting Stick Template application...");
|
|
|
|
// 2. Parse config from env
|
|
let config = Config::from_env();
|
|
|
|
// 3. Connect to MongoDB
|
|
let db = connect_db(&config).await?;
|
|
|
|
// 4. Initialize repositories
|
|
let user_repo = MongoUserRepository::new(db.clone());
|
|
let task_repo = MongoTaskRepository::new(db.clone());
|
|
let dev_repo = MongoDeveloperRepository::new(db.clone());
|
|
let audit_repo = MongoAuditRepository::new(db.clone());
|
|
|
|
// Auto-provision initial administrator if users collection is empty
|
|
let users_count = db.collection::<crate::auth::models::User>("users")
|
|
.count_documents(mongodb::bson::doc! {})
|
|
.await?;
|
|
|
|
if users_count == 0 {
|
|
use rand::{distributions::Alphanumeric, Rng};
|
|
let random_password: String = rand::thread_rng()
|
|
.sample_iter(&Alphanumeric)
|
|
.take(16)
|
|
.map(char::from)
|
|
.collect();
|
|
let password_hash = bcrypt::hash(&random_password, bcrypt::DEFAULT_COST)?;
|
|
|
|
let admin_username = "admin";
|
|
let admin_user = crate::auth::models::User {
|
|
id: None,
|
|
username: admin_username.to_string(),
|
|
password_hash,
|
|
is_admin: true,
|
|
created_at: chrono::Utc::now(),
|
|
};
|
|
|
|
db.collection::<crate::auth::models::User>("users")
|
|
.insert_one(admin_user)
|
|
.await?;
|
|
|
|
info!("\n\n\
|
|
======================================================\n\
|
|
CREATED INITIAL ADMINISTRATOR ACCOUNT:\n\
|
|
Username: {}\n\
|
|
Password: {}\n\
|
|
======================================================\n\n",
|
|
admin_username, random_password);
|
|
}
|
|
|
|
// 5. Initialize shared AppState
|
|
let state = AppState {
|
|
config: config.clone(),
|
|
db,
|
|
user_repo,
|
|
task_repo,
|
|
dev_repo,
|
|
audit_repo,
|
|
};
|
|
|
|
// 6. Build and merge routers by use-case
|
|
let app = Router::new()
|
|
.merge(main_view::router())
|
|
.merge(docs::router())
|
|
.merge(auth::router())
|
|
.merge(tasks::router())
|
|
.merge(developers::router())
|
|
.merge(audit::router())
|
|
.with_state(state);
|
|
|
|
// 7. Bind address and run server
|
|
let host_addr: SocketAddr = format!("{}:{}", config.host, config.port)
|
|
.parse()
|
|
.expect("Invalid HOST or PORT config");
|
|
|
|
info!("Listening on http://{}", host_addr);
|
|
|
|
let listener = tokio::net::TcpListener::bind(host_addr).await?;
|
|
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
|
|
|
|
Ok(())
|
|
}
|