Rename employee-tracking-backend to purenotify_backend and add OFFLINE

mode

- Rename crate and update dependencies to newer versions - Add OFFLINE
runtime mode for loopback-only server without DB - Refactor state
handling with typed Axum routers and state injection - Rename mongodb
module to mongo and fix imports accordingly - Update Cargo.lock with
updated and removed dependencies - Remove no-auth feature and related
code - Simplify health and user routes to generic state parameter Rename
backend to purenotify_backend and add OFFLINE mode

Use OFFLINE env var to run server without DB, binding to loopback only.
Rename mongodb module to mongo and update dependencies. Update
dependencies and fix router state handling.
This commit is contained in:
2025-08-20 20:53:22 -04:00
parent e79d16b87f
commit d3feeef996
14 changed files with 391 additions and 990 deletions

2
.env
View File

@@ -2,4 +2,4 @@
RUST_LOG=info RUST_LOG=info
BIND_ADDRESS=127.0.0.1:3000 BIND_ADDRESS=127.0.0.1:3000
# DATABASE_URL=postgres://gerard@localhost/db (not used yet) MONGO_URI=mongodb://localhost:27017

1248
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,20 @@
[package] [package]
name = "employee-tracking-backend" name = "purenotify_backend"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
axum = "0.8.4" axum = "0.8.4"
bson = { version = "2.9.0", features = ["chrono-0_4"] } bson = { version = "2.15.0", features = ["chrono-0_4"] }
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.41", features = ["serde"] }
sha2 = "0.10.8" sha2 = "0.10.9"
rand = "0.8.5" rand = "0.9.2"
regex = "1.10.4" regex = "1.11.1"
dotenvy = "0.15.7" dotenvy = "0.15.7"
mongodb = "2.8.2" mongodb = "3.2.4"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142" serde_json = "1.0.143"
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-native-tls"] }
tokio = { version = "1.47.1", features = ["full", "rt-multi-thread", "signal"] } tokio = { version = "1.47.1", features = ["full", "rt-multi-thread", "signal"] }
tower-http = { version = "0.6.6", features = ["trace"] } tower-http = { version = "0.6.6", features = ["trace"] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
[features]
no-auth = []

View File

@@ -4,7 +4,6 @@ use std::env;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::str::FromStr; use std::str::FromStr;
#[cfg(feature = "no-auth")]
use tracing::error; use tracing::error;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -22,7 +21,6 @@ impl Config {
let bind_address = SocketAddr::from_str(&bind_address_str) let bind_address = SocketAddr::from_str(&bind_address_str)
.map_err(|e| format!("Invalid BIND_ADDRESS: {}", e))?; .map_err(|e| format!("Invalid BIND_ADDRESS: {}", e))?;
#[cfg(feature = "no-auth")]
if bind_address.ip() != std::net::IpAddr::from([127, 0, 0, 1]) { if bind_address.ip() != std::net::IpAddr::from([127, 0, 0, 1]) {
error!("In no-auth mode, BIND_ADDRESS must be 127.0.0.1"); error!("In no-auth mode, BIND_ADDRESS must be 127.0.0.1");
return Err("In no-auth mode, BIND_ADDRESS must be 127.0.0.1".to_string()); return Err("In no-auth mode, BIND_ADDRESS must be 127.0.0.1".to_string());

View File

@@ -1,9 +1,7 @@
// src/main.rs // src/main.rs
use std::process::exit; use std::{process::exit, sync::Arc};
use std::sync::Arc;
use ::mongodb::Database;
use axum::Router; use axum::Router;
use dotenvy::dotenv; use dotenvy::dotenv;
use tokio::signal; use tokio::signal;
@@ -13,13 +11,14 @@ use tracing_subscriber::{EnvFilter, fmt, prelude::*};
mod config; mod config;
mod handlers; mod handlers;
mod mongodb; mod mongo; // local module wrapping the Mongo client
mod routes; mod routes;
use ::mongodb::Database; // external crate (absolute path avoids name clash)
use config::Config; use config::Config;
use mongodb::MongoDb; use mongo::MongoDb; // your wrapper
// Shared application state // Shared application state for online mode
pub struct AppState { pub struct AppState {
pub db: Database, pub db: Database,
pub config: Config, pub config: Config,
@@ -27,51 +26,86 @@ pub struct AppState {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Load environment variables from .env file // Load .env early
dotenv().ok(); dotenv().ok();
// Initialize tracing // Tracing with a safe fallback if RUST_LOG is unset
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry() tracing_subscriber::registry()
.with(fmt::layer()) .with(fmt::layer())
.with(EnvFilter::from_env("RUST_LOG")) .with(env_filter)
.init(); .init();
// Load config // Load config
let config = match Config::from_env() { let config = match Config::from_env() {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
error!("Failed to load config: {}", e); error!("Failed to load config: {e}");
exit(1); exit(1);
} }
}; };
// Connect to MongoDB using the config // Runtime OFFLINE switch: true if OFFLINE is 1/true/yes/on (case-insensitive)
let mongodb = match MongoDb::connect(&config).await { let offline = std::env::var("OFFLINE")
.ok()
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
.unwrap_or(false);
if offline {
// Enforce loopback binding while offline
if !config.bind_address.ip().is_loopback() {
error!(
"OFFLINE=true requires binding to a loopback address (e.g., 127.0.0.1:<port> or [::1]:<port>), got {}",
config.bind_address
);
exit(1);
}
info!("OFFLINE mode enabled — not connecting to MongoDB");
info!("Server starting on {}", config.bind_address);
// Health-only, no state. Subrouter is typed to `()`.
let app = Router::new()
.nest("/health", routes::health::health::health_routes::<()>())
.layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind(config.bind_address)
.await
.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
return;
}
// --- Online (DB-enabled) path ---
let mongo = match MongoDb::connect(&config).await {
Ok(db) => db, Ok(db) => db,
Err(e) => { Err(e) => {
error!("Failed to connect to MongoDB: {}", e); error!("Failed to connect to MongoDB: {e}");
exit(1); exit(1);
} }
}; };
// Create shared state let shared_state = Arc::new(AppState {
let _shared_state = Arc::new(AppState { db: mongo.database,
db: mongodb.database,
config: config.clone(), config: config.clone(),
}); });
#[cfg(feature = "no-auth")]
info!("NO-AUTH MODE ENABLED");
info!("Server starting on {}", config.bind_address); info!("Server starting on {}", config.bind_address);
// Build the Axum router // Build subrouters typed with the same state as the root
let app = Router::new() let health_router = routes::health::health::health_routes::<Arc<AppState>>();
.nest("/health", routes::health::health::health_routes()) let user_router = routes::user::user::user_routes::<Arc<AppState>>();
.nest("/user", routes::user::user::user_routes())
// Root router typed with state; set state once on the root
let app = Router::<Arc<AppState>>::new()
.nest("/health", health_router)
.nest("/user", user_router)
.with_state(shared_state)
.layer(TraceLayer::new_for_http()); .layer(TraceLayer::new_for_http());
// Run the server
let listener = tokio::net::TcpListener::bind(config.bind_address) let listener = tokio::net::TcpListener::bind(config.bind_address)
.await .await
.unwrap(); .unwrap();

View File

@@ -25,7 +25,7 @@ impl MongoDb {
// Ping the server to verify connection // Ping the server to verify connection
client client
.database("admin") .database("admin")
.run_command(mongodb::bson::doc! {"ping": 1}, None) .run_command(mongodb::bson::doc! {"ping": 1})
.await?; .await?;
println!("✅ Successfully connected to MongoDB!"); println!("✅ Successfully connected to MongoDB!");

View File

@@ -1,9 +1,11 @@
// src/routes/health/health.rs // src/routes/health/healh.rs
use axum::{Router, routing::get}; use axum::{Router, routing::get};
use crate::handlers::health::health::health; pub fn health_routes<S>() -> Router<S>
where
pub fn health_routes() -> Router { S: Clone + Send + Sync + 'static,
Router::new().route("/", get(health)) {
// keep your existing routes/handlers here
Router::new().route("/", get(crate::handlers::health::health::health))
} }

View File

@@ -5,11 +5,12 @@ use axum::{
routing::{get, post}, routing::{get, post},
}; };
use crate::handlers::user::register::register; pub fn user_routes<S>() -> Router<S>
use crate::handlers::user::user::user; where
S: Clone + Send + Sync + 'static,
pub fn user_routes() -> Router { {
// keep your existing routes/handlers here
Router::new() Router::new()
.route("/", get(user)) .route("/", get(crate::handlers::user::user::user))
.route("/register", post(register)) .route("/register", post(crate::handlers::user::register::register))
} }

View File