ocre-cli 0.2.0

Command-line tool for Ocre: create, generate, migrate, run and deploy apps.
//! API keys: the `api_keys` table. Generated by `ocre g auth`.
//!
//! A key is a random token (`ocre::token`, 256 bits) shown once when created;
//! the table keeps only its SHA-256 digest. Clients send it as
//! `Authorization: Bearer <key>` (see `BearerUser` in src/auth_api.rs).

use ocre::{Ctx, Error, Result, Validator, params};
use serde::{Deserialize, Serialize};

/// Columns returned to clients: everything but the digest.
const COLUMNS: &str = "id, user_id, name, last_used_at, created_at";

/// An API key, without its secret.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ApiKey {
    pub id: i64,
    pub user_id: i64,
    pub name: String,
    /// Updated at most once an hour, to save D1 writes.
    pub last_used_at: Option<String>,
    pub created_at: String,
}

/// Values for a new key.
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
pub struct NewApiKey {
    /// What the key is for, e.g. "CI deploys".
    pub name: String,
}

impl NewApiKey {
    pub fn validate(&self) -> Validator {
        let mut v = Validator::new();
        v.required("name", &self.name).max_length("name", &self.name, 100);
        v
    }
}

/// Creates a key for `user_id`. Returns the row and the key itself, which
/// the caller shows once: it cannot be read again.
pub async fn create(ctx: &Ctx, user_id: i64, new: NewApiKey) -> Result<(ApiKey, String)> {
    new.validate().finish()?;
    let key = ocre::token::generate();
    let sql = format!("INSERT INTO api_keys (user_id, name, digest) VALUES (?1, ?2, ?3) RETURNING {COLUMNS}");
    let row = ctx
        .db()?
        .first(&sql, params![user_id, new.name.trim(), ocre::token::digest(&key)])
        .await?
        .ok_or_else(|| Error::internal("INSERT ... RETURNING returned no row"))?;
    Ok((row, key))
}

/// The user's keys, newest first.
pub async fn for_user(ctx: &Ctx, user_id: i64) -> Result<Vec<ApiKey>> {
    let sql = format!("SELECT {COLUMNS} FROM api_keys WHERE user_id = ?1 ORDER BY id DESC");
    ctx.db()?.all(&sql, params![user_id]).await
}

/// Deletes one of the user's keys; `false` when the user has no such key.
pub async fn revoke(ctx: &Ctx, user_id: i64, id: i64) -> Result<bool> {
    Ok(ctx.db()?.execute("DELETE FROM api_keys WHERE id = ?1 AND user_id = ?2", params![id, user_id]).await? > 0)
}

/// The id of the user owning `key`, or `None` for an unknown key. Records
/// the use when the last one is more than an hour old.
pub async fn authenticate(ctx: &Ctx, key: &str) -> Result<Option<i64>> {
    #[derive(Deserialize)]
    struct Row {
        id: i64,
        user_id: i64,
        #[serde(deserialize_with = "ocre::bool_from_sql")]
        stale: bool,
    }
    let db = ctx.db()?;
    let row: Option<Row> = db
        .first(
            "SELECT id, user_id, (last_used_at IS NULL OR last_used_at < datetime('now', '-1 hour')) AS stale \
             FROM api_keys WHERE digest = ?1",
            params![ocre::token::digest(key)],
        )
        .await?;
    let Some(row) = row else { return Ok(None) };
    if row.stale {
        db.execute("UPDATE api_keys SET last_used_at = datetime('now') WHERE id = ?1", params![row.id]).await?;
    }
    Ok(Some(row.user_id))
}