use axum::{
extract::State,
response::Json,
routing::{get, post},
Router,
};
use super::{
config::AuthConfig,
jwt::{create_token_pair, verify_refresh_token},
models::*,
extractors::AuthUser,
};
use crate::error::ApiError;
use crate::extractors::ValidatedJson;
#[async_trait::async_trait]
pub trait UserStore: Send + Sync + 'static {
async fn find_by_email(&self, email: &str) -> Result<Option<StoredUser>, ApiError>;
async fn find_by_id(&self, id: &str) -> Result<Option<StoredUser>, ApiError>;
async fn create(&self, user: CreateUserData) -> Result<StoredUser, ApiError>;
async fn update_password(&self, id: &str, password_hash: &str) -> Result<(), ApiError>;
async fn email_exists(&self, email: &str) -> Result<bool, ApiError>;
}
#[derive(Debug, Clone)]
pub struct StoredUser {
pub id: String,
pub email: String,
pub name: String,
pub password_hash: String,
pub roles: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CreateUserData {
pub email: String,
pub name: String,
pub password_hash: String,
}
#[derive(Clone, Default)]
pub struct InMemoryUserStore {
users: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, StoredUser>>>,
}
impl InMemoryUserStore {
pub fn new() -> Self {
Self::default()
}
}
#[async_trait::async_trait]
impl UserStore for InMemoryUserStore {
async fn find_by_email(&self, email: &str) -> Result<Option<StoredUser>, ApiError> {
let users = self.users.lock().unwrap();
Ok(users.values().find(|u| u.email == email).cloned())
}
async fn find_by_id(&self, id: &str) -> Result<Option<StoredUser>, ApiError> {
let users = self.users.lock().unwrap();
Ok(users.get(id).cloned())
}
async fn create(&self, user: CreateUserData) -> Result<StoredUser, ApiError> {
let mut users = self.users.lock().unwrap();
let id = uuid::Uuid::new_v4().to_string();
let stored = StoredUser {
id: id.clone(),
email: user.email,
name: user.name,
password_hash: user.password_hash,
roles: vec!["user".to_string()],
};
users.insert(id, stored.clone());
Ok(stored)
}
async fn update_password(&self, id: &str, password_hash: &str) -> Result<(), ApiError> {
let mut users = self.users.lock().unwrap();
if let Some(user) = users.get_mut(id) {
user.password_hash = password_hash.to_string();
Ok(())
} else {
Err(ApiError::NotFound("User not found".to_string()))
}
}
async fn email_exists(&self, email: &str) -> Result<bool, ApiError> {
let users = self.users.lock().unwrap();
Ok(users.values().any(|u| u.email == email))
}
}
#[derive(Clone)]
pub struct AuthAppState<S: UserStore> {
pub config: AuthConfig,
pub user_store: S,
}
pub async fn login<S: UserStore>(
State(state): State<AuthAppState<S>>,
ValidatedJson(payload): ValidatedJson<LoginRequest>,
) -> Result<Json<AuthResponse>, ApiError> {
let user = state
.user_store
.find_by_email(&payload.email)
.await?
.ok_or_else(|| ApiError::Unauthorized)?;
let password_valid = super::password::verify_password(&payload.password, &user.password_hash)?;
if !password_valid {
return Err(ApiError::Unauthorized);
}
let token_pair = create_token_pair(&user.id, &user.email, user.roles.clone(), &state.config)?;
Ok(Json(AuthResponse {
access_token: token_pair.access_token,
refresh_token: token_pair.refresh_token,
token_type: token_pair.token_type,
expires_in: token_pair.expires_in,
user: AuthUserInfo {
id: user.id,
email: user.email,
name: user.name,
roles: user.roles,
},
}))
}
pub async fn register<S: UserStore>(
State(state): State<AuthAppState<S>>,
ValidatedJson(payload): ValidatedJson<RegisterRequest>,
) -> Result<Json<AuthResponse>, ApiError> {
super::password::validate_password_strength(&payload.password)?;
if state.user_store.email_exists(&payload.email).await? {
return Err(ApiError::BadRequest("Email already registered".to_string()));
}
let password_hash = super::password::hash_password(&payload.password, &state.config)?;
let user = state
.user_store
.create(CreateUserData {
email: payload.email,
name: payload.name,
password_hash,
})
.await?;
let token_pair = create_token_pair(&user.id, &user.email, user.roles.clone(), &state.config)?;
tracing::info!(user_id = %user.id, "New user registered");
Ok(Json(AuthResponse {
access_token: token_pair.access_token,
refresh_token: token_pair.refresh_token,
token_type: token_pair.token_type,
expires_in: token_pair.expires_in,
user: AuthUserInfo {
id: user.id,
email: user.email,
name: user.name,
roles: user.roles,
},
}))
}
pub async fn refresh_token<S: UserStore>(
State(state): State<AuthAppState<S>>,
ValidatedJson(payload): ValidatedJson<TokenRefreshRequest>,
) -> Result<Json<AuthResponse>, ApiError> {
let claims = verify_refresh_token(&payload.refresh_token, &state.config)?;
let user = state
.user_store
.find_by_id(&claims.sub)
.await?
.ok_or_else(|| ApiError::Unauthorized)?;
let token_pair = create_token_pair(&user.id, &user.email, user.roles.clone(), &state.config)?;
Ok(Json(AuthResponse {
access_token: token_pair.access_token,
refresh_token: token_pair.refresh_token,
token_type: token_pair.token_type,
expires_in: token_pair.expires_in,
user: AuthUserInfo {
id: user.id,
email: user.email,
name: user.name,
roles: user.roles,
},
}))
}
pub async fn logout() -> Json<MessageResponse> {
Json(MessageResponse::new("Successfully logged out"))
}
pub async fn me<S: UserStore>(
user: AuthUser,
State(state): State<AuthAppState<S>>,
) -> Result<Json<AuthUserInfo>, ApiError> {
let stored_user = state
.user_store
.find_by_id(&user.id)
.await?
.ok_or_else(|| ApiError::NotFound("User not found".to_string()))?;
Ok(Json(AuthUserInfo {
id: stored_user.id,
email: stored_user.email,
name: stored_user.name,
roles: stored_user.roles,
}))
}
pub fn auth_routes_with_store<S: UserStore + Clone>(
config: AuthConfig,
user_store: S,
) -> Router {
let state = AuthAppState {
config: config.clone(),
user_store,
};
Router::new()
.route("/auth/login", post(login::<S>))
.route("/auth/register", post(register::<S>))
.route("/auth/refresh", post(refresh_token::<S>))
.route("/auth/logout", post(logout))
.route("/auth/me", get(me::<S>))
.with_state(state)
}
pub fn auth_routes(config: AuthConfig) -> Router {
auth_routes_with_store(config, InMemoryUserStore::new())
}