use std::sync::Arc; use axum::{ extract::State, routing::{get, post}, Json, Router, }; use crate::db; use crate::middleware::auth::AuthUser; use crate::models::auth::{AuthResponse, LoginRequest, RegisterRequest, UserPublic}; use crate::models::error::{ApiError, ApiResult}; use crate::services::auth as auth_service; use crate::AppState; pub fn router() -> Router> { Router::new() .route("/login", post(login)) .route("/register", post(register)) .route("/verify-totp", post(verify_totp)) .route("/refresh", post(refresh)) .route("/setup-totp", post(setup_totp)) .route("/backup-codes", post(backup_codes)) .route("/logout", post(logout)) .route("/me", get(me)) } async fn login( State(state): State>, Json(body): Json, ) -> ApiResult> { // Look up user by email let user = db::users::get_user_by_email(&state.db, &body.email) .await .map_err(|e| ApiError::Internal(e.to_string()))? .ok_or(ApiError::BadRequest("Invalid email or password".to_string()))?; // Verify password let valid = auth_service::verify_password(&body.password, &user.password_hash) .map_err(|e| ApiError::Internal(e.to_string()))?; if !valid { return Err(ApiError::BadRequest("Invalid email or password".to_string())); } // Check if TOTP is enabled (for now, skip TOTP challenge) let requires_totp = user.totp_enabled; // Look up license for the user let license = db::licenses::get_license_by_user(&state.db, user.id) .await .map_err(|e| ApiError::Internal(e.to_string()))?; let license_id = license.as_ref().map(|l| l.id); let role = None; // TODO: look up role from team_members // Create tokens let access_token = auth_service::create_access_token( &state.config, user.id, &user.email, license_id, role, ) .map_err(|e| ApiError::Internal(e.to_string()))?; let refresh_token = auth_service::create_refresh_token( &state.config, user.id, &user.email, ) .map_err(|e| ApiError::Internal(e.to_string()))?; // Update last login timestamp let _ = db::users::update_last_login(&state.db, user.id).await; Ok(Json(AuthResponse { access_token, refresh_token, requires_totp, user: UserPublic { id: user.id, email: user.email, username: user.username, totp_enabled: user.totp_enabled, email_verified: user.email_verified, }, })) } async fn register( State(state): State>, Json(body): Json, ) -> ApiResult> { // Validate input if body.email.is_empty() || !body.email.contains('@') { return Err(ApiError::BadRequest("Invalid email address".to_string())); } if body.username.len() < 3 { return Err(ApiError::BadRequest("Username must be at least 3 characters".to_string())); } if body.password.len() < 8 { return Err(ApiError::BadRequest("Password must be at least 8 characters".to_string())); } // Verify the license key exists and is unclaimed let license = db::licenses::get_license_by_key(&state.db, &body.license_key) .await .map_err(|e| ApiError::Internal(e.to_string()))? .ok_or(ApiError::BadRequest("Invalid license key".to_string()))?; if license.status != "active" { return Err(ApiError::BadRequest("License is not active".to_string())); } // Hash password let password_hash = auth_service::hash_password(&body.password) .map_err(|e| ApiError::Internal(e.to_string()))?; // Create user let user_id = db::users::create_user(&state.db, &body.email, &body.username, &password_hash) .await .map_err(|e| { if e.to_string().contains("duplicate key") || e.to_string().contains("unique") { ApiError::Conflict("Email or username already exists".to_string()) } else { ApiError::Internal(e.to_string()) } })?; // Create tokens let access_token = auth_service::create_access_token( &state.config, user_id, &body.email, Some(license.id), Some("Owner".to_string()), ) .map_err(|e| ApiError::Internal(e.to_string()))?; let refresh_token = auth_service::create_refresh_token( &state.config, user_id, &body.email, ) .map_err(|e| ApiError::Internal(e.to_string()))?; Ok(Json(AuthResponse { access_token, refresh_token, requires_totp: false, user: UserPublic { id: user_id, email: body.email, username: body.username, totp_enabled: false, email_verified: false, }, })) } #[derive(serde::Deserialize)] struct RefreshRequest { refresh_token: String, } async fn refresh( State(state): State>, Json(body): Json, ) -> ApiResult> { // Validate refresh token let claims = auth_service::validate_token(&state.config, &body.refresh_token) .map_err(|_| ApiError::Unauthorized)?; if claims.token_type != "refresh" { return Err(ApiError::Unauthorized); } // Look up user to ensure they still exist let user = db::users::get_user_by_id(&state.db, claims.sub) .await .map_err(|e| ApiError::Internal(e.to_string()))? .ok_or(ApiError::Unauthorized)?; // Get license let license = db::licenses::get_license_by_user(&state.db, user.id) .await .map_err(|e| ApiError::Internal(e.to_string()))?; let license_id = license.as_ref().map(|l| l.id); // Issue new access token let access_token = auth_service::create_access_token( &state.config, user.id, &user.email, license_id, None, ) .map_err(|e| ApiError::Internal(e.to_string()))?; Ok(Json(serde_json::json!({ "access_token": access_token, }))) } async fn verify_totp() -> ApiResult> { // TODO: Implement TOTP verification Err(ApiError::BadRequest("TOTP not yet implemented".to_string())) } async fn setup_totp() -> ApiResult> { // TODO: Implement TOTP setup Err(ApiError::BadRequest("TOTP setup not yet implemented".to_string())) } async fn backup_codes() -> ApiResult> { // TODO: Implement backup code generation Err(ApiError::BadRequest("Backup codes not yet implemented".to_string())) } /// GET /api/auth/me — returns the authenticated user's profile + license async fn me( auth: AuthUser, State(state): State>, ) -> ApiResult> { let user = db::users::get_user_by_id(&state.db, auth.user_id) .await .map_err(|e| ApiError::Internal(e.to_string()))? .ok_or(ApiError::Unauthorized)?; let license = if let Some(lid) = auth.license_id { db::licenses::get_license_by_id(&state.db, lid) .await .map_err(|e| ApiError::Internal(e.to_string()))? } else { db::licenses::get_license_by_user(&state.db, user.id) .await .map_err(|e| ApiError::Internal(e.to_string()))? }; Ok(Json(serde_json::json!({ "user": UserPublic { id: user.id, email: user.email, username: user.username, totp_enabled: user.totp_enabled, email_verified: user.email_verified, }, "license": license, }))) } async fn logout() -> ApiResult> { // Stateless JWT — logout is handled client-side by discarding tokens. // TODO: Implement token blacklisting if needed. Ok(Json(serde_json::json!({ "message": "Logged out" }))) }