Jesús Pérez 09a97ac8f5
chore: update platform submodule to monorepo crates structure
Platform restructured into crates/, added AI service and detector,
       migrated control-center-ui to Leptos 0.8
2026-01-08 21:32:59 +00:00

287 lines
8.0 KiB
Rust

use std::sync::Arc;
use axum::{
extract::{Request, State},
response::Json,
};
use serde::{Deserialize, Serialize};
use tracing::info;
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;
/// Login endpoint
pub async fn login(
State(app_state): State<Arc<AppState>>,
Json(request): Json<LoginRequest>,
) -> Result<Json<ApiResponse<TokenResponse>>> {
// Extract client info from headers (simplified - you might want more
// sophisticated detection)
let client_info = Some(ClientInfo {
user_agent: None, // Could extract from headers
ip_address: None, // Could extract from connection info
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
let user = app_state
.user_service
.get_by_id(user_context.user_id)
.await?;
// Verify current password
if !app_state
.auth_service
.verify_password(&password_request.current_password, &user.password_hash)?
{
return Err(ControlCenterError::Auth(auth::AuthError::Authentication(
"Current password is incorrect".to_string(),
)));
}
// 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())
);
let error_response = ApiResponse::<()>::error("Something went wrong".to_string());
assert!(!error_response.success);
assert!(error_response.data.is_none());
assert_eq!(
error_response.message,
Some("Something went wrong".to_string())
);
}
}