1mod api;
17mod html;
18
19use std::sync::Arc;
20
21use authx_core::events::EventBus;
22use authx_plugins::AdminService;
23use authx_storage::ports::{
24 AuditLogRepository, DeviceCodeRepository, OidcClientRepository,
25 OidcFederationProviderRepository, OrgRepository, SessionRepository, UserRepository,
26};
27use axum::{
28 Router,
29 extract::State,
30 http::{Request, StatusCode},
31 middleware::{self, Next},
32 response::Response,
33};
34use subtle::ConstantTimeEq;
35
36#[derive(Clone)]
38pub struct DashboardState<S> {
39 pub(crate) admin: Arc<AdminService<S>>,
40 pub(crate) token: Arc<String>,
41}
42
43impl<S> DashboardState<S>
44where
45 S: UserRepository
46 + SessionRepository
47 + OrgRepository
48 + AuditLogRepository
49 + OidcClientRepository
50 + OidcFederationProviderRepository
51 + DeviceCodeRepository
52 + Clone
53 + Send
54 + Sync
55 + 'static,
56{
57 pub fn new(storage: S, events: EventBus, session_ttl_secs: i64) -> Self {
58 Self {
59 admin: Arc::new(AdminService::new(storage, events, session_ttl_secs)),
60 token: Arc::new(String::new()),
61 }
62 }
63
64 pub fn router(mut self, admin_token: impl Into<String>) -> Router {
66 self.token = Arc::new(admin_token.into());
67
68 let state = self.clone();
69
70 Router::new()
71 .merge(html::routes())
72 .nest("/api", api::routes::<S>().with_state(self))
73 .layer(middleware::from_fn_with_state(state, bearer_auth::<S>))
74 }
75}
76
77async fn bearer_auth<S>(
78 State(state): State<DashboardState<S>>,
79 req: Request<axum::body::Body>,
80 next: Next,
81) -> Result<Response, StatusCode>
82where
83 S: Clone + Send + Sync + 'static,
84{
85 if req.uri().path() == "/" {
87 return Ok(next.run(req).await);
88 }
89
90 let provided = req
91 .headers()
92 .get("authorization")
93 .and_then(|v| v.to_str().ok())
94 .and_then(|s| s.strip_prefix("Bearer "))
95 .unwrap_or("");
96
97 let token_bytes = state.token.as_bytes();
98 let valid = !provided.is_empty()
99 && provided.len() == token_bytes.len()
100 && provided.as_bytes().ct_eq(token_bytes).unwrap_u8() == 1;
101
102 if !valid {
103 tracing::warn!("dashboard: rejected request — invalid or missing admin token");
104 return Err(StatusCode::UNAUTHORIZED);
105 }
106
107 Ok(next.run(req).await)
108}