ocre-cli 0.2.0

Command-line tool for Ocre: create, generate, migrate, run and deploy apps.
//! Single-use tokens emailed to users: password resets, magic-link logins
//! and email confirmations (the `auth_tokens` table). Generated by
//! `ocre g auth`.
//!
//! The email carries the token; the table keeps only its SHA-256 digest, so a
//! database leak exposes no working link. A token works once, before it
//! expires, and issuing a new one cancels the user's previous token of the
//! same purpose.

use ocre::{Ctx, Result, Statement, params};
use serde::Deserialize;

pub const PASSWORD_RESET: &str = "password_reset";
pub const MAGIC_LINK: &str = "magic_link";
pub const EMAIL_CONFIRMATION: &str = "email_confirmation";

/// Minutes an emailed link of this purpose stays valid: sign-in and reset
/// links are short-lived, confirmation links last a day.
pub fn valid_minutes(purpose: &str) -> i64 {
    if purpose == EMAIL_CONFIRMATION { 24 * 60 } else { 15 }
}

#[derive(Deserialize)]
struct Row {
    user_id: i64,
}

/// A new token for `user_id`, to put in an email link.
pub async fn issue(ctx: &Ctx, user_id: i64, purpose: &str) -> Result<String> {
    let token = ocre::token::generate();
    ctx.db()?
        .batch(vec![
            Statement::new("DELETE FROM auth_tokens WHERE user_id = ?1 AND purpose = ?2", params![user_id, purpose]),
            Statement::new(
                "INSERT INTO auth_tokens (user_id, purpose, digest, expires_at) VALUES (?1, ?2, ?3, datetime('now', ?4))",
                params![user_id, purpose, ocre::token::digest(&token), format!("+{} minutes", valid_minutes(purpose))],
            ),
        ])
        .await?;
    Ok(token)
}

/// The user id of a valid token, without using it up (to show a form).
pub async fn peek(ctx: &Ctx, purpose: &str, token: &str) -> Result<Option<i64>> {
    let row: Option<Row> = ctx
        .db()?
        .first(
            "SELECT user_id FROM auth_tokens WHERE digest = ?1 AND purpose = ?2 AND expires_at > datetime('now')",
            params![ocre::token::digest(token), purpose],
        )
        .await?;
    Ok(row.map(|row| row.user_id))
}

/// Uses the token up: the user id when it is valid, deleted in the same
/// statement so a second use (or a concurrent one) gets `None`.
pub async fn consume(ctx: &Ctx, purpose: &str, token: &str) -> Result<Option<i64>> {
    let row: Option<Row> = ctx
        .db()?
        .first(
            "DELETE FROM auth_tokens WHERE digest = ?1 AND purpose = ?2 AND expires_at > datetime('now') RETURNING user_id",
            params![ocre::token::digest(token), purpose],
        )
        .await?;
    Ok(row.map(|row| row.user_id))
}