Files
hms/crates/erp-health/src/oauth/handler.rs
iven 6841c45846 fix(security): 文件上传 MIME 白名单 + OAuth JWT 密钥路径统一
P0 #1: 媒体文件上传增加 MIME 类型白名单校验(jpeg/png/gif/webp/svg/mp4/webm/pdf)
       和文件大小限制(10MB),扩展名使用白名单清理防止路径遍历攻击。
P0 #2: OAuth JWT 密钥从环境变量改为 State 注入,消除运行时 env::var 依赖,
       FHIR 路由中间件使用闭包捕获 jwt_secret 保持类型安全。
2026-05-17 12:40:02 +08:00

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,
})))
}