use std::{net::SocketAddr, sync::Arc};
use axum::{
Json,
extract::{ConnectInfo, State},
http::StatusCode,
response::{IntoResponse, Response},
};
use dashmap::DashMap;
use serde::Serialize;
use uuid::Uuid;
use crate::{
audit::logger::{AuditEventType, SecretType, get_audit_logger},
error::Result,
session::{SessionStore, unix_now},
};
const ANON_SESSION_TTL_SECS: u64 = 7 * 24 * 3600;
const ANON_RATE_WINDOW_SECS: u64 = 3_600;
const ANON_RATE_MAX: u32 = 10;
#[derive(Debug, Clone)]
struct RateRecord {
count: u32,
window_start: u64,
}
#[derive(Clone)]
pub struct AnonSignupState {
pub session_store: Arc<dyn SessionStore>,
rate_counters: Arc<DashMap<String, RateRecord>>,
}
impl AnonSignupState {
#[must_use]
pub fn new(session_store: Arc<dyn SessionStore>) -> Self {
Self {
session_store,
rate_counters: Arc::new(DashMap::new()),
}
}
fn check_rate_limit(&self, ip: &str, now: u64) -> bool {
let mut record = self.rate_counters.entry(ip.to_string()).or_insert(RateRecord {
count: 0,
window_start: now,
});
if now.saturating_sub(record.window_start) >= ANON_RATE_WINDOW_SECS {
record.count = 0;
record.window_start = now;
}
if record.count >= ANON_RATE_MAX {
return false;
}
record.count += 1;
true
}
}
#[derive(Debug, Serialize)]
pub struct AnonSignupResponse {
pub user_id: String,
pub access_token: String,
pub refresh_token: String,
pub expires_in: u64,
}
pub async fn anon_signup(
State(state): State<Arc<AnonSignupState>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Response {
let logger = get_audit_logger();
let ip = addr.ip().to_string();
let now = match unix_now() {
Ok(t) => t,
Err(e) => {
logger.log_failure(
AuditEventType::AuthFailure,
SecretType::SessionToken,
None,
"anon_signup:clock",
&e.to_string(),
);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
},
};
if !state.check_rate_limit(&ip, now) {
logger.log_failure(
AuditEventType::AuthFailure,
SecretType::SessionToken,
None,
"anon_signup:rate_limited",
"too many anonymous signups from this IP",
);
return (
StatusCode::TOO_MANY_REQUESTS,
Json(serde_json::json!({"error": "rate_limited"})),
)
.into_response();
}
let user_id = format!("anon_{}", Uuid::new_v4().as_simple());
let expires_at = now + ANON_SESSION_TTL_SECS;
match state.session_store.create_session(&user_id, expires_at).await {
Ok(tokens) => {
logger.log_success(
AuditEventType::SessionTokenCreated,
SecretType::SessionToken,
Some(user_id.clone()),
"anon_signup",
);
(
StatusCode::OK,
Json(AnonSignupResponse {
user_id,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_in: tokens.expires_in,
}),
)
.into_response()
},
Err(e) => {
logger.log_failure(
AuditEventType::AuthFailure,
SecretType::SessionToken,
None,
"anon_signup:session_create",
&e.to_string(),
);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
},
}
}
pub async fn upgrade_anonymous_session(
session_store: &dyn SessionStore,
anon_user_id: &str,
new_user_id: &str,
expires_at: u64,
) -> Result<crate::session::TokenPair> {
session_store.revoke_all_sessions(anon_user_id).await?;
session_store.create_session(new_user_id, expires_at).await
}
#[allow(clippy::unwrap_used)] #[cfg(test)]
mod tests;