futureauth 0.4.0

OTP authentication SDK — local session management with FutureAuth OTP delivery
Documentation
use chrono::Utc;
use sqlx::PgPool;
use std::time::Duration;

use crate::error::Result;
use crate::models::{Session, User};

pub async fn create(
    pool: &PgPool,
    user_id: &str,
    ttl: Duration,
    ip_address: Option<&str>,
    user_agent: Option<&str>,
) -> Result<Session> {
    let id = nanoid::nanoid!();
    let token = format!("{}.{}", nanoid::nanoid!(32), nanoid::nanoid!(16));
    let expires_at = Utc::now() + chrono::Duration::seconds(ttl.as_secs() as i64);

    let session = sqlx::query_as::<_, Session>(
        "INSERT INTO session (id, user_id, token, expires_at, ip_address, user_agent)
         VALUES ($1, $2, $3, $4, $5, $6)
         RETURNING *",
    )
    .bind(&id)
    .bind(user_id)
    .bind(&token)
    .bind(expires_at)
    .bind(ip_address)
    .bind(user_agent)
    .fetch_one(pool)
    .await?;

    Ok(session)
}

pub async fn find_by_token(pool: &PgPool, token: &str) -> Result<Option<(User, Session)>> {
    let row = sqlx::query_as::<_, Session>(
        "SELECT * FROM session WHERE token = $1 AND expires_at > NOW()",
    )
    .bind(token)
    .fetch_optional(pool)
    .await?;

    let session = match row {
        Some(s) => s,
        None => return Ok(None),
    };

    let user = sqlx::query_as::<_, User>(r#"SELECT * FROM "user" WHERE id = $1"#)
        .bind(&session.user_id)
        .fetch_optional(pool)
        .await?;

    match user {
        Some(u) => Ok(Some((u, session))),
        None => Ok(None),
    }
}

pub async fn revoke(pool: &PgPool, token: &str) -> Result<()> {
    sqlx::query("DELETE FROM session WHERE token = $1")
        .bind(token)
        .execute(pool)
        .await?;
    Ok(())
}

pub async fn revoke_all_for_user(pool: &PgPool, user_id: &str) -> Result<()> {
    sqlx::query("DELETE FROM session WHERE user_id = $1")
        .bind(user_id)
        .execute(pool)
        .await?;
    Ok(())
}

pub async fn cleanup_expired(pool: &PgPool) -> Result<u64> {
    let result = sqlx::query("DELETE FROM session WHERE expires_at <= NOW()")
        .execute(pool)
        .await?;
    Ok(result.rows_affected())
}