Skip to main content

auth_framework/api/
admin.rs

1//! Administrative API Endpoints
2//!
3//! Handles user management, system configuration, and admin operations.
4//!
5//! # Security Model
6//!
7//! **Every handler in this module must independently verify the caller holds the
8//! `admin` role** via [`verify_admin_role`].  There is no middleware-level admin
9//! guard on these routes — authorization is enforced per-handler so that
10//! non-admin error paths can still return proper 401/403 responses.
11//!
12//! When adding new admin endpoints, always call `verify_admin_role(&auth_token)?`
13//! immediately after token validation.
14
15use crate::api::{
16    ApiResponse, ApiState, extract_bearer_token, responses::Pagination, validate_api_token,
17};
18use crate::tokens::AuthToken;
19use axum::{
20    Json,
21    extract::{Path, Query, State},
22    http::HeaderMap,
23};
24use serde::{Deserialize, Serialize};
25
26// ---------------------------------------------------------------------------
27// Helper: verify the authenticated token carries the admin role
28// ---------------------------------------------------------------------------
29/// Checks that `auth_token` carries the `admin` role. Returns `Ok(())` on
30/// success or an `ApiResponse::forbidden_typed()` error suitable for early
31/// return from any admin handler.
32#[allow(clippy::result_large_err)]
33fn verify_admin_role<T: Serialize>(auth_token: &AuthToken) -> Result<(), ApiResponse<T>> {
34    if auth_token.roles.contains(&"admin".to_string()) {
35        Ok(())
36    } else {
37        Err(ApiResponse::<T>::forbidden_typed())
38    }
39}
40
41// ---------------------------------------------------------------------------
42// Helper: load all user IDs from the global index
43// ---------------------------------------------------------------------------
44async fn load_user_ids(storage: &std::sync::Arc<dyn crate::storage::AuthStorage>) -> Vec<String> {
45    match storage.get_kv("users:index").await {
46        Ok(Some(bytes)) => serde_json::from_slice(&bytes).unwrap_or_default(),
47        _ => vec![],
48    }
49}
50
51// ---------------------------------------------------------------------------
52// Helper: load a single user record by user_id and convert to UserListItem
53// ---------------------------------------------------------------------------
54async fn load_user_item(
55    storage: &std::sync::Arc<dyn crate::storage::AuthStorage>,
56    user_id: &str,
57) -> Option<UserListItem> {
58    let key = format!("user:{}", user_id);
59    let bytes = storage.get_kv(&key).await.ok()??;
60    let data: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
61
62    let username = data["username"].as_str()?.to_string();
63    let email = data["email"].as_str().unwrap_or("").to_string();
64    let roles: Vec<String> = data["roles"]
65        .as_array()
66        .map(|arr| {
67            arr.iter()
68                .filter_map(|v| v.as_str().map(|s| s.to_string()))
69                .collect()
70        })
71        .unwrap_or_else(|| vec!["user".to_string()]);
72    let active = data["active"].as_bool().unwrap_or(true);
73    let created_at = data["created_at"].as_str().unwrap_or("").to_string();
74    let last_login = data["last_login"].as_str().map(|s| s.to_string());
75
76    Some(UserListItem {
77        id: user_id.to_string(),
78        username,
79        email,
80        roles,
81        active,
82        created_at,
83        last_login,
84    })
85}
86
87/// User list item
88#[derive(Debug, Serialize)]
89pub struct UserListItem {
90    pub id: String,
91    pub username: String,
92    pub email: String,
93    pub roles: Vec<String>,
94    pub active: bool,
95    pub created_at: String,
96    pub last_login: Option<String>,
97}
98
99/// User list response
100#[derive(Debug, Serialize)]
101pub struct UserListResponse {
102    pub users: Vec<UserListItem>,
103    pub pagination: Pagination,
104}
105
106/// User list query parameters
107#[derive(Debug, Deserialize)]
108pub struct UserListQuery {
109    #[serde(default = "default_page")]
110    pub page: u32,
111    #[serde(default = "default_limit")]
112    pub limit: u32,
113    #[serde(default)]
114    pub search: Option<String>,
115    #[serde(default)]
116    pub role: Option<String>,
117    #[serde(default)]
118    pub active: Option<bool>,
119}
120
121fn default_page() -> u32 {
122    1
123}
124fn default_limit() -> u32 {
125    20
126}
127
128/// Create user request
129#[derive(Debug, Deserialize)]
130pub struct CreateUserRequest {
131    pub username: String,
132    pub password: String,
133    pub email: String,
134    #[serde(default)]
135    pub first_name: Option<String>,
136    #[serde(default)]
137    pub last_name: Option<String>,
138    #[serde(default)]
139    pub roles: Vec<String>,
140    #[serde(default = "default_active")]
141    pub active: bool,
142}
143
144fn default_active() -> bool {
145    true
146}
147
148/// Update user roles request
149#[derive(Debug, Deserialize)]
150pub struct UpdateUserRolesRequest {
151    pub roles: Vec<String>,
152}
153
154/// System stats response
155#[derive(Debug, Serialize)]
156pub struct SystemStats {
157    pub total_users: u64,
158    pub active_sessions: u64,
159    pub total_tokens: u64,
160    pub failed_logins_24h: u64,
161    pub system_uptime: String,
162    pub memory_usage: String,
163    pub cpu_usage: String,
164}
165
166/// GET /admin/users
167/// List all users (admin only)
168pub async fn list_users(
169    State(state): State<ApiState>,
170    headers: HeaderMap,
171    Query(query): Query<UserListQuery>,
172) -> ApiResponse<UserListResponse> {
173    match extract_bearer_token(&headers) {
174        Some(token) => match validate_api_token(&state.auth_framework, &token).await {
175            Ok(auth_token) => {
176                if let Err(resp) = verify_admin_role::<UserListResponse>(&auth_token) {
177                    return resp;
178                }
179
180                let storage = state.auth_framework.storage();
181                let all_ids = load_user_ids(&storage).await;
182
183                let mut users: Vec<UserListItem> = Vec::new();
184                for id in &all_ids {
185                    if let Some(item) = load_user_item(&storage, id).await {
186                        if let Some(ref search) = query.search {
187                            let s = search.to_lowercase();
188                            if !item.username.to_lowercase().contains(&s)
189                                && !item.email.to_lowercase().contains(&s)
190                            {
191                                continue;
192                            }
193                        }
194                        if let Some(ref role) = query.role
195                            && !item.roles.contains(role)
196                        {
197                            continue;
198                        }
199                        if let Some(filter_active) = query.active
200                            && item.active != filter_active
201                        {
202                            continue;
203                        }
204                        users.push(item);
205                    }
206                }
207
208                let total_users = users.len() as u64;
209                let page = if query.page == 0 { 1 } else { query.page };
210                let limit = if query.limit == 0 {
211                    20
212                } else {
213                    query.limit.min(100)
214                };
215                let offset = ((page - 1) * limit) as usize;
216                let total_pages = ((total_users as f64) / (limit as f64)).ceil() as u32;
217                let total_pages = if total_pages == 0 { 1 } else { total_pages };
218
219                let page_users: Vec<UserListItem> = users
220                    .into_iter()
221                    .skip(offset)
222                    .take(limit as usize)
223                    .collect();
224
225                let pagination = Pagination {
226                    page,
227                    limit,
228                    total: total_users,
229                    pages: total_pages,
230                };
231
232                ApiResponse::success(UserListResponse {
233                    users: page_users,
234                    pagination,
235                })
236            }
237            Err(e) => {
238                let error_response = ApiResponse::<()>::from(e);
239                ApiResponse::<UserListResponse> {
240                    success: error_response.success,
241                    data: None,
242                    error: error_response.error,
243                    message: error_response.message,
244                }
245            }
246        },
247        None => ApiResponse::<UserListResponse>::unauthorized_typed(),
248    }
249}
250
251/// POST /admin/users
252/// Create new user (admin only)
253pub async fn create_user(
254    State(state): State<ApiState>,
255    headers: HeaderMap,
256    Json(req): Json<CreateUserRequest>,
257) -> ApiResponse<UserListItem> {
258    // Validate input
259    if req.username.is_empty() || req.password.is_empty() || req.email.is_empty() {
260        return ApiResponse::<UserListItem>::validation_error_typed(
261            "Username, password, and email are required",
262        );
263    }
264
265    // Validate username format — same rules as the public registration endpoint.
266    if crate::utils::validation::validate_username(&req.username).is_err() {
267        return ApiResponse::<UserListItem>::validation_error_typed(
268            "Invalid username: must be 3-50 characters, start with a letter, and contain only letters, numbers, underscores, or hyphens",
269        );
270    }
271
272    // Enforce length limits on optional name fields.
273    if req.first_name.as_deref().is_some_and(|n| n.len() > 100) {
274        return ApiResponse::<UserListItem>::validation_error_typed(
275            "First name must be 100 characters or fewer",
276        );
277    }
278    if req.last_name.as_deref().is_some_and(|n| n.len() > 100) {
279        return ApiResponse::<UserListItem>::validation_error_typed(
280            "Last name must be 100 characters or fewer",
281        );
282    }
283
284    // Validate email format for consistency with the public registration endpoint.
285    if crate::utils::validation::validate_email(&req.email).is_err() {
286        return ApiResponse::<UserListItem>::validation_error_typed("Invalid email format");
287    }
288
289    // SECURITY (M-12): Use the same password validation as the public register endpoint
290    // so that admin-created accounts cannot bypass complexity requirements.
291    if let Err(e) = crate::utils::validation::validate_password(&req.password) {
292        // Return a generic message to avoid leaking internal validation rule details.
293        tracing::warn!("Admin create_user password validation failed: {e}");
294        return ApiResponse::<UserListItem>::validation_error_typed(
295            "Password does not meet complexity requirements",
296        );
297    }
298
299    match extract_bearer_token(&headers) {
300        Some(token) => {
301            match validate_api_token(&state.auth_framework, &token).await {
302                Ok(auth_token) => {
303                    // Check admin permissions
304                    if let Err(resp) = verify_admin_role(&auth_token) {
305                        return resp;
306                    }
307
308                    match state
309                        .auth_framework
310                        .register_user(&req.username, &req.email, &req.password)
311                        .await
312                    {
313                        Ok(user_id) => {
314                            if !(req.roles.is_empty()
315                                || req.roles.len() == 1 && req.roles[0] == "user")
316                            {
317                                if let Err(e) = state
318                                    .auth_framework
319                                    .update_user_roles(&user_id, &req.roles)
320                                    .await
321                                {
322                                    tracing::warn!("Failed to set roles for new user {}: {}", user_id, e);
323                                }
324                            }
325                            if !req.active {
326                                if let Err(e) = state.auth_framework.set_user_active(&user_id, false).await {
327                                    tracing::warn!("Failed to deactivate new user {}: {}", user_id, e);
328                                }
329                            }
330                            let new_user = UserListItem {
331                                id: user_id.clone(),
332                                username: req.username.clone(),
333                                email: req.email.clone(),
334                                roles: if req.roles.is_empty() {
335                                    vec!["user".to_string()]
336                                } else {
337                                    req.roles.clone()
338                                },
339                                active: req.active,
340                                created_at: chrono::Utc::now().to_rfc3339(),
341                                last_login: None,
342                            };
343                            tracing::info!("Admin created user: {} ({})", req.username, user_id);
344                            ApiResponse::success(new_user)
345                        }
346                        Err(e) => {
347                            let error_response = ApiResponse::<()>::from(e);
348                            ApiResponse::<UserListItem> {
349                                success: error_response.success,
350                                data: None,
351                                error: error_response.error,
352                                message: error_response.message,
353                            }
354                        }
355                    }
356                }
357                Err(e) => {
358                    // Convert AuthError to typed response
359                    let error_response = ApiResponse::<()>::from(e);
360                    ApiResponse::<UserListItem> {
361                        success: error_response.success,
362                        data: None,
363                        error: error_response.error,
364                        message: error_response.message,
365                    }
366                }
367            }
368        }
369        None => ApiResponse::<UserListItem>::unauthorized_typed(),
370    }
371}
372
373/// PUT /admin/users/{user_id}/roles
374/// Update user roles (admin only)
375pub async fn update_user_roles(
376    State(state): State<ApiState>,
377    headers: HeaderMap,
378    Path(user_id): Path<String>,
379    Json(req): Json<UpdateUserRolesRequest>,
380) -> ApiResponse<()> {
381    match extract_bearer_token(&headers) {
382        Some(token) => {
383            match validate_api_token(&state.auth_framework, &token).await {
384                Ok(auth_token) => {
385                    // Check admin permissions
386                    if let Err(resp) = verify_admin_role(&auth_token) {
387                        return resp;
388                    }
389
390                    match state
391                        .auth_framework
392                        .update_user_roles(&user_id, &req.roles)
393                        .await
394                    {
395                        Ok(()) => {
396                            tracing::info!(
397                                "Admin updated roles for user {}: {:?}",
398                                user_id,
399                                req.roles
400                            );
401                            ApiResponse::<()>::ok_with_message("User roles updated successfully")
402                        }
403                        Err(e) => e.into(),
404                    }
405                }
406                Err(e) => e.into(),
407            }
408        }
409        None => ApiResponse::unauthorized(),
410    }
411}
412
413/// DELETE /admin/users/{user_id}
414/// Delete user (admin only)
415pub async fn delete_user(
416    State(state): State<ApiState>,
417    headers: HeaderMap,
418    Path(user_id): Path<String>,
419) -> ApiResponse<()> {
420    match extract_bearer_token(&headers) {
421        Some(token) => {
422            match validate_api_token(&state.auth_framework, &token).await {
423                Ok(auth_token) => {
424                    // Check admin permissions
425                    if let Err(resp) = verify_admin_role(&auth_token) {
426                        return resp;
427                    }
428
429                    // Prevent self-deletion
430                    if auth_token.user_id == user_id {
431                        return ApiResponse::validation_error("Cannot delete your own account");
432                    }
433
434                    match state.auth_framework.get_username_by_id(&user_id).await {
435                        Ok(username) => match state.auth_framework.delete_user(&username).await {
436                            Ok(()) => {
437                                tracing::info!("Admin deleted user: {} ({})", username, user_id);
438                                ApiResponse::<()>::ok_with_message("User deleted successfully")
439                            }
440                            Err(e) => e.into(),
441                        },
442                        Err(e) => e.into(),
443                    }
444                }
445                Err(e) => e.into(),
446            }
447        }
448        None => ApiResponse::unauthorized(),
449    }
450}
451
452/// PUT /admin/users/{user_id}/activate
453/// Activate/deactivate user (admin only)
454#[derive(Debug, Deserialize)]
455pub struct ActivateUserRequest {
456    pub active: bool,
457}
458
459pub async fn activate_user(
460    State(state): State<ApiState>,
461    headers: HeaderMap,
462    Path(user_id): Path<String>,
463    Json(req): Json<ActivateUserRequest>,
464) -> ApiResponse<()> {
465    match extract_bearer_token(&headers) {
466        Some(token) => {
467            match validate_api_token(&state.auth_framework, &token).await {
468                Ok(auth_token) => {
469                    // Check admin permissions
470                    if !auth_token.roles.contains(&"admin".to_string()) {
471                        return ApiResponse::forbidden();
472                    }
473
474                    match state
475                        .auth_framework
476                        .set_user_active(&user_id, req.active)
477                        .await
478                    {
479                        Ok(()) => {
480                            let action = if req.active {
481                                "activated"
482                            } else {
483                                "deactivated"
484                            };
485                            tracing::info!("Admin {} user {}", action, user_id);
486                            ApiResponse::<()>::ok_with_message(format!(
487                                "User {} successfully",
488                                action
489                            ))
490                        }
491                        Err(e) => e.into(),
492                    }
493                }
494                Err(e) => e.into(),
495            }
496        }
497        None => ApiResponse::unauthorized(),
498    }
499}
500
501/// GET /admin/stats
502/// Get system statistics (admin only)
503pub async fn get_system_stats(
504    State(state): State<ApiState>,
505    headers: HeaderMap,
506) -> ApiResponse<SystemStats> {
507    match extract_bearer_token(&headers) {
508        Some(token) => {
509            match validate_api_token(&state.auth_framework, &token).await {
510                Ok(auth_token) => {
511                    // Check admin permissions
512                    if !auth_token.roles.contains(&"admin".to_string()) {
513                        return ApiResponse::forbidden_typed();
514                    }
515
516                    let storage = state.auth_framework.storage();
517                    let total_users = load_user_ids(&storage).await.len() as u64;
518                    let active_sessions = storage.count_active_sessions().await.unwrap_or(0);
519
520                    // Collect real system metrics via the sysinfo crate.
521                    let (system_uptime, memory_usage, cpu_usage) = {
522                        use sysinfo::System;
523                        let mut sys = System::new();
524                        sys.refresh_memory();
525                        sys.refresh_cpu_usage();
526                        let uptime_secs = System::uptime();
527                        let hours = uptime_secs / 3600;
528                        let mins = (uptime_secs % 3600) / 60;
529                        let secs = uptime_secs % 60;
530                        let uptime_str = format!("{hours}h {mins}m {secs}s");
531                        let used_mb = sys.used_memory() as f64 / 1_048_576.0;
532                        let total_mb = sys.total_memory() as f64 / 1_048_576.0;
533                        let mem_str = format!("{used_mb:.1} MB / {total_mb:.1} MB");
534                        let cpu_str = format!("{:.1}%", sys.global_cpu_usage());
535                        (uptime_str, mem_str, cpu_str)
536                    };
537
538                    let stats = SystemStats {
539                        total_users,
540                        active_sessions,
541                        // token count: proxy active sessions — each session
542                        // corresponds to at least one issued JWT token.
543                        total_tokens: active_sessions,
544                        // Persistent audit log not wired; cannot derive 24h
545                        // failure count from in-memory state alone.
546                        failed_logins_24h: 0,
547                        system_uptime,
548                        memory_usage,
549                        cpu_usage,
550                    };
551
552                    ApiResponse::success(stats)
553                }
554                Err(e) => {
555                    let error_response = ApiResponse::<()>::from(e);
556                    ApiResponse::<SystemStats> {
557                        success: error_response.success,
558                        data: None,
559                        error: error_response.error,
560                        message: error_response.message,
561                    }
562                }
563            }
564        }
565        None => ApiResponse::unauthorized_typed(),
566    }
567}
568
569/// GET /admin/audit-logs
570/// Get audit logs (admin only)
571#[derive(Debug, Serialize)]
572pub struct AuditLogEntry {
573    pub id: String,
574    pub timestamp: String,
575    pub user_id: String,
576    pub action: String,
577    pub resource: String,
578    pub ip_address: String,
579    pub user_agent: String,
580    pub result: String,
581    /// Risk level of the event: "low", "medium", "high", or "critical".
582    pub risk_level: String,
583    /// Outcome: "success", "failure", "partial", or "unknown".
584    pub outcome: String,
585    /// Correlation ID linking related events across the same authentication flow.
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub correlation_id: Option<String>,
588}
589
590#[derive(Debug, Serialize)]
591pub struct AuditLogResponse {
592    pub logs: Vec<AuditLogEntry>,
593    pub pagination: Pagination,
594}
595
596#[derive(Debug, Deserialize)]
597pub struct AuditLogQuery {
598    #[serde(default = "default_page")]
599    pub page: u32,
600    #[serde(default = "default_limit")]
601    pub limit: u32,
602    #[serde(default)]
603    pub user_id: Option<String>,
604    #[serde(default)]
605    pub action: Option<String>,
606    #[serde(default)]
607    pub start_date: Option<String>,
608    #[serde(default)]
609    pub end_date: Option<String>,
610    /// Filter by risk level: "low", "medium", "high", or "critical".
611    #[serde(default)]
612    pub risk_level: Option<String>,
613    /// Filter by outcome: "success" or "failure".
614    #[serde(default)]
615    pub outcome: Option<String>,
616    /// Filter by correlation ID to trace a single authentication flow.
617    #[serde(default)]
618    pub correlation_id: Option<String>,
619    /// Filter by client IP address.
620    #[serde(default)]
621    pub ip_address: Option<String>,
622}
623
624pub async fn get_audit_logs(
625    State(state): State<ApiState>,
626    headers: HeaderMap,
627    Query(query): Query<AuditLogQuery>,
628) -> ApiResponse<AuditLogResponse> {
629    match extract_bearer_token(&headers) {
630        Some(token) => {
631            match validate_api_token(&state.auth_framework, &token).await {
632                Ok(auth_token) => {
633                    // Check admin permissions
634                    if !auth_token.roles.contains(&"admin".to_string()) {
635                        return ApiResponse::forbidden_typed();
636                    }
637
638                    let page = if query.page == 0 { 1 } else { query.page };
639                    let limit = if query.limit == 0 {
640                        20
641                    } else {
642                        query.limit.min(100)
643                    };
644                    let offset = ((page - 1) * limit) as usize;
645                    let fetch_limit = offset + limit as usize;
646
647                    match state
648                        .auth_framework
649                        .get_permission_audit_logs(
650                            query.user_id.as_deref(),
651                            query.action.as_deref(),
652                            None,
653                            Some(fetch_limit),
654                        )
655                        .await
656                    {
657                        Ok(all_logs) => {
658                            // Parse and enrich all log entries first so filtering is applied
659                            // before pagination.
660                            let mut parsed: Vec<AuditLogEntry> = all_logs
661                                .into_iter()
662                                .enumerate()
663                                .map(|(i, log_str)| {
664                                    let (ts, rest) = log_str
665                                        .strip_prefix('[')
666                                        .and_then(|s| s.split_once(']'))
667                                        .map(|(t, r)| (t.trim().to_string(), r.trim().to_string()))
668                                        .unwrap_or_else(|| {
669                                            ("unknown".to_string(), log_str.clone())
670                                        });
671                                    let uid = rest
672                                        .split_whitespace()
673                                        .find(|w| w.starts_with("user="))
674                                        .and_then(|w| w.strip_prefix("user="))
675                                        .unwrap_or("system")
676                                        .to_string();
677                                    let raw_outcome = rest
678                                        .split_whitespace()
679                                        .find(|w| w.starts_with("outcome="))
680                                        .and_then(|w| w.strip_prefix("outcome="))
681                                        .unwrap_or("unknown")
682                                        .to_string();
683                                    let ip = rest
684                                        .split_whitespace()
685                                        .find(|w| w.starts_with("ip="))
686                                        .and_then(|w| w.strip_prefix("ip="))
687                                        .unwrap_or("")
688                                        .to_string();
689                                    let ua = rest
690                                        .split_whitespace()
691                                        .find(|w| w.starts_with("ua="))
692                                        .and_then(|w| w.strip_prefix("ua="))
693                                        .unwrap_or("")
694                                        .to_string();
695                                    let corr = rest
696                                        .split_whitespace()
697                                        .find(|w| w.starts_with("correlation_id="))
698                                        .and_then(|w| w.strip_prefix("correlation_id="))
699                                        .map(str::to_string);
700                                    // Derive outcome category and risk level from the raw
701                                    // outcome token present in the log line.
702                                    let outcome = if raw_outcome.contains("success") {
703                                        "success"
704                                    } else if raw_outcome.contains("fail")
705                                        || raw_outcome.contains("error")
706                                    {
707                                        "failure"
708                                    } else {
709                                        "unknown"
710                                    };
711                                    // Risk level: failures + high index numbers treated as
712                                    // higher risk for demonstration; real implementations
713                                    // should propagate risk from the authentication path.
714                                    let risk_level = if outcome == "failure" {
715                                        "medium"
716                                    } else {
717                                        "low"
718                                    };
719                                    AuditLogEntry {
720                                        id: format!("audit_{}", i),
721                                        timestamp: ts,
722                                        user_id: uid,
723                                        action: rest,
724                                        resource: String::new(),
725                                        ip_address: ip,
726                                        user_agent: ua,
727                                        result: raw_outcome,
728                                        risk_level: risk_level.to_string(),
729                                        outcome: outcome.to_string(),
730                                        correlation_id: corr,
731                                    }
732                                })
733                                .collect();
734
735                            // Apply optional client-side filters on the enriched entries.
736                            if let Some(ref filter_ip) = query.ip_address {
737                                parsed.retain(|e| e.ip_address.contains(filter_ip.as_str()));
738                            }
739                            if let Some(ref filter_risk) = query.risk_level {
740                                parsed.retain(|e| e.risk_level == filter_risk.as_str());
741                            }
742                            if let Some(ref filter_outcome) = query.outcome {
743                                parsed.retain(|e| e.outcome == filter_outcome.as_str());
744                            }
745                            if let Some(ref filter_corr) = query.correlation_id {
746                                parsed.retain(|e| {
747                                    e.correlation_id.as_deref() == Some(filter_corr.as_str())
748                                });
749                            }
750
751                            let total = parsed.len() as u64;
752                            let total_pages = ((total as f64) / (limit as f64)).ceil() as u32;
753                            let total_pages = if total_pages == 0 { 1 } else { total_pages };
754
755                            let logs: Vec<AuditLogEntry> = parsed
756                                .into_iter()
757                                .skip(offset)
758                                .take(limit as usize)
759                                .collect();
760
761                            ApiResponse::success(AuditLogResponse {
762                                logs,
763                                pagination: Pagination {
764                                    page,
765                                    limit,
766                                    total,
767                                    pages: total_pages,
768                                },
769                            })
770                        }
771                        Err(e) => {
772                            let error_response = ApiResponse::<()>::from(e);
773                            ApiResponse::<AuditLogResponse> {
774                                success: error_response.success,
775                                data: None,
776                                error: error_response.error,
777                                message: error_response.message,
778                            }
779                        }
780                    }
781                }
782                Err(e) => {
783                    let error_response = ApiResponse::<()>::from(e);
784                    ApiResponse::<AuditLogResponse> {
785                        success: error_response.success,
786                        data: None,
787                        error: error_response.error,
788                        message: error_response.message,
789                    }
790                }
791            }
792        }
793        None => ApiResponse::unauthorized_typed(),
794    }
795}
796
797/// Summary statistics returned by `GET /admin/audit-logs/stats`.
798#[derive(Debug, Serialize)]
799pub struct AuditLogStats {
800    /// Total events recorded in the last 24 hours.
801    pub total_events_24h: u64,
802    /// Failed login events in the last 24 hours.
803    pub failed_logins_24h: u64,
804    /// Successful login events in the last 24 hours.
805    pub successful_logins_24h: u64,
806    /// Events flagged as high-risk or critical in the last 24 hours.
807    pub high_risk_events_24h: u64,
808    /// Distinct user IDs seen in the last 24 hours.
809    pub unique_users_24h: u64,
810    /// Security alerts raised in the last 24 hours.
811    pub security_alerts_24h: u64,
812}
813
814/// GET /admin/audit-logs/stats
815///
816/// Returns aggregated audit-log statistics for the last 24 hours.
817/// Requires an admin bearer token.
818pub async fn get_audit_log_stats(
819    State(state): State<ApiState>,
820    headers: HeaderMap,
821) -> ApiResponse<AuditLogStats> {
822    match extract_bearer_token(&headers) {
823        Some(token) => match validate_api_token(&state.auth_framework, &token).await {
824            Ok(auth_token) => {
825                if !auth_token.roles.contains(&"admin".to_string()) {
826                    return ApiResponse::forbidden_typed();
827                }
828
829                match state.auth_framework.get_security_audit_stats().await {
830                    Ok(sec_stats) => ApiResponse::success(AuditLogStats {
831                        total_events_24h: sec_stats.failed_logins_24h
832                            + sec_stats.successful_logins_24h
833                            + sec_stats.admin_actions_24h,
834                        failed_logins_24h: sec_stats.failed_logins_24h,
835                        successful_logins_24h: sec_stats.successful_logins_24h,
836                        high_risk_events_24h: sec_stats.security_alerts_24h,
837                        unique_users_24h: sec_stats.unique_users_24h,
838                        security_alerts_24h: sec_stats.security_alerts_24h,
839                    }),
840                    Err(e) => {
841                        let error_response = ApiResponse::<()>::from(e);
842                        ApiResponse::<AuditLogStats> {
843                            success: error_response.success,
844                            data: None,
845                            error: error_response.error,
846                            message: error_response.message,
847                        }
848                    }
849                }
850            }
851            Err(e) => {
852                let error_response = ApiResponse::<()>::from(e);
853                ApiResponse::<AuditLogStats> {
854                    success: error_response.success,
855                    data: None,
856                    error: error_response.error,
857                    message: error_response.message,
858                }
859            }
860        },
861        None => ApiResponse::unauthorized_typed(),
862    }
863}
864
865// ─── Admin configuration endpoints ──────────────────────────────────────────
866
867/// Response body for `GET /admin/config`.
868///
869/// Only runtime-mutable fields are exposed. Security-sensitive settings (JWT
870/// secret, signing algorithm, storage backend) are intentionally omitted.
871#[derive(Debug, Serialize)]
872pub struct AdminConfigView {
873    pub token_lifetime_secs: u64,
874    pub refresh_token_lifetime_secs: u64,
875    pub enable_multi_factor: bool,
876    pub rate_limiting_enabled: bool,
877    pub rate_limit_max_requests: u32,
878    pub rate_limit_window_secs: u64,
879    pub rate_limit_burst: u32,
880    pub min_password_length: usize,
881    pub require_password_complexity: bool,
882    pub secure_cookies: bool,
883    pub csrf_protection: bool,
884    pub session_timeout_secs: u64,
885    pub audit_enabled: bool,
886    pub audit_log_success: bool,
887    pub audit_log_failures: bool,
888    pub audit_log_permissions: bool,
889    pub audit_log_tokens: bool,
890}
891
892impl From<crate::config::RuntimeConfig> for AdminConfigView {
893    fn from(c: crate::config::RuntimeConfig) -> Self {
894        Self {
895            token_lifetime_secs: c.token_lifetime_secs,
896            refresh_token_lifetime_secs: c.refresh_token_lifetime_secs,
897            enable_multi_factor: c.enable_multi_factor,
898            rate_limiting_enabled: c.rate_limiting_enabled,
899            rate_limit_max_requests: c.rate_limit_max_requests,
900            rate_limit_window_secs: c.rate_limit_window_secs,
901            rate_limit_burst: c.rate_limit_burst,
902            min_password_length: c.min_password_length,
903            require_password_complexity: c.require_password_complexity,
904            secure_cookies: c.secure_cookies,
905            csrf_protection: c.csrf_protection,
906            session_timeout_secs: c.session_timeout_secs,
907            audit_enabled: c.audit_enabled,
908            audit_log_success: c.audit_log_success,
909            audit_log_failures: c.audit_log_failures,
910            audit_log_permissions: c.audit_log_permissions,
911            audit_log_tokens: c.audit_log_tokens,
912        }
913    }
914}
915
916/// Request body for `PUT /admin/config` — all fields optional.
917///
918/// Omitted fields retain their current values.  This enables partial updates
919/// (patch semantics) without the client needing to re-send unchanged settings.
920#[derive(Debug, Deserialize)]
921pub struct AdminConfigUpdate {
922    #[serde(default)]
923    pub token_lifetime_secs: Option<u64>,
924    #[serde(default)]
925    pub refresh_token_lifetime_secs: Option<u64>,
926    #[serde(default)]
927    pub enable_multi_factor: Option<bool>,
928    #[serde(default)]
929    pub rate_limiting_enabled: Option<bool>,
930    #[serde(default)]
931    pub rate_limit_max_requests: Option<u32>,
932    #[serde(default)]
933    pub rate_limit_window_secs: Option<u64>,
934    #[serde(default)]
935    pub rate_limit_burst: Option<u32>,
936    #[serde(default)]
937    pub min_password_length: Option<usize>,
938    #[serde(default)]
939    pub require_password_complexity: Option<bool>,
940    #[serde(default)]
941    pub secure_cookies: Option<bool>,
942    #[serde(default)]
943    pub csrf_protection: Option<bool>,
944    #[serde(default)]
945    pub session_timeout_secs: Option<u64>,
946    #[serde(default)]
947    pub audit_enabled: Option<bool>,
948    #[serde(default)]
949    pub audit_log_success: Option<bool>,
950    #[serde(default)]
951    pub audit_log_failures: Option<bool>,
952    #[serde(default)]
953    pub audit_log_permissions: Option<bool>,
954    #[serde(default)]
955    pub audit_log_tokens: Option<bool>,
956}
957
958/// GET /admin/config
959///
960/// Returns the current runtime-mutable configuration.  Requires admin bearer token.
961pub async fn get_config(
962    State(state): State<ApiState>,
963    headers: HeaderMap,
964) -> ApiResponse<AdminConfigView> {
965    match extract_bearer_token(&headers) {
966        Some(token) => match validate_api_token(&state.auth_framework, &token).await {
967            Ok(auth_token) => {
968                if !auth_token.roles.contains(&"admin".to_string()) {
969                    return ApiResponse::forbidden_typed();
970                }
971                let cfg = state.auth_framework.runtime_config().await;
972                ApiResponse::success(AdminConfigView::from(cfg))
973            }
974            Err(e) => {
975                let error_response = ApiResponse::<()>::from(e);
976                ApiResponse::<AdminConfigView> {
977                    success: error_response.success,
978                    data: None,
979                    error: error_response.error,
980                    message: error_response.message,
981                }
982            }
983        },
984        None => ApiResponse::unauthorized_typed(),
985    }
986}
987
988/// PUT /admin/config
989///
990/// Applies a partial update to the runtime-mutable configuration.  Requires admin
991/// bearer token.  Returns the updated configuration after applying all changes.
992pub async fn update_config(
993    State(state): State<ApiState>,
994    headers: HeaderMap,
995    Json(update): Json<AdminConfigUpdate>,
996) -> ApiResponse<AdminConfigView> {
997    match extract_bearer_token(&headers) {
998        Some(token) => match validate_api_token(&state.auth_framework, &token).await {
999            Ok(auth_token) => {
1000                if !auth_token.roles.contains(&"admin".to_string()) {
1001                    return ApiResponse::forbidden_typed();
1002                }
1003
1004                // Load current config, merge, then validate via update_runtime_config.
1005                let mut current = state.auth_framework.runtime_config().await;
1006                if let Some(v) = update.token_lifetime_secs {
1007                    current.token_lifetime_secs = v;
1008                }
1009                if let Some(v) = update.refresh_token_lifetime_secs {
1010                    current.refresh_token_lifetime_secs = v;
1011                }
1012                if let Some(v) = update.enable_multi_factor {
1013                    current.enable_multi_factor = v;
1014                }
1015                if let Some(v) = update.rate_limiting_enabled {
1016                    current.rate_limiting_enabled = v;
1017                }
1018                if let Some(v) = update.rate_limit_max_requests {
1019                    current.rate_limit_max_requests = v;
1020                }
1021                if let Some(v) = update.rate_limit_window_secs {
1022                    current.rate_limit_window_secs = v;
1023                }
1024                if let Some(v) = update.rate_limit_burst {
1025                    current.rate_limit_burst = v;
1026                }
1027                if let Some(v) = update.min_password_length {
1028                    current.min_password_length = v;
1029                }
1030                if let Some(v) = update.require_password_complexity {
1031                    current.require_password_complexity = v;
1032                }
1033                if let Some(v) = update.secure_cookies {
1034                    current.secure_cookies = v;
1035                }
1036                if let Some(v) = update.csrf_protection {
1037                    current.csrf_protection = v;
1038                }
1039                if let Some(v) = update.session_timeout_secs {
1040                    current.session_timeout_secs = v;
1041                }
1042                if let Some(v) = update.audit_enabled {
1043                    current.audit_enabled = v;
1044                }
1045                if let Some(v) = update.audit_log_success {
1046                    current.audit_log_success = v;
1047                }
1048                if let Some(v) = update.audit_log_failures {
1049                    current.audit_log_failures = v;
1050                }
1051                if let Some(v) = update.audit_log_permissions {
1052                    current.audit_log_permissions = v;
1053                }
1054                if let Some(v) = update.audit_log_tokens {
1055                    current.audit_log_tokens = v;
1056                }
1057
1058                match state.auth_framework.update_runtime_config(current).await {
1059                    Ok(updated) => ApiResponse::success(AdminConfigView::from(updated)),
1060                    Err(e) => {
1061                        let error_response = ApiResponse::<()>::from(e);
1062                        ApiResponse::<AdminConfigView> {
1063                            success: error_response.success,
1064                            data: None,
1065                            error: error_response.error,
1066                            message: error_response.message,
1067                        }
1068                    }
1069                }
1070            }
1071            Err(e) => {
1072                let error_response = ApiResponse::<()>::from(e);
1073                ApiResponse::<AdminConfigView> {
1074                    success: error_response.success,
1075                    data: None,
1076                    error: error_response.error,
1077                    message: error_response.message,
1078                }
1079            }
1080        },
1081        None => ApiResponse::unauthorized_typed(),
1082    }
1083}