use chrono::Utc;
use sqlx::PgPool;
use std::time::Duration;
use crate::error::{Result, FutureAuthError};
use crate::models::Verification;
pub async fn create(
pool: &PgPool,
identifier: &str,
code: &str,
ttl: Duration,
) -> Result<Verification> {
sqlx::query("DELETE FROM verification WHERE identifier = $1 AND kind = 'otp'")
.bind(identifier)
.execute(pool)
.await?;
let id = nanoid::nanoid!();
let expires_at = Utc::now() + chrono::Duration::seconds(ttl.as_secs() as i64);
let v = sqlx::query_as::<_, Verification>(
"INSERT INTO verification (id, identifier, code, expires_at, kind)
VALUES ($1, $2, $3, $4, 'otp')
RETURNING *",
)
.bind(&id)
.bind(identifier)
.bind(code)
.bind(expires_at)
.fetch_one(pool)
.await?;
Ok(v)
}
const MAX_ATTEMPTS: i32 = 4;
pub async fn verify(pool: &PgPool, identifier: &str, code: &str) -> Result<()> {
let row = sqlx::query_as::<_, Verification>(
"SELECT * FROM verification WHERE identifier = $1 AND kind = 'otp'",
)
.bind(identifier)
.fetch_optional(pool)
.await?;
let v = row.ok_or(FutureAuthError::InvalidOtp)?;
if v.expires_at < Utc::now() {
sqlx::query("DELETE FROM verification WHERE id = $1")
.bind(&v.id)
.execute(pool)
.await?;
return Err(FutureAuthError::OtpExpired);
}
if v.code != code {
let new_attempts = v.attempts + 1;
if new_attempts >= MAX_ATTEMPTS {
sqlx::query("DELETE FROM verification WHERE id = $1")
.bind(&v.id)
.execute(pool)
.await?;
return Err(FutureAuthError::OtpMaxAttempts);
}
sqlx::query("UPDATE verification SET attempts = $1 WHERE id = $2")
.bind(new_attempts)
.bind(&v.id)
.execute(pool)
.await?;
return Err(FutureAuthError::InvalidOtp);
}
sqlx::query("DELETE FROM verification WHERE id = $1")
.bind(&v.id)
.execute(pool)
.await?;
Ok(())
}
pub async fn create_magic_link(
pool: &PgPool,
identifier: &str,
token: &str,
ttl: Duration,
) -> Result<Verification> {
sqlx::query("DELETE FROM verification WHERE identifier = $1 AND kind = 'magic_link'")
.bind(identifier)
.execute(pool)
.await?;
let id = nanoid::nanoid!();
let expires_at = Utc::now() + chrono::Duration::seconds(ttl.as_secs() as i64);
let v = sqlx::query_as::<_, Verification>(
"INSERT INTO verification (id, identifier, code, expires_at, kind)
VALUES ($1, $2, $3, $4, 'magic_link')
RETURNING *",
)
.bind(&id)
.bind(identifier)
.bind(token)
.bind(expires_at)
.fetch_one(pool)
.await?;
Ok(v)
}
pub async fn verify_magic_link(pool: &PgPool, token: &str) -> Result<String> {
let row = sqlx::query_as::<_, Verification>(
"SELECT * FROM verification WHERE code = $1 AND kind = 'magic_link'",
)
.bind(token)
.fetch_optional(pool)
.await?;
let v = row.ok_or(FutureAuthError::InvalidMagicLink)?;
if v.expires_at < Utc::now() {
sqlx::query("DELETE FROM verification WHERE id = $1")
.bind(&v.id)
.execute(pool)
.await?;
return Err(FutureAuthError::MagicLinkExpired);
}
let identifier = v.identifier.clone();
sqlx::query("DELETE FROM verification WHERE id = $1")
.bind(&v.id)
.execute(pool)
.await?;
Ok(identifier)
}
pub async fn cleanup_expired(pool: &PgPool) -> Result<u64> {
let result = sqlx::query("DELETE FROM verification WHERE expires_at <= NOW()")
.execute(pool)
.await?;
Ok(result.rows_affected())
}