perf(struct): ddd

This commit is contained in:
2026-02-03 17:31:08 +08:00
parent 202b5eaad5
commit 4a071bd7c8
64 changed files with 1214 additions and 1189 deletions

View File

@@ -0,0 +1,270 @@
use axum::{
Router,
routing::{get, post, put},
};
use utoipa::OpenApi;
use utoipa_scalar::{Scalar, Servable};
use crate::constants::CANONICAL_BASE;
use crate::docs::ApiDoc;
use crate::middleware as core_middleware;
use crate::presentation::http::handlers::{
app, auth, authorization, client, jwks, permission, platform, role, sso, tenant, user,
};
use crate::presentation::http::state::AppState;
pub fn routes() -> Vec<(&'static str, &'static str)> {
vec![
("GET", "/.well-known/jwks.json"),
("POST", "/tenants/register"),
("POST", "/auth/register"),
("POST", "/auth/login"),
("POST", "/auth/login-code"),
("POST", "/auth/refresh"),
("POST", "/auth/logout"),
("POST", "/auth/code2token"),
("POST", "/internal/auth/code2token"),
("GET", "/me/permissions"),
("POST", "/authorize/check"),
("GET", "/users"),
("GET", "/users/{id}"),
("PATCH", "/users/{id}"),
("DELETE", "/users/{id}"),
("POST", "/users/me/password/reset"),
("POST", "/users/{id}/password/reset"),
("GET", "/users/{id}/roles"),
("PUT", "/users/{id}/roles"),
("GET", "/permissions"),
("GET", "/roles"),
("POST", "/roles"),
("GET", "/roles/{id}"),
("PATCH", "/roles/{id}"),
("DELETE", "/roles/{id}"),
("POST", "/roles/{id}/permissions/grant"),
("POST", "/roles/{id}/permissions/revoke"),
("POST", "/roles/{id}/users/grant"),
("POST", "/roles/{id}/users/revoke"),
("GET", "/platform/tenants/{tenant_id}/enabled-apps"),
("PUT", "/platform/tenants/{tenant_id}/enabled-apps"),
("GET", "/platform/clients"),
("POST", "/platform/clients"),
("PUT", "/platform/clients/{client_id}/redirect-uris"),
("POST", "/platform/clients/{client_id}/rotate-secret"),
("GET", "/platform/apps"),
("POST", "/platform/apps"),
("GET", "/platform/apps/{app_id}"),
("PATCH", "/platform/apps/{app_id}"),
("DELETE", "/platform/apps/{app_id}"),
("POST", "/platform/apps/{app_id}/status-change-requests"),
("GET", "/platform/app-status-change-requests"),
(
"POST",
"/platform/app-status-change-requests/{request_id}/approve",
),
(
"POST",
"/platform/app-status-change-requests/{request_id}/reject",
),
]
}
pub fn build_app(state: AppState) -> Router {
let tenant_required_auth = Router::new()
.route(
"/register",
post(auth::register_handler)
.layer(core_middleware::rate_limit::register_rate_limiter())
.layer(axum::middleware::from_fn(
core_middleware::rate_limit::log_rate_limit_register,
)),
)
.route(
"/login",
post(auth::login_handler)
.layer(core_middleware::rate_limit::login_rate_limiter())
.layer(axum::middleware::from_fn(
core_middleware::rate_limit::log_rate_limit_login,
)),
)
.route(
"/login-code",
post(sso::login_code_handler)
.layer(core_middleware::rate_limit::login_rate_limiter())
.layer(axum::middleware::from_fn(
core_middleware::rate_limit::log_rate_limit_login,
)),
)
.route("/code2token", post(sso::code2token_handler))
.layer(axum::middleware::from_fn(
core_middleware::auth_tenant::validate_auth_tenant,
));
let no_tenant_auth = Router::new().route(
"/refresh",
post(auth::refresh_handler)
.layer(core_middleware::rate_limit::login_rate_limiter())
.layer(axum::middleware::from_fn(
core_middleware::rate_limit::log_rate_limit_login,
)),
);
let public_v1 = Router::new()
.route("/.well-known/jwks.json", get(jwks::jwks_handler))
.route("/tenants/register", post(tenant::create_tenant_handler))
.route(
"/internal/auth/code2token",
post(sso::internal_code2token_handler),
)
.nest(
"/auth",
Router::new()
.merge(tenant_required_auth)
.merge(no_tenant_auth),
)
.with_state(state.clone());
let auth_cfg = core_middleware::auth::AuthMiddlewareConfig {
skip_exact_paths: vec![],
skip_path_prefixes: vec![],
jwt: auth_kit::jwt::JwtVerifyConfig::rs256_from_pem(
"iam-service",
&crate::utils::keys::get_keys().public_pem,
)
.expect("invalid JWT_PUBLIC_KEY_PEM"),
};
let tenant_cfg = core_middleware::TenantMiddlewareConfig {
skip_exact_paths: vec![],
skip_path_prefixes: vec![],
};
let protected_v1 = Router::new()
.route("/auth/logout", post(auth::logout_handler))
.route("/me/permissions", get(authorization::my_permissions_handler))
.route(
"/authorize/check",
post(authorization::authorization_check_handler),
)
.route("/users", get(user::list_users_handler))
.route(
"/users/me/password/reset",
post(user::reset_my_password_handler),
)
.route("/permissions", get(permission::list_permissions_handler))
.route(
"/users/{id}",
get(user::get_user_handler)
.patch(user::update_user_handler)
.delete(user::delete_user_handler),
)
.route(
"/users/{id}/password/reset",
post(user::reset_user_password_handler),
)
.route(
"/users/{id}/roles",
get(user::list_user_roles_handler).put(user::set_user_roles_handler),
)
.route(
"/roles",
get(role::list_roles_handler).post(role::create_role_handler),
)
.route(
"/roles/{id}",
get(role::get_role_handler)
.patch(role::update_role_handler)
.delete(role::delete_role_handler),
)
.route(
"/roles/{id}/permissions/grant",
post(role::grant_role_permissions_handler),
)
.route(
"/roles/{id}/permissions/revoke",
post(role::revoke_role_permissions_handler),
)
.route("/roles/{id}/users/grant", post(role::grant_role_users_handler))
.route(
"/roles/{id}/users/revoke",
post(role::revoke_role_users_handler),
)
.route(
"/tenants/me",
get(tenant::get_tenant_handler)
.patch(tenant::update_tenant_handler)
.delete(tenant::delete_tenant_handler),
)
.route("/tenants/me/status", post(tenant::update_tenant_status_handler))
.layer(axum::middleware::from_fn_with_state(
auth_cfg.clone(),
core_middleware::auth::authenticate_with_config,
))
.layer(axum::middleware::from_fn_with_state(
tenant_cfg.clone(),
core_middleware::resolve_tenant_with_config,
))
.with_state(state.clone());
let platform_v1 = Router::new()
.route(
"/platform/tenants/{tenant_id}/enabled-apps",
get(platform::get_tenant_enabled_apps_handler)
.put(platform::set_tenant_enabled_apps_handler),
)
.route(
"/platform/clients",
get(client::list_clients_handler).post(client::create_client_handler),
)
.route(
"/platform/clients/{client_id}/rotate-secret",
post(client::rotate_client_secret_handler),
)
.route(
"/platform/clients/{client_id}/redirect-uris",
put(client::update_client_redirect_uris_handler),
)
.route("/platform/apps", get(app::list_apps_handler).post(app::create_app_handler))
.route(
"/platform/apps/{app_id}",
get(app::get_app_handler)
.patch(app::update_app_handler)
.delete(app::delete_app_handler),
)
.route(
"/platform/apps/{app_id}/status-change-requests",
post(app::request_app_status_change_handler),
)
.route(
"/platform/app-status-change-requests",
get(app::list_app_status_change_requests_handler),
)
.route(
"/platform/app-status-change-requests/{request_id}/approve",
post(app::approve_app_status_change_handler),
)
.route(
"/platform/app-status-change-requests/{request_id}/reject",
post(app::reject_app_status_change_handler),
)
.layer(axum::middleware::from_fn_with_state(
auth_cfg,
core_middleware::auth::authenticate_with_config,
))
.with_state(state.clone());
let v1 = Router::new()
.merge(public_v1)
.merge(protected_v1)
.merge(platform_v1)
.layer(axum::middleware::from_fn(
common_telemetry::axum_middleware::trace_http_request,
));
Router::new()
.route(
"/favicon.ico",
get(|| async { axum::http::StatusCode::NO_CONTENT }),
)
.merge(Scalar::with_url("/scalar", ApiDoc::openapi()))
.nest(CANONICAL_BASE, v1)
.with_state(state)
}

View 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!({})))
}

View 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!({})))
}

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

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

View 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);
}
}

View 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;

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

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

View 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!({})))
}

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

View 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())
}

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

View File

@@ -0,0 +1,3 @@
pub mod api;
pub mod handlers;
pub mod state;

View File

@@ -0,0 +1,23 @@
use crate::application::services::{
AppService, AuthService, AuthorizationService, ClientService, PermissionService, RoleService,
TenantService, UserService,
};
use crate::domain::repositories::tenant_config_repo::TenantConfigRepo;
use redis::aio::ConnectionManager;
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub auth_service: AuthService,
pub client_service: ClientService,
pub user_service: UserService,
pub role_service: RoleService,
pub tenant_service: TenantService,
pub authorization_service: AuthorizationService,
pub app_service: AppService,
pub permission_service: PermissionService,
pub redis: ConnectionManager,
pub auth_code_jwt_secret: String,
pub tenant_config_repo: Arc<dyn TenantConfigRepo>,
}

1
src/presentation/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod http;