P0 #1: 媒体文件上传增加 MIME 类型白名单校验(jpeg/png/gif/webp/svg/mp4/webm/pdf) 和文件大小限制(10MB),扩展名使用白名单清理防止路径遍历攻击。 P0 #2: OAuth JWT 密钥从环境变量改为 State 注入,消除运行时 env::var 依赖, FHIR 路由中间件使用闭包捕获 jwt_secret 保持类型安全。
128 lines
4.3 KiB
Rust
128 lines
4.3 KiB
Rust
use axum::{
|
|
Extension, Json,
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
};
|
|
use erp_core::error::AppError;
|
|
use erp_core::rbac::require_permission;
|
|
use erp_core::types::TenantContext;
|
|
use uuid::Uuid;
|
|
|
|
use crate::oauth::dto::*;
|
|
use crate::oauth::error::OAuthError;
|
|
use crate::oauth::service::OAuthService;
|
|
use crate::state::HealthState;
|
|
|
|
/// POST /oauth/token — RFC 6749 Client Credentials Grant
|
|
pub async fn token(
|
|
State(state): State<HealthState>,
|
|
Json(req): Json<TokenRequest>,
|
|
) -> Result<(StatusCode, Json<TokenResponse>), (StatusCode, Json<TokenErrorResponse>)> {
|
|
let jwt_secret = &state.jwt_secret;
|
|
|
|
match OAuthService::token(&state.db, &req, jwt_secret).await {
|
|
Ok(resp) => Ok((StatusCode::OK, Json(resp))),
|
|
Err(OAuthError::InvalidClient | OAuthError::ClientInactive) => Err((
|
|
StatusCode::UNAUTHORIZED,
|
|
Json(TokenErrorResponse::invalid_client("客户端认证失败")),
|
|
)),
|
|
Err(OAuthError::InvalidScope) => Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(TokenErrorResponse::invalid_scope(
|
|
"请求的 scope 超出允许范围",
|
|
)),
|
|
)),
|
|
Err(OAuthError::UnsupportedGrantType) => Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(TokenErrorResponse::invalid_grant(
|
|
"仅支持 client_credentials",
|
|
)),
|
|
)),
|
|
Err(OAuthError::RateLimitExceeded) => Err((
|
|
StatusCode::TOO_MANY_REQUESTS,
|
|
Json(TokenErrorResponse::invalid_client("速率限制已超出")),
|
|
)),
|
|
Err(e) => {
|
|
tracing::error!(error = %e, "OAuth token 端点内部错误");
|
|
Err((
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(TokenErrorResponse::invalid_client("内部错误")),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// POST /api/v1/health/oauth/clients — 创建合作方
|
|
pub async fn create_client(
|
|
State(state): State<HealthState>,
|
|
Extension(tenant_ctx): Extension<TenantContext>,
|
|
Json(req): Json<CreateApiClientReq>,
|
|
) -> Result<Json<ApiClientResp>, AppError> {
|
|
require_permission(&tenant_ctx, "health.oauth.manage")?;
|
|
OAuthService::create_client(&state.db, tenant_ctx.tenant_id, &req, tenant_ctx.user_id)
|
|
.await
|
|
.map_err(AppError::from)
|
|
.map(Json)
|
|
}
|
|
|
|
/// GET /api/v1/health/oauth/clients — 列出合作方
|
|
pub async fn list_clients(
|
|
State(state): State<HealthState>,
|
|
Extension(tenant_ctx): Extension<TenantContext>,
|
|
) -> Result<Json<Vec<ApiClientListItem>>, AppError> {
|
|
require_permission(&tenant_ctx, "health.oauth.list")?;
|
|
OAuthService::list_clients(&state.db, tenant_ctx.tenant_id)
|
|
.await
|
|
.map_err(AppError::from)
|
|
.map(Json)
|
|
}
|
|
|
|
/// PUT /api/v1/health/oauth/clients/{id} — 更新合作方
|
|
pub async fn update_client(
|
|
State(state): State<HealthState>,
|
|
Extension(tenant_ctx): Extension<TenantContext>,
|
|
Path(id): Path<Uuid>,
|
|
Json(req): Json<UpdateApiClientReq>,
|
|
) -> Result<Json<ApiClientListItem>, AppError> {
|
|
require_permission(&tenant_ctx, "health.oauth.manage")?;
|
|
OAuthService::update_client(
|
|
&state.db,
|
|
tenant_ctx.tenant_id,
|
|
id,
|
|
&req,
|
|
tenant_ctx.user_id,
|
|
)
|
|
.await
|
|
.map_err(AppError::from)
|
|
.map(Json)
|
|
}
|
|
|
|
/// DELETE /api/v1/health/oauth/clients/{id} — 删除合作方
|
|
pub async fn delete_client(
|
|
State(state): State<HealthState>,
|
|
Extension(tenant_ctx): Extension<TenantContext>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<StatusCode, AppError> {
|
|
require_permission(&tenant_ctx, "health.oauth.manage")?;
|
|
OAuthService::delete_client(&state.db, tenant_ctx.tenant_id, id)
|
|
.await
|
|
.map_err(AppError::from)?;
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|
|
|
|
/// POST /api/v1/health/oauth/clients/{id}/regenerate-secret — 重新生成 secret
|
|
pub async fn regenerate_secret(
|
|
State(state): State<HealthState>,
|
|
Extension(tenant_ctx): Extension<TenantContext>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Json<serde_json::Value>, AppError> {
|
|
require_permission(&tenant_ctx, "health.oauth.manage")?;
|
|
let (client_id, plain) = OAuthService::regenerate_secret(&state.db, tenant_ctx.tenant_id, id)
|
|
.await
|
|
.map_err(AppError::from)?;
|
|
Ok(Json(serde_json::json!({
|
|
"client_id": client_id,
|
|
"client_secret": plain,
|
|
})))
|
|
}
|