perf(struct): ddd
This commit is contained in:
359
src/presentation/http/handlers/app.rs
Normal file
359
src/presentation/http/handlers/app.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{
|
||||
App, AppStatusChangeRequest, ApproveAppStatusChangeRequest, CreateAppRequest, ListAppsQuery,
|
||||
RequestAppStatusChangeRequest, UpdateAppRequest,
|
||||
};
|
||||
use crate::presentation::http::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
http::HeaderMap,
|
||||
};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn require_sensitive_token(headers: &HeaderMap) -> Result<(), AppError> {
|
||||
let Some(expected) = std::env::var("IAM_SENSITIVE_ACTION_TOKEN")
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty())
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let provided = headers
|
||||
.get("X-Sensitive-Token")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if provided == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::PermissionDenied(
|
||||
"sensitive:token_required".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/platform/apps",
|
||||
tag = "App",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = CreateAppRequest,
|
||||
responses(
|
||||
(status = 201, description = "App created", body = App),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn create_app_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Json(payload): Json<CreateAppRequest>,
|
||||
) -> Result<AppResponse<App>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:app:write")
|
||||
.await?;
|
||||
let app = state.app_service.create_app(payload, user_id).await?;
|
||||
Ok(AppResponse::created(app))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/platform/apps",
|
||||
tag = "App",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Apps list", body = [App]),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
ListAppsQuery
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn list_apps_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Query(query): Query<ListAppsQuery>,
|
||||
) -> Result<AppResponse<Vec<App>>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:app:read")
|
||||
.await?;
|
||||
let apps = state.app_service.list_apps(query).await?;
|
||||
Ok(AppResponse::ok(apps))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/platform/apps/{app_id}",
|
||||
tag = "App",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "App detail", body = App),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Not found")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("app_id" = String, Path, description = "App id")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn get_app_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Path(app_id): Path<String>,
|
||||
) -> Result<AppResponse<App>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:app:read")
|
||||
.await?;
|
||||
let app = state.app_service.get_app(&app_id).await?;
|
||||
Ok(AppResponse::ok(app))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/platform/apps/{app_id}",
|
||||
tag = "App",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = UpdateAppRequest,
|
||||
responses(
|
||||
(status = 200, description = "Updated", body = App),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Not found")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("app_id" = String, Path, description = "App id")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn update_app_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Path(app_id): Path<String>,
|
||||
Json(payload): Json<UpdateAppRequest>,
|
||||
) -> Result<AppResponse<App>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:app:write")
|
||||
.await?;
|
||||
let app = state
|
||||
.app_service
|
||||
.update_app(&app_id, payload, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(app))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/platform/apps/{app_id}/status-change-requests",
|
||||
tag = "App",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = RequestAppStatusChangeRequest,
|
||||
responses(
|
||||
(status = 201, description = "Request created", body = AppStatusChangeRequest),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Not found")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("app_id" = String, Path, description = "App id")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn request_app_status_change_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Path(app_id): Path<String>,
|
||||
Json(payload): Json<RequestAppStatusChangeRequest>,
|
||||
) -> Result<AppResponse<AppStatusChangeRequest>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:app:write")
|
||||
.await?;
|
||||
let req = state
|
||||
.app_service
|
||||
.request_status_change(&app_id, payload, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::created(req))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/platform/app-status-change-requests",
|
||||
tag = "App",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Requests list", body = [AppStatusChangeRequest]),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("status" = Option<String>, Query, description = "pending/approved/applied/rejected"),
|
||||
("page" = Option<u32>, Query, description = "页码,默认 1"),
|
||||
("page_size" = Option<u32>, Query, description = "每页数量,默认 20,最大 200")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn list_app_status_change_requests_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||
) -> Result<AppResponse<Vec<AppStatusChangeRequest>>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:app:read")
|
||||
.await?;
|
||||
let status = params.get("status").cloned();
|
||||
let page = params.get("page").and_then(|v| v.parse::<u32>().ok());
|
||||
let page_size = params.get("page_size").and_then(|v| v.parse::<u32>().ok());
|
||||
let rows = state
|
||||
.app_service
|
||||
.list_status_change_requests(status, page, page_size)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(rows))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/platform/app-status-change-requests/{request_id}/approve",
|
||||
tag = "App",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = ApproveAppStatusChangeRequest,
|
||||
responses(
|
||||
(status = 200, description = "Approved", body = AppStatusChangeRequest),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Not found")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("request_id" = String, Path, description = "Request id (UUID)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn approve_app_status_change_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Path(request_id): Path<Uuid>,
|
||||
Json(payload): Json<ApproveAppStatusChangeRequest>,
|
||||
) -> Result<AppResponse<AppStatusChangeRequest>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:app:approve")
|
||||
.await?;
|
||||
let row = state
|
||||
.app_service
|
||||
.approve_status_change(request_id, payload.effective_at, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(row))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/platform/app-status-change-requests/{request_id}/reject",
|
||||
tag = "App",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Rejected", body = AppStatusChangeRequest),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Not found")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("request_id" = String, Path, description = "Request id (UUID)"),
|
||||
("reason" = Option<String>, Query, description = "Reject reason")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn reject_app_status_change_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Path(request_id): Path<Uuid>,
|
||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||
) -> Result<AppResponse<AppStatusChangeRequest>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:app:approve")
|
||||
.await?;
|
||||
let reason = params.get("reason").cloned();
|
||||
let row = state
|
||||
.app_service
|
||||
.reject_status_change(request_id, reason, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(row))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/platform/apps/{app_id}",
|
||||
tag = "App",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Deleted"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Not found")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Sensitive-Token" = Option<String>, Header, description = "二次验证令牌(当 IAM_SENSITIVE_ACTION_TOKEN 设置时必填)"),
|
||||
("app_id" = String, Path, description = "App id")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, headers))]
|
||||
pub async fn delete_app_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
headers: HeaderMap,
|
||||
Path(app_id): Path<String>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:app:delete")
|
||||
.await?;
|
||||
require_sensitive_token(&headers)?;
|
||||
state.app_service.delete_app(&app_id, user_id).await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({})))
|
||||
}
|
||||
106
src/presentation/http/handlers/auth.rs
Normal file
106
src/presentation/http/handlers/auth.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use crate::presentation::http::state::AppState;
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{
|
||||
CreateUserRequest, LoginRequest, LoginResponse, RefreshTokenRequest, UserResponse,
|
||||
};
|
||||
use axum::{Json, extract::State};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/auth/register",
|
||||
tag = "Auth",
|
||||
request_body = CreateUserRequest,
|
||||
responses(
|
||||
(status = 201, description = "User created", body = UserResponse),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 429, description = "Too many requests")
|
||||
),
|
||||
params(
|
||||
("X-Tenant-ID" = String, Header, description = "Tenant UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn register_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateUserRequest>,
|
||||
) -> Result<AppResponse<UserResponse>, AppError> {
|
||||
let user = state.auth_service.register(tenant_id, payload).await?;
|
||||
Ok(AppResponse::created(UserResponse {
|
||||
id: user.id,
|
||||
email: user.email.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/auth/login",
|
||||
tag = "Auth",
|
||||
request_body = LoginRequest,
|
||||
responses(
|
||||
(status = 200, description = "Login successful", body = LoginResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 429, description = "Too many requests")
|
||||
),
|
||||
params(
|
||||
("X-Tenant-ID" = String, Header, description = "Tenant UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn login_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> Result<AppResponse<LoginResponse>, AppError> {
|
||||
let response = state.auth_service.login(tenant_id, payload).await?;
|
||||
Ok(AppResponse::ok(response))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/auth/refresh",
|
||||
tag = "Auth",
|
||||
request_body = RefreshTokenRequest,
|
||||
responses(
|
||||
(status = 200, description = "Token refreshed", body = LoginResponse),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn refresh_handler(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<RefreshTokenRequest>,
|
||||
) -> Result<AppResponse<LoginResponse>, AppError> {
|
||||
let response = state
|
||||
.auth_service
|
||||
.refresh_access_token(payload.refresh_token)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(response))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/auth/logout",
|
||||
tag = "Auth",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Logged out"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn logout_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
state.auth_service.logout(user_id).await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({})))
|
||||
}
|
||||
91
src/presentation/http/handlers/authorization.rs
Normal file
91
src/presentation/http/handlers/authorization.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{AuthorizationCheckRequest, AuthorizationCheckResponse};
|
||||
use crate::presentation::http::state::AppState;
|
||||
use axum::{Json, extract::State};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/me/permissions",
|
||||
tag = "Me",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "当前用户权限列表", body = [String]),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn my_permissions_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
) -> Result<AppResponse<Vec<String>>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
|
||||
let permissions = state
|
||||
.authorization_service
|
||||
.list_permissions_for_user(tenant_id, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(permissions))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/authorize/check",
|
||||
tag = "Policy",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = AuthorizationCheckRequest,
|
||||
responses(
|
||||
(status = 200, description = "鉴权校验结果", body = AuthorizationCheckResponse),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "租户不匹配")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, body))]
|
||||
pub async fn authorization_check_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Json(body): Json<AuthorizationCheckRequest>,
|
||||
) -> Result<AppResponse<AuthorizationCheckResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
|
||||
let allowed = match state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, body.permission.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(()) => true,
|
||||
Err(AppError::PermissionDenied(_)) => false,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
Ok(AppResponse::ok(AuthorizationCheckResponse { allowed }))
|
||||
}
|
||||
174
src/presentation/http/handlers/client.rs
Normal file
174
src/presentation/http/handlers/client.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use crate::presentation::http::state::AppState;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{
|
||||
ClientSummary, CreateClientRequest, CreateClientResponse, RotateClientSecretResponse,
|
||||
UpdateClientRedirectUrisRequest,
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/platform/clients",
|
||||
tag = "Client",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = CreateClientRequest,
|
||||
responses(
|
||||
(status = 201, description = "Created", body = CreateClientResponse),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn create_client_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Json(payload): Json<CreateClientRequest>,
|
||||
) -> Result<AppResponse<CreateClientResponse>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:client:write")
|
||||
.await?;
|
||||
|
||||
let secret = state
|
||||
.client_service
|
||||
.create_client(
|
||||
payload.client_id.clone(),
|
||||
payload.name.clone(),
|
||||
payload.redirect_uris.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(AppResponse::created(CreateClientResponse {
|
||||
client_id: payload.client_id,
|
||||
client_secret: secret,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/platform/clients/{client_id}/redirect-uris",
|
||||
tag = "Client",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = UpdateClientRedirectUrisRequest,
|
||||
responses(
|
||||
(status = 200, description = "Updated"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Not found")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("client_id" = String, Path, description = "clientId")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn update_client_redirect_uris_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Path(client_id): Path<String>,
|
||||
Json(payload): Json<UpdateClientRedirectUrisRequest>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:client:write")
|
||||
.await?;
|
||||
|
||||
state
|
||||
.client_service
|
||||
.set_redirect_uris(client_id, payload.redirect_uris)
|
||||
.await?;
|
||||
|
||||
Ok(AppResponse::ok(serde_json::json!({})))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/platform/clients/{client_id}/rotate-secret",
|
||||
tag = "Client",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Rotated", body = RotateClientSecretResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Not found")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("client_id" = String, Path, description = "clientId")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn rotate_client_secret_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Path(client_id): Path<String>,
|
||||
) -> Result<AppResponse<RotateClientSecretResponse>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:client:write")
|
||||
.await?;
|
||||
|
||||
let secret = state.client_service.rotate_secret(client_id.clone()).await?;
|
||||
Ok(AppResponse::ok(RotateClientSecretResponse {
|
||||
client_id,
|
||||
client_secret: secret,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/platform/clients",
|
||||
tag = "Client",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "OK", body = [ClientSummary]),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn list_clients_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
) -> Result<AppResponse<Vec<ClientSummary>>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:client:read")
|
||||
.await?;
|
||||
|
||||
let rows = state.client_service.list_clients().await?;
|
||||
let clients = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(client_id, name, redirect_uris, created_at, updated_at)| ClientSummary {
|
||||
client_id,
|
||||
name,
|
||||
redirect_uris,
|
||||
created_at,
|
||||
updated_at,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
Ok(AppResponse::ok(clients))
|
||||
}
|
||||
94
src/presentation/http/handlers/jwks.rs
Normal file
94
src/presentation/http/handlers/jwks.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Jwks {
|
||||
keys: Vec<Jwk>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Jwk {
|
||||
kty: &'static str,
|
||||
kid: String,
|
||||
#[serde(rename = "use")]
|
||||
use_field: &'static str,
|
||||
alg: &'static str,
|
||||
n: String,
|
||||
e: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExtraJwkKey {
|
||||
kid: String,
|
||||
public_key_pem: String,
|
||||
}
|
||||
|
||||
fn build_jwks(extra_json: Option<&str>) -> Result<Jwks, &'static str> {
|
||||
let keys = crate::utils::keys::get_keys();
|
||||
let mut jwk_keys = vec![Jwk {
|
||||
kty: "RSA",
|
||||
kid: keys.kid.clone(),
|
||||
use_field: "sig",
|
||||
alg: "RS256",
|
||||
n: keys.public_n.clone(),
|
||||
e: keys.public_e.clone(),
|
||||
}];
|
||||
|
||||
if let Some(extra_json) = extra_json {
|
||||
let extra: Vec<ExtraJwkKey> =
|
||||
serde_json::from_str(extra_json).map_err(|_| "Invalid JWT_JWKS_EXTRA_KEYS_JSON")?;
|
||||
for k in extra {
|
||||
let (n, e) = crate::utils::keys::jwk_components_from_public_pem(&k.public_key_pem)
|
||||
.map_err(|_| "Invalid public_key_pem in JWT_JWKS_EXTRA_KEYS_JSON")?;
|
||||
jwk_keys.push(Jwk {
|
||||
kty: "RSA",
|
||||
kid: k.kid,
|
||||
use_field: "sig",
|
||||
alg: "RS256",
|
||||
n,
|
||||
e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Jwks { keys: jwk_keys })
|
||||
}
|
||||
|
||||
pub async fn jwks_handler() -> Response {
|
||||
let extra_json = std::env::var("JWT_JWKS_EXTRA_KEYS_JSON").ok();
|
||||
match build_jwks(extra_json.as_deref()) {
|
||||
Ok(jwks) => (StatusCode::OK, Json(jwks)).into_response(),
|
||||
Err(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_jwks;
|
||||
|
||||
#[test]
|
||||
fn build_jwks_rejects_invalid_extra_json() {
|
||||
assert!(matches!(
|
||||
build_jwks(Some("not-json")),
|
||||
Err("Invalid JWT_JWKS_EXTRA_KEYS_JSON")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_jwks_accepts_extra_key() {
|
||||
let active_public_pem = crate::utils::keys::get_keys().public_pem.clone();
|
||||
let extra_json = serde_json::json!([{
|
||||
"kid": "extra-kid",
|
||||
"public_key_pem": active_public_pem
|
||||
}])
|
||||
.to_string();
|
||||
|
||||
let jwks = build_jwks(Some(&extra_json)).unwrap();
|
||||
assert!(jwks.keys.iter().any(|k| k.kid == "extra-kid"));
|
||||
assert!(jwks.keys.len() >= 2);
|
||||
}
|
||||
}
|
||||
|
||||
11
src/presentation/http/handlers/mod.rs
Normal file
11
src/presentation/http/handlers/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod auth;
|
||||
pub mod app;
|
||||
pub mod authorization;
|
||||
pub mod client;
|
||||
pub mod jwks;
|
||||
pub mod permission;
|
||||
pub mod platform;
|
||||
pub mod role;
|
||||
pub mod sso;
|
||||
pub mod tenant;
|
||||
pub mod user;
|
||||
48
src/presentation/http/handlers/permission.rs
Normal file
48
src/presentation/http/handlers/permission.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{ListPermissionsQuery, Permission};
|
||||
use crate::presentation::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/permissions",
|
||||
tag = "Permission",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Permission list", body = [Permission]),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
ListPermissionsQuery
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn list_permissions_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Query(query): Query<ListPermissionsQuery>,
|
||||
) -> Result<AppResponse<Vec<Permission>>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "role:read")
|
||||
.await?;
|
||||
let rows = state.permission_service.list_permissions(query).await?;
|
||||
Ok(AppResponse::ok(rows))
|
||||
}
|
||||
92
src/presentation/http/handlers/platform.rs
Normal file
92
src/presentation/http/handlers/platform.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{TenantEnabledAppsResponse, UpdateTenantEnabledAppsRequest};
|
||||
use crate::presentation::http::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/platform/tenants/{tenant_id}/enabled-apps",
|
||||
tag = "Tenant",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "获取租户已开通应用列表", body = TenantEnabledAppsResponse),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("tenant_id" = String, Path, description = "租户 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn get_tenant_enabled_apps_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Path(tenant_id): Path<Uuid>,
|
||||
) -> Result<AppResponse<TenantEnabledAppsResponse>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:tenant:enabled_apps:read")
|
||||
.await?;
|
||||
let (enabled_apps, version, updated_at) = state.tenant_service.get_enabled_apps(tenant_id).await?;
|
||||
Ok(AppResponse::ok(TenantEnabledAppsResponse {
|
||||
tenant_id,
|
||||
enabled_apps,
|
||||
version,
|
||||
updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/platform/tenants/{tenant_id}/enabled-apps",
|
||||
tag = "Tenant",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = UpdateTenantEnabledAppsRequest,
|
||||
responses(
|
||||
(status = 200, description = "更新租户已开通应用列表", body = TenantEnabledAppsResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到"),
|
||||
(status = 409, description = "版本冲突")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("tenant_id" = String, Path, description = "租户 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn set_tenant_enabled_apps_handler(
|
||||
State(state): State<AppState>,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
Path(tenant_id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateTenantEnabledAppsRequest>,
|
||||
) -> Result<AppResponse<TenantEnabledAppsResponse>, AppError> {
|
||||
state
|
||||
.authorization_service
|
||||
.require_platform_permission(user_id, "iam:tenant:enabled_apps:write")
|
||||
.await?;
|
||||
let (enabled_apps, version, updated_at) = state
|
||||
.tenant_service
|
||||
.set_enabled_apps(tenant_id, payload.enabled_apps, payload.expected_version, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(TenantEnabledAppsResponse {
|
||||
tenant_id,
|
||||
enabled_apps,
|
||||
version,
|
||||
updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
437
src/presentation/http/handlers/role.rs
Normal file
437
src/presentation/http/handlers/role.rs
Normal file
@@ -0,0 +1,437 @@
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{
|
||||
CreateRoleRequest, RolePermissionsRequest, RoleResponse, RoleUsersRequest, UpdateRoleRequest,
|
||||
};
|
||||
use crate::presentation::http::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/roles",
|
||||
tag = "Role",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = CreateRoleRequest,
|
||||
responses(
|
||||
(status = 201, description = "角色创建成功", body = RoleResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn create_role_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Json(payload): Json<CreateRoleRequest>,
|
||||
) -> Result<AppResponse<RoleResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "role:write")
|
||||
.await?;
|
||||
|
||||
let role = state
|
||||
.role_service
|
||||
.create_role(tenant_id, payload, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::created(RoleResponse {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/roles",
|
||||
tag = "Role",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "角色列表", body = [RoleResponse]),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn list_roles_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
) -> Result<AppResponse<Vec<RoleResponse>>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "role:read")
|
||||
.await?;
|
||||
|
||||
let roles = state.role_service.list_roles(tenant_id).await?;
|
||||
let response = roles
|
||||
.into_iter()
|
||||
.map(|r| RoleResponse {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
})
|
||||
.collect();
|
||||
Ok(AppResponse::ok(response))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/roles/{id}",
|
||||
tag = "Role",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "角色详情", body = RoleResponse),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "角色 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn get_role_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(role_id): Path<Uuid>,
|
||||
) -> Result<AppResponse<RoleResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "role:read")
|
||||
.await?;
|
||||
let role = state.role_service.get_role(tenant_id, role_id).await?;
|
||||
Ok(AppResponse::ok(RoleResponse {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/roles/{id}",
|
||||
tag = "Role",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = UpdateRoleRequest,
|
||||
responses(
|
||||
(status = 200, description = "角色更新成功", body = RoleResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "角色 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn update_role_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(role_id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateRoleRequest>,
|
||||
) -> Result<AppResponse<RoleResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "role:write")
|
||||
.await?;
|
||||
let role = state
|
||||
.role_service
|
||||
.update_role(tenant_id, role_id, payload, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(RoleResponse {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/roles/{id}",
|
||||
tag = "Role",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "角色删除成功"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "角色 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn delete_role_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(role_id): Path<Uuid>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "role:write")
|
||||
.await?;
|
||||
state
|
||||
.role_service
|
||||
.delete_role(tenant_id, role_id, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({})))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/roles/{id}/permissions/grant",
|
||||
tag = "Role",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = RolePermissionsRequest,
|
||||
responses(
|
||||
(status = 200, description = "绑定权限成功"),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "角色 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn grant_role_permissions_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(role_id): Path<Uuid>,
|
||||
Json(payload): Json<RolePermissionsRequest>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "role:write")
|
||||
.await?;
|
||||
state
|
||||
.role_service
|
||||
.grant_permissions_to_role(tenant_id, role_id, payload.permission_codes, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({})))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/roles/{id}/permissions/revoke",
|
||||
tag = "Role",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = RolePermissionsRequest,
|
||||
responses(
|
||||
(status = 200, description = "解绑权限成功"),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "角色 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn revoke_role_permissions_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(role_id): Path<Uuid>,
|
||||
Json(payload): Json<RolePermissionsRequest>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "role:write")
|
||||
.await?;
|
||||
state
|
||||
.role_service
|
||||
.revoke_permissions_from_role(tenant_id, role_id, payload.permission_codes, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({})))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/roles/{id}/users/grant",
|
||||
tag = "Role",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = RoleUsersRequest,
|
||||
responses(
|
||||
(status = 200, description = "批量授予角色成功"),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "角色 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn grant_role_users_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(role_id): Path<Uuid>,
|
||||
Json(payload): Json<RoleUsersRequest>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "user:write")
|
||||
.await?;
|
||||
state
|
||||
.role_service
|
||||
.grant_role_to_users(tenant_id, role_id, payload.user_ids, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({})))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/roles/{id}/users/revoke",
|
||||
tag = "Role",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = RoleUsersRequest,
|
||||
responses(
|
||||
(status = 200, description = "批量回收角色成功"),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "角色 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn revoke_role_users_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(role_id): Path<Uuid>,
|
||||
Json(payload): Json<RoleUsersRequest>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "user:write")
|
||||
.await?;
|
||||
state
|
||||
.role_service
|
||||
.revoke_role_from_users(tenant_id, role_id, payload.user_ids, user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({})))
|
||||
}
|
||||
219
src/presentation/http/handlers/sso.rs
Normal file
219
src/presentation/http/handlers/sso.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use crate::application::use_cases::exchange_code::{ExchangeCodeUseCase, Execute as _};
|
||||
use crate::presentation::http::state::AppState;
|
||||
use crate::middleware::TenantId;
|
||||
use crate::models::{Code2TokenRequest, Code2TokenResponse, LoginCodeRequest, LoginCodeResponse};
|
||||
use anyhow::anyhow;
|
||||
use axum::{Json, extract::State};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
|
||||
use redis::AsyncCommands;
|
||||
use serde::Deserialize;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn map_domain_error(e: crate::domain::DomainError) -> AppError {
|
||||
match e {
|
||||
crate::domain::DomainError::Unauthorized => AppError::AuthError("Unauthorized".into()),
|
||||
crate::domain::DomainError::InvalidArgument(s) => AppError::BadRequest(s),
|
||||
crate::domain::DomainError::Unexpected => AppError::AnyhowError(anyhow!("Unexpected")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, serde::Serialize)]
|
||||
struct AuthCodeClaims {
|
||||
sub: String,
|
||||
tenant_id: String,
|
||||
client_id: Option<String>,
|
||||
redirect_uri: Option<String>,
|
||||
exp: usize,
|
||||
iat: usize,
|
||||
iss: String,
|
||||
jti: String,
|
||||
}
|
||||
|
||||
fn redis_key(jti: &str) -> String {
|
||||
format!("iam:auth_code:{}", jti)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/auth/code2token",
|
||||
tag = "Auth",
|
||||
request_body = Code2TokenRequest,
|
||||
responses(
|
||||
(status = 200, description = "Token issued", body = Code2TokenResponse),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
params(
|
||||
("X-Tenant-ID" = String, Header, description = "Tenant UUID (required for external calls)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn code2token_handler(
|
||||
TenantId(expected_tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<Code2TokenRequest>,
|
||||
) -> Result<AppResponse<Code2TokenResponse>, AppError> {
|
||||
let ok = state
|
||||
.tenant_config_repo
|
||||
.validate_client_pair(
|
||||
expected_tenant_id,
|
||||
&payload.client_id,
|
||||
&payload.client_secret,
|
||||
)
|
||||
.await
|
||||
.map_err(map_domain_error)?;
|
||||
if !ok {
|
||||
return Err(AppError::AuthError("Invalid client credentials".into()));
|
||||
}
|
||||
|
||||
let use_case = ExchangeCodeUseCase {
|
||||
auth_service: state.auth_service.clone(),
|
||||
redis: state.redis.clone(),
|
||||
auth_code_jwt_secret: state.auth_code_jwt_secret.clone(),
|
||||
};
|
||||
let res = use_case.execute(payload).await.map_err(map_domain_error)?;
|
||||
if res.tenant_id != expected_tenant_id {
|
||||
return Err(AppError::AuthError("Invalid code".into()));
|
||||
}
|
||||
|
||||
Ok(AppResponse::ok(Code2TokenResponse {
|
||||
access_token: res.access_token,
|
||||
refresh_token: res.refresh_token,
|
||||
token_type: res.token_type,
|
||||
expires_in: res.expires_in,
|
||||
tenant_id: res.tenant_id.to_string(),
|
||||
user_id: res.user_id.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/internal/auth/code2token",
|
||||
tag = "Auth",
|
||||
request_body = Code2TokenRequest,
|
||||
responses(
|
||||
(status = 200, description = "Token issued", body = Code2TokenResponse),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
params(
|
||||
("X-Internal-Token" = String, Header, description = "Pre-shared internal token"),
|
||||
("X-Tenant-ID" = Option<String>, Header, description = "Optional tenant UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn internal_code2token_handler(
|
||||
headers: axum::http::HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<Code2TokenRequest>,
|
||||
) -> Result<AppResponse<Code2TokenResponse>, AppError> {
|
||||
let expected = std::env::var("INTERNAL_EXCHANGE_PSK")
|
||||
.map_err(|_| AppError::ConfigError("INTERNAL_EXCHANGE_PSK is required".into()))?;
|
||||
let token = headers
|
||||
.get("X-Internal-Token")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
if token != expected {
|
||||
return Err(AppError::AuthError("Invalid internal token".into()));
|
||||
}
|
||||
|
||||
state
|
||||
.client_service
|
||||
.verify_client_secret(&payload.client_id, &payload.client_secret)
|
||||
.await?;
|
||||
|
||||
let use_case = ExchangeCodeUseCase {
|
||||
auth_service: state.auth_service.clone(),
|
||||
redis: state.redis.clone(),
|
||||
auth_code_jwt_secret: state.auth_code_jwt_secret.clone(),
|
||||
};
|
||||
let res = use_case.execute(payload).await.map_err(map_domain_error)?;
|
||||
Ok(AppResponse::ok(Code2TokenResponse {
|
||||
access_token: res.access_token,
|
||||
refresh_token: res.refresh_token,
|
||||
token_type: res.token_type,
|
||||
expires_in: res.expires_in,
|
||||
tenant_id: res.tenant_id.to_string(),
|
||||
user_id: res.user_id.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/auth/login-code",
|
||||
tag = "Auth",
|
||||
request_body = LoginCodeRequest,
|
||||
responses(
|
||||
(status = 200, description = "Code issued", body = LoginCodeResponse),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 429, description = "Too many requests")
|
||||
),
|
||||
params(
|
||||
("X-Tenant-ID" = String, Header, description = "Tenant UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn login_code_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<LoginCodeRequest>,
|
||||
) -> Result<AppResponse<LoginCodeResponse>, AppError> {
|
||||
let redirect_uri = state
|
||||
.client_service
|
||||
.assert_redirect_uri_allowed(&payload.client_id, &payload.redirect_uri)
|
||||
.await?;
|
||||
|
||||
let user = state
|
||||
.auth_service
|
||||
.verify_user_credentials(tenant_id, payload.email, payload.password)
|
||||
.await?;
|
||||
|
||||
let now = chrono::Utc::now().timestamp() as usize;
|
||||
let exp = now + 5 * 60;
|
||||
let jti = Uuid::new_v4().to_string();
|
||||
|
||||
let claims = AuthCodeClaims {
|
||||
sub: user.id.to_string(),
|
||||
tenant_id: user.tenant_id.to_string(),
|
||||
client_id: Some(payload.client_id),
|
||||
redirect_uri: Some(redirect_uri.clone()),
|
||||
exp,
|
||||
iat: now,
|
||||
iss: "iam-service".to_string(),
|
||||
jti: jti.clone(),
|
||||
};
|
||||
|
||||
let header = Header::new(Algorithm::HS256);
|
||||
let code = encode(
|
||||
&header,
|
||||
&claims,
|
||||
&EncodingKey::from_secret(state.auth_code_jwt_secret.as_bytes()),
|
||||
)
|
||||
.map_err(|e| AppError::AnyhowError(anyhow!(e)))?;
|
||||
|
||||
let value = serde_json::json!({
|
||||
"user_id": user.id.to_string(),
|
||||
"tenant_id": user.tenant_id.to_string(),
|
||||
"client_id": claims.client_id,
|
||||
"redirect_uri": claims.redirect_uri
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mut conn = state.redis.clone();
|
||||
let _: () = conn
|
||||
.set_ex(redis_key(&jti), value, 5 * 60)
|
||||
.await
|
||||
.map_err(|e| AppError::AnyhowError(anyhow!(e)))?;
|
||||
|
||||
let mut u = url::Url::parse(&redirect_uri)
|
||||
.map_err(|_| AppError::BadRequest("redirectUri is invalid".into()))?;
|
||||
u.query_pairs_mut().append_pair("code", &code);
|
||||
|
||||
Ok(AppResponse::ok(LoginCodeResponse {
|
||||
redirect_to: u.to_string(),
|
||||
expires_at: exp,
|
||||
}))
|
||||
}
|
||||
239
src/presentation/http/handlers/tenant.rs
Normal file
239
src/presentation/http/handlers/tenant.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{
|
||||
CreateTenantRequest, TenantResponse, UpdateTenantRequest, UpdateTenantStatusRequest,
|
||||
};
|
||||
use crate::presentation::http::state::AppState;
|
||||
use axum::{Json, extract::State, http::HeaderMap};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
|
||||
fn require_sensitive_token(headers: &HeaderMap) -> Result<(), AppError> {
|
||||
let Some(expected) = std::env::var("IAM_SENSITIVE_ACTION_TOKEN")
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty())
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let provided = headers
|
||||
.get("X-Sensitive-Token")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if provided == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::PermissionDenied(
|
||||
"sensitive:token_required".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tenants/register",
|
||||
tag = "Tenant",
|
||||
request_body = CreateTenantRequest,
|
||||
responses(
|
||||
(status = 201, description = "租户创建成功", body = TenantResponse),
|
||||
(status = 400, description = "请求参数错误")
|
||||
),
|
||||
params(
|
||||
("X-Sensitive-Token" = Option<String>, Header, description = "二次验证令牌(当 IAM_SENSITIVE_ACTION_TOKEN 设置时必填)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn create_tenant_handler(
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateTenantRequest>,
|
||||
) -> Result<AppResponse<TenantResponse>, AppError> {
|
||||
require_sensitive_token(&headers)?;
|
||||
let tenant = state.tenant_service.create_tenant(payload).await?;
|
||||
Ok(AppResponse::created(TenantResponse {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
status: tenant.status,
|
||||
config: tenant.config,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/tenants/me",
|
||||
tag = "Tenant",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "获取当前租户信息", body = TenantResponse),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn get_tenant_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
) -> Result<AppResponse<TenantResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "tenant:read")
|
||||
.await?;
|
||||
let tenant = state.tenant_service.get_tenant(tenant_id).await?;
|
||||
Ok(AppResponse::ok(TenantResponse {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
status: tenant.status,
|
||||
config: tenant.config,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/tenants/me",
|
||||
tag = "Tenant",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = UpdateTenantRequest,
|
||||
responses(
|
||||
(status = 200, description = "租户更新成功", body = TenantResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn update_tenant_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Json(payload): Json<UpdateTenantRequest>,
|
||||
) -> Result<AppResponse<TenantResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "tenant:write")
|
||||
.await?;
|
||||
let tenant = state
|
||||
.tenant_service
|
||||
.update_tenant(tenant_id, payload)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(TenantResponse {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
status: tenant.status,
|
||||
config: tenant.config,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tenants/me/status",
|
||||
tag = "Tenant",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = UpdateTenantStatusRequest,
|
||||
responses(
|
||||
(status = 200, description = "租户状态更新成功", body = TenantResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn update_tenant_status_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Json(payload): Json<UpdateTenantStatusRequest>,
|
||||
) -> Result<AppResponse<TenantResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "tenant:write")
|
||||
.await?;
|
||||
let tenant = state
|
||||
.tenant_service
|
||||
.update_tenant_status(tenant_id, payload)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(TenantResponse {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
status: tenant.status,
|
||||
config: tenant.config,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/tenants/me",
|
||||
tag = "Tenant",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "租户删除成功"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn delete_tenant_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
) -> Result<AppResponse<()>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "tenant:write")
|
||||
.await?;
|
||||
state.tenant_service.delete_tenant(tenant_id).await?;
|
||||
Ok(AppResponse::ok_empty())
|
||||
}
|
||||
457
src/presentation/http/handlers/user.rs
Normal file
457
src/presentation/http/handlers/user.rs
Normal file
@@ -0,0 +1,457 @@
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{
|
||||
AdminResetUserPasswordRequest, AdminResetUserPasswordResponse, ResetMyPasswordRequest,
|
||||
RoleResponse, UpdateUserRequest, UpdateUserRolesRequest, UserResponse,
|
||||
};
|
||||
use crate::presentation::http::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
http::HeaderMap,
|
||||
};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use serde::Deserialize;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListUsersQuery {
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
fn require_sensitive_token(headers: &HeaderMap) -> Result<(), AppError> {
|
||||
let Some(expected) = std::env::var("IAM_SENSITIVE_ACTION_TOKEN")
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty())
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let provided = headers
|
||||
.get("X-Sensitive-Token")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if provided == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::PermissionDenied(
|
||||
"sensitive:token_required".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/users",
|
||||
tag = "User",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "用户列表", body = [UserResponse]),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("page" = Option<u32>, Query, description = "页码,默认 1"),
|
||||
("page_size" = Option<u32>, Query, description = "每页数量,默认 20,最大 200")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn list_users_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Query(query): Query<ListUsersQuery>,
|
||||
) -> Result<AppResponse<Vec<UserResponse>>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "user:read")
|
||||
.await?;
|
||||
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
if page == 0 || page_size == 0 || page_size > 200 {
|
||||
return Err(AppError::BadRequest("Invalid pagination parameters".into()));
|
||||
}
|
||||
|
||||
let users = state
|
||||
.user_service
|
||||
.list_users(tenant_id, page, page_size)
|
||||
.await?;
|
||||
let response = users
|
||||
.into_iter()
|
||||
.map(|u| UserResponse {
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
})
|
||||
.collect();
|
||||
Ok(AppResponse::ok(response))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/users/{id}",
|
||||
tag = "User",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "用户详情", body = UserResponse),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "用户 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn get_user_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(target_user_id): Path<Uuid>,
|
||||
) -> Result<AppResponse<UserResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "user:read")
|
||||
.await?;
|
||||
|
||||
let user = state
|
||||
.user_service
|
||||
.get_user_by_id(tenant_id, target_user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(UserResponse {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/users/{id}",
|
||||
tag = "User",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = UpdateUserRequest,
|
||||
responses(
|
||||
(status = 200, description = "用户更新成功", body = UserResponse),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "用户 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn update_user_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(target_user_id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateUserRequest>,
|
||||
) -> Result<AppResponse<UserResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "user:write")
|
||||
.await?;
|
||||
|
||||
let user = state
|
||||
.user_service
|
||||
.update_user(tenant_id, target_user_id, payload)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(UserResponse {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/users/{id}",
|
||||
tag = "User",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "用户删除成功"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "用户 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn delete_user_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(target_user_id): Path<Uuid>,
|
||||
) -> Result<AppResponse<()>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, user_id, "user:write")
|
||||
.await?;
|
||||
state
|
||||
.user_service
|
||||
.delete_user(tenant_id, target_user_id)
|
||||
.await?;
|
||||
Ok(AppResponse::ok_empty())
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/users/{id}/roles",
|
||||
tag = "User",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "用户角色列表", body = [RoleResponse]),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "用户 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn list_user_roles_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id: actor_user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(target_user_id): Path<Uuid>,
|
||||
) -> Result<AppResponse<Vec<RoleResponse>>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, actor_user_id, "user:read")
|
||||
.await?;
|
||||
|
||||
state
|
||||
.user_service
|
||||
.get_user_by_id(tenant_id, target_user_id)
|
||||
.await?;
|
||||
|
||||
let roles = state
|
||||
.role_service
|
||||
.list_roles_for_user(tenant_id, target_user_id)
|
||||
.await?;
|
||||
let response = roles
|
||||
.into_iter()
|
||||
.map(|r| RoleResponse {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
})
|
||||
.collect();
|
||||
Ok(AppResponse::ok(response))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/users/{id}/roles",
|
||||
tag = "User",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = UpdateUserRolesRequest,
|
||||
responses(
|
||||
(status = 200, description = "用户角色更新成功", body = [RoleResponse]),
|
||||
(status = 400, description = "请求参数错误"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "未找到")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "用户 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn set_user_roles_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id: actor_user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Path(target_user_id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateUserRolesRequest>,
|
||||
) -> Result<AppResponse<Vec<RoleResponse>>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, actor_user_id, "user:write")
|
||||
.await?;
|
||||
|
||||
let roles = state
|
||||
.role_service
|
||||
.set_roles_for_user(tenant_id, target_user_id, payload.role_ids)
|
||||
.await?;
|
||||
|
||||
let response = roles
|
||||
.into_iter()
|
||||
.map(|r| RoleResponse {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
})
|
||||
.collect();
|
||||
Ok(AppResponse::ok(response))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/users/me/password/reset",
|
||||
tag = "User",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = ResetMyPasswordRequest,
|
||||
responses(
|
||||
(status = 200, description = "Password reset success"),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
pub async fn reset_my_password_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
Json(payload): Json<ResetMyPasswordRequest>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.user_service
|
||||
.reset_my_password(
|
||||
tenant_id,
|
||||
user_id,
|
||||
payload.current_password,
|
||||
payload.new_password,
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({})))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/users/{id}/password/reset",
|
||||
tag = "User",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = AdminResetUserPasswordRequest,
|
||||
responses(
|
||||
(status = 200, description = "Password reset", body = AdminResetUserPasswordResponse),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Not found")
|
||||
),
|
||||
params(
|
||||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||||
("X-Sensitive-Token" = Option<String>, Header, description = "二次验证令牌(当 IAM_SENSITIVE_ACTION_TOKEN 设置时必填)"),
|
||||
("X-Tenant-ID" = String, Header, description = "租户 UUID(可选,若提供需与 Token 中 tenant_id 一致)"),
|
||||
("id" = String, Path, description = "用户 UUID")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, headers, payload))]
|
||||
pub async fn reset_user_password_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
State(state): State<AppState>,
|
||||
AuthContext {
|
||||
tenant_id: auth_tenant_id,
|
||||
user_id: actor_user_id,
|
||||
..
|
||||
}: AuthContext,
|
||||
headers: HeaderMap,
|
||||
Path(target_user_id): Path<Uuid>,
|
||||
Json(payload): Json<AdminResetUserPasswordRequest>,
|
||||
) -> Result<AppResponse<AdminResetUserPasswordResponse>, AppError> {
|
||||
if auth_tenant_id != tenant_id {
|
||||
return Err(AppError::PermissionDenied("tenant:mismatch".into()));
|
||||
}
|
||||
state
|
||||
.authorization_service
|
||||
.require_permission(tenant_id, actor_user_id, "user:password:reset:any")
|
||||
.await?;
|
||||
require_sensitive_token(&headers)?;
|
||||
let temp = state
|
||||
.user_service
|
||||
.reset_user_password_as_admin(tenant_id, actor_user_id, target_user_id, payload.length)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(AdminResetUserPasswordResponse {
|
||||
temporary_password: temp,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user