# 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 細節

```rust
// 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 細節

```rust
// 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 細節

```rust
#[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 細節

```rust
// 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 細節

```rust
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 細節

```rust
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 細節

```rust
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 細節

```json
// 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
    }
  ]
}
```

```bash
momentry api-key batch-create --file keys.json
```

### AKO-FEAT-05 細節

```rust
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** |

---

## 環境變數

```bash
# 效能
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 模組
