fix(handlers): add handlers
This commit is contained in:
67
src/handlers/auth.rs
Normal file
67
src/handlers/auth.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use crate::handlers::AppState;
|
||||
use crate::middleware::TenantId;
|
||||
use crate::models::{CreateUserRequest, LoginRequest, LoginResponse, 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(
|
||||
// 1. 自动注入 TenantId (由中间件解析)
|
||||
TenantId(tenant_id): TenantId,
|
||||
// 2. 获取全局状态中的 Service
|
||||
State(state): State<AppState>,
|
||||
// 3. 获取 Body
|
||||
Json(payload): Json<CreateUserRequest>,
|
||||
) -> Result<AppResponse<UserResponse>, AppError> {
|
||||
let user = state.auth_service.register(tenant_id, payload).await?;
|
||||
|
||||
// 转换为 Response DTO (隐藏密码等敏感信息)
|
||||
let response = UserResponse {
|
||||
id: user.id,
|
||||
email: user.email.clone(),
|
||||
};
|
||||
|
||||
Ok(AppResponse::created(response))
|
||||
}
|
||||
|
||||
/// 登录接口
|
||||
#[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))
|
||||
}
|
||||
59
src/handlers/authorization.rs
Normal file
59
src/handlers/authorization.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::handlers::AppState;
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use axum::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))]
|
||||
/// 查询当前登录用户在当前租户下的权限编码列表。
|
||||
///
|
||||
/// 用途:
|
||||
/// - 快速自查当前令牌是否携带期望的权限(便于联调与排障)。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
/// - Header `X-Tenant-ID`(可选;若提供需与 Token 中 tenant_id 一致,否则返回 403)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:权限字符串数组(如 `user:read`)
|
||||
///
|
||||
/// 异常:
|
||||
/// - `401`:未携带或无法解析访问令牌
|
||||
/// - `403`:租户不匹配或无权访问
|
||||
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))
|
||||
}
|
||||
@@ -1,48 +1,28 @@
|
||||
use crate::middleware::TenantId;
|
||||
use crate::models::{CreateUserRequest, UserResponse};
|
||||
use crate::services::AuthService;
|
||||
use axum::{Json, extract::State};
|
||||
use common_telemetry::AppError; // 引入刚刚写的中间件类型
|
||||
pub mod authorization;
|
||||
pub mod auth;
|
||||
pub mod role;
|
||||
pub mod tenant;
|
||||
pub mod user;
|
||||
|
||||
use crate::services::{AuthService, AuthorizationService, RoleService, TenantService, UserService};
|
||||
|
||||
pub use auth::{login_handler, register_handler};
|
||||
pub use authorization::my_permissions_handler;
|
||||
pub use role::{create_role_handler, list_roles_handler};
|
||||
pub use tenant::{
|
||||
create_tenant_handler, delete_tenant_handler, get_tenant_handler, update_tenant_handler,
|
||||
update_tenant_status_handler,
|
||||
};
|
||||
pub use user::{
|
||||
delete_user_handler, get_user_handler, list_users_handler, update_user_handler,
|
||||
};
|
||||
|
||||
// 状态对象,包含 Service
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub auth_service: AuthService,
|
||||
}
|
||||
|
||||
/// 注册接口
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/auth/register",
|
||||
request_body = CreateUserRequest,
|
||||
responses(
|
||||
(status = 201, description = "User created", body = UserResponse),
|
||||
(status = 400, description = "Bad request")
|
||||
),
|
||||
params(
|
||||
("X-Tenant-ID" = String, Header, description = "Tenant UUID")
|
||||
)
|
||||
)]
|
||||
pub async fn register_handler(
|
||||
// 1. 自动注入 TenantId (由中间件解析)
|
||||
TenantId(tenant_id): TenantId,
|
||||
// 2. 获取全局状态中的 Service
|
||||
State(state): State<AppState>,
|
||||
// 3. 获取 Body
|
||||
Json(payload): Json<CreateUserRequest>,
|
||||
) -> Result<Json<UserResponse>, AppError> {
|
||||
let user = state
|
||||
.auth_service
|
||||
.register(tenant_id, payload)
|
||||
.await
|
||||
.map_err(AppError::BadRequest)?;
|
||||
|
||||
// 转换为 Response DTO (隐藏密码等敏感信息)
|
||||
let response = UserResponse {
|
||||
id: user.id,
|
||||
email: user.email.clone(),
|
||||
// ...
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
pub user_service: UserService,
|
||||
pub role_service: RoleService,
|
||||
pub tenant_service: TenantService,
|
||||
pub authorization_service: AuthorizationService,
|
||||
}
|
||||
|
||||
132
src/handlers/role.rs
Normal file
132
src/handlers/role.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use crate::handlers::AppState;
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{CreateRoleRequest, RoleResponse};
|
||||
use axum::{Json, extract::State};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
|
||||
#[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))]
|
||||
/// 在当前租户下创建角色。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 角色归属到当前租户(由 `TenantId` 决定),禁止跨租户写入。
|
||||
/// - 需要具备 `role:write` 权限,否则返回 403。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
/// - Body `CreateRoleRequest`(必填)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `201`:返回新建角色信息(含 `id`)
|
||||
///
|
||||
/// 异常:
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
/// - `400`:请求参数错误
|
||||
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).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))]
|
||||
/// 查询当前租户下的角色列表。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 仅返回当前租户角色;若 `X-Tenant-ID` 与 Token 不一致则返回 403。
|
||||
/// - 需要具备 `role:read` 权限。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:角色列表
|
||||
///
|
||||
/// 异常:
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
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))
|
||||
}
|
||||
294
src/handlers/tenant.rs
Normal file
294
src/handlers/tenant.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use crate::handlers::AppState;
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{
|
||||
CreateTenantRequest, TenantResponse, UpdateTenantRequest, UpdateTenantStatusRequest,
|
||||
};
|
||||
use axum::{Json, extract::State};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use tracing::instrument;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tenants/register",
|
||||
tag = "Tenant",
|
||||
request_body = CreateTenantRequest,
|
||||
responses(
|
||||
(status = 201, description = "租户创建成功", body = TenantResponse),
|
||||
(status = 400, description = "请求参数错误")
|
||||
)
|
||||
)]
|
||||
#[instrument(skip(state, payload))]
|
||||
/// 创建租户(公开接口)。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 新租户默认 `status=active`。
|
||||
/// - `config` 未提供时默认 `{}`。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Body `CreateTenantRequest`(必填)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `201`:返回新建租户信息(含 `id`)
|
||||
///
|
||||
/// 异常:
|
||||
/// - `400`:请求参数错误
|
||||
pub async fn create_tenant_handler(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateTenantRequest>,
|
||||
) -> Result<AppResponse<TenantResponse>, AppError> {
|
||||
let tenant = state.tenant_service.create_tenant(payload).await?;
|
||||
let response = TenantResponse {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
status: tenant.status,
|
||||
config: tenant.config,
|
||||
};
|
||||
Ok(AppResponse::created(response))
|
||||
}
|
||||
|
||||
#[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))]
|
||||
/// 获取当前登录用户所属租户的信息。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 若同时提供 `X-Tenant-ID` 与 Token 中租户不一致,返回 403(tenant:mismatch)。
|
||||
/// - 需要具备 `tenant:read` 权限。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
/// - Header `X-Tenant-ID`(可选;若提供需与 Token 一致)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:租户信息
|
||||
///
|
||||
/// 异常:
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
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?;
|
||||
let response = TenantResponse {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
status: tenant.status,
|
||||
config: tenant.config,
|
||||
};
|
||||
Ok(AppResponse::ok(response))
|
||||
}
|
||||
|
||||
#[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))]
|
||||
/// 更新当前租户的基础信息(名称 / 配置)。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 只允许更新当前登录租户;租户不一致返回 403。
|
||||
/// - 需要具备 `tenant:write` 权限。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
/// - Body `UpdateTenantRequest`:`name` / `config` 为可选字段,未提供则保持不变
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:更新后的租户信息
|
||||
///
|
||||
/// 异常:
|
||||
/// - `400`:请求参数错误
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
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?;
|
||||
let response = TenantResponse {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
status: tenant.status,
|
||||
config: tenant.config,
|
||||
};
|
||||
Ok(AppResponse::ok(response))
|
||||
}
|
||||
|
||||
#[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))]
|
||||
/// 更新当前租户状态(如 active / disabled)。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 只允许更新当前登录租户;租户不一致返回 403。
|
||||
/// - 需要具备 `tenant:write` 权限。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
/// - Body `UpdateTenantStatusRequest.status`(必填)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:更新后的租户信息
|
||||
///
|
||||
/// 异常:
|
||||
/// - `400`:请求参数错误
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
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?;
|
||||
let response = TenantResponse {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
status: tenant.status,
|
||||
config: tenant.config,
|
||||
};
|
||||
Ok(AppResponse::ok(response))
|
||||
}
|
||||
|
||||
#[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))]
|
||||
/// 删除当前租户。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 只允许删除当前登录租户;租户不一致返回 403。
|
||||
/// - 需要具备 `tenant:write` 权限。
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:删除成功(空响应)
|
||||
///
|
||||
/// 异常:
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
/// - `404`:租户不存在
|
||||
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())
|
||||
}
|
||||
293
src/handlers/user.rs
Normal file
293
src/handlers/user.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
use crate::handlers::AppState;
|
||||
use crate::middleware::TenantId;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::models::{UpdateUserRequest, UserResponse};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
};
|
||||
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>,
|
||||
}
|
||||
|
||||
#[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))]
|
||||
/// 分页查询当前租户下的用户列表。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 仅返回当前租户用户;租户不一致返回 403。
|
||||
/// - 需要具备 `user:read` 权限。
|
||||
/// - 分页参数约束:`page>=1`,`page_size` 范围 `1..=200`。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
/// - Query `page` / `page_size`(可选)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:用户列表
|
||||
///
|
||||
/// 异常:
|
||||
/// - `400`:分页参数非法
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
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))]
|
||||
/// 根据用户 ID 查询用户详情。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 仅允许查询当前租户用户;租户不一致返回 403。
|
||||
/// - 需要具备 `user:read` 权限。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Path `id`:用户 UUID
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:用户信息
|
||||
///
|
||||
/// 异常:
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
/// - `404`:用户不存在
|
||||
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))]
|
||||
/// 更新指定用户信息(目前支持更新邮箱)。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 仅允许更新当前租户用户;租户不一致返回 403。
|
||||
/// - 需要具备 `user:write` 权限。
|
||||
/// - `UpdateUserRequest` 中未提供的字段保持不变。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Path `id`:用户 UUID
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
/// - Body `UpdateUserRequest`(必填)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:更新后的用户信息
|
||||
///
|
||||
/// 异常:
|
||||
/// - `400`:请求参数错误
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
/// - `404`:用户不存在
|
||||
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))]
|
||||
/// 删除指定用户。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 仅允许删除当前租户用户;租户不一致返回 403。
|
||||
/// - 需要具备 `user:write` 权限。
|
||||
///
|
||||
/// 输入:
|
||||
/// - Path `id`:用户 UUID
|
||||
/// - Header `Authorization: Bearer <access_token>`(必填)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `200`:删除成功(空响应)
|
||||
///
|
||||
/// 异常:
|
||||
/// - `401`:未认证
|
||||
/// - `403`:租户不匹配或无权限
|
||||
/// - `404`:用户不存在
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user