1use 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 },
25 }
26};
27
28pub 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
48pub 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
127pub 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
140use 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 *count = 1;
158 *last_attempt = now;
159 false
160 } else if *count >= max_attempts {
161 true
163 } else {
164 *count += 1;
166 *last_attempt = now;
167 false
168 }
169 }
170 None => {
171 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}