use std::collections::BTreeMap;
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use gatekeep::{DenialReason, DenyShape, Locale, ReasonCatalog, ReasonCode};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DenialError {
Forbidden,
NotFound,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DenialBody {
pub error: DenialError,
pub message: String,
pub reason: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DenialResponse {
pub status: StatusCode,
pub body: DenialBody,
}
impl IntoResponse for DenialResponse {
fn into_response(self) -> Response {
(self.status, Json(self.body)).into_response()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DenialResponseConfig {
forbidden_fallback: String,
hidden_fallback: String,
hidden_reason: Option<ReasonCode>,
}
impl DenialResponseConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_forbidden_fallback(mut self, message: impl Into<String>) -> Self {
self.forbidden_fallback = message.into();
self
}
#[must_use]
pub fn with_hidden_fallback(mut self, message: impl Into<String>) -> Self {
self.hidden_fallback = message.into();
self
}
pub fn try_with_hidden_reason(
mut self,
reason: impl Into<String>,
) -> gatekeep::GatekeepResult<Self> {
self.hidden_reason = Some(ReasonCode::new(reason)?);
Ok(self)
}
pub(crate) fn denied<C: ReasonCatalog>(
&self,
shape: DenyShape,
reason: Option<&DenialReason>,
locale: &Locale,
catalog: &C,
) -> DenialResponse {
match shape {
DenyShape::Forbidden => self.forbidden(reason, locale, catalog),
DenyShape::Hidden => self.hidden(locale, catalog),
}
}
fn forbidden<C: ReasonCatalog>(
&self,
reason: Option<&DenialReason>,
locale: &Locale,
catalog: &C,
) -> DenialResponse {
let message = reason.map_or_else(
|| self.forbidden_fallback.clone(),
|reason| catalog.render(reason, locale),
);
DenialResponse {
status: StatusCode::FORBIDDEN,
body: DenialBody {
error: DenialError::Forbidden,
message,
reason: reason.map(|reason| reason.code.as_str().to_owned()),
},
}
}
fn hidden<C: ReasonCatalog>(&self, locale: &Locale, catalog: &C) -> DenialResponse {
let message = self.hidden_reason.as_ref().map_or_else(
|| self.hidden_fallback.clone(),
|code| {
let reason = DenialReason {
code: code.clone(),
params: BTreeMap::new(),
shape: DenyShape::Forbidden,
};
catalog.render(&reason, locale)
},
);
DenialResponse {
status: StatusCode::NOT_FOUND,
body: DenialBody {
error: DenialError::NotFound,
message,
reason: None,
},
}
}
}
impl Default for DenialResponseConfig {
fn default() -> Self {
Self {
forbidden_fallback: "forbidden".to_owned(),
hidden_fallback: "not found".to_owned(),
hidden_reason: None,
}
}
}