use dashmap::DashSet;
use std::time::{SystemTime, UNIX_EPOCH};
use std::vec::Vec;
use uuid::Uuid;
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(get_all, set_all))]
#[derive(Debug, Clone)]
pub struct RiRevokedTokenInfo {
pub token_id: String,
pub user_id: String,
pub revoked_at: u64,
pub expires_at: u64,
pub reason: Option<String>,
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiRevokedTokenInfo {
#[new]
fn py_new(
token_id: String,
user_id: String,
revoked_at: u64,
expires_at: u64,
reason: Option<String>,
) -> Self {
Self {
token_id,
user_id,
revoked_at,
expires_at,
reason,
}
}
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
pub struct RiJWTRevocationList {
revoked_tokens: DashSet<String>,
token_info: DashMap<String, RiRevokedTokenInfo>,
max_tokens: usize,
}
use dashmap::DashMap;
const DEFAULT_MAX_REVOKED_TOKENS: usize = 10000;
impl RiJWTRevocationList {
pub fn new() -> Self {
Self {
revoked_tokens: DashSet::new(),
token_info: DashMap::new(),
max_tokens: DEFAULT_MAX_REVOKED_TOKENS,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
revoked_tokens: DashSet::with_capacity(capacity),
token_info: DashMap::with_capacity(capacity),
max_tokens: capacity,
}
}
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiJWTRevocationList {
#[new]
fn py_new() -> Self {
Self {
revoked_tokens: DashSet::new(),
token_info: DashMap::new(),
max_tokens: DEFAULT_MAX_REVOKED_TOKENS,
}
}
#[pyo3(name = "get_revoked_count")]
fn get_revoked_count_impl(&self) -> usize {
self.revoked_tokens.len()
}
#[pyo3(name = "is_revoked")]
fn is_revoked_impl(&self, token: &str) -> bool {
self.revoked_tokens.contains(token)
}
#[pyo3(name = "revoke_token")]
fn revoke_token_impl(&self, token: String, user_id: String, reason: Option<String>, ttl_secs: u64) {
self.revoke_token(&token, &user_id, reason, ttl_secs);
}
#[pyo3(name = "revoke_by_user")]
fn revoke_by_user_impl(&self, user_id: &str) -> bool {
let mut removed = false;
let to_remove: Vec<String> = self.token_info
.iter()
.filter(|x| x.user_id == user_id)
.map(|x| x.token_id.clone())
.collect();
for token in to_remove {
self.revoked_tokens.remove(&token);
self.token_info.remove(&token);
removed = true;
}
removed
}
#[pyo3(name = "cleanup")]
fn cleanup_impl(&self) -> usize {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let expired: Vec<String> = self.token_info
.iter()
.filter(|x| x.expires_at <= now)
.map(|x| x.token_id.clone())
.collect();
for token in &expired {
self.revoked_tokens.remove(token);
self.token_info.remove(token);
}
expired.len()
}
}
impl RiJWTRevocationList {
pub fn revoke_token(
&self,
token: &str,
user_id: &str,
reason: Option<String>,
ttl_secs: u64,
) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
self.revoked_tokens.insert(token.to_string());
let info = RiRevokedTokenInfo {
token_id: Uuid::new_v4().to_string(),
user_id: user_id.to_string(),
revoked_at: now,
expires_at: now + ttl_secs,
reason,
};
self.token_info.insert(token.to_string(), info);
self.cleanup_expired();
}
pub fn revoke_all_user_tokens(&self, user_id: &str, reason: Option<String>) -> usize {
let mut count = 0;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
for entry in self.token_info.iter() {
let info = entry.value();
if info.user_id == user_id {
self.revoked_tokens.insert(entry.key().clone());
let updated_info = RiRevokedTokenInfo {
token_id: info.token_id.clone(),
user_id: info.user_id.clone(),
revoked_at: info.revoked_at,
expires_at: now + 86400,
reason: reason.clone(),
};
self.token_info.insert(entry.key().clone(), updated_info);
count += 1;
}
}
count
}
pub fn is_revoked(&self, token: &str) -> bool {
if self.revoked_tokens.contains(token) {
if let Some(info) = self.token_info.get(token) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
if now > info.expires_at {
self.remove_revoked_token(token);
return false;
}
}
return true;
}
false
}
pub fn get_revocation_info(&self, token: &str) -> Option<RiRevokedTokenInfo> {
self.token_info.get(token).map(|i| i.clone())
}
fn remove_revoked_token(&self, token: &str) {
self.revoked_tokens.remove(token);
self.token_info.remove(token);
}
fn cleanup_expired(&self) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let tokens_to_remove: Vec<String> = self
.token_info
.iter()
.filter(|entry| entry.expires_at <= now)
.map(|entry| entry.key().clone())
.collect();
for token in tokens_to_remove {
self.remove_revoked_token(&token);
}
while self.revoked_tokens.len() > self.max_tokens {
if let Some(entry) = self.token_info.iter().next() {
self.remove_revoked_token(entry.key());
} else {
break;
}
}
}
pub fn get_revoked_count(&self) -> usize {
self.revoked_tokens.len()
}
pub fn clear(&self) {
self.revoked_tokens.clear();
self.token_info.clear();
}
}
impl Default for RiJWTRevocationList {
fn default() -> Self {
Self::new()
}
}