1use crate::error::{CollabError, Result};
4use crate::models::User;
5use argon2::{
6 password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
7 Argon2,
8};
9use chrono::{DateTime, Duration, Utc};
10use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Claims {
17 pub sub: String,
19 pub username: String,
21 pub exp: i64,
23 pub iat: i64,
25}
26
27impl Claims {
28 #[must_use]
30 pub fn new(user_id: Uuid, username: String, expires_in: Duration) -> Self {
31 let now = Utc::now();
32 Self {
33 sub: user_id.to_string(),
34 username,
35 exp: (now + expires_in).timestamp(),
36 iat: now.timestamp(),
37 }
38 }
39
40 #[must_use]
42 pub fn is_expired(&self) -> bool {
43 Utc::now().timestamp() > self.exp
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Token {
50 pub access_token: String,
52 pub token_type: String,
54 pub expires_at: DateTime<Utc>,
56}
57
58#[derive(Debug, Clone, Deserialize)]
60pub struct Credentials {
61 pub username: String,
63 pub password: String,
65}
66
67#[derive(Debug, Clone)]
69pub struct Session {
70 pub user_id: Uuid,
72 pub username: String,
74 pub expires_at: DateTime<Utc>,
76}
77
78pub struct AuthService {
80 jwt_secret: String,
82 token_expiration: Duration,
84}
85
86impl AuthService {
87 #[must_use]
89 pub const fn new(jwt_secret: String) -> Self {
90 Self {
91 jwt_secret,
92 token_expiration: Duration::hours(24),
93 }
94 }
95
96 #[must_use]
98 pub const fn with_expiration(mut self, expiration: Duration) -> Self {
99 self.token_expiration = expiration;
100 self
101 }
102
103 pub fn hash_password(&self, password: &str) -> Result<String> {
105 let salt = SaltString::generate(&mut OsRng);
106 let argon2 = Argon2::default();
107
108 let password_hash = argon2
109 .hash_password(password.as_bytes(), &salt)
110 .map_err(|e| CollabError::Internal(format!("Password hashing failed: {e}")))?
111 .to_string();
112
113 Ok(password_hash)
114 }
115
116 pub fn verify_password(&self, password: &str, hash: &str) -> Result<bool> {
118 let parsed_hash = PasswordHash::new(hash)
119 .map_err(|e| CollabError::Internal(format!("Invalid password hash: {e}")))?;
120
121 let argon2 = Argon2::default();
122
123 Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
124 }
125
126 pub fn generate_token(&self, user: &User) -> Result<Token> {
128 let claims = Claims::new(user.id, user.username.clone(), self.token_expiration);
129 let expires_at = Utc::now() + self.token_expiration;
130
131 let token = encode(
132 &Header::default(),
133 &claims,
134 &EncodingKey::from_secret(self.jwt_secret.as_bytes()),
135 )
136 .map_err(|e| CollabError::Internal(format!("Token generation failed: {e}")))?;
137
138 Ok(Token {
139 access_token: token,
140 token_type: "Bearer".to_string(),
141 expires_at,
142 })
143 }
144
145 pub fn verify_token(&self, token: &str) -> Result<Claims> {
147 let token_data = decode::<Claims>(
148 token,
149 &DecodingKey::from_secret(self.jwt_secret.as_bytes()),
150 &Validation::default(),
151 )
152 .map_err(|e| CollabError::AuthenticationFailed(format!("Invalid token: {e}")))?;
153
154 if token_data.claims.is_expired() {
155 return Err(CollabError::AuthenticationFailed("Token expired".to_string()));
156 }
157
158 Ok(token_data.claims)
159 }
160
161 pub fn create_session(&self, token: &str) -> Result<Session> {
163 let claims = self.verify_token(token)?;
164
165 let user_id = Uuid::parse_str(&claims.sub)
166 .map_err(|e| CollabError::Internal(format!("Invalid user ID in token: {e}")))?;
167
168 Ok(Session {
169 user_id,
170 username: claims.username,
171 expires_at: DateTime::from_timestamp(claims.exp, 0)
172 .ok_or_else(|| CollabError::Internal("Invalid timestamp".to_string()))?,
173 })
174 }
175
176 #[must_use]
178 pub fn generate_invitation_token(&self) -> String {
179 use blake3::hash;
180 let random_data =
181 format!("{}{}", Uuid::new_v4(), Utc::now().timestamp_nanos_opt().unwrap_or(0));
182 hash(random_data.as_bytes()).to_hex().to_string()
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_password_hashing() {
192 let auth = AuthService::new("test_secret".to_string());
193 let password = "test_password_123";
194
195 let hash = auth.hash_password(password).unwrap();
196 assert!(auth.verify_password(password, &hash).unwrap());
197 assert!(!auth.verify_password("wrong_password", &hash).unwrap());
198 }
199
200 #[test]
201 fn test_token_generation() {
202 let auth = AuthService::new("test_secret".to_string());
203 let user =
204 User::new("testuser".to_string(), "test@example.com".to_string(), "hash".to_string());
205
206 let token = auth.generate_token(&user).unwrap();
207 assert_eq!(token.token_type, "Bearer");
208 assert!(!token.access_token.is_empty());
209 }
210
211 #[test]
212 fn test_token_verification() {
213 let auth = AuthService::new("test_secret".to_string());
214 let user =
215 User::new("testuser".to_string(), "test@example.com".to_string(), "hash".to_string());
216
217 let token = auth.generate_token(&user).unwrap();
218 let claims = auth.verify_token(&token.access_token).unwrap();
219
220 assert_eq!(claims.username, "testuser");
221 assert!(!claims.is_expired());
222 }
223
224 #[test]
225 fn test_session_creation() {
226 let auth = AuthService::new("test_secret".to_string());
227 let user =
228 User::new("testuser".to_string(), "test@example.com".to_string(), "hash".to_string());
229
230 let token = auth.generate_token(&user).unwrap();
231 let session = auth.create_session(&token.access_token).unwrap();
232
233 assert_eq!(session.username, "testuser");
234 }
235
236 #[test]
237 fn test_invitation_token_generation() {
238 let auth = AuthService::new("test_secret".to_string());
239 let token1 = auth.generate_invitation_token();
240 let token2 = auth.generate_invitation_token();
241
242 assert!(!token1.is_empty());
243 assert!(!token2.is_empty());
244 assert_ne!(token1, token2); }
246}