veza/veza-stream-server/src/auth/mod.rs
senke 31c9f2af0c feat: global update including storybook setup and backend fixes
- Web: Setup Storybook, added addons, configured Tailwind, added stories for UI components.
- Backend: Updated API router, database, workers, and auth in common.
- Stream Server: Removed SQLx queries and updated auth.
- Docs & Scripts: Updated documentation and recovery scripts.
2026-02-02 19:34:14 +01:00

599 lines
19 KiB
Rust

use axum::{
extract::{Request, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::Response,
Json,
};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
// Note: Use tracing::debug! macro directly instead of importing
use crate::config::Config;
pub mod token_validator;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // Subject (user ID) - Changed to String for UUID
pub username: String, // Username
pub email: Option<String>,
pub roles: Vec<Role>,
pub permissions: Vec<Permission>,
pub exp: u64, // Expiration time
pub iat: u64, // Issued at
pub iss: String, // Issuer
pub aud: String, // Audience
pub session_id: String, // Session ID pour la révocation
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Role {
Admin,
Moderator,
User,
Premium,
Artist,
Guest,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Permission {
// Permissions de streaming
StreamAudio,
StreamHighQuality,
StreamUnlimited,
// Permissions de contenu
UploadAudio,
DeleteAudio,
ModifyMetadata,
// Permissions administratives
ViewAnalytics,
ManageUsers,
SystemAdmin,
// Permissions sociales
CreatePlaylists,
ShareContent,
Comment,
Like,
// Permissions avancées
AccessAPI,
ManageSubscriptions,
ViewReports,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
pub remember_me: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginResponse {
pub access_token: String,
pub refresh_token: String,
pub token_type: String,
pub expires_in: u64,
pub user_info: UserInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
pub id: String, // Changed to String for UUID
pub username: String,
pub email: Option<String>,
pub roles: Vec<Role>,
pub permissions: Vec<Permission>,
pub subscription_tier: SubscriptionTier,
pub created_at: u64,
pub last_login: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SubscriptionTier {
Free,
Premium,
Artist,
Enterprise,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefreshTokenRequest {
pub refresh_token: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenValidationResult {
pub valid: bool,
pub claims: Option<Claims>,
pub error: Option<String>,
}
pub struct AuthManager {
config: Arc<Config>,
encoding_key: EncodingKey,
decoding_key: DecodingKey,
validation: Validation,
revoked_tokens: Arc<tokio::sync::RwLock<HashMap<String, u64>>>, // session_id -> revocation_time
}
impl AuthManager {
pub fn new(config: Arc<Config>) -> Result<Self, AuthError> {
let jwt_secret =
config
.security
.jwt_secret
.as_ref()
.ok_or(AuthError::ConfigurationError(
"JWT_SECRET not configured".to_string(),
))?;
let encoding_key = EncodingKey::from_secret(jwt_secret.as_bytes());
let decoding_key = DecodingKey::from_secret(jwt_secret.as_bytes());
let mut validation = Validation::new(Algorithm::HS256);
validation.set_audience(&["veza-services"]);
validation.set_issuer(&["veza-platform"]);
Ok(Self {
config,
encoding_key,
decoding_key,
validation,
revoked_tokens: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
})
}
pub async fn authenticate_user(
&self,
username: &str,
password: &str,
) -> Result<UserInfo, AuthError> {
// Simuler une authentification (à remplacer par votre logique réelle)
if username == "admin" && password == "admin123" {
Ok(UserInfo {
id: "550e8400-e29b-41d4-a716-446655440001".to_string(), // UUID
username: username.to_string(),
email: Some("admin@example.com".to_string()),
roles: vec![Role::Admin],
permissions: vec![
Permission::StreamAudio,
Permission::StreamHighQuality,
Permission::StreamUnlimited,
Permission::ViewAnalytics,
Permission::ManageUsers,
Permission::SystemAdmin,
Permission::AccessAPI,
],
subscription_tier: SubscriptionTier::Enterprise,
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
last_login: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
})
} else if username == "user" && password == "user123" {
Ok(UserInfo {
id: "550e8400-e29b-41d4-a716-446655440002".to_string(), // UUID
username: username.to_string(),
email: Some("user@example.com".to_string()),
roles: vec![Role::User],
permissions: vec![
Permission::StreamAudio,
Permission::CreatePlaylists,
Permission::ShareContent,
Permission::Comment,
Permission::Like,
],
subscription_tier: SubscriptionTier::Free,
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
.saturating_sub(86400),
last_login: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
})
} else {
Err(AuthError::InvalidCredentials)
}
}
pub async fn generate_tokens(
&self,
user_info: &UserInfo,
remember_me: bool,
) -> Result<(String, String), AuthError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let session_id = uuid::Uuid::new_v4().to_string();
// Durée d'expiration selon remember_me
let expires_in = if remember_me {
self.config.security.jwt_expiration.as_secs() * 7 // 7 fois plus long
} else {
self.config.security.jwt_expiration.as_secs()
};
let claims = Claims {
sub: user_info.id.clone(), // Changed to String
username: user_info.username.clone(),
email: user_info.email.clone(),
roles: user_info.roles.clone(),
permissions: user_info.permissions.clone(),
exp: now + expires_in,
iat: now,
iss: "stream_server".to_string(),
aud: "stream_server".to_string(),
session_id: session_id.clone(),
};
let access_token = encode(&Header::default(), &claims, &self.encoding_key)
.map_err(|e| AuthError::TokenGenerationError(e.to_string()))?;
// Refresh token avec une durée plus longue
let refresh_claims = Claims {
exp: now + (expires_in * 2), // 2x plus long que l'access token
..claims.clone()
};
let refresh_token = encode(&Header::default(), &refresh_claims, &self.encoding_key)
.map_err(|e| AuthError::TokenGenerationError(e.to_string()))?;
Ok((access_token, refresh_token))
}
pub async fn validate_token(&self, token: &str) -> TokenValidationResult {
match decode::<Claims>(token, &self.decoding_key, &self.validation) {
Ok(token_data) => {
let claims = token_data.claims;
// Vérifier si le token est révoqué
let revoked_tokens = self.revoked_tokens.read().await;
if revoked_tokens.contains_key(&claims.session_id) {
return TokenValidationResult {
valid: false,
claims: None,
error: Some("Token has been revoked".to_string()),
};
}
// Vérifier l'expiration
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if claims.exp < now {
return TokenValidationResult {
valid: false,
claims: None,
error: Some("Token has expired".to_string()),
};
}
TokenValidationResult {
valid: true,
claims: Some(claims),
error: None,
}
}
Err(e) => TokenValidationResult {
valid: false,
claims: None,
error: Some(e.to_string()),
},
}
}
pub async fn refresh_token(&self, refresh_token: &str) -> Result<(String, String), AuthError> {
let validation_result = self.validate_token(refresh_token).await;
if !validation_result.valid {
return Err(AuthError::InvalidToken(
validation_result
.error
.unwrap_or("Invalid refresh token".to_string()),
));
}
let claims = validation_result.claims.ok_or(AuthError::InvalidToken("No claims in token".to_string()))?;
// Créer un nouveau UserInfo à partir des claims
let user_info = UserInfo {
id: claims.sub,
username: claims.username,
email: claims.email,
roles: claims.roles,
permissions: claims.permissions,
subscription_tier: SubscriptionTier::Free, // À déterminer selon la logique métier
created_at: claims.iat,
last_login: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
};
// Révoquer l'ancien token
self.revoke_token(&claims.session_id).await;
// Générer de nouveaux tokens
self.generate_tokens(&user_info, false).await
}
pub async fn revoke_token(&self, session_id: &str) {
let mut revoked_tokens = self.revoked_tokens.write().await;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
revoked_tokens.insert(session_id.to_string(), now);
// Nettoyer les anciens tokens révoqués (plus de 24h)
let cutoff = now - (24 * 3600);
revoked_tokens.retain(|_, &mut revocation_time| revocation_time > cutoff);
}
pub fn has_permission(&self, claims: &Claims, required_permission: Permission) -> bool {
claims.permissions.contains(&required_permission)
}
pub fn has_role(&self, claims: &Claims, required_role: Role) -> bool {
claims.roles.contains(&required_role)
}
pub fn has_any_role(&self, claims: &Claims, required_roles: &[Role]) -> bool {
claims
.roles
.iter()
.any(|role| required_roles.contains(role))
}
pub async fn login(&self, request: LoginRequest) -> Result<LoginResponse, AuthError> {
let user_info = self
.authenticate_user(&request.username, &request.password)
.await?;
let (access_token, refresh_token) = self
.generate_tokens(&user_info, request.remember_me.unwrap_or(false))
.await?;
Ok(LoginResponse {
access_token,
refresh_token,
token_type: "Bearer".to_string(),
expires_in: self.config.security.jwt_expiration.as_secs(),
user_info,
})
}
}
impl Clone for AuthManager {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
encoding_key: self.encoding_key.clone(),
decoding_key: self.decoding_key.clone(),
validation: self.validation.clone(),
revoked_tokens: self.revoked_tokens.clone(),
}
}
}
// Middleware d'authentification
pub async fn auth_middleware(
State(auth_manager): State<Arc<AuthManager>>,
mut request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let headers = request.headers();
let token = extract_token_from_headers(headers).ok_or(StatusCode::UNAUTHORIZED)?;
let validation_result = auth_manager.validate_token(&token).await;
if !validation_result.valid {
tracing::warn!("Token validation failed: {:?}", validation_result.error);
return Err(StatusCode::UNAUTHORIZED);
}
let claims = validation_result.claims.ok_or(StatusCode::UNAUTHORIZED)?;
// Ajouter les claims à la requête pour les handlers suivants
request.extensions_mut().insert(claims);
Ok(next.run(request).await)
}
// Middleware de vérification des permissions
pub fn require_permission(
required_permission: Permission,
) -> impl Fn(
Request,
Next,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Response, StatusCode>> + Send>,
> + Clone {
move |request: Request, next: Next| {
let required_permission = required_permission.clone();
Box::pin(async move {
let claims = request
.extensions()
.get::<Claims>()
.ok_or(StatusCode::UNAUTHORIZED)?;
if !claims.permissions.contains(&required_permission) {
tracing::warn!(
"User {} lacks required permission: {:?}",
claims.username,
required_permission
);
return Err(StatusCode::FORBIDDEN);
}
Ok(next.run(request).await)
})
}
}
// Middleware de vérification des rôles
pub fn require_role(
required_role: Role,
) -> impl Fn(
Request,
Next,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Response, StatusCode>> + Send>,
> + Clone {
move |request: Request, next: Next| {
let required_role = required_role.clone();
Box::pin(async move {
let claims = request
.extensions()
.get::<Claims>()
.ok_or(StatusCode::UNAUTHORIZED)?;
if !claims.roles.contains(&required_role) {
tracing::warn!(
"User {} lacks required role: {:?}",
claims.username,
required_role
);
return Err(StatusCode::FORBIDDEN);
}
Ok(next.run(request).await)
})
}
}
fn extract_token_from_headers(headers: &HeaderMap) -> Option<String> {
let auth_header = headers.get("Authorization")?;
let auth_str = auth_header.to_str().ok()?;
if auth_str.starts_with("Bearer ") {
Some(auth_str[7..].to_string())
} else {
None
}
}
// Handlers pour les routes d'authentification
pub async fn login_handler(
State(auth_manager): State<Arc<AuthManager>>,
Json(request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, (StatusCode, String)> {
match auth_manager.login(request).await {
Ok(response) => Ok(Json(response)),
Err(e) => {
tracing::error!("Login failed: {:?}", e);
match e {
AuthError::InvalidCredentials => {
Err((StatusCode::UNAUTHORIZED, "Invalid credentials".to_string()))
}
AuthError::TokenGenerationError(msg) => {
Err((StatusCode::INTERNAL_SERVER_ERROR, msg))
}
_ => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Authentication failed".to_string(),
)),
}
}
}
}
pub async fn refresh_handler(
State(auth_manager): State<Arc<AuthManager>>,
Json(request): Json<RefreshTokenRequest>,
) -> Result<Json<LoginResponse>, (StatusCode, String)> {
match auth_manager.refresh_token(&request.refresh_token).await {
Ok((access_token, refresh_token)) => {
// Valider le refresh token pour récupérer les claims
let validation_result = auth_manager.validate_token(&request.refresh_token).await;
let claims = validation_result.claims.ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Failed to extract claims".to_string()))?;
// Créer UserInfo à partir des claims avec i64 aligned
let user_info = UserInfo {
id: claims.sub, // ALIGNED WITH i64 - use claims.sub directly
username: claims.username.clone(),
email: claims.email.clone(),
roles: claims.roles.clone(),
permissions: claims.permissions.clone(),
subscription_tier: SubscriptionTier::Free,
created_at: claims.iat,
last_login: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
};
let response = LoginResponse {
access_token,
refresh_token,
token_type: "Bearer".to_string(),
expires_in: auth_manager.config.security.jwt_expiration.as_secs(),
user_info,
};
Ok(Json(response))
}
Err(e) => {
tracing::error!("Token refresh failed: {:?}", e);
Err((StatusCode::UNAUTHORIZED, "Token refresh failed".to_string()))
}
}
}
pub async fn logout_handler(
State(auth_manager): State<Arc<AuthManager>>,
request: Request,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
if let Some(claims) = request.extensions().get::<Claims>() {
auth_manager.revoke_token(&claims.session_id).await;
tracing::debug!("User {} logged out", claims.username);
}
Ok(Json(serde_json::json!({
"message": "Successfully logged out"
})))
}
pub async fn user_info_handler(request: Request) -> Result<Json<Claims>, StatusCode> {
let claims = request
.extensions()
.get::<Claims>()
.ok_or(StatusCode::UNAUTHORIZED)?;
Ok(Json(claims.clone()))
}
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Token generation error: {0}")]
TokenGenerationError(String),
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Database error: {0}")]
DatabaseError(String),
}