use axum::{
body::Body,
extract::{Extension, State},
http::{Method, Request, StatusCode},
middleware::Next,
response::{IntoResponse, Json, Response},
};
use jamjet_state::{ApiToken, StateBackend, TenantId};
use serde_json::json;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Role {
Viewer = 0,
Reviewer = 1,
Developer = 2,
Operator = 3,
}
impl Role {
pub fn parse_role(s: &str) -> Self {
match s {
"operator" => Self::Operator,
"developer" => Self::Developer,
"reviewer" => Self::Reviewer,
_ => Self::Viewer,
}
}
pub fn can_write(&self) -> bool {
matches!(self, Self::Operator | Self::Developer)
}
pub fn can_admin(&self) -> bool {
matches!(self, Self::Operator)
}
}
#[derive(Clone)]
pub struct AuthState {
pub backend: Arc<dyn StateBackend>,
}
pub async fn require_auth(
State(auth): State<AuthState>,
mut req: Request<Body>,
next: Next,
) -> Response {
let token = match extract_bearer(req.headers()) {
Some(t) => t,
None => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({ "error": "missing Authorization: Bearer <token>" })),
)
.into_response();
}
};
match auth.backend.validate_token(&token).await {
Ok(Some(info)) => {
let tenant_id = TenantId::from(info.tenant_id.clone());
req.extensions_mut().insert(tenant_id);
req.extensions_mut().insert(info);
next.run(req).await
}
Ok(None) => (
StatusCode::UNAUTHORIZED,
Json(json!({ "error": "invalid or expired token" })),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": format!("auth error: {e}") })),
)
.into_response(),
}
}
pub async fn require_write_role(req: Request<Body>, next: Next) -> Response {
let is_mutating = matches!(
req.method(),
&Method::POST | &Method::PUT | &Method::PATCH | &Method::DELETE
);
if is_mutating {
let role = req
.extensions()
.get::<ApiToken>()
.map(|t| Role::parse_role(&t.role))
.unwrap_or(Role::Viewer);
if !role.can_write() {
return (
StatusCode::FORBIDDEN,
Json(json!({
"error": "insufficient role — developer or operator required",
"required": "developer",
})),
)
.into_response();
}
}
next.run(req).await
}
pub async fn require_operator_role(
Extension(token): Extension<ApiToken>,
req: Request<Body>,
next: Next,
) -> Response {
if !Role::parse_role(&token.role).can_admin() {
return (
StatusCode::FORBIDDEN,
Json(json!({
"error": "insufficient role — operator required",
"required": "operator",
})),
)
.into_response();
}
next.run(req).await
}
fn extract_bearer(headers: &axum::http::HeaderMap) -> Option<String> {
let value = headers
.get(axum::http::header::AUTHORIZATION)?
.to_str()
.ok()?;
value.strip_prefix("Bearer ").map(str::to_string)
}