Compare commits
15 Commits
96b30b90cb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fbd53f821f | |||
| f6d0d9b5ec | |||
| d3feeef996 | |||
| e79d16b87f | |||
| 73e4701daa | |||
| 26832acc31 | |||
| f004dcf0c9 | |||
| 5919966954 | |||
| ed612bd717 | |||
| 8a05f4edac | |||
| 34a6f211de | |||
| 611301dfda | |||
| 985443ca91 | |||
| 29dbca70c4 | |||
| 3d7da03bcf |
4
.env
4
.env
@@ -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
1675
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -1,14 +1,19 @@
|
|||||||
[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"
|
||||||
|
|||||||
51
Dockerfile
Normal file
51
Dockerfile
Normal 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"]
|
||||||
174
README.md
174
README.md
@@ -0,0 +1,174 @@
|
|||||||
|
# PureNotify Backend
|
||||||
|
|
||||||
|
This is the backend service for the PureNotify application, written in Rust. It's built to be a high-performance, reliable, and scalable foundation for sending notifications.
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- **Asynchronous:** Built with `tokio` and `axum` for non-blocking I/O and high concurrency.
|
||||||
|
- **Configurable:** Easily configure the application using environment variables.
|
||||||
|
- **Logging:** Integrated structured logging with `tracing` for better observability.
|
||||||
|
- **Graceful Shutdown:** Ensures the server shuts down cleanly without dropping active connections.
|
||||||
|
- **Health Check:** A dedicated endpoint to monitor the service's health.
|
||||||
|
|
||||||
|
## 🛠️ Technologies Used
|
||||||
|
|
||||||
|
- **[Rust](https://www.rust-lang.org/)**: The core programming language.
|
||||||
|
- **[Axum](https://github.com/tokio-rs/axum)**: A web application framework that focuses on ergonomics and modularity.
|
||||||
|
- **[Tokio](https://tokio.rs/)**: An asynchronous runtime for the Rust programming language.
|
||||||
|
- **[Serde](https://serde.rs/)**: A framework for serializing and deserializing Rust data structures efficiently.
|
||||||
|
- **[Dotenvy](https://github.com/dotenv-rs/dotenv)**: For loading environment variables from a `.env` file.
|
||||||
|
- **[Tracing](https://github.com/tokio-rs/tracing)**: A framework for instrumenting Rust programs to collect structured, event-based diagnostic information.
|
||||||
|
|
||||||
|
## ⚙️ Getting Started
|
||||||
|
|
||||||
|
Follow these instructions to get a copy of the project up and running on your local machine for development and testing purposes.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
You need to have the Rust toolchain installed on your system. If you don't have it, you can install it from [rustup.rs](https://rustup.rs/).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation & Running
|
||||||
|
|
||||||
|
1. **Clone the repository:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/your-username/purenotify_backend.git
|
||||||
|
cd purenotify_backend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create a `.env` file:**
|
||||||
|
Copy the example environment file to create your own local configuration.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
You can modify the `.env` file to change the server's configuration.
|
||||||
|
|
||||||
|
3. **Build the project:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start, and you should see log output in your terminal indicating that it's running.
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
The application is configured using environment variables. These can be set in a `.env` file in the project root or directly in your shell.
|
||||||
|
|
||||||
|
- `BIND_ADDRESS`: The IP address and port the server should listen on.
|
||||||
|
- **Default:** `127.0.0.1:3000`
|
||||||
|
- `RUST_LOG`: Controls the log level for the application.
|
||||||
|
- **Example:** `RUST_LOG=info,purenotify_backend=debug` will set the default log level to `info` and the log level for this crate to `debug`.
|
||||||
|
- **Default:** Reads from the environment; if not set, logging may be minimal.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
Here are the available API endpoints for the service.
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
- **Endpoint:** `/health`
|
||||||
|
- **Method:** `GET`
|
||||||
|
- **Description:** Used to verify that the service is running and healthy.
|
||||||
|
- **Success Response:**
|
||||||
|
- **Code:** `200 OK`
|
||||||
|
- **Content:** `{
|
||||||
|
"message": "health check successful",
|
||||||
|
"data": {},
|
||||||
|
"success": true,
|
||||||
|
"error": false
|
||||||
|
}`
|
||||||
|
|
||||||
|
#### Example Usage
|
||||||
|
|
||||||
|
You can use `curl` to check the health of the service:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl http://127.0.0.1:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Output:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "health check successful",
|
||||||
|
"data": {},
|
||||||
|
"success": true,
|
||||||
|
"error": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📂 Project Structure
|
||||||
|
|
||||||
|
The project follows a standard Rust project layout. The main application logic is located in the `src/` directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.rs # Application entry point, server setup
|
||||||
|
├── config.rs # Configuration management
|
||||||
|
├── handlers/ # Business logic for handling requests
|
||||||
|
│ └── health/
|
||||||
|
│ └── health.rs
|
||||||
|
├── routes/ # API route definitions
|
||||||
|
│ └── health/
|
||||||
|
│ └── health.rs
|
||||||
|
└── utils/ # Utility functions and shared modules
|
||||||
|
```
|
||||||
|
|
||||||
|
- `main.rs`: Initializes the server, logging, configuration, and wires up the routes.
|
||||||
|
- `config.rs`: Defines the `Config` struct and handles loading configuration from the environment.
|
||||||
|
- `handlers/`: Contains the core logic for each API endpoint. Each handler is responsible for processing a request and returning a response.
|
||||||
|
- `routes/`: Defines the Axum `Router` for different parts of the application. These modules map URL paths to their corresponding handlers.
|
||||||
|
- `utils/`: A place for helper functions or modules that are used across different parts of the application.
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! If you'd like to contribute, please fork the repository and use a feature branch. Pull requests are warmly welcome.
|
||||||
|
|
||||||
|
1. Fork the repository.
|
||||||
|
2. Create your feature branch (`git checkout -b feature/fooBar`).
|
||||||
|
3. Commit your changes (`git commit -am 'Add some fooBar'`).
|
||||||
|
4. Push to the branch (`git push origin feature/fooBar`).
|
||||||
|
5. Create a new Pull Request.
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||||
|
|||||||
38
docker-compose.yml
Normal file
38
docker-compose.yml
Normal 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:
|
||||||
@@ -6,10 +6,11 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[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 {
|
||||||
@@ -20,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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/handlers/health/health.rs
Normal file
20
src/handlers/health/health.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// src/handlers/health/health.rs
|
||||||
|
|
||||||
|
use axum::Json;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub async fn health() -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(json!(
|
||||||
|
{
|
||||||
|
"message": "health check successful",
|
||||||
|
"data": {},
|
||||||
|
"success": true,
|
||||||
|
"error": false,
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
3
src/handlers/health/mod.rs
Normal file
3
src/handlers/health/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// src/handlers/health/mod.rs
|
||||||
|
|
||||||
|
pub mod health;
|
||||||
4
src/handlers/mod.rs
Normal file
4
src/handlers/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/handlers/mod.rs
|
||||||
|
|
||||||
|
pub mod health;
|
||||||
|
pub mod user;
|
||||||
4
src/handlers/user/mod.rs
Normal file
4
src/handlers/user/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/handlers/user/mod.rs
|
||||||
|
|
||||||
|
pub mod register;
|
||||||
|
pub mod user;
|
||||||
40
src/handlers/user/register.rs
Normal file
40
src/handlers/user/register.rs
Normal 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,
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
48
src/handlers/user/user.rs
Normal file
48
src/handlers/user/user.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// src/handlers/user/user.rs
|
||||||
|
|
||||||
|
use axum::Json;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub async fn user() -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(json!(
|
||||||
|
{
|
||||||
|
"message": "health check successful",
|
||||||
|
"data": {
|
||||||
|
"id": "usr_123456789",
|
||||||
|
"username": "john_doe",
|
||||||
|
"email": "john.doe@example.com",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"role": "user",
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2024-01-15T10:30:00Z",
|
||||||
|
"updated_at": "2024-08-15T14:20:00Z",
|
||||||
|
"profile": {
|
||||||
|
"avatar_url": "https://api.example.com/avatars/john_doe.png",
|
||||||
|
"bio": "Software developer passionate about Rust",
|
||||||
|
"location": "San Francisco, CA",
|
||||||
|
"website": "https://johndoe.dev"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "dark",
|
||||||
|
"language": "en",
|
||||||
|
"notifications_enabled": true,
|
||||||
|
"email_verified": true
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"total_posts": 42,
|
||||||
|
"total_comments": 156,
|
||||||
|
"total_likes": 523,
|
||||||
|
"account_age_days": 213
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": true,
|
||||||
|
"error": false,
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// src/health.rs
|
|
||||||
|
|
||||||
use axum::Json;
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
pub async fn health() -> impl IntoResponse {
|
|
||||||
(StatusCode::OK, Json(json!({ "status": "ok" })))
|
|
||||||
}
|
|
||||||
88
src/main.rs
88
src/main.rs
@@ -1,9 +1,8 @@
|
|||||||
// src/main.rs
|
// src/main.rs
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::{process::exit, sync::Arc};
|
||||||
use std::process::exit;
|
|
||||||
|
|
||||||
use axum::{Router, routing::get};
|
use axum::Router;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
@@ -11,41 +10,102 @@ use tracing::{error, info};
|
|||||||
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod health;
|
mod handlers;
|
||||||
|
mod mongo; // local module wrapping the Mongo client
|
||||||
|
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>>();
|
||||||
.route("/health", get(health::health))
|
let user_router = routes::user::user::user_routes::<Arc<AppState>>();
|
||||||
|
|
||||||
|
// 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
6
src/mongo/mod.rs
Normal 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
5
src/mongo/models/mod.rs
Normal 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
98
src/mongo/models/user.rs
Normal 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
38
src/mongo/mongodb.rs
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
191
src/mongo/repositories/user/mongodb_user_repository.rs
Normal file
191
src/mongo/repositories/user/mongodb_user_repository.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/mongo/repositories/user/user_repository.rs
Normal file
53
src/mongo/repositories/user/user_repository.rs
Normal 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>;
|
||||||
|
}
|
||||||
11
src/routes/health/health.rs
Normal file
11
src/routes/health/health.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// src/routes/health/healh.rs
|
||||||
|
|
||||||
|
use axum::{Router, routing::get};
|
||||||
|
|
||||||
|
pub fn health_routes<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
// keep your existing routes/handlers here
|
||||||
|
Router::new().route("/", get(crate::handlers::health::health::health))
|
||||||
|
}
|
||||||
4
src/routes/health/mod.rs
Normal file
4
src/routes/health/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/routes/health/mod.rs
|
||||||
|
|
||||||
|
pub mod health;
|
||||||
|
|
||||||
4
src/routes/mod.rs
Normal file
4
src/routes/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/routes/mod.rs
|
||||||
|
|
||||||
|
pub mod health;
|
||||||
|
pub mod user;
|
||||||
3
src/routes/user/mod.rs
Normal file
3
src/routes/user/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// src/routes/user/mod.rs
|
||||||
|
|
||||||
|
pub mod user;
|
||||||
16
src/routes/user/user.rs
Normal file
16
src/routes/user/user.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// src/routes/user/user.rs
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn user_routes<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
// keep your existing routes/handlers here
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(crate::handlers::user::user::user))
|
||||||
|
.route("/register", post(crate::handlers::user::register::register))
|
||||||
|
}
|
||||||
203
src/utils/crypto/crypto.md
Normal file
203
src/utils/crypto/crypto.md
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# Crypto Utility Module (`crypto.rs`)
|
||||||
|
|
||||||
|
This document provides detailed documentation for the `CryptoUtils` module, a Rust implementation for essential cryptographic operations. The module offers functionalities for symmetric encryption/decryption and asymmetric key pair management.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Overview](#overview)
|
||||||
|
2. [Dependencies](#dependencies)
|
||||||
|
3. [Error Handling](#error-handling)
|
||||||
|
4. [Core Structures](#core-structures)
|
||||||
|
- [`KeyPair`](#keypair)
|
||||||
|
5. [Static Properties](#static-properties)
|
||||||
|
- [`KEY`](#key)
|
||||||
|
- [`IV`](#iv)
|
||||||
|
6. [API Functions](#api-functions)
|
||||||
|
- [Symmetric Encryption](#symmetric-encryption)
|
||||||
|
- [`encrypt`](#encrypt)
|
||||||
|
- [`decrypt`](#decrypt)
|
||||||
|
- [Asymmetric Key Management](#asymmetric-key-management)
|
||||||
|
- [`generate_key_pair`](#generate_key_pair)
|
||||||
|
- [`save_keys_to_files`](#save_keys_to_files)
|
||||||
|
- [`load_keys_from_files`](#load_keys_from_files)
|
||||||
|
- [`init_keys`](#init_keys)
|
||||||
|
7. [Usage Examples](#usage-examples)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
The `CryptoUtils` module provides a set of static methods to perform common cryptographic tasks. It is designed to be a centralized utility for handling both symmetric (AES-256-CBC) and asymmetric (RSA-4096) cryptography.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
This module requires the following dependencies to be added to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
openssl = "0.10"
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
once_cell = "1.19" # For lazy static initialization
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
The module defines a custom `CryptoError` enum to handle various failure scenarios, providing clear and specific error information.
|
||||||
|
|
||||||
|
- `OpenSsl`: Wraps errors from the `openssl` crate.
|
||||||
|
- `Io`: For file system I/O errors (e.g., reading/writing keys).
|
||||||
|
- `Hex`: For errors during hex encoding/decoding.
|
||||||
|
- `Utf8`: For errors converting byte slices to UTF-8 strings.
|
||||||
|
- `Custom`: For other specific, custom error messages.
|
||||||
|
|
||||||
|
### Core Structures
|
||||||
|
|
||||||
|
#### `KeyPair`
|
||||||
|
|
||||||
|
A public struct that holds a pair of RSA keys.
|
||||||
|
|
||||||
|
- `private_key: String`: The PEM-encoded private key.
|
||||||
|
- `public_key: String`: The PEM-encoded public key.
|
||||||
|
|
||||||
|
### Static Properties
|
||||||
|
|
||||||
|
#### `KEY`
|
||||||
|
|
||||||
|
A statically initialized 32-byte array used as the secret key for AES-256-CBC encryption and decryption. It is derived by applying SHA-256 to a hardcoded salt phrase, ensuring a consistent key across the application.
|
||||||
|
|
||||||
|
#### `IV`
|
||||||
|
|
||||||
|
A 16-byte initialization vector used for the AES-256-CBC algorithm.
|
||||||
|
|
||||||
|
### API Functions
|
||||||
|
|
||||||
|
All functions are implemented as static methods on the `CryptoUtils` struct.
|
||||||
|
|
||||||
|
#### Symmetric Encryption
|
||||||
|
|
||||||
|
##### `encrypt`
|
||||||
|
|
||||||
|
`pub fn encrypt(secret: &str) -> Result<String, CryptoError>`
|
||||||
|
|
||||||
|
Encrypts a string slice using AES-256-CBC.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `secret`: The plaintext string to encrypt.
|
||||||
|
- **Returns**: A `Result` containing the hex-encoded ciphertext string or a `CryptoError`.
|
||||||
|
|
||||||
|
##### `decrypt`
|
||||||
|
|
||||||
|
`pub fn decrypt(encrypted_secret: &str) -> Result<String, CryptoError>`
|
||||||
|
|
||||||
|
Decrypts a hex-encoded ciphertext string using AES-256-CBC.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `encrypted_secret`: The hex-encoded ciphertext.
|
||||||
|
- **Returns**: A `Result` containing the decrypted plaintext string or a `CryptoError`.
|
||||||
|
|
||||||
|
#### Asymmetric Key Management
|
||||||
|
|
||||||
|
##### `generate_key_pair`
|
||||||
|
|
||||||
|
`pub fn generate_key_pair() -> Result<KeyPair, CryptoError>`
|
||||||
|
|
||||||
|
Generates a new 4096-bit RSA key pair.
|
||||||
|
|
||||||
|
- **Returns**: A `Result` containing a `KeyPair` struct with the new PEM-encoded keys or a `CryptoError`.
|
||||||
|
|
||||||
|
##### `save_keys_to_files`
|
||||||
|
|
||||||
|
`pub fn save_keys_to_files(keys: &KeyPair, directory: &Path) -> Result<(), CryptoError>`
|
||||||
|
|
||||||
|
Saves a `KeyPair` to the specified directory in two files: `private.pem` and `public.pem`.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `keys`: A reference to the `KeyPair` to save.
|
||||||
|
- `directory`: The path to the directory where the keys will be saved.
|
||||||
|
|
||||||
|
##### `load_keys_from_files`
|
||||||
|
|
||||||
|
`pub fn load_keys_from_files(directory: &Path) -> Result<KeyPair, CryptoError>`
|
||||||
|
|
||||||
|
Loads an RSA key pair from `private.pem` and `public.pem` files in a given directory.
|
||||||
|
|
||||||
|
- **Parameters**:
|
||||||
|
- `directory`: The path to the directory containing the key files.
|
||||||
|
- **Returns**: A `Result` containing the loaded `KeyPair` or a `CryptoError`.
|
||||||
|
|
||||||
|
##### `init_keys`
|
||||||
|
|
||||||
|
`pub fn init_keys() -> Result<KeyPair, CryptoError>`
|
||||||
|
|
||||||
|
A convenience function that initializes the RSA key pair for the application. It first checks if the keys exist in the default `./keys` directory.
|
||||||
|
|
||||||
|
- If the keys exist, it loads them.
|
||||||
|
- If they do not exist, it generates a new pair and saves them to the `./keys` directory.
|
||||||
|
- **Returns**: A `Result` containing the initialized `KeyPair` or a `CryptoError`.
|
||||||
|
|
||||||
|
### Usage Examples
|
||||||
|
|
||||||
|
#### Example 1: AES Encryption and Decryption
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use your_project::utils::crypto::crypto::CryptoUtils;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let secret_message = "This is a highly confidential message.";
|
||||||
|
|
||||||
|
// Encrypt the message
|
||||||
|
match CryptoUtils::encrypt(secret_message) {
|
||||||
|
Ok(encrypted) => {
|
||||||
|
println!("Original: {}", secret_message);
|
||||||
|
println!("Encrypted: {}", encrypted);
|
||||||
|
|
||||||
|
// Decrypt the message
|
||||||
|
match CryptoUtils::decrypt(&encrypted) {
|
||||||
|
Ok(decrypted) => {
|
||||||
|
println!("Decrypted: {}", decrypted);
|
||||||
|
assert_eq!(secret_message, decrypted);
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Decryption failed: {}", e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Encryption failed: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example 2: RSA Key Pair Initialization and Management
|
||||||
|
|
||||||
|
This example demonstrates how to ensure RSA keys are available for the application.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use your_project::utils::crypto::crypto::CryptoUtils;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Clean up previous keys for demonstration purposes
|
||||||
|
if Path::new("keys").exists() {
|
||||||
|
fs::remove_dir_all("keys").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Attempting to initialize keys...");
|
||||||
|
|
||||||
|
// Use init_keys to either generate or load keys
|
||||||
|
match CryptoUtils::init_keys() {
|
||||||
|
Ok(keys) => {
|
||||||
|
println!("Keys initialized successfully.");
|
||||||
|
println!("Public Key (first 50 chars): {}...", &keys.public_key[..50]);
|
||||||
|
|
||||||
|
// Calling it again should now load the existing keys
|
||||||
|
println!("\nCalling init_keys again...");
|
||||||
|
let loaded_keys = CryptoUtils::init_keys().unwrap();
|
||||||
|
assert_eq!(keys.public_key, loaded_keys.public_key);
|
||||||
|
println!("Keys loaded successfully from files.");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to initialize keys: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
167
src/utils/crypto/crypto.rs
Normal file
167
src/utils/crypto/crypto.rs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use openssl::error::ErrorStack;
|
||||||
|
use openssl::pkey::PKey;
|
||||||
|
use openssl::rsa::{Padding, Rsa};
|
||||||
|
use openssl::symm::{Cipher, Crypter, Mode};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
// --- Error Handling ---
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CryptoError {
|
||||||
|
OpenSsl(ErrorStack),
|
||||||
|
Io(io::Error),
|
||||||
|
Hex(hex::FromHexError),
|
||||||
|
Utf8(std::string::FromUtf8Error),
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ErrorStack> for CryptoError {
|
||||||
|
fn from(err: ErrorStack) -> Self {
|
||||||
|
CryptoError::OpenSsl(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for CryptoError {
|
||||||
|
fn from(err: io::Error) -> Self {
|
||||||
|
CryptoError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<hex::FromHexError> for CryptoError {
|
||||||
|
fn from(err: hex::FromHexError) -> Self {
|
||||||
|
CryptoError::Hex(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::string::FromUtf8Error> for CryptoError {
|
||||||
|
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||||
|
CryptoError::Utf8(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for CryptoError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
CryptoError::OpenSsl(e) => write!(f, "OpenSSL error: {}", e),
|
||||||
|
CryptoError::Io(e) => write!(f, "IO error: {}", e),
|
||||||
|
CryptoError::Hex(e) => write!(f, "Hex decoding error: {}", e),
|
||||||
|
CryptoError::Utf8(e) => write!(f, "UTF-8 conversion error: {}", e),
|
||||||
|
CryptoError::Custom(s) => write!(f, "Crypto error: {}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CryptoError {}
|
||||||
|
|
||||||
|
// --- KeyPair Structure ---
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct KeyPair {
|
||||||
|
pub private_key: String,
|
||||||
|
pub public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Crypto Utility ---
|
||||||
|
|
||||||
|
pub struct CryptoUtils;
|
||||||
|
|
||||||
|
static KEY: Lazy<[u8; 32]> = Lazy::new(|| {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(b"z&R*3mN@wS5gY!8c*P#L5bQm&8wT3vNxE!UW4ex7HJKLfghRT");
|
||||||
|
hasher.finalize().into()
|
||||||
|
});
|
||||||
|
|
||||||
|
const IV: &[u8; 16] = b"6234567890123456";
|
||||||
|
|
||||||
|
impl CryptoUtils {
|
||||||
|
/// Encrypts a string using AES-256-CBC.
|
||||||
|
pub fn encrypt(secret: &str) -> Result<String, CryptoError> {
|
||||||
|
let cipher = Cipher::aes_256_cbc();
|
||||||
|
let data = secret.as_bytes();
|
||||||
|
|
||||||
|
let mut encrypter = Crypter::new(cipher, Mode::Encrypt, &KEY, Some(IV))?;
|
||||||
|
encrypter.pad(true);
|
||||||
|
|
||||||
|
let mut encrypted = vec![0; data.len() + cipher.block_size()];
|
||||||
|
let count = encrypter.update(data, &mut encrypted)?;
|
||||||
|
let rest = encrypter.finalize(&mut encrypted[count..])?;
|
||||||
|
|
||||||
|
encrypted.truncate(count + rest);
|
||||||
|
Ok(hex::encode(encrypted))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts a string using AES-256-CBC.
|
||||||
|
pub fn decrypt(encrypted_secret: &str) -> Result<String, CryptoError> {
|
||||||
|
let cipher = Cipher::aes_256_cbc();
|
||||||
|
let data = hex::decode(encrypted_secret)?;
|
||||||
|
|
||||||
|
let mut decrypter = Crypter::new(cipher, Mode::Decrypt, &KEY, Some(IV))?;
|
||||||
|
decrypter.pad(true);
|
||||||
|
|
||||||
|
let mut decrypted = vec![0; data.len() + cipher.block_size()];
|
||||||
|
let count = decrypter.update(&data, &mut decrypted)?;
|
||||||
|
let rest = decrypter.finalize(&mut decrypted[count..])?;
|
||||||
|
|
||||||
|
decrypted.truncate(count + rest);
|
||||||
|
Ok(String::from_utf8(decrypted)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a 4096-bit RSA key pair in PEM format.
|
||||||
|
pub fn generate_key_pair() -> Result<KeyPair, CryptoError> {
|
||||||
|
let rsa = Rsa::generate(4096)?;
|
||||||
|
let pkey = PKey::from_rsa(rsa)?;
|
||||||
|
|
||||||
|
let private_key = pkey
|
||||||
|
.private_key_to_pem_pkcs8()?
|
||||||
|
.iter()
|
||||||
|
.map(|&c| c as char)
|
||||||
|
.collect();
|
||||||
|
let public_key = pkey
|
||||||
|
.public_key_to_pem()?
|
||||||
|
.iter()
|
||||||
|
.map(|&c| c as char)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(KeyPair {
|
||||||
|
private_key,
|
||||||
|
public_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the given KeyPair to files in the specified directory.
|
||||||
|
pub fn save_keys_to_files(keys: &KeyPair, directory: &Path) -> Result<(), CryptoError> {
|
||||||
|
fs::create_dir_all(directory)?;
|
||||||
|
fs::write(directory.join("private.pem"), &keys.private_key)?;
|
||||||
|
fs::write(directory.join("public.pem"), &keys.public_key)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a KeyPair from files in the specified directory.
|
||||||
|
pub fn load_keys_from_files(directory: &Path) -> Result<KeyPair, CryptoError> {
|
||||||
|
let private_key = fs::read_to_string(directory.join("private.pem"))?;
|
||||||
|
let public_key = fs::read_to_string(directory.join("public.pem"))?;
|
||||||
|
Ok(KeyPair {
|
||||||
|
private_key,
|
||||||
|
public_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes RSA key pair.
|
||||||
|
/// If keys exist in the default 'keys' directory, they are loaded.
|
||||||
|
/// Otherwise, new keys are generated and saved.
|
||||||
|
pub fn init_keys() -> Result<KeyPair, CryptoError> {
|
||||||
|
let key_path = PathBuf::from("keys");
|
||||||
|
|
||||||
|
if key_path.join("private.pem").exists() && key_path.join("public.pem").exists() {
|
||||||
|
Self::load_keys_from_files(&key_path)
|
||||||
|
} else {
|
||||||
|
let keys = Self::generate_key_pair()?;
|
||||||
|
Self::save_keys_to_files(&keys, &key_path)?;
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
src/utils/jwt/jwt.md
Normal file
186
src/utils/jwt/jwt.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# JWT Utility Module Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `JWTUtils` module provides a comprehensive suite of tools for creating, signing, verifying, and managing JSON Web Tokens (JWTs) in Rust. It is designed to work seamlessly with the `CryptoUtils` module for RSA key management. This implementation handles JWTs manually using the `openssl` crate for cryptographic operations, avoiding external JWT-specific libraries.
|
||||||
|
|
||||||
|
The module supports standard claims like `exp` (expiration time), `iat` (issued at), and `iss` (issuer), and allows for custom payloads.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
To use this module, ensure the following dependencies are included in your `Cargo.toml` file:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
openssl = "0.10"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
chrono = "0.4"
|
||||||
|
base64 = "0.21"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Components
|
||||||
|
|
||||||
|
### `JWTError` Enum
|
||||||
|
|
||||||
|
A custom error type that consolidates all potential failures within the module, including issues from `openssl`, `serde_json`, `base64`, and invalid token logic.
|
||||||
|
|
||||||
|
- `OpenSsl(openssl::error::ErrorStack)`: An error from the OpenSSL library.
|
||||||
|
- `SerdeJson(serde_json::Error)`: An error during JSON serialization or deserialization.
|
||||||
|
- `Base64(base64::DecodeError)`: An error during Base64 decoding.
|
||||||
|
- `Crypto(String)`: An error related to cryptographic key loading from `CryptoUtils`.
|
||||||
|
- `InvalidTokenFormat(String)`: The token string is malformed (e.g., wrong number of segments).
|
||||||
|
- `Validation(String)`: The token failed a validation check (e.g., signature invalid, expired).
|
||||||
|
- `Custom(String)`: A generic error for other specific issues.
|
||||||
|
|
||||||
|
### `JWTOptions` Struct
|
||||||
|
|
||||||
|
Defines the configurable options for creating a JWT.
|
||||||
|
|
||||||
|
- `algorithm: String`: The signing algorithm (defaults to `"RS256"`).
|
||||||
|
- `expires_in: i64`: The token's lifetime in seconds. Defaults to `3600` (1 hour) or the value of the `JWT_EXPIRES_IN` environment variable.
|
||||||
|
- `issuer: String`: The issuer of the token. Defaults to an empty string or the value of the `JWT_ISSUER` environment variable.
|
||||||
|
|
||||||
|
### `JWTUtils` Struct
|
||||||
|
|
||||||
|
The main struct for handling JWT operations. An instance of `JWTUtils` is typically created to generate a new token.
|
||||||
|
|
||||||
|
- `payload: Value`: The custom payload for the JWT, represented as a `serde_json::Value`.
|
||||||
|
- `private_key: String`: The PEM-encoded RSA private key for signing.
|
||||||
|
- `public_key: String`: The PEM-encoded RSA public key for verification.
|
||||||
|
- `options: JWTOptions`: The configuration options for the token.
|
||||||
|
|
||||||
|
## Instantiation
|
||||||
|
|
||||||
|
### `new(payload: Value, private_key: Option<String>, public_key: Option<String>, options: Option<JWTOptions>) -> Result<Self, JWTError>`
|
||||||
|
|
||||||
|
Creates a new `JWTUtils` instance.
|
||||||
|
|
||||||
|
- **payload**: The custom data to include in the token.
|
||||||
|
- **private_key / public_key**: Optional RSA keys. If not provided, the constructor will attempt to load them from the default `keys/` directory using `CryptoUtils::load_keys_from_files()`.
|
||||||
|
- **options**: Optional `JWTOptions`. If not provided, default values will be used.
|
||||||
|
|
||||||
|
## Instance Methods
|
||||||
|
|
||||||
|
### `create_token(&self) -> Result<String, JWTError>`
|
||||||
|
|
||||||
|
Generates and signs a complete JWT string. The process involves:
|
||||||
|
|
||||||
|
1. Creating the JWT header (`{"alg": "RS256", "typ": "JWT"}`).
|
||||||
|
2. Processing the payload by adding standard claims (`iat`, `exp`, `iss`).
|
||||||
|
3. Base64URL-encoding the header and payload.
|
||||||
|
4. Creating a signature by signing the encoded header and payload with the RSA private key.
|
||||||
|
5. Combining the three parts into the final `header.payload.signature` format.
|
||||||
|
|
||||||
|
### `verify(&self, token: &str) -> bool`
|
||||||
|
|
||||||
|
Verifies the signature of a given JWT using the public key stored in the `JWTUtils` instance.
|
||||||
|
|
||||||
|
- **Returns**: `true` if the signature is valid, `false` otherwise.
|
||||||
|
- **Note**: This method **only** checks the signature. It does not validate the expiration time or other claims. For comprehensive validation, use `decode_and_verify`.
|
||||||
|
|
||||||
|
## Static Methods
|
||||||
|
|
||||||
|
### `decode(token: &str) -> Result<(Value, Value), JWTError>`
|
||||||
|
|
||||||
|
Decodes a JWT string into its header and payload components without verifying the signature.
|
||||||
|
|
||||||
|
- **Returns**: A tuple `(header, payload)` where both elements are of type `serde_json::Value`.
|
||||||
|
- **Use Case**: Useful for inspecting token data when the signature's validity is not a concern (e.g., for logging or preliminary checks).
|
||||||
|
|
||||||
|
### `is_expired(token: &str) -> bool`
|
||||||
|
|
||||||
|
Checks if a token has expired. It decodes the token and compares the `exp` claim to the current UTC time.
|
||||||
|
|
||||||
|
- **Returns**: `true` if the token is expired, has no `exp` claim, or is malformed. `false` otherwise.
|
||||||
|
|
||||||
|
### `decode_and_verify(token: &str) -> Result<(Value, Value), JWTError>`
|
||||||
|
|
||||||
|
A comprehensive function that performs all necessary validations on a token:
|
||||||
|
|
||||||
|
1. Verifies the token's signature using the public key loaded from the default `keys/` directory.
|
||||||
|
2. Checks if the token has expired.
|
||||||
|
3. If both checks pass, it decodes and returns the header and payload.
|
||||||
|
|
||||||
|
- **Returns**: `Ok((header, payload))` on success, or a `JWTError` if validation fails for any reason.
|
||||||
|
|
||||||
|
### `refresh_token(old_token: &str) -> Result<String, JWTError>`
|
||||||
|
|
||||||
|
Generates a new token based on the payload of an old token. The `iat` and `exp` claims are stripped from the original payload and replaced with new ones.
|
||||||
|
|
||||||
|
- **Note**: This function loads the RSA keys from the `keys/` directory to sign the new token.
|
||||||
|
|
||||||
|
### `validate_claims(token: &str, required_claims: &[&str]) -> bool`
|
||||||
|
|
||||||
|
Checks for the presence of specific keys in the token's payload.
|
||||||
|
|
||||||
|
- **required_claims**: A slice of strings representing the keys that must be present.
|
||||||
|
- **Returns**: `true` if all required claims exist, `false` otherwise.
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Example 1: Creating a JWT
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use serde_json::json;
|
||||||
|
use your_project::utils::jwt::JWTUtils;
|
||||||
|
|
||||||
|
fn create_new_token() {
|
||||||
|
// The custom data for the token
|
||||||
|
let payload = json!({
|
||||||
|
"user_id": "12345",
|
||||||
|
"roles": ["admin", "user"]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keys can be provided directly or loaded automatically from the 'keys/' directory
|
||||||
|
// If None, the constructor will try to load them from files.
|
||||||
|
let jwt_instance = JWTUtils::new(payload, None, None, None).unwrap();
|
||||||
|
|
||||||
|
match jwt_instance.create_token() {
|
||||||
|
Ok(token) => println!("Generated Token: {}", token),
|
||||||
|
Err(e) => eprintln!("Error creating token: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Verifying and Decoding a JWT
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use your_project::utils::jwt::JWTUtils;
|
||||||
|
|
||||||
|
fn validate_and_read_token(token: &str) {
|
||||||
|
match JWTUtils::decode_and_verify(token) {
|
||||||
|
Ok((header, payload)) => {
|
||||||
|
println!("Token is valid!");
|
||||||
|
println!("Header: {:?}", header);
|
||||||
|
println!("Payload: {:?}", payload);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Token validation failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can also check for specific claims
|
||||||
|
if JWTUtils::validate_claims(token, &["user_id", "roles"]) {
|
||||||
|
println!("All required claims are present.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Refreshing a Token
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use your_project::utils::jwt::JWTUtils;
|
||||||
|
|
||||||
|
fn refresh_existing_token(old_token: &str) {
|
||||||
|
match JWTUtils::refresh_token(old_token) {
|
||||||
|
Ok(new_token) => {
|
||||||
|
println!("Token refreshed successfully!");
|
||||||
|
println!("New Token: {}", new_token);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to refresh token: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
320
src/utils/jwt/jwt.rs
Normal file
320
src/utils/jwt/jwt.rs
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
// Add the following dependencies to your Cargo.toml file:
|
||||||
|
// openssl = "0.10"
|
||||||
|
// serde = { version = "1.0", features = ["derive"] }
|
||||||
|
// serde_json = "1.0"
|
||||||
|
// chrono = "0.4"
|
||||||
|
// base64 = "0.21"
|
||||||
|
|
||||||
|
use crate::utils::crypto::crypto::CryptoUtils;
|
||||||
|
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use openssl::{
|
||||||
|
hash::MessageDigest,
|
||||||
|
pkey::{PKey, Private},
|
||||||
|
rsa::Rsa,
|
||||||
|
sign::{Signer, Verifier},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Value, from_str, from_value, to_string};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
// --- Error Handling ---
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum JWTError {
|
||||||
|
OpenSsl(openssl::error::ErrorStack),
|
||||||
|
SerdeJson(serde_json::Error),
|
||||||
|
Base64(base64::DecodeError),
|
||||||
|
Crypto(String),
|
||||||
|
InvalidTokenFormat(String),
|
||||||
|
Validation(String),
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for JWTError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
JWTError::OpenSsl(e) => write!(f, "OpenSSL error: {}", e),
|
||||||
|
JWTError::SerdeJson(e) => write!(f, "JSON serialization error: {}", e),
|
||||||
|
JWTError::Base64(e) => write!(f, "Base64 decoding error: {}", e),
|
||||||
|
JWTError::Crypto(s) => write!(f, "Crypto error: {}", s),
|
||||||
|
JWTError::InvalidTokenFormat(s) => write!(f, "Invalid token format: {}", s),
|
||||||
|
JWTError::Validation(s) => write!(f, "Token validation failed: {}", s),
|
||||||
|
JWTError::Custom(s) => write!(f, "JWT error: {}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for JWTError {}
|
||||||
|
|
||||||
|
impl From<openssl::error::ErrorStack> for JWTError {
|
||||||
|
fn from(err: openssl::error::ErrorStack) -> JWTError {
|
||||||
|
JWTError::OpenSsl(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for JWTError {
|
||||||
|
fn from(err: serde_json::Error) -> JWTError {
|
||||||
|
JWTError::SerdeJson(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<base64::DecodeError> for JWTError {
|
||||||
|
fn from(err: base64::DecodeError) -> JWTError {
|
||||||
|
JWTError::Base64(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Structures ---
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct JWTHeader {
|
||||||
|
alg: String,
|
||||||
|
typ: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct JWTOptions {
|
||||||
|
pub algorithm: String,
|
||||||
|
pub expires_in: i64, // seconds
|
||||||
|
pub issuer: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for JWTOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
let expires_in_str = env::var("JWT_EXPIRES_IN").unwrap_or_else(|_| "3600".to_string());
|
||||||
|
let expires_in = expires_in_str.parse::<i64>().unwrap_or(3600);
|
||||||
|
let issuer = env::var("JWT_ISSUER").unwrap_or_default();
|
||||||
|
|
||||||
|
JWTOptions {
|
||||||
|
algorithm: "RS256".to_string(),
|
||||||
|
expires_in,
|
||||||
|
issuer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JWTUtils {
|
||||||
|
payload: Value,
|
||||||
|
private_key: String,
|
||||||
|
public_key: String,
|
||||||
|
options: JWTOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper Functions ---
|
||||||
|
|
||||||
|
fn base64url_encode<T: AsRef<[u8]>>(input: T) -> String {
|
||||||
|
URL_SAFE_NO_PAD.encode(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base64url_decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
|
||||||
|
URL_SAFE_NO_PAD.decode(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Implementation ---
|
||||||
|
|
||||||
|
impl JWTUtils {
|
||||||
|
pub fn new(
|
||||||
|
payload: Value,
|
||||||
|
private_key: Option<String>,
|
||||||
|
public_key: Option<String>,
|
||||||
|
options: Option<JWTOptions>,
|
||||||
|
) -> Result<Self, JWTError> {
|
||||||
|
let opts = options.unwrap_or_default();
|
||||||
|
|
||||||
|
let (priv_key, pub_key) = match (private_key, public_key) {
|
||||||
|
(Some(priv_k), Some(pub_k)) => (priv_k, pub_k),
|
||||||
|
(priv_k, pub_k) => {
|
||||||
|
let keys = CryptoUtils::load_keys_from_files("keys")
|
||||||
|
.map_err(|e| JWTError::Crypto(e.to_string()))?;
|
||||||
|
(
|
||||||
|
priv_k.unwrap_or(keys.private_key),
|
||||||
|
pub_k.unwrap_or(keys.public_key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(JWTUtils {
|
||||||
|
payload,
|
||||||
|
private_key: priv_key,
|
||||||
|
public_key: pub_key,
|
||||||
|
options: opts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create JWT header
|
||||||
|
fn create_header(&self) -> JWTHeader {
|
||||||
|
JWTHeader {
|
||||||
|
alg: self.options.algorithm.clone(),
|
||||||
|
typ: "JWT".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process payload with standard claims
|
||||||
|
fn process_payload(&self) -> Result<String, JWTError> {
|
||||||
|
let mut payload_obj = self
|
||||||
|
.payload
|
||||||
|
.as_object()
|
||||||
|
.ok_or_else(|| JWTError::Custom("Payload must be a JSON object".to_string()))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let iat = now.timestamp();
|
||||||
|
let exp = (now + Duration::seconds(self.options.expires_in)).timestamp();
|
||||||
|
|
||||||
|
payload_obj.insert("iat".to_string(), iat.into());
|
||||||
|
payload_obj.insert("exp".to_string(), exp.into());
|
||||||
|
payload_obj.insert("iss".to_string(), self.options.issuer.clone().into());
|
||||||
|
|
||||||
|
Ok(to_string(&payload_obj)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign the JWT components
|
||||||
|
fn sign(&self, header_base64: &str, payload_base64: &str) -> Result<String, JWTError> {
|
||||||
|
let signature_input = format!("{}.{}", header_base64, payload_base64);
|
||||||
|
let keypair = Rsa::private_key_from_pem(self.private_key.as_bytes())?;
|
||||||
|
let pkey = PKey::from_rsa(keypair)?;
|
||||||
|
|
||||||
|
let mut signer = Signer::new(MessageDigest::sha256(), &pkey)?;
|
||||||
|
signer.update(signature_input.as_bytes())?;
|
||||||
|
let signature = signer.sign_to_vec()?;
|
||||||
|
|
||||||
|
Ok(base64url_encode(&signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create complete JWT
|
||||||
|
pub fn create_token(&self) -> Result<String, JWTError> {
|
||||||
|
let header = self.create_header();
|
||||||
|
let processed_payload = self.process_payload()?;
|
||||||
|
|
||||||
|
let header_base64 = base64url_encode(to_string(&header)?);
|
||||||
|
let payload_base64 = base64url_encode(processed_payload);
|
||||||
|
let signature = self.sign(&header_base64, &payload_base64)?;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"{}.{}.{}",
|
||||||
|
header_base64, payload_base64, signature
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify JWT token signature
|
||||||
|
pub fn verify(&self, token: &str) -> bool {
|
||||||
|
let parts: Vec<&str> = token.split('.').collect();
|
||||||
|
if parts.len() != 3 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature_input = format!("{}.{}", parts[0], parts[1]);
|
||||||
|
let signature = match base64url_decode(parts[2]) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = match PKey::public_key_from_pem(self.public_key.as_bytes()) {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut verifier = match Verifier::new(MessageDigest::sha256(), &key) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
verifier.update(signature_input.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
verifier.verify(&signature).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode JWT token without verification
|
||||||
|
pub fn decode(token: &str) -> Result<(Value, Value), JWTError> {
|
||||||
|
let parts: Vec<&str> = token.split('.').collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return Err(JWTError::InvalidTokenFormat(
|
||||||
|
"Token must have at least two parts".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_json = String::from_utf8(base64url_decode(parts[0])?)
|
||||||
|
.map_err(|_| JWTError::InvalidTokenFormat("Header is not valid UTF-8".to_string()))?;
|
||||||
|
let payload_json = String::from_utf8(base64url_decode(parts[1])?)
|
||||||
|
.map_err(|_| JWTError::InvalidTokenFormat("Payload is not valid UTF-8".to_string()))?;
|
||||||
|
|
||||||
|
let header: Value = from_str(&header_json)?;
|
||||||
|
let payload: Value = from_str(&payload_json)?;
|
||||||
|
|
||||||
|
Ok((header, payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if token is expired
|
||||||
|
pub fn is_expired(token: &str) -> bool {
|
||||||
|
match Self::decode(token) {
|
||||||
|
Ok((_, payload)) => {
|
||||||
|
if let Some(exp) = payload.get("exp").and_then(|v| v.as_i64()) {
|
||||||
|
let now = Utc::now().timestamp();
|
||||||
|
exp < now
|
||||||
|
} else {
|
||||||
|
true // No expiration claim, consider it expired for safety
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => true, // Invalid token, consider it expired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A combined decode and verify function
|
||||||
|
pub fn decode_and_verify(token: &str) -> Result<(Value, Value), JWTError> {
|
||||||
|
let jwt = JWTUtils::new(Value::Null, None, None, None)?;
|
||||||
|
|
||||||
|
if !jwt.verify(token) {
|
||||||
|
return Err(JWTError::Validation(
|
||||||
|
"Signature verification failed".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if Self::is_expired(token) {
|
||||||
|
return Err(JWTError::Validation("Token has expired".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::decode(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh a token
|
||||||
|
pub fn refresh_token(old_token: &str) -> Result<String, JWTError> {
|
||||||
|
let (_, payload_val) = Self::decode(old_token)?;
|
||||||
|
let mut payload_obj = payload_val
|
||||||
|
.as_object()
|
||||||
|
.ok_or_else(|| JWTError::Custom("Payload is not an object".to_string()))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
payload_obj.remove("exp");
|
||||||
|
payload_obj.remove("iat");
|
||||||
|
|
||||||
|
// The keys must be loaded from files for this static method
|
||||||
|
let keys = CryptoUtils::load_keys_from_files("keys")
|
||||||
|
.map_err(|e| JWTError::Crypto(e.to_string()))?;
|
||||||
|
|
||||||
|
let jwt = JWTUtils::new(
|
||||||
|
Value::Object(payload_obj),
|
||||||
|
Some(keys.private_key),
|
||||||
|
Some(keys.public_key),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
jwt.create_token()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate that specific claims are present in the token
|
||||||
|
pub fn validate_claims(token: &str, required_claims: &[&str]) -> bool {
|
||||||
|
match Self::decode(token) {
|
||||||
|
Ok((_, payload)) => {
|
||||||
|
if let Some(payload_obj) = payload.as_object() {
|
||||||
|
required_claims
|
||||||
|
.iter()
|
||||||
|
.all(|claim| payload_obj.contains_key(*claim))
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/utils/password/password.rs
Normal file
83
src/utils/password/password.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user