feat: add MongoDB support with connection pooling and repository pattern

This commit is contained in:
2025-08-16 06:38:30 -04:00
parent 8a05f4edac
commit ed612bd717
12 changed files with 1226 additions and 49 deletions

2
.env
View File

@@ -1,4 +1,4 @@
// .env.example (copy to .env for local use)
# .env.example (copy to .env for local use)
RUST_LOG=info
BIND_ADDRESS=127.0.0.1:3000

1022
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,10 @@ edition = "2024"
[dependencies]
axum = "0.8.4"
bson = { version = "2.9.0", features = ["chrono-0_4"] }
chrono = { version = "0.4", features = ["serde"] }
dotenvy = "0.15.7"
mongodb = "2.8.2"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-native-tls"] }

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# Use the official Rust image as a base
FROM rust:bookworm as builder
# Set the working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
# Add any other dependencies required by your project
# For example, if you use postgres/mysql/sqlite, you might need libpq-dev, libmysqlclient-dev, libsqlite3-dev
&& rm -rf /var/lib/apt/lists/*
# Copy the Cargo.toml and Cargo.lock first to leverage Docker cache
# This layer only rebuilds if dependencies change
COPY Cargo.toml Cargo.lock ./
# Create a dummy src directory and main.rs to build dependencies
# This caches the dependency build
RUN mkdir -p src && echo "fn main() {println!(\"hello world\");}" > src/main.rs
# Build dependencies
RUN cargo build --release && rm -rf src
# Copy the actual source code
COPY . .
# Build the release binary
RUN cargo build --release
# --- Start a new stage for a smaller final image ---
FROM debian:bookworm-slim
# Set the working directory
WORKDIR /app
# Install runtime dependencies if any
# For example, if your Rust application dynamically links to OpenSSL, you might need libssl3
RUN apt-get update && apt-get install -y \
libssl3 \
# Add any other runtime dependencies here
&& rm -rf /var/lib/apt/lists/*
# Copy the built binary from the builder stage
COPY --from=builder /app/target/release/employee-tracking-backend .
# Expose the port your application listens on
EXPOSE 3000
# Set the entrypoint command to run your application
CMD ["./employee-tracking-backend"]

View File

@@ -55,12 +55,34 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo build
```
4. **Run the application:**
4. **Run the application database with Docker (Recommended):**
If you have Docker installed
5. **Run the application (Manual with Docker):**
First, start the MongoDB container:
```sh
docker run \
--name mongodb \
-p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=password123 \
-e MONGO_INITDB_DATABASE=purenotify \
-v mongodb_data:/data/db \
mongo:latest
```
(Note: The `purenotify` database name here should match `DATABASE_NAME` in your `.env` or `config.rs` for the backend to connect correctly. The `MONGODB_URI` for the backend would be `mongodb://127.0.0.1:27017` or `mongodb://localhost:27017`.)
Then, run the Rust application:
For development, you can run the project directly with `cargo run`:
```sh
cargo run
```
For a release build, run:
```sh
cargo run --release
```

33
docker-compose.yml Normal file
View File

@@ -0,0 +1,33 @@
version: "3.8"
services:
purenotify_backend:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
# These should match the defaults or your specific configuration in config.rs
BIND_ADDRESS: "0.0.0.0:3000"
MONGODB_URI: "mongodb://mongodb:27017"
DATABASE_NAME: "purenotify"
RUST_LOG: "info,tower_http=debug,mongodb=debug"
depends_on:
- mongodb
# Optional: If you want to enable the no-auth feature for local development
# command: cargo run --features "no-auth"
mongodb:
image: mongo:6.0
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
# Optional: MongoDB authentication (highly recommended for production)
# MONGO_INITDB_ROOT_USERNAME: your_mongo_username
# MONGO_INITDB_ROOT_PASSWORD: your_mongo_password
# MONGODB_REPLICA_SET_NAME: rs0 # Uncomment for replica set
volumes:
mongodb_data:

View File

@@ -4,10 +4,14 @@ use std::env;
use std::net::SocketAddr;
use std::str::FromStr;
#[derive(Debug)]
#[cfg(feature = "no-auth")]
use tracing::error;
#[derive(Debug, Clone)]
pub struct Config {
pub bind_address: SocketAddr,
// pub database_url: Option<String>,
pub mongodb_uri: String,
pub database_name: String,
}
impl Config {
@@ -24,11 +28,15 @@ impl Config {
return Err("In no-auth mode, BIND_ADDRESS must be 127.0.0.1".to_string());
}
// let database_url = env::var("DATABASE_URL").ok();
let mongodb_uri =
env::var("MONGODB_URI").unwrap_or_else(|_| "mongodb://localhost:27017".to_string());
let database_name = env::var("DATABASE_NAME").unwrap_or_else(|_| "purenotify".to_string());
Ok(Self {
bind_address,
// database_url,
mongodb_uri,
database_name,
})
}
}

View File

@@ -1,7 +1,9 @@
// src/main.rs
use std::process::exit;
use std::sync::Arc;
use ::mongodb::Database;
use axum::Router;
use dotenvy::dotenv;
use tokio::signal;
@@ -11,9 +13,17 @@ use tracing_subscriber::{EnvFilter, fmt, prelude::*};
mod config;
mod handlers;
mod mongodb;
mod routes;
use config::Config;
use mongodb::MongoDb;
// Shared application state
pub struct AppState {
pub db: Database,
pub config: Config,
}
#[tokio::main]
async fn main() {
@@ -35,6 +45,21 @@ async fn main() {
}
};
// Connect to MongoDB using the config
let mongodb = match MongoDb::connect(&config).await {
Ok(db) => db,
Err(e) => {
error!("Failed to connect to MongoDB: {}", e);
exit(1);
}
};
// Create shared state
let _shared_state = Arc::new(AppState {
db: mongodb.database,
config: config.clone(),
});
#[cfg(feature = "no-auth")]
info!("NO-AUTH MODE ENABLED");
@@ -42,7 +67,6 @@ async fn main() {
// Build the Axum router
let app = Router::new()
// .nest("/health", routes::health::health::health_routes())
.nest("/health", routes::health::health::health_routes())
.nest("/user", routes::user::user::user_routes())
.layer(TraceLayer::new_for_http());

6
src/mongodb/mod.rs Normal file
View File

@@ -0,0 +1,6 @@
// src/mongodb/mod.rs
pub mod models;
pub mod mongodb;
pub use mongodb::MongoDb;

View File

@@ -0,0 +1,5 @@
// src/mongodb/models/mod.rs
pub mod user;
// Re-exports can be added here when needed

View File

@@ -0,0 +1,49 @@
use bson::oid::ObjectId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct User {
#[serde(rename = "_id")]
id: ObjectId,
username: String,
email: String,
first_name: String,
last_name: String,
age: u32,
is_active: bool,
phone_number: String,
password: String,
salt: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
last_login: DateTime<Utc>,
role: String,
profile: Option<Profile>,
preferences: Option<Preferences>,
stats: Option<Stats>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Profile {
avatar_url: String,
bio: String,
location: String,
website: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Preferences {
theme: String,
language: String,
notifications_enabled: bool,
email_verified: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Stats {
total_posts: u32,
total_comments: u32,
total_likes: u32,
account_age_days: u32,
}

38
src/mongodb/mongodb.rs Normal file
View File

@@ -0,0 +1,38 @@
use crate::config::Config;
use mongodb::options::{ClientOptions, ServerApi, ServerApiVersion};
use mongodb::{Client, Database};
pub struct MongoDb {
// pub client: Client,
pub database: Database,
}
impl MongoDb {
pub async fn connect(config: &Config) -> Result<Self, mongodb::error::Error> {
// Parse connection string from config
let mut client_options = ClientOptions::parse(&config.mongodb_uri).await?;
// Set the server API version (optional but recommended for MongoDB Atlas)
let server_api = ServerApi::builder().version(ServerApiVersion::V1).build();
client_options.server_api = Some(server_api);
// Optional: Set additional options
client_options.app_name = Some("PureNotify".to_string());
// Create client
let client = Client::with_options(client_options)?;
// Ping the server to verify connection
client
.database("admin")
.run_command(mongodb::bson::doc! {"ping": 1}, None)
.await?;
println!("✅ Successfully connected to MongoDB!");
// Get database handle using the database_name from config
let database = client.database(&config.database_name);
Ok(MongoDb { database })
}
}