use crate::domain::error::{AppError, AppResult}; use crate::domain::models::ApiKeyRecord; use crate::interfaces::http::api::AppState; use crate::interfaces::http::routes::RequireAuth; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use axum::{ extract::{Path, State}, routing::{delete, get, post}, Json, Router, }; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use uuid::Uuid; pub fn router() -> Router { Router::new() .route("/keys", get(list_keys)) .route("/keys", post(create_key)) .route("/keys/:id", delete(revoke_key)) } #[derive(Serialize)] pub struct NewKeyResponse { pub key_id: String, pub secret: String, // Only shown once pub name: String, } #[derive(Deserialize)] pub struct CreateKeyRequest { pub name: String, } async fn list_keys( RequireAuth(merchant_id): RequireAuth, State(state): State, ) -> AppResult>> { let keys = ApiKeyRecord::list_for_merchant(&state.pool, &merchant_id).await?; Ok(Json(keys)) } async fn create_key( RequireAuth(merchant_id): RequireAuth, State(state): State, Json(payload): Json, ) -> AppResult> { let key_id = format!("vk_{}", Uuid::new_v4().simple()); let secret = Uuid::new_v4().simple().to_string(); // Hash the secret let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); let secret_hash = argon2 .hash_password(secret.as_bytes(), &salt) .map_err(|_| AppError::Internal("Hash failed".to_string()))? .to_string(); let record = ApiKeyRecord { key_id: key_id.clone(), merchant_id, name: payload.name.clone(), secret_hash, scopes: serde_json::json!(["read", "write"]), last_used_at: None, created_at: chrono::Utc::now().naive_utc(), }; ApiKeyRecord::create(&state.pool, &record).await?; Ok(Json(NewKeyResponse { key_id, secret, // Hand back raw secret only this once name: payload.name, })) } async fn revoke_key( RequireAuth(merchant_id): RequireAuth, Path(id): Path, State(state): State, ) -> AppResult> { ApiKeyRecord::delete(&state.pool, &id, &merchant_id).await?; Ok(Json(serde_json::json!({ "success": true }))) }