use axum::{ extract::{Request, State}, http::StatusCode, middleware::Next, response::{IntoResponse, Redirect, Response}, }; use axum_extra::extract::cookie::SignedCookieJar; use super::rbac::RbacConfig; use super::session; use crate::AppState; use jacquard_common::types::did::Did; /// Admin session data injected into request extensions. #[derive(Debug, Clone)] pub struct AdminSession { pub did: String, pub handle: String, pub roles: Vec, } /// Pre-computed permission flags for template rendering and quick checks. #[derive(Debug, Clone)] pub struct AdminPermissions { pub can_view_accounts: bool, pub can_manage_takedowns: bool, pub can_delete_account: bool, pub can_reset_password: bool, pub can_create_account: bool, pub can_manage_invites: bool, pub can_create_invite: bool, pub can_send_email: bool, pub can_request_crawl: bool, } impl AdminPermissions { pub fn compute(rbac: &RbacConfig, did: &str) -> Self { Self { can_view_accounts: rbac.can_access_endpoint(did, "com.atproto.admin.getAccountInfo") || rbac.can_access_endpoint(did, "com.atproto.admin.getAccountInfos"), can_manage_takedowns: rbac .can_access_endpoint(did, "com.atproto.admin.updateSubjectStatus"), can_delete_account: rbac.can_access_endpoint(did, "com.atproto.admin.deleteAccount"), can_reset_password: rbac .can_access_endpoint(did, "com.atproto.admin.updateAccountPassword"), can_create_account: rbac.can_access_endpoint(did, "com.atproto.server.createAccount"), can_manage_invites: rbac.can_access_endpoint(did, "com.atproto.admin.getInviteCodes"), can_create_invite: rbac.can_access_endpoint(did, "com.atproto.server.createInviteCode"), can_send_email: rbac.can_access_endpoint(did, "com.atproto.admin.sendEmail"), can_request_crawl: rbac.can_access_endpoint(did, "com.atproto.sync.requestCrawl"), } } } /// Middleware that checks for a valid admin session cookie. /// If valid, injects AdminSession and AdminPermissions into request extensions. /// If invalid or missing, redirects to /admin/login. pub async fn admin_auth_middleware( State(state): State, jar: SignedCookieJar, mut req: Request, next: Next, ) -> Response { let rbac = match &state.admin_rbac_config { Some(rbac) => rbac, None => return StatusCode::NOT_FOUND.into_response(), }; // Extract session ID from signed cookie let session_id = match jar.get("__gatekeeper_admin_session") { Some(cookie) => cookie.value().to_string(), None => return Redirect::to("/admin/login").into_response(), }; // Look up session in database let session_row = match session::get_session(&state.pds_gatekeeper_pool, &session_id).await { Ok(Some(row)) => row, Ok(None) => return Redirect::to("/admin/login").into_response(), Err(e) => { tracing::error!("Failed to look up admin session: {}", e); return Redirect::to("/admin/login").into_response(); } }; // Verify the DID is still a valid member if !rbac.is_member(&session_row.did) { return Redirect::to("/admin/login").into_response(); } let oauth_client = if let Some(client) = &state.admin_oauth_client { client } else { return Redirect::to("/admin/login").into_response(); }; let did: Did = session_row.did.clone().into(); let oauth_session_id = session_row.oauth_session_id.clone(); match oauth_client.restore(&did, oauth_session_id.as_str()).await { Ok(_) => {} Err(e) => { tracing::error!("Failed to restore admin session: {}", e); let error_msg = e.to_string(); return Redirect::to(&format!("/admin/login?error={}", error_msg)).into_response(); } } let roles = rbac.get_member_roles(&session_row.did); let permissions = AdminPermissions::compute(rbac, &session_row.did); let admin_session = AdminSession { did: session_row.did, handle: session_row.handle, roles, }; req.extensions_mut().insert(admin_session); req.extensions_mut().insert(permissions); next.run(req).await }