1use reqwest::Client;
29use serde::{Deserialize, Serialize};
30use std::sync::Arc;
31use thiserror::Error;
32
33pub const DEFAULT_VALIDATE_URL: &str = "https://harbor-black.vercel.app/api/validate";
34
35#[derive(Debug, Error)]
36pub enum HarborError {
37 #[error("Invalid or revoked API key")]
38 InvalidKey,
39 #[error("Missing API key")]
40 MissingKey,
41 #[error("HTTP error: {0}")]
42 Http(#[from] reqwest::Error),
43 #[error("Harbor validation service unavailable")]
44 ServiceUnavailable,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct KeyInfo {
50 #[serde(rename = "keyId")]
51 pub key_id: String,
52 #[serde(rename = "projectId")]
53 pub project_id: String,
54 pub plan: String,
55 #[serde(rename = "callsThisMonth", default)]
56 pub calls_this_month: u64,
57 pub name: String,
58 pub country: Option<String>,
59}
60
61#[derive(Deserialize)]
62struct ValidateResponse {
63 valid: bool,
64 #[serde(rename = "keyId")]
65 key_id: Option<String>,
66 #[serde(rename = "projectId")]
67 project_id: Option<String>,
68 plan: Option<String>,
69 #[serde(rename = "callsThisMonth")]
70 calls_this_month: Option<u64>,
71 name: Option<String>,
72 country: Option<String>,
73 error: Option<String>,
74}
75
76pub async fn validate(api_key: &str) -> Result<KeyInfo, HarborError> {
91 validate_with_url(api_key, DEFAULT_VALIDATE_URL).await
92}
93
94pub async fn validate_with_url(api_key: &str, url: &str) -> Result<KeyInfo, HarborError> {
96 let client = Client::builder()
97 .timeout(std::time::Duration::from_secs(5))
98 .user_agent(format!("harbor-sdk-rust/{}", env!("CARGO_PKG_VERSION")))
99 .build()
100 .map_err(|_| HarborError::ServiceUnavailable)?;
101
102 let response = client
103 .get(url)
104 .query(&[("key", api_key)])
105 .send()
106 .await?;
107
108 let result: ValidateResponse = response.json().await?;
109
110 if !result.valid {
111 return Err(HarborError::InvalidKey);
112 }
113
114 Ok(KeyInfo {
115 key_id: result.key_id.unwrap_or_default(),
116 project_id: result.project_id.unwrap_or_default(),
117 plan: result.plan.unwrap_or_else(|| "free".to_string()),
118 calls_this_month: result.calls_this_month.unwrap_or(0),
119 name: result.name.unwrap_or_default(),
120 country: result.country,
121 })
122}
123
124#[cfg(feature = "axum")]
127pub mod axum_middleware {
128 use super::*;
129 use axum::{
130 body::Body,
131 extract::Request,
132 http::StatusCode,
133 middleware::Next,
134 response::{IntoResponse, Response},
135 Extension,
136 };
137
138 pub async fn harbor_auth(
153 axum::extract::State(project_id): axum::extract::State<String>,
154 mut req: Request,
155 next: Next,
156 ) -> Response {
157 let api_key = req
158 .headers()
159 .get("x-harbor-key")
160 .and_then(|v| v.to_str().ok())
161 .map(|s| s.to_string());
162
163 let api_key = match api_key {
164 Some(k) => k,
165 None => {
166 return (
167 StatusCode::UNAUTHORIZED,
168 r#"{"error":"Missing API key"}"#,
169 )
170 .into_response()
171 }
172 };
173
174 match validate(&api_key).await {
175 Ok(info) if info.project_id == project_id || project_id.is_empty() => {
176 req.extensions_mut().insert(info);
177 next.run(req).await
178 }
179 Ok(_) => (
180 StatusCode::FORBIDDEN,
181 r#"{"error":"Key does not belong to this project"}"#,
182 )
183 .into_response(),
184 Err(_) => (
185 StatusCode::UNAUTHORIZED,
186 r#"{"error":"Invalid or revoked API key"}"#,
187 )
188 .into_response(),
189 }
190 }
191}
192
193#[derive(Clone)]
195pub struct HarborLayer {
196 project_id: Arc<String>,
197 validate_url: Arc<String>,
198}
199
200impl HarborLayer {
201 pub fn new(project_id: impl Into<String>) -> Self {
202 Self {
203 project_id: Arc::new(project_id.into()),
204 validate_url: Arc::new(DEFAULT_VALIDATE_URL.to_string()),
205 }
206 }
207
208 pub fn with_url(mut self, url: impl Into<String>) -> Self {
209 self.validate_url = Arc::new(url.into());
210 self
211 }
212}