use std::sync::OnceLock;
use actix_web::HttpRequest;
use crate::errors::AppError;
static EXPECTED: OnceLock<Option<String>> = OnceLock::new();
fn expected() -> Option<&'static str> {
EXPECTED
.get_or_init(|| std::env::var("ADMIN_TOKEN").ok().filter(|s| !s.is_empty()))
.as_deref()
}
pub fn require_admin(req: &HttpRequest) -> Result<(), AppError> {
let expected = expected().ok_or_else(|| {
AppError::Forbidden(
"admin endpoints are disabled (set ADMIN_TOKEN env var to enable)".into(),
)
})?;
let presented = req
.headers()
.get("X-Admin-Token")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if constant_time_eq(presented.as_bytes(), expected.as_bytes()) {
Ok(())
} else {
Err(AppError::Forbidden(
"invalid or missing X-Admin-Token".into(),
))
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::constant_time_eq;
#[test]
fn ct_eq_equal() {
assert!(constant_time_eq(b"hunter2", b"hunter2"));
assert!(constant_time_eq(b"", b""));
}
#[test]
fn ct_eq_different_content() {
assert!(!constant_time_eq(b"hunter2", b"hunter3"));
}
#[test]
fn ct_eq_different_length() {
assert!(!constant_time_eq(b"abc", b"abcd"));
assert!(!constant_time_eq(b"abcd", b"abc"));
}
}