2026-01-08 21:32:59 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2025-10-07 10:59:52 +01:00
|
|
|
use axum::{
|
|
|
|
|
extract::{Request, State},
|
|
|
|
|
response::Json,
|
|
|
|
|
};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use tracing::info;
|
|
|
|
|
|
2026-01-08 21:32:59 +00:00
|
|
|
use crate::error::{auth, ControlCenterError, Result};
|
|
|
|
|
use crate::middleware::RequestExt;
|
|
|
|
|
use crate::models::{ClientInfo, LoginRequest, LogoutRequest, RefreshTokenRequest, TokenResponse};
|
|
|
|
|
use crate::services::AuthService;
|
|
|
|
|
use crate::AppState;
|
|
|
|
|
|
2025-10-07 10:59:52 +01:00
|
|
|
/// Login endpoint
|
|
|
|
|
pub async fn login(
|
|
|
|
|
State(app_state): State<Arc<AppState>>,
|
|
|
|
|
Json(request): Json<LoginRequest>,
|
|
|
|
|
) -> Result<Json<ApiResponse<TokenResponse>>> {
|
2026-01-08 21:32:59 +00:00
|
|
|
// Extract client info from headers (simplified - you might want more
|
|
|
|
|
// sophisticated detection)
|
2025-10-07 10:59:52 +01:00
|
|
|
let client_info = Some(ClientInfo {
|
|
|
|
|
user_agent: None, // Could extract from headers
|
2026-01-08 21:32:59 +00:00
|
|
|
ip_address: None, // Could extract from connection info
|
2025-10-07 10:59:52 +01:00
|
|
|
device_type: None,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let token_response = app_state.auth_service.login(request, client_info).await?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse::success(token_response)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Refresh token endpoint
|
|
|
|
|
pub async fn refresh_token(
|
|
|
|
|
State(app_state): State<Arc<AppState>>,
|
|
|
|
|
Json(request): Json<RefreshTokenRequest>,
|
|
|
|
|
) -> Result<Json<ApiResponse<TokenResponse>>> {
|
|
|
|
|
let token_response = app_state.auth_service.refresh_token(request).await?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse::success(token_response)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Logout endpoint
|
|
|
|
|
pub async fn logout(
|
|
|
|
|
State(app_state): State<Arc<AppState>>,
|
|
|
|
|
Json(logout_request): Json<LogoutRequest>,
|
|
|
|
|
) -> Result<Json<ApiResponse<String>>> {
|
|
|
|
|
// For now, we'll extract user_id from the token in logout_request
|
|
|
|
|
// This would need to be updated to work with proper authentication middleware
|
|
|
|
|
let user_id = uuid::Uuid::new_v4(); // Placeholder - this should come from JWT validation
|
|
|
|
|
|
|
|
|
|
app_state
|
|
|
|
|
.auth_service
|
|
|
|
|
.logout(logout_request, user_id)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse::success(
|
|
|
|
|
"Logged out successfully".to_string(),
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Verify token endpoint
|
|
|
|
|
pub async fn verify_token(
|
|
|
|
|
State(app_state): State<Arc<AppState>>,
|
|
|
|
|
request: Request,
|
|
|
|
|
) -> Result<Json<ApiResponse<TokenVerificationResponse>>> {
|
|
|
|
|
let user_context = request.require_user_context()?;
|
|
|
|
|
|
|
|
|
|
// Get user details
|
|
|
|
|
let user = app_state
|
|
|
|
|
.user_service
|
|
|
|
|
.get_user_response_by_id(user_context.user_id)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let response = TokenVerificationResponse {
|
|
|
|
|
valid: true,
|
|
|
|
|
user,
|
|
|
|
|
session_id: user_context.session_id,
|
|
|
|
|
roles: user_context.roles.clone(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse::success(response)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get current user sessions
|
|
|
|
|
pub async fn get_sessions(
|
|
|
|
|
State(app_state): State<Arc<AppState>>,
|
|
|
|
|
request: Request,
|
|
|
|
|
) -> Result<Json<ApiResponse<Vec<crate::models::SessionResponse>>>> {
|
|
|
|
|
let user_context = request.require_user_context()?;
|
|
|
|
|
|
|
|
|
|
let sessions = app_state
|
|
|
|
|
.auth_service
|
|
|
|
|
.get_user_sessions(user_context.user_id)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let session_responses = sessions
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(crate::models::SessionResponse::from)
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse::success(session_responses)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Invalidate all sessions (force logout everywhere)
|
|
|
|
|
pub async fn invalidate_all_sessions(
|
|
|
|
|
State(app_state): State<Arc<AppState>>,
|
|
|
|
|
request: Request,
|
|
|
|
|
) -> Result<Json<ApiResponse<String>>> {
|
|
|
|
|
let user_context = request.require_user_context()?;
|
|
|
|
|
|
|
|
|
|
let logout_request = LogoutRequest {
|
|
|
|
|
session_id: None,
|
|
|
|
|
all_sessions: Some(true),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
app_state
|
|
|
|
|
.auth_service
|
|
|
|
|
.logout(logout_request, user_context.user_id)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse::success(
|
|
|
|
|
"All sessions invalidated successfully".to_string(),
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Health check endpoint (no auth required)
|
|
|
|
|
pub async fn health_check() -> Json<ApiResponse<HealthCheckResponse>> {
|
|
|
|
|
Json(ApiResponse::success(HealthCheckResponse {
|
|
|
|
|
status: "healthy".to_string(),
|
|
|
|
|
timestamp: chrono::Utc::now(),
|
|
|
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
|
|
|
service: "control-center".to_string(),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Token verification response
|
|
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
pub struct TokenVerificationResponse {
|
|
|
|
|
pub valid: bool,
|
|
|
|
|
pub user: crate::models::UserResponse,
|
|
|
|
|
pub session_id: uuid::Uuid,
|
|
|
|
|
pub roles: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Health check response
|
|
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
pub struct HealthCheckResponse {
|
|
|
|
|
pub status: String,
|
|
|
|
|
pub timestamp: chrono::DateTime<chrono::Utc>,
|
|
|
|
|
pub version: String,
|
|
|
|
|
pub service: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generic API response wrapper
|
|
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
pub struct ApiResponse<T> {
|
|
|
|
|
pub success: bool,
|
|
|
|
|
pub data: Option<T>,
|
|
|
|
|
pub message: Option<String>,
|
|
|
|
|
pub timestamp: chrono::DateTime<chrono::Utc>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> ApiResponse<T> {
|
|
|
|
|
pub fn success(data: T) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
success: true,
|
|
|
|
|
data: Some(data),
|
|
|
|
|
message: None,
|
|
|
|
|
timestamp: chrono::Utc::now(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn success_with_message(data: T, message: String) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
success: true,
|
|
|
|
|
data: Some(data),
|
|
|
|
|
message: Some(message),
|
|
|
|
|
timestamp: chrono::Utc::now(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn error(message: String) -> ApiResponse<()> {
|
|
|
|
|
ApiResponse {
|
|
|
|
|
success: false,
|
|
|
|
|
data: None,
|
|
|
|
|
message: Some(message),
|
|
|
|
|
timestamp: chrono::Utc::now(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Password change request
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct ChangePasswordRequest {
|
|
|
|
|
pub current_password: String,
|
|
|
|
|
pub new_password: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Change password endpoint (authenticated users only)
|
|
|
|
|
pub async fn change_password(
|
|
|
|
|
State(app_state): State<Arc<AppState>>,
|
|
|
|
|
Json(password_request): Json<ChangePasswordRequest>,
|
|
|
|
|
request: Request,
|
|
|
|
|
) -> Result<Json<ApiResponse<String>>> {
|
|
|
|
|
let user_context = request.require_user_context()?;
|
|
|
|
|
|
|
|
|
|
// Get current user
|
2026-01-08 21:32:59 +00:00
|
|
|
let user = app_state
|
|
|
|
|
.user_service
|
|
|
|
|
.get_by_id(user_context.user_id)
|
|
|
|
|
.await?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
// Verify current password
|
|
|
|
|
if !app_state
|
|
|
|
|
.auth_service
|
|
|
|
|
.verify_password(&password_request.current_password, &user.password_hash)?
|
|
|
|
|
{
|
2026-01-08 21:32:59 +00:00
|
|
|
return Err(ControlCenterError::Auth(auth::AuthError::Authentication(
|
2025-10-07 10:59:52 +01:00
|
|
|
"Current password is incorrect".to_string(),
|
2026-01-08 21:32:59 +00:00
|
|
|
)));
|
2025-10-07 10:59:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hash new password
|
|
|
|
|
let new_password_hash = AuthService::hash_password(&password_request.new_password)?;
|
|
|
|
|
|
|
|
|
|
// Update user password
|
|
|
|
|
let update_request = crate::models::UpdateUserRequest {
|
|
|
|
|
password: Some(new_password_hash),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
app_state
|
|
|
|
|
.user_service
|
|
|
|
|
.update_user(user_context.user_id, update_request)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
// Optionally invalidate all other sessions (force re-login)
|
|
|
|
|
let logout_request = LogoutRequest {
|
|
|
|
|
session_id: None,
|
|
|
|
|
all_sessions: Some(true),
|
|
|
|
|
};
|
|
|
|
|
app_state
|
|
|
|
|
.auth_service
|
|
|
|
|
.logout(logout_request, user_context.user_id)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
info!("Password changed for user: {}", user.username);
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse::success(
|
|
|
|
|
"Password changed successfully. Please log in again.".to_string(),
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_api_response_creation() {
|
|
|
|
|
let success_response = ApiResponse::success("test data".to_string());
|
|
|
|
|
assert!(success_response.success);
|
|
|
|
|
assert_eq!(success_response.data, Some("test data".to_string()));
|
|
|
|
|
assert!(success_response.message.is_none());
|
|
|
|
|
|
|
|
|
|
let success_with_message = ApiResponse::success_with_message(
|
|
|
|
|
"test data".to_string(),
|
|
|
|
|
"Operation completed".to_string(),
|
|
|
|
|
);
|
|
|
|
|
assert!(success_with_message.success);
|
|
|
|
|
assert_eq!(success_with_message.data, Some("test data".to_string()));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
success_with_message.message,
|
|
|
|
|
Some("Operation completed".to_string())
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-08 21:32:59 +00:00
|
|
|
let error_response = ApiResponse::<()>::error("Something went wrong".to_string());
|
2025-10-07 10:59:52 +01:00
|
|
|
assert!(!error_response.success);
|
|
|
|
|
assert!(error_response.data.is_none());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
error_response.message,
|
|
|
|
|
Some("Something went wrong".to_string())
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-08 21:32:59 +00:00
|
|
|
}
|