Skip to main content

adminx/utils/
auth.rs

1// adminx/src/utils/auth.rs
2use crate::models::adminx_model::{AdminxUser};
3use crate::configs::initializer::AdminxConfig;
4use mongodb::{
5    bson::{doc, DateTime as BsonDateTime},
6};
7use bcrypt::{hash, DEFAULT_COST};
8use anyhow::{Result};
9use crate::{custom_error_expression};
10use serde::{Serialize, Deserialize};
11use actix_session::Session;
12use actix_web::{Error, web};
13use jsonwebtoken::{decode, DecodingKey, Validation};
14use crate::{
15    utils::{
16        database::{
17            get_adminx_database
18        },
19        ubson::{
20            convert_to_bson
21        },
22        structs::{
23            Claims // ✅ Use Claims from structs.rs, not jwt.rs
24        },
25    }
26};
27
28// Updated to use config instead of env::var
29pub async fn extract_claims_from_session(
30    session: &Session,
31    config: &AdminxConfig,
32) -> Result<Claims, Error> {
33    let token = session
34        .get::<String>("admintoken")
35        .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid session"))?
36        .ok_or_else(|| actix_web::error::ErrorUnauthorized("Missing token in session"))?;
37    
38    let token_data = decode::<Claims>(
39        &token,
40        &DecodingKey::from_secret(config.jwt_secret.as_bytes()),
41        &Validation::default(),
42    )
43    .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token"))?;
44    
45    Ok(token_data.claims)
46}
47
48// Convenience function for extracting claims from request context
49pub async fn extract_claims_from_request(
50    session: &Session,
51    config: &web::Data<AdminxConfig>,
52) -> Result<Claims, Error> {
53    extract_claims_from_session(session, config.as_ref()).await
54}
55
56#[derive(Debug, Serialize, Deserialize, Clone)]
57#[serde(rename_all = "lowercase")]
58pub enum AdminxStatus {
59    Active,
60    Inactive,
61    Suspended,
62}
63
64pub enum InitOutcome {
65    Created,
66    Updated,
67}
68
69#[derive(Debug)]
70pub struct NewAdminxUser {
71    pub username: String,
72    pub email: String,
73    pub password: String,
74    pub status: AdminxStatus,
75    pub delete: bool,
76}
77
78pub async fn initiate_auth(adminx: NewAdminxUser) -> Result<InitOutcome, actix_web::Error> {
79    let db = get_adminx_database();
80    let collection = db.collection::<AdminxUser>("adminxs");
81    
82    let now = BsonDateTime::now();
83    let hashed_pwd = hash(&adminx.password, DEFAULT_COST)
84        .map_err(|e| custom_error_expression!(bad_request, 400, format!("Failed to hash password: {e}")))?;
85        
86    match collection.find_one(doc! { "email": &adminx.email }, None).await {
87        Ok(Some(_exist)) => {
88            let status_bson = convert_to_bson(&adminx.status)?;
89            let update_doc = doc! {
90                "username": adminx.username,
91                "password": hashed_pwd,
92                "delete": adminx.delete,
93                "status": status_bson,
94                "updated_at": now,
95            };
96            collection.update_one(
97                doc! { "email": &adminx.email },
98                doc! { "$set": update_doc },
99                None,
100            )
101            .await
102            .map_err(|e| custom_error_expression!(bad_request, 400, e.to_string()))?;
103            Ok(InitOutcome::Updated)
104        }
105        Ok(None) => {
106            let new_user = AdminxUser {
107                id: None,
108                username: adminx.username,
109                email: adminx.email,
110                password: hashed_pwd,
111                delete: adminx.delete,
112                status: adminx.status,
113                created_at: now,
114                updated_at: now,
115            };
116            collection.insert_one(new_user, None)
117                .await
118                .map_err(|e| custom_error_expression!(invalid_request, 422, format!("User creation failed: {e}")))?;
119            Ok(InitOutcome::Created)
120        }
121        Err(e) => {
122            return Err(custom_error_expression!(internal_error, 500, format!("DB error: {e}")))?;
123        }
124    }
125}
126
127// Additional security utilities that can use config
128pub fn validate_session_config(config: &AdminxConfig) -> Result<(), String> {
129    if config.jwt_secret.len() < 32 {
130        return Err("JWT_SECRET must be at least 32 characters long".to_string());
131    }
132    
133    if !config.session_secret.is_empty() && config.session_secret.len() < 64 {
134        return Err("SESSION_SECRET must be at least 64 characters long".to_string());
135    }
136    
137    Ok(())
138}
139
140// Rate limiting helper (optional enhancement)
141use std::collections::HashMap;
142use std::time::{Duration, Instant};
143use std::sync::Mutex;
144
145lazy_static::lazy_static! {
146    static ref LOGIN_ATTEMPTS: Mutex<HashMap<String, (u32, Instant)>> = Mutex::new(HashMap::new());
147}
148
149pub fn is_rate_limited(email: &str, max_attempts: u32, window: Duration) -> bool {
150    let mut attempts = LOGIN_ATTEMPTS.lock().unwrap();
151    let now = Instant::now();
152    
153    match attempts.get_mut(email) {
154        Some((count, last_attempt)) => {
155            if now.duration_since(*last_attempt) > window {
156                // Reset counter if outside window
157                *count = 1;
158                *last_attempt = now;
159                false
160            } else if *count >= max_attempts {
161                // Still within rate limit
162                true
163            } else {
164                // Increment counter
165                *count += 1;
166                *last_attempt = now;
167                false
168            }
169        }
170        None => {
171            // First attempt
172            attempts.insert(email.to_string(), (1, now));
173            false
174        }
175    }
176}
177
178pub fn reset_rate_limit(email: &str) {
179    let mut attempts = LOGIN_ATTEMPTS.lock().unwrap();
180    attempts.remove(email);
181}