1use crate::AppState;
2use crate::config::AuthRequest;
3use crate::crypto::{calcul_cipher, derive_key_from_secret, encrypt};
4use crate::proxy::client_ip;
5use crate::security::generate_token;
6use actix_web::{HttpRequest, HttpResponse, Responder, web};
7use argon2::Argon2;
8use argon2::password_hash::{PasswordHash, PasswordVerifier};
9use chrono::{Duration, Utc, TimeZone};
10use chrono_tz::Tz;
11use rand::rngs::OsRng;
12use rand::seq::SliceRandom;
13use blake3;
14use hex;
15use crate::AppConfig;
16use std::sync::Arc;
17use tracing::{info, warn};
18
19pub fn verify_password(input: &str, stored_hash: &str) -> bool {
20 match PasswordHash::new(stored_hash) {
21 Ok(parsed) => Argon2::default()
22 .verify_password(input.as_bytes(), &parsed)
23 .is_ok(),
24 Err(_) => false,
25 }
26}
27
28pub fn generate_random_string(len: usize) -> String {
29 let charset: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^*()+-=";
30 let mut rng = OsRng;
31
32 let base: Vec<u8> = (0..len)
33 .map(|_| *charset.choose(&mut rng).unwrap())
34 .collect();
35
36 let now = Utc::now().timestamp() as u64;
37 let shift: u8 = (now ^ (now >> 3) ^ (now << 1)).wrapping_rem(97) as u8;
38
39 let random_char: Vec<u8> = base
40 .into_iter()
41 .map(|byte| {
42 let idx = charset.iter().position(|&c| c == byte).unwrap_or(0);
43 let new_idx = (idx as u8 + shift) as usize % charset.len();
44 charset[new_idx]
45 })
46 .collect();
47
48 let mut full_input = random_char.clone();
49 full_input.extend_from_slice(&now.to_le_bytes());
50
51 let hash = blake3::hash(&full_input);
52
53 hex::encode(hash.as_bytes())
54}
55
56fn get_expiry_with_timezone(config: Arc<AppConfig>, optional_timestamp: Option<i64>) -> String {
57 let tz: Tz = config
58 .timezone
59 .parse()
60 .expect("Invalid timezone in config");
61
62 let utc_now = optional_timestamp
63 .map(|ts| Utc.timestamp_opt(ts, 0).single().expect("Invalid timestamp"))
64 .unwrap_or_else(Utc::now);
65
66 let local_time = utc_now.with_timezone(&tz);
67 let expiry = local_time + Duration::seconds(config.token_expiry_seconds);
68
69 expiry.timestamp().to_string()
70}
71
72pub async fn auth(
73 req: HttpRequest,
74 auth: web::Json<AuthRequest>,
75 data: web::Data<AppState>,
76) -> impl Responder {
77 let ip = client_ip(&req).expect("?").to_string();
78
79 if let Some(index_user) = data
80 .config
81 .users
82 .iter()
83 .enumerate()
84 .find(|(_, user)| {
85 user.username == auth.username && verify_password(&auth.password, &user.password)
86 })
87 .map(|(i, _)| i)
88 {
89 let user = &data.config.users[index_user];
90
91
92 let expiry = get_expiry_with_timezone(data.config.clone(), None);
93
94 let id_token = generate_random_string(48);
95
96 let token = generate_token(
97 &auth.username,
98 &data.config,
99 &expiry,
100 &id_token,
101 );
102
103 let key = derive_key_from_secret(&data.config.secret);
104
105 let cipher_token = format!(
106 "{}|{}|{}|{}",
107 calcul_cipher(token.clone()),
108 expiry,
109 index_user,
110 id_token
111 );
112
113 let token_encrypt = encrypt(&cipher_token, &key);
114
115 let expiry_ts: i64 = expiry
116 .parse()
117 .expect("Invalid timestamp string");
118
119 let expiry_utc = Utc
120 .timestamp_opt(expiry_ts, 0)
121 .single()
122 .expect("Invalid timestamp value");
123
124 let tz: Tz = data.config
125 .timezone
126 .parse()
127 .expect("Invalid timezone");
128
129 let expiry_dt_local = expiry_utc.with_timezone(&tz);
130 let expires_at_str = expiry_dt_local.format("%Y-%m-%d %H:%M:%S").to_string();
131
132 info!(
133 "[{}] new token generated for user {} expirated at {}",
134 ip, user.username, expires_at_str
135 );
136 HttpResponse::Ok().json(serde_json::json!({
137 "token": token_encrypt,
138 "expires_at": expires_at_str,
139 }))
140 } else {
141 warn!("Invalid credential for enter user {}.", auth.username);
142 return HttpResponse::Unauthorized().body("Invalid credentials");
143 }
144}