Compare commits

10 Commits

Author SHA1 Message Date
fbd53f821f Merge branch 'main' of https://git.gabens.lol/thatnewyorker/employee-tracking-backend 2025-08-20 21:14:24 -04:00
f6d0d9b5ec Update MongoDB URI to use Docker hostname 2025-08-20 21:14:23 -04:00
d3feeef996 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.
2025-08-20 20:53:22 -04:00
e79d16b87f implement Password utils 2025-08-17 17:07:07 -04:00
73e4701daa implement MongoDB user 2025-08-17 16:39:56 -04:00
26832acc31 implement MongoDB user repository with async support 2025-08-17 16:36:07 -04:00
f004dcf0c9 dockercompose file fix 2025-08-16 07:00:42 -04:00
5919966954 Add user registration endpoint 2025-08-16 06:56:21 -04:00
ed612bd717 feat: add MongoDB support with connection pooling and repository pattern 2025-08-16 06:38:30 -04:00
96b30b90cb first commit 2025-08-14 12:09:17 -04:00
19 changed files with 1731 additions and 719 deletions

4
.env
View File

@@ -1,5 +1,5 @@
// .env.example (copy to .env for local use) # .env.example (copy to .env for local use)
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://mongodb:27017

1675
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +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.15.0", features = ["chrono-0_4"] }
chrono = { version = "0.4.41", features = ["serde"] }
sha2 = "0.10.9"
rand = "0.9.2"
regex = "1.11.1"
dotenvy = "0.15.7" dotenvy = "0.15.7"
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 = []

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 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`: For development, you can run the project directly with `cargo run`:
```sh ```sh
cargo run cargo run
``` ```
For a release build, run: For a release build, run:
```sh ```sh
cargo run --release cargo run --release
``` ```

38
docker-compose.yml Normal file
View File

@@ -0,0 +1,38 @@
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
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password123
MONGO_INITDB_DATABASE: purenotify
# 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,13 @@ use std::env;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::str::FromStr; use std::str::FromStr;
#[derive(Debug)] use tracing::error;
#[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub bind_address: SocketAddr, pub bind_address: SocketAddr,
// pub database_url: Option<String>, pub mongodb_uri: String,
pub database_name: String,
} }
impl Config { impl Config {
@@ -18,17 +21,20 @@ 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());
} }
// 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 { Ok(Self {
bind_address, bind_address,
// database_url, mongodb_uri,
database_name,
}) })
} }
} }

View File

@@ -1,3 +1,4 @@
// src/handlers/user/mod.rs // src/handlers/user/mod.rs
pub mod register;
pub mod user; pub mod user;

View File

@@ -0,0 +1,40 @@
// src/handlers/register/register.rs
use axum::Json;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::Deserialize;
use serde_json::json;
#[derive(Debug, Deserialize)]
pub struct RegisterPayload {
pub email: String,
pub password: String,
}
impl RegisterPayload {
pub fn new(email: String, password: String) -> Self {
RegisterPayload { email, password }
}
}
pub async fn register(Json(_payload): Json<RegisterPayload>) -> impl IntoResponse {
// TODO: Implement user registration logic using the user repository in ./src/mongodb/repositories/user
(
StatusCode::OK,
Json(json!(
{
"message": "new user registered",
"data": {
"user": {
"email" : _payload.email,
"password": _payload.password,
}
},
"success": true,
"error": false,
}
)),
)
}

View File

@@ -1,6 +1,6 @@
// src/main.rs // src/main.rs
use std::process::exit; use std::{process::exit, sync::Arc};
use axum::Router; use axum::Router;
use dotenvy::dotenv; use dotenvy::dotenv;
@@ -11,43 +11,101 @@ use tracing_subscriber::{EnvFilter, fmt, prelude::*};
mod config; mod config;
mod handlers; mod handlers;
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 mongo::MongoDb; // your wrapper
// Shared application state for online mode
pub struct AppState {
pub db: Database,
pub config: Config,
}
#[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);
} }
}; };
#[cfg(feature = "no-auth")] // Runtime OFFLINE switch: true if OFFLINE is 1/true/yes/on (case-insensitive)
info!("NO-AUTH MODE ENABLED"); 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,
Err(e) => {
error!("Failed to connect to MongoDB: {e}");
exit(1);
}
};
let shared_state = Arc::new(AppState {
db: mongo.database,
config: config.clone(),
});
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("/health", routes::health::health::health_routes())
.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();

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

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

5
src/mongo/models/mod.rs Normal file
View File

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

98
src/mongo/models/user.rs Normal file
View File

@@ -0,0 +1,98 @@
// models/user.rs
use bson::oid::ObjectId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct User {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
pub username: String,
pub email: String,
pub first_name: String,
pub last_name: String,
pub age: u32,
pub is_active: bool,
pub phone_number: String,
pub password: String,
pub salt: String,
#[serde(default = "chrono::Utc::now")]
pub created_at: DateTime<Utc>,
#[serde(default = "chrono::Utc::now")]
pub updated_at: DateTime<Utc>,
pub last_login: Option<DateTime<Utc>>,
pub role: String,
pub profile: Option<Profile>,
pub preferences: Option<Preferences>,
pub stats: Option<Stats>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Profile {
pub avatar_url: String,
pub bio: String,
pub location: String,
pub website: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Preferences {
pub theme: String,
pub language: String,
pub notifications_enabled: bool,
pub email_verified: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Stats {
pub total_posts: u32,
pub total_comments: u32,
pub total_likes: u32,
pub account_age_days: u32,
}
impl User {
pub fn new(username: String, email: String, password: String, salt: String) -> Self {
let now = chrono::Utc::now();
Self {
id: None,
username,
email,
first_name: String::new(),
last_name: String::new(),
age: 0,
is_active: true,
phone_number: String::new(),
password,
salt,
created_at: now,
updated_at: now,
last_login: None,
role: "user".to_string(),
profile: None,
preferences: None,
stats: None,
}
}
pub fn with_profile(mut self, profile: Profile) -> Self {
self.profile = Some(profile);
self
}
pub fn with_preferences(mut self, preferences: Preferences) -> Self {
self.preferences = Some(preferences);
self
}
pub fn with_stats(mut self, stats: Stats) -> Self {
self.stats = Some(stats);
self
}
pub fn update_last_login(&mut self) {
self.last_login = Some(chrono::Utc::now());
self.updated_at = chrono::Utc::now();
}
}

38
src/mongo/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})
.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 })
}
}

View File

@@ -0,0 +1,191 @@
use async_trait::async_trait;
use bson::oid::ObjectId;
use futures::TryStreamExt;
use mongodb::Collection;
use mongodb::bson::doc;
use mongodb::options::FindOptions;
use super::user_repository::{UserError, UserRepository};
use crate::models::user::User;
pub struct MongoUserRepository {
collection: Collection<User>,
}
impl MongoUserRepository {
pub fn new(collection: Collection<User>) -> Self {
Self { collection }
}
}
#[async_trait]
impl UserRepository for MongoUserRepository {
async fn create(&self, mut user: User) -> Result<User, UserError> {
// Validate required fields
if user.username.is_empty() {
return Err(UserError::ValidationError(
"Username is required".to_string(),
));
}
if user.email.is_empty() {
return Err(UserError::ValidationError("Email is required".to_string()));
}
// Check for existing users
if self.exists_by_username(user.username.clone()).await? {
return Err(UserError::DuplicateKey("username".to_string()));
}
if self.exists_by_email(user.email.clone()).await? {
return Err(UserError::DuplicateKey("email".to_string()));
}
// Set timestamps
let now = chrono::Utc::now();
user.created_at = now;
user.updated_at = now;
user.id = None; // Let MongoDB generate the ID
let result = self.collection.insert_one(&user, None).await?;
// Return the created user with the new ID
user.id = result.inserted_id.as_object_id();
Ok(user)
}
async fn get(&self, id: ObjectId) -> Result<User, UserError> {
let user = self.collection.find_one(doc! {"_id": id}, None).await?;
user.ok_or(UserError::NotFound)
}
async fn update(&self, id: ObjectId, mut user: User) -> Result<User, UserError> {
// Update the timestamp
user.updated_at = chrono::Utc::now();
user.id = Some(id);
let result = self
.collection
.replace_one(doc! {"_id": id}, &user, None)
.await?;
if result.matched_count == 0 {
return Err(UserError::NotFound);
}
Ok(user)
}
async fn delete(&self, id: ObjectId) -> Result<(), UserError> {
let result = self.collection.delete_one(doc! {"_id": id}, None).await?;
if result.deleted_count == 0 {
return Err(UserError::NotFound);
}
Ok(())
}
async fn list(&self, limit: Option<i64>, skip: Option<u64>) -> Result<Vec<User>, UserError> {
let find_options = FindOptions::builder().limit(limit).skip(skip).build();
let cursor = self.collection.find(None, find_options).await?;
let users: Vec<User> = cursor.try_collect().await?;
Ok(users)
}
async fn search(&self, query: String) -> Result<Vec<User>, UserError> {
// Use regex for partial matching or text search
let filter = doc! {
"$or": [
{"username": {"$regex": &query, "$options": "i"}},
{"email": {"$regex": &query, "$options": "i"}},
{"first_name": {"$regex": &query, "$options": "i"}},
{"last_name": {"$regex": &query, "$options": "i"}}
]
};
let cursor = self.collection.find(filter, None).await?;
let users: Vec<User> = cursor.try_collect().await?;
Ok(users)
}
async fn count(&self) -> Result<u64, UserError> {
let count = self.collection.count_documents(None, None).await?;
Ok(count)
}
async fn count_by_name(&self, name: String) -> Result<u64, UserError> {
let filter = doc! {
"$or": [
{"first_name": &name},
{"last_name": &name}
]
};
let count = self.collection.count_documents(filter, None).await?;
Ok(count)
}
async fn count_by_email(&self, email: String) -> Result<u64, UserError> {
let count = self
.collection
.count_documents(doc! {"email": email}, None)
.await?;
Ok(count)
}
async fn count_by_phone(&self, phone: String) -> Result<u64, UserError> {
let count = self
.collection
.count_documents(doc! {"phone_number": phone}, None)
.await?;
Ok(count)
}
async fn count_by_id(&self, id: ObjectId) -> Result<u64, UserError> {
let count = self
.collection
.count_documents(doc! {"_id": id}, None)
.await?;
Ok(count)
}
async fn find_by_email(&self, email: String) -> Result<Option<User>, UserError> {
let user = self
.collection
.find_one(doc! {"email": email}, None)
.await?;
Ok(user)
}
async fn find_by_username(&self, username: String) -> Result<Option<User>, UserError> {
let user = self
.collection
.find_one(doc! {"username": username}, None)
.await?;
Ok(user)
}
async fn exists_by_email(&self, email: String) -> Result<bool, UserError> {
let count = self.count_by_email(email).await?;
Ok(count > 0)
}
async fn exists_by_username(&self, username: String) -> Result<bool, UserError> {
let count = self
.collection
.count_documents(doc! {"username": username}, None)
.await?;
Ok(count > 0)
}
async fn get_active_users(&self) -> Result<Vec<User>, UserError> {
let cursor = self.collection.find(doc! {"is_active": true}, None).await?;
let users: Vec<User> = cursor.try_collect().await?;
Ok(users)
}
async fn get_users_by_role(&self, role: String) -> Result<Vec<User>, UserError> {
let cursor = self.collection.find(doc! {"role": role}, None).await?;
let users: Vec<User> = cursor.try_collect().await?;
Ok(users)
}
}

View File

@@ -0,0 +1,53 @@
use async_trait::async_trait;
use bson::oid::ObjectId;
use crate::models::user::User;
// Define custom error type
#[derive(Debug)]
pub enum UserError {
MongoError(mongodb::error::Error),
NotFound,
ValidationError(String),
DuplicateKey(String),
}
impl std::fmt::Display for UserError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UserError::MongoError(e) => write!(f, "MongoDB error: {}", e),
UserError::NotFound => write!(f, "User not found"),
UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
UserError::DuplicateKey(field) => write!(f, "Duplicate key error: {}", field),
}
}
}
impl std::error::Error for UserError {}
impl From<mongodb::error::Error> for UserError {
fn from(error: mongodb::error::Error) -> Self {
UserError::MongoError(error)
}
}
// Repository trait
#[async_trait]
pub trait UserRepository {
async fn create(&self, user: User) -> Result<User, UserError>;
async fn get(&self, id: ObjectId) -> Result<User, UserError>;
async fn update(&self, id: ObjectId, user: User) -> Result<User, UserError>;
async fn delete(&self, id: ObjectId) -> Result<(), UserError>;
async fn list(&self, limit: Option<i64>, skip: Option<u64>) -> Result<Vec<User>, UserError>;
async fn search(&self, query: String) -> Result<Vec<User>, UserError>;
async fn count(&self) -> Result<u64, UserError>;
async fn count_by_name(&self, name: String) -> Result<u64, UserError>;
async fn count_by_email(&self, email: String) -> Result<u64, UserError>;
async fn count_by_phone(&self, phone: String) -> Result<u64, UserError>;
async fn count_by_id(&self, id: ObjectId) -> Result<u64, UserError>;
async fn find_by_email(&self, email: String) -> Result<Option<User>, UserError>;
async fn find_by_username(&self, username: String) -> Result<Option<User>, UserError>;
async fn exists_by_email(&self, email: String) -> Result<bool, UserError>;
async fn exists_by_username(&self, username: String) -> Result<bool, UserError>;
async fn get_active_users(&self) -> Result<Vec<User>, UserError>;
async fn get_users_by_role(&self, role: String) -> Result<Vec<User>, UserError>;
}

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

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

View File

@@ -0,0 +1,83 @@
use sha2::{Digest, Sha256};
use rand::Rng;
use regex::Regex;
pub struct PasswordUtils;
impl PasswordUtils {
pub fn hash_password(password: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
format!("{:x}", hasher.finalize())
}
pub fn compare_password(password: &str, hash: &str) -> bool {
Self::hash_password(password) == *hash
}
pub fn generate_salt() -> String {
let salt: [u8; 16] = rand::thread_rng().gen();
hex::encode(salt)
}
pub fn hash_password_with_salt(password: &str, salt: &str) -> String {
let mut hasher = Sha256::new();
hasher.update((password.to_owned() + salt).as_bytes());
format!("{:x}", hasher.finalize())
}
pub fn compare_password_with_salt(password: &str, hash: &str, salt: &str) -> bool {
Self::hash_password_with_salt(password, salt) == *hash
}
pub fn generate_password_reset_token() -> String {
let token: [u8; 32] = rand::thread_rng().gen();
hex::encode(token)
}
// This method in the JS was incorrect (verify_password_reset_token was comparing a hash to itself)
// A proper verification would involve hashing the provided token and comparing it to a stored hash.
// For now, I'll just return true, implying a successful generation and that the token is "valid" on its own.
// In a real application, you'd store the hashed token in the database and compare it during verification.
pub fn verify_password_reset_token(_token: &str) -> bool {
// In a real application, you would hash the token provided and compare it to a stored hash.
// For demonstration, we'll just return true.
true
}
pub fn hash_password_with_salt_and_pepper(password: &str, salt: &str, pepper: &str) -> String {
let mut hasher = Sha256::new();
hasher.update((password.to_owned() + salt + pepper).as_bytes());
format!("{:x}", hasher.finalize())
}
pub fn compare_password_with_salt_and_pepper(password: &str, hash: &str, salt: &str, pepper: &str) -> bool {
Self::hash_password_with_salt_and_pepper(password, salt, pepper) == *hash
}
pub fn check_password_strength(password: &str) -> bool {
let min_length = 8;
let has_upper_case = Regex::new(r"[A-Z]").unwrap();
let has_lower_case = Regex::new(r"[a-z]").unwrap();
let has_numbers = Regex::new(r"\d").unwrap();
let has_special_chars = Regex::new(r"[!@#$%^&*]").unwrap();
password.len() >= min_length
&& has_upper_case.is_match(password)
&& has_lower_case.is_match(password)
&& has_numbers.is_match(password)
&& has_special_chars.is_match(password)
}
pub fn generate_password(length: usize) -> String {
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()";
let mut rng = rand::thread_rng();
let password: String = (0..length)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect();
password
}
}