Files
Genarrative/server-rs/crates/api-server/src/external_api_auth.rs
T
kdletters 95d6bc2ae7 接入外部 OpenAPI 与 API Key 管理
新增外部编辑器 OpenAPI 路由与 openapi.json 导出

新增账号级 API Key 表、鉴权、创建、列表和撤销链路

新增个人中心开发者 API Key 管理弹窗

补充前端契约、客户端方法、测试与项目文档
2026-06-19 15:31:53 +08:00

92 lines
2.6 KiB
Rust

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<String>,
}
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<AppState>,
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
let request_id = request
.extensions()
.get::<RequestContext>()
.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<String, AppError> {
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))
}