Files
momentry_core/docs/API_KEY_OPTIMIZATION.md
accusys 383201cacd feat: Initial v0.9 release with API Key authentication
## v0.9.20260325_144654

### Features
- API Key Authentication System
- Job Worker System
- V2 Backup Versioning

### Bug Fixes
- get_processor_results_by_job column mapping

Co-authored-by: OpenCode
2026-03-25 14:53:41 +08:00

10 KiB

API Key Management 優化計畫

項目 內容
版本 V1.0
日期 2026-03-21
狀態 規劃中

版本歷史

版本 日期 目的 操作人 工具/模型
V1.0 2026-03-21 創建優化計畫 OpenCode -

任務編碼規則

AKO-{類別}-{序號}
AKO = API Key Optimization
類別:
  - CODE = 程式碼品質
  - PERF = 效能優化
  - SEC  = 安全性
  - FEAT = 功能增強
  - DOC  = 文件

Phase 1: 程式碼品質 (CODE)

編碼 任務 描述 優先級 預估工時 狀態
AKO-CODE-01 修復 from_str 警告 重命名為 parse_scope 或實作 FromStr trait 🔴 0.5h 待辦
AKO-CODE-02 函數參數重構 使用 Config struct 減少參數數量 🔴 1h 待辦
AKO-CODE-03 抽象 CRUD Trait 建立 ExternalTokenStore trait 統一 Gitea/n8n 🟡 3h 待辦
AKO-CODE-04 錯誤處理統一 使用 thiserror 定義自訂錯誤類型 🟡 2h 待辦

AKO-CODE-01 細節

// Before
impl GiteaScope {
    pub fn from_str(s: &str) -> Option<Self> { ... }
}

// After: Option A - Rename
impl GiteaScope {
    pub fn parse(s: &str) -> Option<Self> { ... }
}

// After: Option B - Implement FromStr
impl std::str::FromStr for GiteaScope {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> { ... }
}

AKO-CODE-02 細節

// Before
pub async fn create_api_key(
    &self,
    key_id: &str,
    key_hash: &str,
    key_prefix: &str,
    name: &str,
    key_type: &str,
    user_id: Option<i64>,
    service_name: Option<&str>,
    permissions: &serde_json::Value,
    expires_at: Option<DateTime<Utc>>,
) -> Result<i64>

// After
pub struct CreateApiKeyConfig<'a> {
    pub key_id: &'a str,
    pub key_hash: &'a str,
    pub key_prefix: &'a str,
    pub name: &'a str,
    pub key_type: &'a str,
    pub user_id: Option<i64>,
    pub service_name: Option<&'a str>,
    pub permissions: &'a serde_json::Value,
    pub expires_at: Option<DateTime<Utc>>,
}

pub async fn create_api_key(&self, config: CreateApiKeyConfig<'_>) -> Result<i64>

AKO-CODE-03 細節

#[async_trait]
pub trait ExternalTokenStore<T> {
    async fn create(&self, record: T) -> Result<i64>;
    async fn get_by_label(&self, label: &str) -> Result<Option<T>>;
    async fn list(&self) -> Result<Vec<T>>;
    async fn delete(&self, label: &str) -> Result<()>;
    async fn update_verification(&self, label: &str) -> Result<()>;
}

Phase 2: 效能優化 (PERF)

編碼 任務 描述 優先級 預估工時 狀態
AKO-PERF-01 連線池配置外部化 使用環境變數控制 max_connections 🟡 0.5h 待辦
AKO-PERF-02 API Key 驗證快取 使用 Moka 快取減少資料庫查詢 🔴 2h 待辦
AKO-PERF-03 批次查詢優化 合併多次查詢為單一 SQL 🟡 1h 待辦
AKO-PERF-04 非同步日誌寫入 使用 channel 非同步寫入審計日誌 🟢 2h 待辦

AKO-PERF-01 細節

// Before
let pool_options = PgPoolOptions::new()
    .max_connections(10)
    .acquire_timeout(std::time::Duration::from_secs(60));

// After
let max_conn = std::env::var("DB_MAX_CONNECTIONS")
    .unwrap_or_else(|_| "10".to_string())
    .parse()
    .unwrap_or(10);
    
let pool_options = PgPoolOptions::new()
    .max_connections(max_conn)
    .acquire_timeout(std::time::Duration::from_secs(60));

AKO-PERF-02 細節

use moka::future::Cache;
use std::time::Duration;

pub struct ApiKeyCache {
    cache: Cache<String, CachedApiKey>,
}

pub struct CachedApiKey {
    pub record: ApiKeyRecord,
    pub cached_at: chrono::DateTime<chrono::Utc>,
}

impl ApiKeyCache {
    pub fn new(ttl_seconds: u64, max_capacity: u64) -> Self {
        Self {
            cache: Cache::builder()
                .time_to_live(Duration::from_secs(ttl_seconds))
                .max_capacity(max_capacity)
                .build(),
        }
    }

    pub async fn get(&self, key_hash: &str) -> Option<ApiKeyRecord> {
        self.cache.get(key_hash).await.map(|c| c.record)
    }

    pub async fn insert(&self, key_hash: String, record: ApiKeyRecord) {
        self.cache.insert(key_hash, CachedApiKey {
            record,
            cached_at: chrono::Utc::now(),
        }).await;
    }

    pub async fn invalidate(&self, key_hash: &str) {
        self.cache.invalidate(key_hash).await;
    }
}

Phase 3: 安全性 (SEC)

編碼 任務 描述 優先級 預估工時 狀態
AKO-SEC-01 Constant-time 比較 使用 subtle crate 防止 timing attack 🔴 0.5h 待辦
AKO-SEC-02 Rate Limiter 限制驗證失敗重試次數 🔴 2h 待辦
AKO-SEC-03 IP 黑名單 支援封鎖特定 IP 🟡 1.5h 待辦
AKO-SEC-04 審計日誌加密 敏感欄位加密儲存 🟡 2h 待辦
AKO-SEC-05 Key 強度檢查 驗證建立的 Key 符合強度要求 🟢 1h 待辦

AKO-SEC-01 細節

use subtle::ConstantTimeEq;

// Before
if stored_hash == computed_hash {
    // valid
}

// After
if bool::from(stored_hash.as_bytes().ct_eq(computed_hash.as_bytes())) {
    // valid
}

AKO-SEC-02 細節

use moka::future::Cache;

pub struct RateLimiter {
    attempts: Cache<String, AttemptInfo>,
    max_attempts: u32,
    window_seconds: u64,
}

pub struct AttemptInfo {
    pub count: u32,
    pub first_attempt: chrono::DateTime<chrono::Utc>,
    pub locked_until: Option<chrono::DateTime<chrono::Utc>>,
}

impl RateLimiter {
    pub async fn check(&self, identifier: &str) -> Result<()> {
        if let Some(info) = self.attempts.get(identifier).await {
            if let Some(locked_until) = info.locked_until {
                if chrono::Utc::now() < locked_until {
                    anyhow::bail!("Account locked until {}", locked_until);
                }
            }
        }
        Ok(())
    }

    pub async fn record_failure(&self, identifier: &str) -> Result<()> {
        let mut info = self.attempts.get(identifier).await
            .unwrap_or(AttemptInfo {
                count: 0,
                first_attempt: chrono::Utc::now(),
                locked_until: None,
            });
        
        info.count += 1;
        
        if info.count >= self.max_attempts {
            info.locked_until = Some(
                chrono::Utc::now() + chrono::Duration::seconds(self.window_seconds as i64)
            );
        }
        
        self.attempts.insert(identifier.to_string(), info).await;
        Ok(())
    }

    pub async fn record_success(&self, identifier: &str) {
        self.attempts.invalidate(identifier).await;
    }
}

Phase 4: 功能增強 (FEAT)

編碼 任務 描述 優先級 預估工時 狀態
AKO-FEAT-01 批量建立 Key 支援 JSON 檔案批量匯入 🟡 3h 待辦
AKO-FEAT-02 批量撤銷 Key 支援條件式批量撤銷 🟡 2h 待辦
AKO-FEAT-03 Key 匯出 匯出 Key 列表(不含明文) 🟢 1.5h 待辦
AKO-FEAT-04 Key 匯入 匯入 Key 元數據 🟢 1.5h 待辦
AKO-FEAT-05 Webhook 通知 異常發生時發送 Webhook 🟡 3h 待辦
AKO-FEAT-06 Email 通知 Key 到期前提醒 🟢 4h 待辦
AKO-FEAT-07 統計報表 生成使用統計報表 🟢 2h 待辦
AKO-FEAT-08 清理過期記錄 自動清理過期的 Key 記錄 🟢 1h 待辦

AKO-FEAT-01 細節

// keys.json
{
  "keys": [
    {
      "name": "ci-service-1",
      "key_type": "service",
      "permissions": ["read", "write"],
      "ttl_days": 90
    },
    {
      "name": "ci-service-2",
      "key_type": "service",
      "permissions": ["read"],
      "ttl_days": 180
    }
  ]
}
momentry api-key batch-create --file keys.json

AKO-FEAT-05 細節

pub struct WebhookConfig {
    pub url: String,
    pub secret: String,
    pub events: Vec<WebhookEvent>,
}

pub enum WebhookEvent {
    KeyCreated,
    KeyRevoked,
    KeyExpired,
    AnomalyDetected,
    RotationRequired,
}

pub struct WebhookNotifier {
    client: Client,
    config: WebhookConfig,
}

impl WebhookNotifier {
    pub async fn notify(&self, event: WebhookEvent, payload: serde_json::Value) -> Result<()> {
        if !self.config.events.contains(&event) {
            return Ok(());
        }

        let signature = self.sign(&payload);
        
        self.client.post(&self.config.url)
            .header("X-Webhook-Signature", signature)
            .json(&serde_json::json!({
                "event": event,
                "timestamp": chrono::Utc::now(),
                "payload": payload,
            }))
            .send()
            .await?;
            
        Ok(())
    }
}

Phase 5: 文件 (DOC)

編碼 任務 描述 優先級 預估工時 狀態
AKO-DOC-01 API 文件自動生成 使用 utoipa 生成 OpenAPI 🟢 3h 待辦
AKO-DOC-02 CHANGELOG.md 建立變更日誌 🟢 1h 待辦
AKO-DOC-03 架構圖 添加系統架構圖 🟢 2h 待辦
AKO-DOC-04 整合測試文件 記錄整合測試流程 🟢 1h 待辦

總工時估算

Phase 工時 任務數
CODE 6.5h 4
PERF 5.5h 4
SEC 7h 5
FEAT 18h 8
DOC 7h 4
總計 44h 25

環境變數

# 效能
DB_MAX_CONNECTIONS=10
CACHE_TTL_SECONDS=300
CACHE_MAX_CAPACITY=10000

# 安全
RATE_LIMIT_MAX_ATTEMPTS=5
RATE_LIMIT_WINDOW_SECONDS=900

# 通知
WEBHOOK_URL=https://example.com/webhook
WEBHOOK_SECRET=your-secret

參考文件

  • docs/API_KEY_MANAGEMENT.md - API Key 管理系統設計
  • docs/PENDING_ISSUES.md - 待解決問題追蹤
  • src/core/api_key/ - API Key 模組