use axum::http::HeaderMap;
use std::sync::Arc;
use uuid::Uuid;
use crate::callback::AuthCallback;
use crate::errors::AppError;
use crate::repositories::{MembershipEntity, API_KEY_PREFIX};
use crate::services::{EmailService, TokenContext};
use crate::AppState;
use super::extract_access_token;
#[derive(Debug, Clone)]
pub struct AuthenticatedUser {
pub user_id: Uuid,
pub session_id: Option<Uuid>,
pub org_id: Option<Uuid>,
pub role: Option<String>,
pub is_api_key_auth: bool,
}
pub fn get_default_org_context(
memberships: &[MembershipEntity],
is_system_admin: bool,
) -> TokenContext {
let admin_flag = if is_system_admin { Some(true) } else { None };
if let Some(membership) = memberships.first() {
return TokenContext {
org_id: Some(membership.org_id),
role: Some(membership.role.as_str().to_string()),
is_system_admin: admin_flag,
};
}
TokenContext {
is_system_admin: admin_flag,
..Default::default()
}
}
pub async fn authenticate<C: AuthCallback, E: EmailService>(
state: &Arc<AppState<C, E>>,
headers: &HeaderMap,
) -> Result<AuthenticatedUser, AppError> {
let token = extract_access_token(headers, &state.config.cookie.access_cookie_name)
.ok_or(AppError::InvalidToken)?;
if token.starts_with(API_KEY_PREFIX) {
authenticate_api_key(state, &token).await
} else {
authenticate_jwt(state, &token).await
}
}
async fn authenticate_api_key<C: AuthCallback, E: EmailService>(
state: &Arc<AppState<C, E>>,
api_key: &str,
) -> Result<AuthenticatedUser, AppError> {
let api_key_entity = state
.api_key_repo
.find_by_key(api_key)
.await?
.ok_or(AppError::InvalidToken)?;
let _ = state.api_key_repo.update_last_used(api_key_entity.id).await;
let user = state
.user_repo
.find_by_id(api_key_entity.user_id)
.await?
.ok_or(AppError::InvalidToken)?;
if state.config.email.require_verification && user.email.is_some() && !user.email_verified {
return Err(AppError::Forbidden("Email not verified".into()));
}
Ok(AuthenticatedUser {
user_id: user.id,
session_id: None,
org_id: None,
role: None,
is_api_key_auth: true,
})
}
async fn authenticate_jwt<C: AuthCallback, E: EmailService>(
state: &Arc<AppState<C, E>>,
token: &str,
) -> Result<AuthenticatedUser, AppError> {
let claims = state.jwt_service.validate_access_token(token)?;
let session = state
.session_repo
.find_by_id(claims.sid)
.await?
.ok_or(AppError::InvalidToken)?;
if session.expires_at <= chrono::Utc::now() {
return Err(AppError::TokenExpired);
}
if session.user_id != claims.sub || session.is_revoked() {
return Err(AppError::InvalidToken);
}
Ok(AuthenticatedUser {
user_id: claims.sub,
session_id: Some(claims.sid),
org_id: claims.org_id,
role: claims.role,
is_api_key_auth: false,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repositories::{MembershipEntity, OrgRole};
use chrono::Utc;
#[test]
fn test_authenticated_user_fields() {
let user = AuthenticatedUser {
user_id: Uuid::new_v4(),
session_id: Some(Uuid::new_v4()),
org_id: Some(Uuid::new_v4()),
role: Some("owner".to_string()),
is_api_key_auth: false,
};
assert!(!user.is_api_key_auth);
assert!(user.session_id.is_some());
}
#[test]
fn test_authenticated_user_api_key_auth() {
let user = AuthenticatedUser {
user_id: Uuid::new_v4(),
session_id: None,
org_id: Some(Uuid::new_v4()),
role: Some("owner".to_string()),
is_api_key_auth: true,
};
assert!(user.is_api_key_auth);
assert!(user.session_id.is_none());
}
#[test]
fn test_get_default_org_context_uses_first_membership() {
let user_id = Uuid::new_v4();
let org_id = Uuid::new_v4();
let memberships = vec![
MembershipEntity {
id: Uuid::new_v4(),
user_id,
org_id,
role: OrgRole::Member,
joined_at: Utc::now(),
},
];
let context = get_default_org_context(&memberships, false);
assert_eq!(context.org_id, Some(org_id));
assert_eq!(context.role.as_deref(), Some("member"));
assert_eq!(context.is_system_admin, None);
let admin_context = get_default_org_context(&memberships, true);
assert_eq!(admin_context.is_system_admin, Some(true));
}
#[test]
fn test_get_default_org_context_empty_memberships() {
let context = get_default_org_context(&[], false);
assert_eq!(context.org_id, None);
assert_eq!(context.role, None);
}
}