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:
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user