v2: API key export endpoint (POST, rate-limited)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
master
oabrivard 3 months ago
parent 191e1c716b
commit 7eb24cfd9a

@ -169,6 +169,34 @@ pub async fn test_key(
}
}
/// `POST /api/v1/user/api-keys/export`
///
/// Returns decrypted API keys for settings export backup.
/// Rate-limited to 3 calls per minute. Uses POST to require CSRF token.
pub async fn export_keys(
auth_user: AuthUser,
State(state): State<AppState>,
) -> Result<impl IntoResponse, AppError> {
if !state.auth_rate_limiter.check(&format!("key-export:{}", auth_user.id)) {
return Err(AppError::RateLimited("Too many export requests".into()));
}
let stored_keys = db::api_keys::list_for_user(&state.pool, auth_user.id).await?;
let master_key = encryption::MasterKey::from_hex(&state.config.master_encryption_key)?;
let mut exported = Vec::new();
for key in &stored_keys {
let decrypted = encryption::decrypt(&master_key, &key.encrypted_key, &key.nonce)?;
exported.push(serde_json::json!({
"provider_name": key.provider_name,
"api_key": decrypted,
}));
}
tracing::info!(user_id = %auth_user.id, key_count = exported.len(), "API keys exported");
Ok(Json(exported))
}
/// Get a default model identifier for a provider from the admin config.
///
/// Looks up the admin provider configuration for the given provider name

@ -49,6 +49,7 @@ pub fn build_router(state: AppState, config: &AppConfig) -> Router {
.route("/user/api-keys", post(handlers::api_keys::create))
.route("/user/api-keys/{provider}", delete(handlers::api_keys::delete))
.route("/user/api-keys/{provider}/test", post(handlers::api_keys::test_key))
.route("/user/api-keys/export", post(handlers::api_keys::export_keys))
// Generation routes (authenticated) — registered before /syntheses/{id}
// to avoid ambiguity with path parameter matching
.route("/syntheses/generate", post(handlers::generation::trigger_generate))

Loading…
Cancel
Save