use axum::{ extract::State, http::{header, StatusCode}, response::{IntoResponse, Json}, routing::{get, post}, Router, }; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use crate::shared::state::AppState; const BOOTSTRAP_SECRET_ENV: &str = "GB_BOOTSTRAP_SECRET"; #[derive(Debug, Deserialize)] pub struct LoginRequest { pub email: String, pub password: String, pub remember: Option, } #[derive(Debug, Serialize)] pub struct LoginResponse { pub success: bool, pub user_id: Option, pub session_id: Option, pub access_token: Option, pub refresh_token: Option, pub expires_in: Option, pub requires_2fa: bool, pub session_token: Option, pub redirect: Option, pub message: Option, } #[derive(Debug, Serialize)] pub struct CurrentUserResponse { pub id: String, pub username: String, pub email: Option, pub first_name: Option, pub last_name: Option, pub display_name: Option, pub roles: Vec, pub organization_id: Option, pub avatar_url: Option, } #[derive(Debug, Serialize)] pub struct ErrorResponse { pub error: String, pub details: Option, } #[derive(Debug, Serialize)] pub struct LogoutResponse { pub success: bool, pub message: String, } #[derive(Debug, Deserialize)] pub struct TwoFactorRequest { pub session_token: String, pub code: String, pub trust_device: Option, } #[derive(Debug, Deserialize)] pub struct RefreshTokenRequest { pub refresh_token: String, } #[derive(Debug, Deserialize)] pub struct BootstrapAdminRequest { pub bootstrap_secret: String, pub email: String, pub username: String, pub password: String, pub first_name: String, pub last_name: String, pub organization_name: Option, } #[derive(Debug, Serialize)] pub struct BootstrapResponse { pub success: bool, pub message: String, pub user_id: Option, pub organization_id: Option, } pub fn configure() -> Router> { Router::new() .route("/api/auth/login", post(login)) .route("/api/auth/logout", post(logout)) .route("/api/auth/me", get(get_current_user)) .route("/api/auth/refresh", post(refresh_token)) .route("/api/auth/2fa/verify", post(verify_2fa)) .route("/api/auth/2fa/resend", post(resend_2fa)) .route("/api/auth/bootstrap", post(bootstrap_admin)) } pub async fn login( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { info!("Login attempt for: {}", req.email); let client = { let auth_service = state.auth_service.lock().await; auth_service.client().clone() }; let http_client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .map_err(|e| { error!("Failed to create HTTP client: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Internal server error".to_string(), details: None, }), ) })?; let pat_path = std::path::Path::new("./botserver-stack/conf/directory/admin-pat.txt"); let admin_token = std::fs::read_to_string(pat_path) .map(|s| s.trim().to_string()) .unwrap_or_default(); if admin_token.is_empty() { error!("Admin PAT token not found"); return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Authentication service not configured".to_string(), details: None, }), )); } let search_url = format!("{}/v2/users", client.api_url()); let search_body = serde_json::json!({ "queries": [{ "emailQuery": { "emailAddress": req.email, "method": "TEXT_QUERY_METHOD_EQUALS" } }] }); let user_response = http_client .post(&search_url) .bearer_auth(&admin_token) .json(&search_body) .send() .await .map_err(|e| { error!("Failed to search user: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Authentication service error".to_string(), details: None, }), ) })?; if !user_response.status().is_success() { let error_text = user_response.text().await.unwrap_or_default(); error!("User search failed: {}", error_text); return Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Invalid email or password".to_string(), details: None, }), )); } let user_data: serde_json::Value = user_response.json().await.map_err(|e| { error!("Failed to parse user response: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Authentication service error".to_string(), details: None, }), ) })?; let user_id = user_data .get("result") .and_then(|r| r.as_array()) .and_then(|arr| arr.first()) .and_then(|u| u.get("userId")) .and_then(|id| id.as_str()) .map(String::from); let user_id = match user_id { Some(id) => id, None => { error!("User not found: {}", req.email); return Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Invalid email or password".to_string(), details: None, }), )); } }; let session_url = format!("{}/v2/sessions", client.api_url()); let session_body = serde_json::json!({ "checks": { "user": { "userId": user_id }, "password": { "password": req.password } } }); let session_response = http_client .post(&session_url) .bearer_auth(&admin_token) .json(&session_body) .send() .await .map_err(|e| { error!("Failed to create session: {}", e); ( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Authentication failed".to_string(), details: None, }), ) })?; if !session_response.status().is_success() { let status = session_response.status(); let error_text = session_response.text().await.unwrap_or_default(); error!("Session creation failed: {} - {}", status, error_text); if error_text.contains("password") || error_text.contains("invalid") { return Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Invalid email or password".to_string(), details: None, }), )); } return Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Authentication failed".to_string(), details: None, }), )); } let session_data: serde_json::Value = session_response.json().await.map_err(|e| { error!("Failed to parse session response: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Invalid response from authentication server".to_string(), details: None, }), ) })?; let session_id = session_data .get("sessionId") .and_then(|s| s.as_str()) .map(String::from); let session_token = session_data .get("sessionToken") .and_then(|s| s.as_str()) .map(String::from); info!("Login successful for: {} (user_id: {})", req.email, user_id); Ok(Json(LoginResponse { success: true, user_id: Some(user_id), session_id: session_id.clone(), access_token: session_id, refresh_token: None, expires_in: Some(3600), requires_2fa: false, session_token, redirect: Some("/".to_string()), message: Some("Login successful".to_string()), })) } pub async fn logout( State(_state): State>, headers: axum::http::HeaderMap, ) -> Result, (StatusCode, Json)> { let token = headers .get(header::AUTHORIZATION) .and_then(|v| v.to_str().ok()) .and_then(|auth| auth.strip_prefix("Bearer ")) .map(String::from); if let Some(ref _token) = token { info!("User logged out"); } Ok(Json(LogoutResponse { success: true, message: "Logged out successfully".to_string(), })) } pub async fn get_current_user( State(state): State>, headers: axum::http::HeaderMap, ) -> Result, (StatusCode, Json)> { let session_token = headers .get(header::AUTHORIZATION) .and_then(|v| v.to_str().ok()) .and_then(|auth| auth.strip_prefix("Bearer ")) .ok_or_else(|| { ( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Missing authorization token".to_string(), details: None, }), ) })?; let client = { let auth_service = state.auth_service.lock().await; auth_service.client().clone() }; let pat_path = std::path::Path::new("./botserver-stack/conf/directory/admin-pat.txt"); let admin_token = std::fs::read_to_string(pat_path) .map(|s| s.trim().to_string()) .unwrap_or_default(); if admin_token.is_empty() { error!("Admin PAT token not found for user lookup"); return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Authentication service not configured".to_string(), details: None, }), )); } let http_client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .map_err(|e| { error!("Failed to create HTTP client: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Internal server error".to_string(), details: None, }), ) })?; let session_url = format!("{}/v2/sessions/{}", client.api_url(), session_token); let session_response = http_client .get(&session_url) .bearer_auth(&admin_token) .send() .await .map_err(|e| { error!("Failed to get session: {}", e); ( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Session validation failed".to_string(), details: None, }), ) })?; if !session_response.status().is_success() { let error_text = session_response.text().await.unwrap_or_default(); error!("Session lookup failed: {}", error_text); return Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Invalid or expired session".to_string(), details: None, }), )); } let session_data: serde_json::Value = session_response.json().await.map_err(|e| { error!("Failed to parse session response: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to parse session data".to_string(), details: None, }), ) })?; let user_id = session_data .get("session") .and_then(|s| s.get("factors")) .and_then(|f| f.get("user")) .and_then(|u| u.get("id")) .and_then(|id| id.as_str()) .unwrap_or_default() .to_string(); if user_id.is_empty() { return Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Invalid session - no user found".to_string(), details: None, }), )); } let user_url = format!("{}/v2/users/{}", client.api_url(), user_id); let user_response = http_client .get(&user_url) .bearer_auth(&admin_token) .send() .await .map_err(|e| { error!("Failed to get user: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to fetch user data".to_string(), details: None, }), ) })?; if !user_response.status().is_success() { let error_text = user_response.text().await.unwrap_or_default(); error!("User lookup failed: {}", error_text); return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to fetch user data".to_string(), details: None, }), )); } let user_data: serde_json::Value = user_response.json().await.map_err(|e| { error!("Failed to parse user response: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to parse user data".to_string(), details: None, }), ) })?; let user = user_data.get("user").unwrap_or(&user_data); let human = user.get("human"); let username = user .get("userName") .and_then(|v| v.as_str()) .unwrap_or_default() .to_string(); let email = human .and_then(|h| h.get("email")) .and_then(|e| e.get("email")) .and_then(|v| v.as_str()) .map(String::from); let first_name = human .and_then(|h| h.get("profile")) .and_then(|p| p.get("givenName")) .and_then(|v| v.as_str()) .map(String::from); let last_name = human .and_then(|h| h.get("profile")) .and_then(|p| p.get("familyName")) .and_then(|v| v.as_str()) .map(String::from); let display_name = human .and_then(|h| h.get("profile")) .and_then(|p| p.get("displayName")) .and_then(|v| v.as_str()) .map(String::from); let organization_id = user .get("details") .and_then(|d| d.get("resourceOwner")) .and_then(|v| v.as_str()) .map(String::from); info!("User profile loaded for: {} ({})", username, user_id); Ok(Json(CurrentUserResponse { id: user_id, username, email, first_name, last_name, display_name, roles: vec!["admin".to_string()], organization_id, avatar_url: None, })) } pub async fn refresh_token( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let client = { let auth_service = state.auth_service.lock().await; auth_service.client().clone() }; let token_url = format!("{}/oauth/v2/token", client.api_url()); let http_client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .map_err(|e| { error!("Failed to create HTTP client: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Internal server error".to_string(), details: None, }), ) })?; let params = [ ("grant_type", "refresh_token"), ("refresh_token", &req.refresh_token), ("scope", "openid profile email offline_access"), ]; let response = http_client .post(&token_url) .form(¶ms) .send() .await .map_err(|e| { error!("Failed to refresh token: {}", e); ( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Token refresh failed".to_string(), details: None, }), ) })?; if !response.status().is_success() { return Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Invalid or expired refresh token".to_string(), details: None, }), )); } let token_data: serde_json::Value = response.json().await.map_err(|e| { error!("Failed to parse token response: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Invalid response from authentication server".to_string(), details: None, }), ) })?; let access_token = token_data .get("access_token") .and_then(|t| t.as_str()) .map(String::from); let refresh_token = token_data .get("refresh_token") .and_then(|t| t.as_str()) .map(String::from); let expires_in = token_data.get("expires_in").and_then(|t| t.as_i64()); Ok(Json(LoginResponse { success: true, user_id: None, session_id: None, access_token, refresh_token, expires_in, requires_2fa: false, session_token: None, redirect: None, message: Some("Token refreshed successfully".to_string()), })) } pub async fn verify_2fa( State(_state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { info!( "2FA verification attempt for session: {}", req.session_token ); Err(( StatusCode::NOT_IMPLEMENTED, Json(ErrorResponse { error: "2FA verification not yet implemented".to_string(), details: Some("This feature will be available in a future update".to_string()), }), )) } pub async fn resend_2fa( State(_state): State>, Json(_req): Json, ) -> impl IntoResponse { ( StatusCode::NOT_IMPLEMENTED, Json(ErrorResponse { error: "2FA resend not yet implemented".to_string(), details: Some("This feature will be available in a future update".to_string()), }), ) } pub async fn bootstrap_admin( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { info!("Bootstrap admin request received"); let expected_secret = std::env::var(BOOTSTRAP_SECRET_ENV).unwrap_or_default(); if expected_secret.is_empty() { warn!("Bootstrap endpoint called but GB_BOOTSTRAP_SECRET not set"); return Err(( StatusCode::FORBIDDEN, Json(ErrorResponse { error: "Bootstrap not enabled".to_string(), details: Some("Set GB_BOOTSTRAP_SECRET environment variable to enable bootstrap".to_string()), }), )); } if req.bootstrap_secret != expected_secret { warn!("Bootstrap attempt with invalid secret"); return Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "Invalid bootstrap secret".to_string(), details: None, }), )); } let client = { let auth_service = state.auth_service.lock().await; auth_service.client().clone() }; let existing_users = client.list_users(1, 0).await.unwrap_or_default(); if !existing_users.is_empty() { let has_admin = existing_users.iter().any(|u| { u.get("roles") .and_then(|r| r.as_array()) .map(|roles| { roles.iter().any(|r| { r.as_str() .map(|s| s.to_lowercase().contains("admin")) .unwrap_or(false) }) }) .unwrap_or(false) }); if has_admin { return Err(( StatusCode::CONFLICT, Json(ErrorResponse { error: "Admin user already exists".to_string(), details: Some("Bootstrap can only be used for initial setup".to_string()), }), )); } } let user_id = match client .create_user(&req.email, &req.first_name, &req.last_name, Some(&req.username)) .await { Ok(id) => { info!("Bootstrap admin user created: {}", id); id } Err(e) => { error!("Failed to create bootstrap admin: {}", e); return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to create admin user".to_string(), details: Some(e.to_string()), }), )); } }; if let Err(e) = set_user_password(&client, &user_id, &req.password).await { error!("Failed to set admin password: {}", e); } let org_name = req.organization_name.unwrap_or_else(|| "Default Organization".to_string()); let org_id = match create_organization(&client, &org_name).await { Ok(id) => { info!("Bootstrap organization created: {}", id); Some(id) } Err(e) => { warn!("Failed to create organization (may already exist): {}", e); None } }; if let Some(ref oid) = org_id { let admin_roles = vec![ "admin".to_string(), "org_owner".to_string(), "user_manager".to_string(), ]; if let Err(e) = client.add_org_member(oid, &user_id, admin_roles).await { error!("Failed to add admin to organization: {}", e); } else { info!("Admin user added to organization with admin roles"); } } info!( "Bootstrap complete: admin user {} created successfully", req.username ); Ok(Json(BootstrapResponse { success: true, message: format!( "Admin user '{}' created successfully. You can now login with your credentials.", req.username ), user_id: Some(user_id), organization_id: org_id, })) } async fn set_user_password( client: &crate::directory::client::ZitadelClient, user_id: &str, password: &str, ) -> Result<(), String> { let url = format!("{}/v2/users/{}/password", client.api_url(), user_id); let body = serde_json::json!({ "newPassword": { "password": password, "changeRequired": false } }); let response = client .http_post(url) .await .json(&body) .send() .await .map_err(|e| e.to_string())?; if response.status().is_success() { Ok(()) } else { let error_text = response.text().await.unwrap_or_default(); Err(format!("Failed to set password: {}", error_text)) } } async fn create_organization( client: &crate::directory::client::ZitadelClient, name: &str, ) -> Result { let url = format!("{}/v2/organizations", client.api_url()); let body = serde_json::json!({ "name": name }); let response = client .http_post(url) .await .json(&body) .send() .await .map_err(|e| e.to_string())?; if response.status().is_success() { let data: serde_json::Value = response.json().await.map_err(|e| e.to_string())?; let org_id = data .get("organizationId") .or_else(|| data.get("id")) .and_then(|v| v.as_str()) .ok_or_else(|| "No organization ID in response".to_string())? .to_string(); Ok(org_id) } else { let error_text = response.text().await.unwrap_or_default(); Err(format!("Failed to create organization: {}", error_text)) } }