2.7 KiB · text History c20d4f7
use anyhow::{Context, Result};
use chrono::Utc;
use sea_orm::*;
use uuid::Uuid;

use crate::entities::{api_tokens, users};

/// Generate a new API token. Format: `dms_<32 url-safe chars>` so they're
/// distinguishable from session UUIDs at a glance.
pub fn generate_api_token() -> String {
    let body = Uuid::new_v4().simple().to_string();
    format!("dms_{body}")
}

pub async fn create_api_token(
    db: &DatabaseConnection,
    user_id: Uuid,
    label: Option<String>,
    expires_in_days: Option<i64>,
) -> Result<api_tokens::Model> {
    let token = generate_api_token();
    let expires_at = expires_in_days.map(|d| (Utc::now() + chrono::Duration::days(d)).into());
    let active = api_tokens::ActiveModel {
        id: Set(Uuid::now_v7()),
        user_id: Set(user_id),
        token: Set(token),
        label: Set(label),
        last_used_at: Set(None),
        expires_at: Set(expires_at),
        created_at: Set(Utc::now().into()),
    };
    active.insert(db).await.context("insert api_token")
}

/// Look up the user owning a token. Returns Ok(None) for unknown / expired
/// tokens. Side-effect: bumps last_used_at when the token is valid.
pub async fn validate_api_token(
    db: &DatabaseConnection,
    token: &str,
) -> Result<Option<users::Model>> {
    let row = api_tokens::Entity::find()
        .filter(api_tokens::Column::Token.eq(token))
        .one(db)
        .await
        .context("query api_token")?;
    let Some(row) = row else { return Ok(None); };
    if let Some(exp) = row.expires_at
        && exp < Utc::now()
    {
        return Ok(None);
    }
    // Bump last_used_at — best effort, don't fail validation on update error.
    let mut update: api_tokens::ActiveModel = row.clone().into();
    update.last_used_at = Set(Some(Utc::now().into()));
    let _ = update.update(db).await;

    let user = users::Entity::find_by_id(row.user_id)
        .filter(users::Column::IsActive.eq(true))
        .one(db)
        .await
        .context("query user for api_token")?;
    Ok(user)
}

pub async fn delete_api_token(
    db: &DatabaseConnection,
    user_id: Uuid,
    token_id: Uuid,
) -> Result<u64> {
    let result = api_tokens::Entity::delete_many()
        .filter(api_tokens::Column::Id.eq(token_id))
        .filter(api_tokens::Column::UserId.eq(user_id))
        .exec(db)
        .await
        .context("delete api_token")?;
    Ok(result.rows_affected)
}

pub async fn list_api_tokens(
    db: &DatabaseConnection,
    user_id: Uuid,
) -> Result<Vec<api_tokens::Model>> {
    api_tokens::Entity::find()
        .filter(api_tokens::Column::UserId.eq(user_id))
        .order_by_desc(api_tokens::Column::CreatedAt)
        .all(db)
        .await
        .context("list api_tokens")
}