use axum::{ extract::{Request, State}, http::{HeaderMap, StatusCode, header::AUTHORIZATION}, middleware::Next, response::Response, }; use spacetime_client::ExternalApiKeyAuthenticateRecordInput; use tracing::warn; use crate::{ editor_project::current_utc_micros, external_api_keys::{hash_external_api_key, map_external_api_key_error}, http_error::AppError, request_context::RequestContext, state::AppState, }; #[derive(Clone, Debug)] pub struct ExternalApiPrincipal { owner_user_id: String, key_id: String, scopes: Vec, } impl ExternalApiPrincipal { pub fn owner_user_id(&self) -> &str { self.owner_user_id.as_str() } pub fn key_id(&self) -> &str { self.key_id.as_str() } pub fn has_scope(&self, scope: &str) -> bool { self.scopes.iter().any(|item| item == scope) } } pub async fn require_external_api_key( State(state): State, mut request: Request, next: Next, ) -> Result { let request_id = request .extensions() .get::() .map(|context| context.request_id().to_string()) .unwrap_or_else(|| "unknown".to_string()); let raw_key = extract_external_api_bearer(request.headers())?; let key = state .spacetime_client() .authenticate_external_api_key(ExternalApiKeyAuthenticateRecordInput { key_hash: hash_external_api_key(raw_key.as_str()), used_at_micros: current_utc_micros(), }) .await .map_err(|error| { warn!( %request_id, error = %error, "外部 API Key 校验失败" ); map_external_api_key_error(error) })?; let principal = ExternalApiPrincipal { owner_user_id: key.owner_user_id, key_id: key.key_id, scopes: key.scopes, }; request.extensions_mut().insert(principal.clone()); let mut response = next.run(request).await; response.extensions_mut().insert(principal); Ok(response) } fn extract_external_api_bearer(headers: &HeaderMap) -> Result { let authorization = headers .get(AUTHORIZATION) .and_then(|value| value.to_str().ok()) .map(str::trim) .ok_or_else(|| AppError::from_status(StatusCode::UNAUTHORIZED))?; authorization .strip_prefix("Bearer ") .or_else(|| authorization.strip_prefix("bearer ")) .map(str::trim) .filter(|token| !token.is_empty()) .map(ToOwned::to_owned) .ok_or_else(|| AppError::from_status(StatusCode::UNAUTHORIZED)) }