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