use crate::error::{CollabError, Result};
use crate::models::User;
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use chrono::{DateTime, Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub username: String,
pub exp: i64,
pub iat: i64,
}
impl Claims {
#[must_use]
pub fn new(user_id: Uuid, username: String, expires_in: Duration) -> Self {
let now = Utc::now();
Self {
sub: user_id.to_string(),
username,
exp: (now + expires_in).timestamp(),
iat: now.timestamp(),
}
}
#[must_use]
pub fn is_expired(&self) -> bool {
Utc::now().timestamp() > self.exp
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Token {
pub access_token: String,
pub token_type: String,
pub expires_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Credentials {
pub username: String,
pub password: String,
}
#[derive(Debug, Clone)]
pub struct Session {
pub user_id: Uuid,
pub username: String,
pub expires_at: DateTime<Utc>,
}
pub struct AuthService {
jwt_secret: String,
token_expiration: Duration,
}
impl AuthService {
#[must_use]
pub const fn new(jwt_secret: String) -> Self {
Self {
jwt_secret,
token_expiration: Duration::hours(24),
}
}
#[must_use]
pub const fn with_expiration(mut self, expiration: Duration) -> Self {
self.token_expiration = expiration;
self
}
pub fn hash_password(&self, password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| CollabError::Internal(format!("Password hashing failed: {e}")))?
.to_string();
Ok(password_hash)
}
pub fn verify_password(&self, password: &str, hash: &str) -> Result<bool> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| CollabError::Internal(format!("Invalid password hash: {e}")))?;
let argon2 = Argon2::default();
Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
}
pub fn generate_token(&self, user: &User) -> Result<Token> {
let claims = Claims::new(user.id, user.username.clone(), self.token_expiration);
let expires_at = Utc::now() + self.token_expiration;
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(self.jwt_secret.as_bytes()),
)
.map_err(|e| CollabError::Internal(format!("Token generation failed: {e}")))?;
Ok(Token {
access_token: token,
token_type: "Bearer".to_string(),
expires_at,
})
}
pub fn verify_token(&self, token: &str) -> Result<Claims> {
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(self.jwt_secret.as_bytes()),
&Validation::default(),
)
.map_err(|e| CollabError::AuthenticationFailed(format!("Invalid token: {e}")))?;
if token_data.claims.is_expired() {
return Err(CollabError::AuthenticationFailed("Token expired".to_string()));
}
Ok(token_data.claims)
}
pub fn create_session(&self, token: &str) -> Result<Session> {
let claims = self.verify_token(token)?;
let user_id = Uuid::parse_str(&claims.sub)
.map_err(|e| CollabError::Internal(format!("Invalid user ID in token: {e}")))?;
Ok(Session {
user_id,
username: claims.username,
expires_at: DateTime::from_timestamp(claims.exp, 0)
.ok_or_else(|| CollabError::Internal("Invalid timestamp".to_string()))?,
})
}
#[must_use]
pub fn generate_invitation_token(&self) -> String {
use blake3::hash;
let random_data =
format!("{}{}", Uuid::new_v4(), Utc::now().timestamp_nanos_opt().unwrap_or(0));
hash(random_data.as_bytes()).to_hex().to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_password_hashing() {
let auth = AuthService::new("test_secret".to_string());
let password = "test_password_123";
let hash = auth.hash_password(password).unwrap();
assert!(auth.verify_password(password, &hash).unwrap());
assert!(!auth.verify_password("wrong_password", &hash).unwrap());
}
#[test]
fn test_token_generation() {
let auth = AuthService::new("test_secret".to_string());
let user =
User::new("testuser".to_string(), "test@example.com".to_string(), "hash".to_string());
let token = auth.generate_token(&user).unwrap();
assert_eq!(token.token_type, "Bearer");
assert!(!token.access_token.is_empty());
}
#[test]
fn test_token_verification() {
let auth = AuthService::new("test_secret".to_string());
let user =
User::new("testuser".to_string(), "test@example.com".to_string(), "hash".to_string());
let token = auth.generate_token(&user).unwrap();
let claims = auth.verify_token(&token.access_token).unwrap();
assert_eq!(claims.username, "testuser");
assert!(!claims.is_expired());
}
#[test]
fn test_session_creation() {
let auth = AuthService::new("test_secret".to_string());
let user =
User::new("testuser".to_string(), "test@example.com".to_string(), "hash".to_string());
let token = auth.generate_token(&user).unwrap();
let session = auth.create_session(&token.access_token).unwrap();
assert_eq!(session.username, "testuser");
}
#[test]
fn test_invitation_token_generation() {
let auth = AuthService::new("test_secret".to_string());
let token1 = auth.generate_invitation_token();
let token2 = auth.generate_invitation_token();
assert!(!token1.is_empty());
assert!(!token2.is_empty());
assert_ne!(token1, token2); }
}