use axum::Json; use axum::extract::State; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; use std::sync::Arc; use crate::config::JwtSettings; use crate::error::JwtError; use crate::keys::SigningKeyCache; use crate::rotation::TokenPair; #[derive(Debug, Clone)] pub struct JwtState { pub db: DatabaseConnection, pub settings: JwtSettings, pub cache: Arc>, } impl JwtState { pub fn new( db: DatabaseConnection, settings: JwtSettings, cache: Arc>, ) -> Self { Self { db, settings, cache, } } } // ---- Response error mapping ---- impl IntoResponse for JwtError { fn into_response(self) -> Response { let status = match &self { JwtError::MissingHeader | JwtError::InvalidHeader => StatusCode::UNAUTHORIZED, JwtError::Expired | JwtError::InvalidSignature | JwtError::InvalidEncoding | JwtError::UnknownKid(_) | JwtError::InvalidAudience | JwtError::InvalidIssuer | JwtError::NotYetValid => StatusCode::UNAUTHORIZED, JwtError::SessionRevoked | JwtError::SessionReplaced | JwtError::SessionNotFound => { StatusCode::UNAUTHORIZED } JwtError::NoActiveKey => StatusCode::SERVICE_UNAVAILABLE, JwtError::Other(m) if m == "admin required" => StatusCode::FORBIDDEN, _ => StatusCode::INTERNAL_SERVER_ERROR, }; ( status, Json(serde_json::json!({ "error": self.to_string() })), ) .into_response() } } // ---- Request / response DTOs ---- #[derive(Debug, Deserialize)] pub struct LogoutRequest { pub refresh_token: String, } #[derive(Debug, Serialize)] pub struct TokenPairResponse { pub access_token: String, pub refresh_token: String, pub token_type: String, pub expires_in: i64, } impl From for TokenPairResponse { fn from(p: TokenPair) -> Self { Self { access_token: p.access_token, refresh_token: p.refresh_token, token_type: "Bearer".into(), expires_in: p.access_claims.exp - p.access_claims.iat, } } } // ---- Handlers ---- pub async fn logout_handler( State(state): State, Json(req): Json, ) -> Result { use crate::session; let kid = crate::signer::extract_kid(&req.refresh_token).ok_or(JwtError::InvalidHeader)?; let cache = state.cache.read().await; let cached = cache .get(&kid) .ok_or(JwtError::UnknownKid(kid.to_string()))?; let claims = crate::signer::decode_refresh_token( &req.refresh_token, &cached.decoding, cached.algorithm, &state.settings.issuer, &state.settings.audience, state.settings.clock_skew_seconds, )?; let sess = session::find_session_by_hash(&state.db, &session::hash_token(&req.refresh_token)) .await? .ok_or(JwtError::SessionNotFound)?; session::revoke_session(&state.db, sess.id).await?; let _ = claims; Ok(StatusCode::NO_CONTENT) } /// Admin-only: rotate the current signing key, activating a new one and /// placing the previous one into a grace window. Returns the new kid. pub async fn rotate_keys_handler( State(_state): State, ) -> Result, JwtError> { // TODO: generate a new Ed25519 key, insert it as active with the old key // entering the grace window, then reload the cache. Err(JwtError::Config("rotate_keys not yet implemented".into())) }