use std::time::Duration; use crate::cache::Cache; use crate::config::pow::PowConfig; use crate::session::Session; use crate::errors::{AuthError, AuthResult}; use crate::handlers::challenge::{ChallengeResponse, PowProofRequest, SESSION_CHALLENGE}; use crate::share::crypto::{now_secs, random_alnum}; pub const USED_KEY_PREFIX: &str = "auth:pow:used:"; pub async fn issue(session: &Session, config: &PowConfig) -> AuthResult { let now = now_secs(); let challenge = ChallengeResponse { context: random_alnum(32), difficulty: config.difficulty, issued_at: now, expires_at: now + config.expires_secs, }; session.set(SESSION_CHALLENGE, &challenge)?; Ok(challenge) } pub async fn verify( session: &Session, cache: &Cache, proof: &PowProofRequest, expected_payload: &[u8], ) -> AuthResult<()> { let challenge = session .get::(SESSION_CHALLENGE)? .ok_or(AuthError::Expired)?; crate::pow::hash::verify_pow(&challenge, proof, expected_payload)?; let remaining = challenge.expires_at.saturating_sub(now_secs()).max(1); let used_key = used_key(&challenge.context); let claimed = cache .set_nx_ex(&used_key, 1, Duration::from_secs(remaining)) .await?; if !claimed { return Err(AuthError::Expired); } let _ = session.remove(SESSION_CHALLENGE); Ok(()) } pub fn used_key(context: &str) -> String { format!("{USED_KEY_PREFIX}{context}") }