a06673b
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<tokio::sync::RwLock<SigningKeyCache>>,
}
impl JwtState {
pub fn new(
db: DatabaseConnection,
settings: JwtSettings,
cache: Arc<tokio::sync::RwLock<SigningKeyCache>>,
) -> 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<TokenPair> 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<JwtState>,
Json(req): Json<LogoutRequest>,
) -> Result<StatusCode, JwtError> {
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<JwtState>,
) -> Result<Json<serde_json::Value>, 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()))
}