use rocket::request::{FromRequest, Outcome, Request};
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::State;
use std::collections::HashMap;
use std::sync::RwLock;
use std::time::{Duration, Instant};
use uuid::Uuid;
use crate::{LoginRequest, TokenResponse, AuthError, DEFAULT_TOKEN_TTL_SECONDS};
pub fn get_token_ttl() -> Duration {
let ttl_seconds = std::env::var("TOKEN_TTL_SECONDS")
.unwrap_or_else(|_| DEFAULT_TOKEN_TTL_SECONDS.to_string())
.parse::<u64>()
.unwrap_or(DEFAULT_TOKEN_TTL_SECONDS);
Duration::from_secs(ttl_seconds)
}
pub struct TokenStore(RwLock<HashMap<String, Instant>>);
impl TokenStore {
pub fn new() -> Self {
TokenStore(RwLock::new(HashMap::new()))
}
pub fn insert(&self, token: String) {
self.0.write().unwrap().insert(token, Instant::now());
}
pub fn validate(&self, token: &str) -> bool {
let mut map = self.0.write().unwrap();
if let Some(&t0) = map.get(token) {
if t0.elapsed() <= get_token_ttl() {
return true;
} else {
map.remove(token);
}
}
false
}
pub fn cleanup_expired(&self) {
let mut map = self.0.write().unwrap();
let ttl = get_token_ttl();
map.retain(|_, &mut timestamp| timestamp.elapsed() <= ttl);
}
}
impl Default for TokenStore {
fn default() -> Self {
Self::new()
}
}
pub fn login_handler(body: Json<LoginRequest>, store: &State<TokenStore>) -> Result<Json<TokenResponse>, Status> {
let expected = std::env::var("ADMIN_PASSWORD").unwrap_or_default();
if body.password == expected {
let token = Uuid::new_v4().to_string();
store.insert(token.clone());
Ok(Json(TokenResponse {
token,
username: None,
role: None,
}))
} else {
Err(Status::Unauthorized)
}
}
pub struct AuthGuard;
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthGuard {
type Error = AuthError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, AuthError> {
let store = match req.guard::<&State<TokenStore>>().await {
Outcome::Success(store) => store,
_ => return Outcome::Error((Status::InternalServerError, AuthError::StorageError("Token store not available".to_string()))),
};
match req.headers().get_one("Authorization") {
Some(auth_header) => {
let token = if auth_header.starts_with("Bearer ") {
&auth_header[7..]
} else {
auth_header
};
if store.validate(token) {
Outcome::Success(AuthGuard)
} else {
Outcome::Error((Status::Unauthorized, AuthError::TokenExpired))
}
},
None => Outcome::Error((Status::Unauthorized, AuthError::TokenNotFound)),
}
}
}
pub type Admin = AuthGuard;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_store() {
let store = TokenStore::new();
let token = "test-token".to_string();
assert!(!store.validate(&token));
store.insert(token.clone());
assert!(store.validate(&token));
}
#[test]
fn test_token_ttl() {
let ttl = get_token_ttl();
assert_eq!(ttl, Duration::from_secs(DEFAULT_TOKEN_TTL_SECONDS));
}
}