1use 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#[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
41async 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
51async 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#[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#[derive(Debug, Serialize)]
101pub struct UserListResponse {
102 pub users: Vec<UserListItem>,
103 pub pagination: Pagination,
104}
105
106#[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#[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#[derive(Debug, Deserialize)]
150pub struct UpdateUserRolesRequest {
151 pub roles: Vec<String>,
152}
153
154#[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
166pub 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
251pub async fn create_user(
254 State(state): State<ApiState>,
255 headers: HeaderMap,
256 Json(req): Json<CreateUserRequest>,
257) -> ApiResponse<UserListItem> {
258 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 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 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 if crate::utils::validation::validate_email(&req.email).is_err() {
286 return ApiResponse::<UserListItem>::validation_error_typed("Invalid email format");
287 }
288
289 if let Err(e) = crate::utils::validation::validate_password(&req.password) {
292 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 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 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
373pub 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 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
413pub 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 if let Err(resp) = verify_admin_role(&auth_token) {
426 return resp;
427 }
428
429 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#[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 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
501pub 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 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 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 total_tokens: active_sessions,
544 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#[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 pub risk_level: String,
583 pub outcome: String,
585 #[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 #[serde(default)]
612 pub risk_level: Option<String>,
613 #[serde(default)]
615 pub outcome: Option<String>,
616 #[serde(default)]
618 pub correlation_id: Option<String>,
619 #[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 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 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 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 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 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#[derive(Debug, Serialize)]
799pub struct AuditLogStats {
800 pub total_events_24h: u64,
802 pub failed_logins_24h: u64,
804 pub successful_logins_24h: u64,
806 pub high_risk_events_24h: u64,
808 pub unique_users_24h: u64,
810 pub security_alerts_24h: u64,
812}
813
814pub 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#[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#[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
958pub 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
988pub 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 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}