use actix_web::{HttpResponse, web}; use serde::{Deserialize, Serialize}; use crate::api::response::ApiResponse; use crate::error::AppError; use crate::service::AppService; use crate::session::Session; #[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct IssueApiKeyRequest { pub service_name: String, pub scopes: Vec, pub ttl_hours: Option, } #[derive(Debug, Serialize, utoipa::ToSchema)] pub struct IssueApiKeyResponse { pub api_key: String, pub service_name: String, pub service_id: String, pub scopes: Vec, pub expires_at: i64, } #[utoipa::path( post, path = "/api/v1/internal/api-keys", tag = "Internal", operation_id = "internalIssueApiKey", request_body = IssueApiKeyRequest, responses( (status = 200, description = "API key issued", body = ApiResponse), (status = 401, description = "Authentication required"), (status = 403, description = "Admin permission required"), ), security(("session_cookie" = [])) )] pub async fn issue_api_key( session: Session, service: web::Data, body: web::Json, ) -> Result { let user_uid = session.user().ok_or(AppError::Unauthorized)?; let is_owner: bool = sqlx::query_scalar( "SELECT EXISTS(SELECT 1 FROM workspace WHERE owner_id = $1 AND deleted_at IS NULL)", ) .bind(user_uid) .fetch_one(service.ctx.db.reader()) .await .map_err(AppError::Database)?; if !is_owner { return Err(AppError::Forbidden( "workspace owner permission required".into(), )); } let ttl_secs = body.ttl_hours.map(|h| h * 3600); let (api_key, identity) = service .internal_auth .issue_api_key(&body.service_name, body.scopes.clone(), ttl_secs) .await?; Ok(HttpResponse::Ok().json(ApiResponse::new(IssueApiKeyResponse { api_key, service_name: identity.service_name, service_id: identity.service_id, scopes: identity.scopes, expires_at: identity.expires_at, }))) }