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 api_key_id: Option<Uuid>,
pub raw_api_key: Option<String>,
pub is_system_admin: Option<bool>,
pub email_verified: Option<bool>,
}
pub fn get_default_org_context(
memberships: &[MembershipEntity],
is_system_admin: bool,
email_verified: 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,
email_verified: Some(email_verified),
};
}
TokenContext {
is_system_admin: admin_flag,
email_verified: Some(email_verified),
..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,
api_key_id: Some(api_key_entity.id),
raw_api_key: Some(api_key.to_string()),
is_system_admin: if user.is_system_admin { Some(true) } else { None },
email_verified: Some(user.email_verified),
})
}
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,
api_key_id: None,
raw_api_key: None,
is_system_admin: claims.is_system_admin,
email_verified: claims.email_verified,
})
}
#[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,
api_key_id: None,
raw_api_key: None,
is_system_admin: None,
email_verified: None,
};
assert!(!user.is_api_key_auth);
assert!(user.session_id.is_some());
}
#[test]
fn test_authenticated_user_api_key_auth() {
let key_id = Uuid::new_v4();
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,
api_key_id: Some(key_id),
raw_api_key: Some("ck_test123".to_string()),
is_system_admin: None,
email_verified: None,
};
assert!(user.is_api_key_auth);
assert!(user.session_id.is_none());
assert_eq!(user.api_key_id, Some(key_id));
assert!(user.raw_api_key.is_some());
}
#[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, true);
assert_eq!(context.org_id, Some(org_id));
assert_eq!(context.role.as_deref(), Some("member"));
assert_eq!(context.is_system_admin, None);
assert_eq!(context.email_verified, Some(true));
let admin_context = get_default_org_context(&memberships, true, false);
assert_eq!(admin_context.is_system_admin, Some(true));
assert_eq!(admin_context.email_verified, Some(false));
}
#[test]
fn test_get_default_org_context_empty_memberships() {
let context = get_default_org_context(&[], false, false);
assert_eq!(context.org_id, None);
assert_eq!(context.role, None);
}
}