- 拆分 web.rs 为 web/ 子模块;统一 client_ip 提取 - core: user_scope SQL 复用、env_map N+1 消除、FETCH_ALL 上限调整 - entries 列表页并行查询;PgPool 去 Arc;结构化 NotFound 等错误 - CI: SSH 私钥安全写入;crypto/hex 与依赖清理;MCP 输入长度校验 - AGENTS: API Key 明文存储设计说明
66 lines
2.0 KiB
Rust
66 lines
2.0 KiB
Rust
use anyhow::Result;
|
|
use sqlx::PgPool;
|
|
use uuid::Uuid;
|
|
|
|
use crate::error::AppError;
|
|
|
|
const KEY_PREFIX: &str = "sk_";
|
|
|
|
/// Generate a new API key: `sk_<64 hex chars>` = 67 characters total.
|
|
pub fn generate_api_key() -> String {
|
|
use rand::RngExt;
|
|
let mut bytes = [0u8; 32];
|
|
rand::rng().fill(&mut bytes);
|
|
format!("{}{}", KEY_PREFIX, ::hex::encode(bytes))
|
|
}
|
|
|
|
/// Return the user's existing API key, or generate and store a new one if NULL.
|
|
/// Uses a transaction with atomic update to prevent TOCTOU race conditions.
|
|
pub async fn ensure_api_key(pool: &PgPool, user_id: Uuid) -> Result<String> {
|
|
let mut tx = pool.begin().await?;
|
|
|
|
// Lock the row and check existing key
|
|
let existing: (Option<String>,) =
|
|
sqlx::query_as("SELECT api_key FROM users WHERE id = $1 FOR UPDATE")
|
|
.bind(user_id)
|
|
.fetch_optional(&mut *tx)
|
|
.await?
|
|
.ok_or(AppError::NotFoundUser)?;
|
|
|
|
if let Some(key) = existing.0 {
|
|
tx.commit().await?;
|
|
return Ok(key);
|
|
}
|
|
|
|
// Generate and store new key atomically
|
|
let new_key = generate_api_key();
|
|
sqlx::query("UPDATE users SET api_key = $1 WHERE id = $2")
|
|
.bind(&new_key)
|
|
.bind(user_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
tx.commit().await?;
|
|
Ok(new_key)
|
|
}
|
|
|
|
/// Generate a fresh API key for the user, replacing the old one.
|
|
pub async fn regenerate_api_key(pool: &PgPool, user_id: Uuid) -> Result<String> {
|
|
let new_key = generate_api_key();
|
|
sqlx::query("UPDATE users SET api_key = $1 WHERE id = $2")
|
|
.bind(&new_key)
|
|
.bind(user_id)
|
|
.execute(pool)
|
|
.await?;
|
|
Ok(new_key)
|
|
}
|
|
|
|
/// Validate a Bearer token. Returns the `user_id` if the key matches.
|
|
pub async fn validate_api_key(pool: &PgPool, raw_key: &str) -> Result<Option<Uuid>> {
|
|
let row: Option<(Uuid,)> = sqlx::query_as("SELECT id FROM users WHERE api_key = $1")
|
|
.bind(raw_key)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
Ok(row.map(|(id,)| id))
|
|
}
|