use std::time::Duration;
use jiff::Timestamp;
use super::{AuthFactor, factor_code};
use crate::auth::infrastructure_failure;
pub(in crate::auth) const FREE_FAILURES: i64 = 5;
const FIRST_WAIT: Duration = Duration::from_secs(30);
const LONGEST_WAIT: Duration = Duration::from_hours(1);
fn wait_after(failures: i64) -> Duration {
if failures < FREE_FAILURES {
return Duration::ZERO;
}
let doublings = u32::try_from(failures - FREE_FAILURES).unwrap_or(u32::MAX);
FIRST_WAIT
.checked_mul(2u32.saturating_pow(doublings.min(31)))
.map_or(LONGEST_WAIT, |wait| wait.min(LONGEST_WAIT))
}
pub(in crate::auth) fn wait(factor: &AuthFactor, code: &str, now: Timestamp) -> Option<Duration> {
factor_code(code)?;
let retry_at = factor.retry_at?;
let left = now.duration_until(retry_at).unsigned_abs();
(retry_at > now).then_some(left)
}
pub(in crate::auth) async fn refused(
ex: &mut dyn toasty::Executor,
factor: &AuthFactor,
now: Timestamp,
) -> topcoat::Result<i64> {
let failures = factor.failures.saturating_add(1);
let wait = wait_after(failures);
let retry_at = (!wait.is_zero())
.then(|| now.checked_add(wait).ok())
.flatten();
AuthFactor::filter(AuthFactor::fields().id().eq(factor.id))
.update()
.failures(failures)
.retry_at(retry_at)
.exec(ex)
.await
.map_err(infrastructure_failure)?;
Ok(failures)
}
pub(in crate::auth) async fn admitted(
ex: &mut dyn toasty::Executor,
factor: &AuthFactor,
) -> topcoat::Result<()> {
if factor.failures == 0 && factor.retry_at.is_none() {
return Ok(());
}
AuthFactor::filter(AuthFactor::fields().id().eq(factor.id))
.update()
.failures(0)
.retry_at(None)
.exec(ex)
.await
.map_err(infrastructure_failure)
}
#[cfg(test)]
mod tests;