Refactor environment module: Rename enviroment to environment and consolidate environment configuration management

- Updated all references from `enviroment` to `environment` across the codebase.
- Removed the old `enviroment` module and replaced it with a new `environment` module that includes centralized configuration management.
- Enhanced OTP generation to include secure hashing and expiration handling.
- Improved CSRF token generation and validation with better error handling.
- Cleaned up logging statements in various modules for clarity and consistency.
- Updated response formatting to include versioning from Cargo.toml.
- Removed unused mock test module from utils.
This commit is contained in:
MythEclipse
2025-09-26 23:15:33 +07:00
parent c12da948aa
commit 5859af5294
32 changed files with 164 additions and 141 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ use surrealdb::{opt::auth::Root, sql::Thing, Uuid}; // Added Uuid
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
+1 -1
View File
@@ -5,7 +5,7 @@ use surrealdb::sql::Thing;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
+1 -1
View File
@@ -5,7 +5,7 @@ use surrealdb::opt::auth::Root;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
+1 -1
View File
@@ -7,7 +7,7 @@ use surrealdb::opt::auth::Root;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
+1 -1
View File
@@ -5,7 +5,7 @@ use surrealdb::engine::any;
use surrealdb::opt::auth::Root;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
@@ -5,7 +5,7 @@ use surrealdb::opt::auth::Root;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
let env = &imphnen_libs::environment::ENV;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
username: &env.surrealdb_username,
+1 -1
View File
@@ -5,7 +5,7 @@ use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
+1 -1
View File
@@ -5,7 +5,7 @@ use std::error::Error;
use surrealdb::{opt::auth::Root, sql::Thing};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env = &imphnen_libs::enviroment::ENV;
let env = &imphnen_libs::environment::ENV;
use surrealdb::engine::any;
let db = any::connect(&env.surrealdb_url).await?;
db.signin(Root {
@@ -187,11 +187,11 @@ impl MentorsService {
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
match auth_repo
.query_store_otp(final_user_email.clone(), otp)
.query_store_otp(final_user_email.clone(), otp.clone())
.await
{
Ok(_) => {
let message = format!("your otp code is {otp}");
let message = format!("your otp code is {}", otp.code);
if let Err(_err) =
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
{
-1
View File
@@ -32,7 +32,6 @@ pub use imphnen_utils::{
get_id,
logger,
make_thing,
mock_test,
query_builder,
query_list,
response_format,
+4 -5
View File
@@ -3,13 +3,14 @@ use super::UserCacheSchema;
use imphnen_entities::{PermissionsQueryDto, RolesDetailQueryDto, UsersDetailQueryDto};
use crate::ResourceEnum;
use anyhow::{Result, anyhow, bail};
use chrono::{Duration, Utc};
use chrono::Utc;
use surrealdb::sql::Thing;
use tracing::instrument;
use tracing::info;
use async_trait::async_trait;
use imphnen_libs::AuthRepositoryTrait;
use imphnen_libs::SurrealMemClient;
use imphnen_utils::generate_otp::OtpData;
pub struct AuthRepository {
@@ -162,15 +163,13 @@ impl AuthRepository {
}
}
#[instrument(skip(self, email, otp), err)]
pub async fn query_store_otp(&self, email: String, otp: u32) -> Result<String> {
let expires_at = Utc::now() + Duration::seconds(300);
pub async fn query_store_otp(&self, email: String, otp: OtpData) -> Result<String> {
let table: String = ResourceEnum::OtpCache.to_string();
info!(query = %format!("CREATE {}:{}", table, email), "Executing SurrealDB query");
let record: Option<AuthOtpSchema> = self
.db
.create((table.as_str(), email.as_str()))
.content(AuthOtpSchema { otp, expires_at })
.content(AuthOtpSchema { otp: otp.code, hash: otp.hash, expires_at: otp.expires_at })
.await?;
match record {
Some(_) => Ok("Success store otp".to_string()),
+1
View File
@@ -4,5 +4,6 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthOtpSchema {
pub otp: u32,
pub hash: String,
pub expires_at: DateTime<Utc>,
}
+5 -5
View File
@@ -1,7 +1,7 @@
use std::pin::Pin;
use std::future::Future;
use imphnen_utils as generate_otp;
use imphnen_libs::enviroment;
use imphnen_libs::environment;
use super::{
AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto,
AuthRefreshTokenRequestDto, AuthRegisterRequestDto, AuthRepository,
@@ -310,9 +310,9 @@ impl AuthServiceTrait for AuthService {
phone_number: payload.phone_number,
};
let otp = generate_otp::OtpManager::generate_otp();
match auth_repo.query_store_otp(new_user.email.clone(), otp).await {
match auth_repo.query_store_otp(new_user.email.clone(), otp.clone()).await {
Ok(_) => {
let message = format!("your otp code is {otp}");
let message = format!("your otp code is {}", otp.code);
if let Err(err_send) =
send_email(&new_user.email, "OTP Verification", &message)
{
@@ -383,7 +383,7 @@ impl AuthServiceTrait for AuthService {
let auth_repo = AuthRepository::new(state.surrealdb_mem.clone());
let _ = auth_repo.query_get_stored_otp(payload.email.clone()).await;
let otp = generate_otp::OtpManager::generate_otp();
let message = format!("Your OTP code is {otp}");
let message = format!("Your OTP code is {}", otp.code);
match auth_repo.query_store_otp(payload.email.clone(), otp).await {
Ok(_) => match send_email(&payload.email, "OTP Verification", &message) {
Ok(_) => common_response(StatusCode::OK, "OTP resent successfully"),
@@ -477,7 +477,7 @@ impl AuthServiceTrait for AuthService {
}
};
let env = &enviroment::ENV;
let env = &environment::ENV;
let fe_url = env.fe_url.clone();
let message = format!(
"You have requested a password reset. Please click the link below to continue: {fe_url}/auth/reset-password?token={token}"
@@ -7,7 +7,7 @@ use axum::{
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use std::sync::Arc;
use imphnen_libs::enviroment::ENV; // Import ENV
use imphnen_libs::environment::ENV; // Import ENV
use crate::v1::auth::google::google_oauth_service::{AuthRequest, GoogleOauthService, GoogleOauthServiceImpl};
use imphnen_entities::error_dto::error::Error;
@@ -12,7 +12,7 @@ use oauth2::TokenResponse;
use tracing::{info, error};
use imphnen_entities::error_dto::error::Error;
use imphnen_libs::{jsonwebtoken::{encode_access_token, encode_refresh_token}, enviroment::Env, AppState};
use imphnen_libs::{jsonwebtoken::{encode_access_token, encode_refresh_token}, environment::Env, AppState};
use imphnen_utils::{generate_oauth_csrf_token, validate_oauth_csrf_token, validate_csrf_token};
use crate::v1::auth::TokenDto;
use crate::v1::auth::auth_service::AuthServiceTrait;
+1 -1
View File
@@ -79,7 +79,7 @@ impl TeamsService {
}
async fn generate_invitation_token() -> String {
format!("team_{}_{}", Uuid::new_v4(), OtpManager::generate_otp())
format!("team_{}_{}", Uuid::new_v4(), OtpManager::generate_otp().code)
}
async fn get_user_info_with_privacy(
+1 -1
View File
@@ -7,7 +7,7 @@ use crate::{surrealdb_init_mem, surrealdb_init_ws, SurrealMemClient, SurrealWsCl
use axum::{Router, serve};
use std::{future::Future, net::SocketAddr};
use tokio::net::TcpListener;
use crate::enviroment::ENV;
use crate::environment::ENV;
/// Initialize and start the Axum server with SurrealDB connections.
///
+1 -1
View File
@@ -4,7 +4,7 @@
//! for authentication purposes, including access tokens, refresh tokens,
//! and password reset tokens.
use crate::enviroment::ENV;
use crate::environment::ENV;
use axum::http::StatusCode;
use chrono::{Duration, TimeDelta, Utc};
use jsonwebtoken::{
+3 -3
View File
@@ -3,7 +3,7 @@
//! This module provides functionality for sending emails through SMTP
//! with proper error handling and logging.
use crate::enviroment::ENV;
use crate::environment::ENV;
use lettre::message::Mailbox;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
@@ -88,7 +88,7 @@ fn build_email_message(
to: &str,
subject: &str,
body: &str,
env: &crate::enviroment::Env,
env: &crate::environment::Env,
) -> Result<Message, Box<dyn Error>> {
let sender_name = env.smtp_name.replace("-", " "); // Normalize sender name
@@ -107,7 +107,7 @@ fn build_email_message(
///
/// # Returns
/// Configured SMTP transport or error
fn create_smtp_transport(env: &crate::enviroment::Env) -> Result<SmtpTransport, Box<dyn Error>> {
fn create_smtp_transport(env: &crate::environment::Env) -> Result<SmtpTransport, Box<dyn Error>> {
let credentials = Credentials::new(
env.smtp_email.clone(),
env.smtp_password.replace("-", " "), // Normalize password
+19 -2
View File
@@ -1,8 +1,25 @@
/*!
# imphnen-libs
A collection of utility libraries and services for the imphnen project, providing integrations
with various external services and common functionality.
This crate includes modules for:
- Password hashing with Argon2 (`argon`)
- Axum web framework utilities (`axum`)
- Environment configuration (`environment`)
- JWT token handling (`jsonwebtoken`)
- Email sending with Lettre (`lettre`)
- MinIO object storage client (`minio`)
- Service abstractions (`services`)
- SurrealDB database client (`surrealdb`)
*/
use std::sync::Arc;
pub mod argon;
pub mod axum;
pub mod enviroment;
pub mod environment;
pub mod jsonwebtoken;
pub mod lettre;
pub mod minio;
@@ -11,7 +28,7 @@ pub mod surrealdb;
pub use argon::{hash_password, verify_password};
pub use axum::axum_init;
pub use enviroment::{ENV, Env};
pub use environment::{ENV, Env};
pub use imphnen_entities::{
MessageResponseDto,
MetaRequestDto,
+1 -30
View File
@@ -4,7 +4,7 @@ use chrono::Utc;
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::enviroment::ENV;
use crate::environment::ENV;
@@ -108,12 +108,6 @@ impl MinioService {
let url = format!("https://{}/{}/{}", host, self.bucket_name, object_name);
// Debug logging
log::debug!("MinIO Endpoint config: {}", self.endpoint);
log::debug!("MinIO Region config: {}", self.region);
log::debug!("Upload URL: {}", url);
log::debug!("Object name: {}", object_name);
log::debug!("File hash: {}", short_hash);
let now = Utc::now();
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
@@ -135,7 +129,6 @@ impl MinioService {
canonical_uri, canonical_headers, signed_headers, payload_hash
);
log::debug!("Canonical request:\n{}", canonical_request);
let scope = format!("{}/{}/s3/aws4_request", date_stamp, self.region);
let string_to_sign = format!(
@@ -153,7 +146,6 @@ impl MinioService {
mac.update(string_to_sign.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
log::debug!("Generated signature: {}", signature);
let auth_header = format!(
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
@@ -211,13 +203,6 @@ impl MinioService {
let url = format!("https://{}/{}/{}", host, self.bucket_name, object_name);
// Debug logging
log::debug!("MinIO Endpoint config: {}", self.endpoint);
log::debug!("MinIO Region config: {}", self.region);
log::debug!("MinIO Access Key: {}", self.access_key);
log::debug!("MinIO Bucket: {}", self.bucket_name);
log::debug!("Extracted host: {}", host);
log::debug!("Final URL: {}", url);
let now = Utc::now();
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
@@ -242,14 +227,6 @@ impl MinioService {
canonical_uri, canonical_headers, signed_headers, payload_hash
);
// Debug logging
log::debug!("URL: {}", url);
log::debug!("Host: {}", host);
log::debug!("Bucket: {}", self.bucket_name);
log::debug!("Object: {}", object_name);
log::debug!("Canonical URI: {}", canonical_uri);
log::debug!("Payload hash: {}", payload_hash);
log::debug!("Canonical Request:\n{}", canonical_request);
let scope = format!("{}/{}/s3/aws4_request", date_stamp, self.region);
let string_to_sign = format!(
@@ -259,15 +236,12 @@ impl MinioService {
hex::encode(Sha256::digest(canonical_request.as_bytes()))
);
log::debug!("Scope: {}", scope);
log::debug!("String to sign:\n{}", string_to_sign);
let signing_key = self.get_signature_key(&date_stamp)?;
let mut mac = Hmac::<Sha256>::new_from_slice(&signing_key)?;
mac.update(string_to_sign.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
log::debug!("Generated signature: {}", signature);
let auth_header = format!(
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
@@ -475,15 +449,12 @@ impl MinioService {
// Debug logging for signature calculation
log::debug!("Region: {}", self.region);
log::debug!("Scope: {}", scope);
log::debug!("String to sign:\n{}", string_to_sign);
let signing_key = self.get_signature_key(&date_stamp)?;
let mut mac = Hmac::<Sha256>::new_from_slice(&signing_key)?;
mac.update(string_to_sign.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
log::debug!("Final signature: {}", signature);
let auth_header = format!(
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
+1 -1
View File
@@ -3,7 +3,7 @@
//! This module provides utilities for initializing SurrealDB connections
//! for both WebSocket and in-memory databases, along with resource definitions.
use crate::enviroment::ENV;
use crate::environment::ENV;
use surrealdb::engine::any;
use surrealdb::engine::local::{Db, Mem};
use surrealdb::opt::auth::Root;
@@ -1,5 +1,5 @@
use axum::http::{HeaderValue, Method, header};
use imphnen_libs::enviroment::ENV;
use imphnen_libs::environment::ENV;
use tower_http::cors::CorsLayer;
pub fn cors_middleware() -> CorsLayer {
+20 -26
View File
@@ -1,9 +1,14 @@
//! CSRF token generation and validation utilities.
//!
//! This module provides stateless CSRF token management using signed tokens
//! with timestamp validation to prevent cross-site request forgery attacks.
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use sha2::{Sha256, Digest};
use imphnen_entities::error_dto::error::Error;
use tracing::{info, error}; // Added this line
use tracing::error;
#[derive(Debug, Serialize, Deserialize)]
struct CsrfPayload {
@@ -24,33 +29,28 @@ pub fn generate_csrf_token(secret: &str) -> Result<String, Error> {
.duration_since(UNIX_EPOCH)
.map_err(|_| Error::Auth("Failed to get timestamp".to_string()))?
.as_secs();
info!("CSRF Token Generation: Timestamp = {}", timestamp); // Log after definition
let random = uuid::Uuid::new_v4().to_string();
info!("CSRF Token Generation: Random string generated."); // Log after definition
let payload = CsrfPayload {
timestamp,
random,
};
let payload_json = serde_json::to_string(&payload)
.map_err(|e| { // Changed to capture error
.map_err(|e| {
error!("CSRF Token Generation: Failed to serialize CSRF payload: {:?}", e);
Error::Auth("Failed to serialize CSRF payload".to_string())
})?;
info!("CSRF Token Generation: Payload JSON = {}", payload_json); // Log after definition
let payload_b64 = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
info!("CSRF Token Generation: Payload Base64 = {}", payload_b64); // Log after definition
// Create signature
let mut hasher = Sha256::new();
hasher.update(payload_b64.as_bytes());
hasher.update(secret.as_bytes());
let signature = URL_SAFE_NO_PAD.encode(hasher.finalize());
info!("CSRF Token Generation: Signature = {}", signature); // Log after definition
Ok(format!("{}.{}", payload_b64, signature))
}
@@ -60,35 +60,29 @@ pub fn generate_oauth_csrf_token(secret: &str, pkce_verifier: &str) -> Result<St
.duration_since(UNIX_EPOCH)
.map_err(|_| Error::Auth("Failed to get timestamp".to_string()))?
.as_secs();
info!("OAuth CSRF Token Generation: Timestamp = {}", timestamp); // Log after definition
let random = uuid::Uuid::new_v4().to_string();
info!("OAuth CSRF Token Generation: Random string generated."); // Log after definition
let payload = OAuthCsrfPayload {
timestamp,
random,
pkce_verifier: pkce_verifier.to_string(),
};
info!("OAuth CSRF Token Generation: PKCE Verifier = {}", pkce_verifier); // Log after use in payload
let payload_json = serde_json::to_string(&payload)
.map_err(|e| { // Changed to capture error
.map_err(|e| {
error!("OAuth CSRF Token Generation: Failed to serialize payload: {:?}", e);
Error::Auth("Failed to serialize OAuth CSRF payload".to_string())
})?;
info!("OAuth CSRF Token Generation: Payload JSON = {}", payload_json); // Log after definition
let payload_b64 = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
info!("OAuth CSRF Token Generation: Payload Base64 = {}", payload_b64); // Log after definition
// Create signature
let mut hasher = Sha256::new();
hasher.update(payload_b64.as_bytes());
hasher.update(secret.as_bytes());
let signature = URL_SAFE_NO_PAD.encode(hasher.finalize());
info!("OAuth CSRF Token Generation: Signature = {}", signature); // Log after definition
Ok(format!("{}.{}", payload_b64, signature))
}
+10 -19
View File
@@ -1,11 +1,16 @@
use tracing::{info, error};
//! Email extraction utilities from authentication tokens.
//!
//! This module provides functions to extract email addresses from JWT tokens
//! and Google OAuth access tokens, supporting both synchronous and asynchronous
//! validation methods.
use tracing::{error, info};
use crate::decode_access_token;
use axum::http::{HeaderMap, header::AUTHORIZATION};
/// Extracts the email from the Authorization header, if present and valid.
/// Supports both our internal JWT tokens and Google access tokens.
pub fn extract_email(headers: &HeaderMap) -> Option<String> {
info!(?headers, "extract_email called with headers");
let auth_header = match headers.get(AUTHORIZATION) {
Some(h) => h,
None => {
@@ -27,16 +32,13 @@ pub fn extract_email(headers: &HeaderMap) -> Option<String> {
return None;
}
};
info!(token, "Extracted bearer token in extract_email");
// First try to decode as our internal JWT token
match decode_access_token(token) {
Ok(data) => {
info!(email = %data.claims.sub, "Successfully decoded internal access token in extract_email");
Some(data.claims.sub)
}
Err(_) => {
info!("Failed to decode as internal JWT, checking if it's a Google token");
// If it fails, it might be a Google access token
// For Google tokens, we need async validation, so we'll return None here
// and handle Google tokens separately in the calling code
@@ -48,7 +50,6 @@ pub fn extract_email(headers: &HeaderMap) -> Option<String> {
/// Async version that can handle Google access tokens
pub async fn extract_email_async(headers: &HeaderMap) -> Option<String> {
info!(?headers, "extract_email_async called with headers");
let auth_header = match headers.get(AUTHORIZATION) {
Some(h) => h,
None => {
@@ -70,16 +71,13 @@ pub async fn extract_email_async(headers: &HeaderMap) -> Option<String> {
return None;
}
};
info!(token, "Extracted bearer token in extract_email_async");
// First try to decode as our internal JWT token
match decode_access_token(token) {
Ok(data) => {
info!(email = %data.claims.sub, "Successfully decoded internal access token in extract_email_async");
Some(data.claims.sub)
}
Err(_) => {
info!("Failed to decode as internal JWT, trying Google token validation");
// If it fails, try to validate as Google access token
extract_email_from_google_token(token).await
}
@@ -126,14 +124,11 @@ async fn extract_email_from_google_token(token: &str) -> Option<String> {
/// Extracts the email from a JWT token string.
/// Supports both our internal JWT tokens and Google access tokens.
pub fn extract_email_token(token: String) -> Option<String> {
info!(token = %token, "extract_email_token called with token");
match decode_access_token(&token) {
Ok(data) => {
info!(email = %data.claims.sub, "Successfully decoded token in extract_email_token");
Some(data.claims.sub)
}
Err(_) => {
info!("Failed to decode as internal JWT in extract_email_token, checking if it's a Google token");
// If it fails, it might be a Google access token
// For Google tokens, we need async validation, so we'll return None here
// and handle Google tokens separately in the calling code
@@ -151,20 +146,16 @@ fn is_jwt(token: &str) -> bool {
/// Async version of extract_email_token that can handle Google access tokens
pub async fn extract_email_token_async(token: String) -> Option<String> {
info!(token = %token, "extract_email_token_async called with token");
if is_jwt(&token) {
match decode_access_token(&token) {
Ok(data) => {
info!(email = %data.claims.sub, "Successfully decoded internal token in extract_email_token_async");
return Some(data.claims.sub);
}
Err(_) => {
info!("Failed to decode as internal JWT in extract_email_token_async, trying Google token validation");
}
}
}
// If it's not a valid internal JWT, try to validate as Google access token
extract_email_from_google_token(&token).await
}
+38 -6
View File
@@ -1,13 +1,45 @@
//! OTP generation utilities with time-based expiration and secure hashing.
//!
//! This module provides functionality to generate one-time passwords (OTPs) with
//! a 5-minute expiration time and SHA256 hashing for secure storage and validation,
//! preventing replay attacks.
use rand::{Rng, rng};
use sha2::{Sha256, Digest};
use chrono::{DateTime, Utc, Duration};
/// Represents an OTP with its code, hashed value and expiration time
#[derive(Debug, Clone)]
pub struct OtpData {
pub code: u32,
pub hash: String,
pub expires_at: DateTime<Utc>,
}
pub struct OtpManager;
impl OtpManager {
pub fn generate_otp() -> u32 {
rng().random_range(100_000..1_000_000)
}
/// Generates a new OTP with a 5-minute expiration and SHA256 hash for secure storage
pub fn generate_otp() -> OtpData {
let code = rng().random_range(100_000..1_000_000);
let otp_str = code.to_string();
let mut hasher = Sha256::new();
hasher.update(otp_str.as_bytes());
let hash = format!("{:x}", hasher.finalize());
let expires_at = Utc::now() + Duration::minutes(5);
OtpData { code, hash, expires_at }
}
pub fn validate_otp(stored_otp: u32, user_otp: u32) -> bool {
stored_otp == user_otp
}
/// Validates the user-provided OTP against the stored OTP data
/// Checks both hash match and expiration
pub fn validate_otp(stored: &OtpData, user_otp: u32) -> bool {
if Utc::now() > stored.expires_at {
return false;
}
let user_otp_str = user_otp.to_string();
let mut hasher = Sha256::new();
hasher.update(user_otp_str.as_bytes());
let user_hash = format!("{:x}", hasher.finalize());
user_hash == stored.hash
}
}
+8 -1
View File
@@ -1,3 +1,11 @@
//! # imphnen-utils
//!
//! A collection of utility functions and types for the imphnen project.
//!
//! This crate provides various utilities including OTP generation with expiration and hashing,
//! CSRF token management, email extraction from tokens, query building for SurrealDB,
//! and standardized response formatting.
pub mod bind_filter;
pub mod csrf_token;
pub mod extract_email;
@@ -6,7 +14,6 @@ pub mod generate_otp;
pub mod get_id;
pub mod logger;
pub mod make_thing;
pub mod mock_test;
pub mod query_builder;
pub mod query_list;
pub mod response_format;
-1
View File
@@ -1 +0,0 @@
+24 -17
View File
@@ -1,3 +1,9 @@
//! Query builder utilities for SurrealDB.
//!
//! This module provides builders for constructing SurrealDB queries with
//! support for pagination, filtering, sorting, and binding parameters.
//! Includes both list queries and detail queries with unique binding keys.
use anyhow::Result;
use imphnen_libs::MetaRequestDto;
use serde_json::{Map, Value};
@@ -131,11 +137,11 @@ impl ListQueryBuilder {
format!(
r#"
SELECT {} FROM {}
{}
{}
LIMIT {} START {}
{}
SELECT {} FROM {}
{}
{}
LIMIT {} START {}
{}
"#,
select_clause,
self.resource,
@@ -166,6 +172,7 @@ pub struct DetailQueryBuilder {
fetch_fields: Vec<String>,
conditions: Vec<String>,
bindings: Map<String, Value>,
binding_counter: usize,
}
impl DetailQueryBuilder {
@@ -178,6 +185,7 @@ impl DetailQueryBuilder {
fetch_fields: vec![],
conditions: vec![],
bindings: Map::new(),
binding_counter: 0,
}
}
@@ -202,7 +210,6 @@ impl DetailQueryBuilder {
self
}
// Modified with_where method
pub fn with_where(
mut self,
field: impl Into<String>,
@@ -213,11 +220,11 @@ impl DetailQueryBuilder {
}
let field_str = field.into();
if let Some(val) = value {
// Using a distinct binding key to avoid conflicts
self.conditions.push(format!("{field_str} = $value_where"));
self
.bindings
.insert("value_where".to_string(), Value::String(val.into()));
// Using a unique binding key to avoid conflicts
let key = format!("value_where_{}", self.binding_counter);
self.binding_counter += 1;
self.conditions.push(format!("{field_str} = ${key}"));
self.bindings.insert(key, Value::String(val.into()));
} else {
// If no value, assume it's a direct condition string (e.g., "is_active = true")
self.conditions.push(field_str);
@@ -231,15 +238,15 @@ impl DetailQueryBuilder {
}
pub fn with_thing_equals(mut self, field: &str, thing: &Thing) -> Self {
let condition = build_thing_condition(field, thing);
self.conditions.push(condition);
self
let condition = build_thing_condition(field, thing);
self.conditions.push(condition);
self
}
pub fn with_things_equals(mut self, conditions: &[(&str, &Thing)]) -> Self {
let condition = build_multi_thing_condition(conditions);
self.conditions.push(condition);
self
let condition = build_multi_thing_condition(conditions);
self.conditions.push(condition);
self
}
pub fn with_select_fields(mut self, fields: Vec<&str>) -> Self {
+13 -7
View File
@@ -1,7 +1,13 @@
//! Standardized response formatting utilities.
//!
//! This module provides consistent response formatting for API endpoints,
//! including success responses, error responses, and list responses with
//! configurable versioning from Cargo.toml.
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
use serde_json::json;
@@ -13,7 +19,7 @@ pub fn success_response<T: Serialize>(params: ResponseSuccessDto<T>) -> Response
StatusCode::OK,
Json(json!({
"data": params.data,
"version": "0.1.0",
"version": env!("CARGO_PKG_VERSION"),
})),
)
.into_response()
@@ -27,7 +33,7 @@ pub fn success_list_response<T: Serialize>(
Json(json!({
"data": params.data,
"meta": params.meta,
"version": "0.1.0",
"version": env!("CARGO_PKG_VERSION"),
})),
)
.into_response()
@@ -38,7 +44,7 @@ pub fn common_response(status: StatusCode, message: &str) -> Response {
status,
Json(json!({
"message": message,
"version": "0.1.0",
"version": env!("CARGO_PKG_VERSION"),
})),
)
.into_response()
@@ -49,7 +55,7 @@ pub fn success_created_response<T: Serialize>(params: ResponseSuccessDto<T>) ->
StatusCode::CREATED,
Json(json!({
"data": params.data,
"version": "0.1.0",
"version": env!("CARGO_PKG_VERSION"),
})),
)
.into_response()
@@ -17,7 +17,7 @@ mod tests {
use imphnen_iam::v1::users::users_dto::{UsersDetailItemDto, UsersCreateRequestDto}; // Corrected: removed UserDto alias, used UsersCreateRequestDto
use imphnen_entities::error_dto::ErrorResponse;
use imphnen_libs::jsonwebtoken::generate_jwt;
use imphnen_libs::enviroment::{ENV, Env}; // Import ENV and Env
use imphnen_libs::environment::{ENV, Env}; // Import ENV and Env
mock! {
pub GoogleOauthServiceMock {}