feat: 添加用户权限洞察查询与示例
This commit is contained in:
@@ -28,6 +28,13 @@
|
|||||||
|
|
||||||
## 4. 安全与合规
|
## 4. 安全与合规
|
||||||
- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。
|
- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。
|
||||||
|
- [ ] 现状梳理:租户解析/过滤已具备(TenantResolutionMiddleware、TenantAwareDbContext),JWT 已写入 roles/permissions/tenant_id(JwtTokenService),PermissionAuthorize 已在 Admin API 使用,CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询,Swagger 缺少示例与多租户示例。
|
||||||
|
- [ ] 差距与步骤:
|
||||||
|
- [ ] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤(TenantAwareDbContext 或 Dapper 参数化)。
|
||||||
|
- [ ] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。
|
||||||
|
- [ ] 为洞察接口和 /auth/profile 增加 Swagger 示例,包含 tenant_id、roles、permissions,展示 Bearer 示例与租户 Header 示例。
|
||||||
|
- [ ] 若用 Dapper 读侧,SQL 必须参数化并显式过滤 tenant_id。
|
||||||
|
- [ ] 计划顺序:Step A 设计应用层洞察 DTO/Query;Step B Admin API 只读端点(Authorize/PermissionAuthorize);Step C Swagger 示例扩展;Step D 校验租户过滤与忽略路径配置。
|
||||||
- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。
|
- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。
|
||||||
- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。
|
- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。
|
||||||
- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。
|
- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。
|
||||||
|
|||||||
@@ -55,6 +55,29 @@ public sealed class AuthController(IAdminAuthService authService) : BaseApiContr
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前用户信息
|
/// 获取当前用户信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 示例:
|
||||||
|
/// <code>
|
||||||
|
/// GET /api/admin/v1/auth/profile
|
||||||
|
/// Header: Authorization: Bearer <JWT>
|
||||||
|
/// 响应:
|
||||||
|
/// {
|
||||||
|
/// "success": true,
|
||||||
|
/// "code": 200,
|
||||||
|
/// "message": "操作成功",
|
||||||
|
/// "data": {
|
||||||
|
/// "userId": "900123456789012345",
|
||||||
|
/// "account": "admin@tenant1",
|
||||||
|
/// "displayName": "租户管理员",
|
||||||
|
/// "tenantId": "100000000000000001",
|
||||||
|
/// "merchantId": null,
|
||||||
|
/// "roles": ["TenantAdmin"],
|
||||||
|
/// "permissions": ["identity:permission:read", "merchant:read", "order:read"],
|
||||||
|
/// "avatar": "https://cdn.example.com/avatar.png"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </remarks>
|
||||||
[HttpGet("profile")]
|
[HttpGet("profile")]
|
||||||
[PermissionAuthorize("identity:profile:read")]
|
[PermissionAuthorize("identity:profile:read")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<CurrentUserProfile>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<CurrentUserProfile>), StatusCodes.Status200OK)]
|
||||||
@@ -70,4 +93,41 @@ public sealed class AuthController(IAdminAuthService authService) : BaseApiContr
|
|||||||
var profile = await authService.GetProfileAsync(userId, cancellationToken);
|
var profile = await authService.GetProfileAsync(userId, cancellationToken);
|
||||||
return ApiResponse<CurrentUserProfile>.Ok(profile);
|
return ApiResponse<CurrentUserProfile>.Ok(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询指定用户的角色与权限概览(当前租户范围)。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 示例:
|
||||||
|
/// <code>
|
||||||
|
/// GET /api/admin/v1/auth/permissions/900123456789012346
|
||||||
|
/// Header: Authorization: Bearer <JWT>
|
||||||
|
/// 响应:
|
||||||
|
/// {
|
||||||
|
/// "success": true,
|
||||||
|
/// "code": 200,
|
||||||
|
/// "data": {
|
||||||
|
/// "userId": "900123456789012346",
|
||||||
|
/// "tenantId": "100000000000000001",
|
||||||
|
/// "merchantId": "200000000000000001",
|
||||||
|
/// "account": "ops.manager",
|
||||||
|
/// "displayName": "运营经理",
|
||||||
|
/// "roles": ["OpsManager", "Reporter"],
|
||||||
|
/// "permissions": ["delivery:read", "order:read", "payment:read"],
|
||||||
|
/// "createdAt": "2025-12-01T08:30:00Z"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </remarks>
|
||||||
|
[HttpGet("permissions/{userId:long}")]
|
||||||
|
[PermissionAuthorize("identity:permission:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<UserPermissionDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<UserPermissionDto>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<UserPermissionDto>> GetUserPermissions(long userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await authService.GetUserPermissionsAsync(userId, cancellationToken);
|
||||||
|
return result is null
|
||||||
|
? ApiResponse<UserPermissionDto>.Error(ErrorCodes.NotFound, "用户不存在或不属于当前租户")
|
||||||
|
: ApiResponse<UserPermissionDto>.Ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
using TakeoutSaaS.Application.Identity.Queries;
|
||||||
|
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户权限洞察接口。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/admin/v{version:apiVersion}/users/permissions")]
|
||||||
|
public sealed class UserPermissionsController(IAdminAuthService authService) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询当前租户用户的角色与权限概览。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 示例:
|
||||||
|
/// <code>
|
||||||
|
/// GET /api/admin/v1/users/permissions?keyword=ops&page=1&pageSize=20&sortBy=createdAt&sortDescending=true
|
||||||
|
/// Header: Authorization: Bearer <JWT>
|
||||||
|
/// 响应:
|
||||||
|
/// {
|
||||||
|
/// "success": true,
|
||||||
|
/// "code": 200,
|
||||||
|
/// "data": {
|
||||||
|
/// "items": [
|
||||||
|
/// {
|
||||||
|
/// "userId": "900123456789012346",
|
||||||
|
/// "tenantId": "100000000000000001",
|
||||||
|
/// "merchantId": "200000000000000001",
|
||||||
|
/// "account": "ops.manager",
|
||||||
|
/// "displayName": "运营经理",
|
||||||
|
/// "roles": ["OpsManager", "Reporter"],
|
||||||
|
/// "permissions": ["delivery:read", "order:read", "payment:read"],
|
||||||
|
/// "createdAt": "2025-12-01T08:30:00Z"
|
||||||
|
/// }
|
||||||
|
/// ],
|
||||||
|
/// "page": 1,
|
||||||
|
/// "pageSize": 20,
|
||||||
|
/// "totalCount": 1,
|
||||||
|
/// "totalPages": 1
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </remarks>
|
||||||
|
[HttpGet]
|
||||||
|
[PermissionAuthorize("identity:permission:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PagedResult<UserPermissionDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PagedResult<UserPermissionDto>>> Search(
|
||||||
|
[FromQuery] SearchUserPermissionsQuery query,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await authService.SearchUserPermissionsAsync(
|
||||||
|
query.Keyword,
|
||||||
|
query.Page,
|
||||||
|
query.PageSize,
|
||||||
|
query.SortBy,
|
||||||
|
query.SortDescending,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<PagedResult<UserPermissionDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.Identity.Abstractions;
|
namespace TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
|
|
||||||
@@ -13,4 +14,6 @@ public interface IAdminAuthService
|
|||||||
Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
|
Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
|
||||||
Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default);
|
Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default);
|
||||||
Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default);
|
Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default);
|
||||||
|
Task<UserPermissionDto?> GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default);
|
||||||
|
Task<PagedResult<UserPermissionDto>> SearchUserPermissionsAsync(string? keyword, int page, int pageSize, string? sortBy, bool sortDescending, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户权限概览 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UserPermissionDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户 ID(雪花,序列化为字符串)。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long UserId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID(雪花,序列化为字符串)。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID(雪花,序列化为字符串,可空)。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||||
|
public long? MerchantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 登录账号。
|
||||||
|
/// </summary>
|
||||||
|
public string Account { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 展示名称。
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色集合。
|
||||||
|
/// </summary>
|
||||||
|
public string[] Roles { get; init; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 权限集合。
|
||||||
|
/// </summary>
|
||||||
|
public string[] Permissions { get; init; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
using TakeoutSaaS.Application.Identity.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按用户 ID 获取权限概览处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetUserPermissionsQueryHandler(
|
||||||
|
IIdentityUserRepository identityUserRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetUserPermissionsQuery, UserPermissionDto?>
|
||||||
|
{
|
||||||
|
private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<UserPermissionDto?> Handle(GetUserPermissionsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||||
|
var user = await _identityUserRepository.FindByIdAsync(request.UserId, cancellationToken);
|
||||||
|
if (user == null || user.TenantId != tenantId)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserPermissionDto
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
TenantId = user.TenantId,
|
||||||
|
MerchantId = user.MerchantId,
|
||||||
|
Account = user.Account,
|
||||||
|
DisplayName = user.DisplayName,
|
||||||
|
Roles = user.Roles,
|
||||||
|
Permissions = user.Permissions,
|
||||||
|
CreatedAt = user.CreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
using TakeoutSaaS.Application.Identity.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户用户权限分页查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchUserPermissionsQueryHandler(
|
||||||
|
IIdentityUserRepository identityUserRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SearchUserPermissionsQuery, PagedResult<UserPermissionDto>>
|
||||||
|
{
|
||||||
|
private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PagedResult<UserPermissionDto>> Handle(SearchUserPermissionsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||||
|
var users = await _identityUserRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
|
||||||
|
|
||||||
|
var sorted = SortUsers(users, request.SortBy, request.SortDescending);
|
||||||
|
var paged = sorted
|
||||||
|
.Skip((request.Page - 1) * request.PageSize)
|
||||||
|
.Take(request.PageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var items = paged.Select(user => new UserPermissionDto
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
TenantId = user.TenantId,
|
||||||
|
MerchantId = user.MerchantId,
|
||||||
|
Account = user.Account,
|
||||||
|
DisplayName = user.DisplayName,
|
||||||
|
Roles = user.Roles,
|
||||||
|
Permissions = user.Permissions,
|
||||||
|
CreatedAt = user.CreatedAt
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new PagedResult<UserPermissionDto>(items, request.Page, request.PageSize, users.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IOrderedEnumerable<Domain.Identity.Entities.IdentityUser> SortUsers(
|
||||||
|
IReadOnlyCollection<Domain.Identity.Entities.IdentityUser> users,
|
||||||
|
string? sortBy,
|
||||||
|
bool sortDescending)
|
||||||
|
{
|
||||||
|
return sortBy?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"account" => sortDescending
|
||||||
|
? users.OrderByDescending(x => x.Account)
|
||||||
|
: users.OrderBy(x => x.Account),
|
||||||
|
"displayname" => sortDescending
|
||||||
|
? users.OrderByDescending(x => x.DisplayName)
|
||||||
|
: users.OrderBy(x => x.DisplayName),
|
||||||
|
_ => sortDescending
|
||||||
|
? users.OrderByDescending(x => x.CreatedAt)
|
||||||
|
: users.OrderBy(x => x.CreatedAt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按用户 ID 获取角色/权限概览。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetUserPermissionsQuery : IRequest<UserPermissionDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户 ID(雪花)。
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按租户分页查询用户的角色/权限概览。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchUserPermissionsQuery : IRequest<PagedResult<UserPermissionDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 关键字(账号或展示名称)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码,从 1 开始。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段(account/displayName/createdAt)。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否倒序。
|
||||||
|
/// </summary>
|
||||||
|
public bool SortDescending { get; init; } = true;
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ using TakeoutSaaS.Domain.Identity.Entities;
|
|||||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.Identity.Services;
|
namespace TakeoutSaaS.Application.Identity.Services;
|
||||||
|
|
||||||
@@ -15,8 +17,11 @@ public sealed class AdminAuthService(
|
|||||||
IIdentityUserRepository userRepository,
|
IIdentityUserRepository userRepository,
|
||||||
IPasswordHasher<IdentityUser> passwordHasher,
|
IPasswordHasher<IdentityUser> passwordHasher,
|
||||||
IJwtTokenService jwtTokenService,
|
IJwtTokenService jwtTokenService,
|
||||||
IRefreshTokenStore refreshTokenStore) : IAdminAuthService
|
IRefreshTokenStore refreshTokenStore,
|
||||||
|
ITenantProvider tenantProvider) : IAdminAuthService
|
||||||
{
|
{
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 管理后台登录:验证账号密码并生成令牌。
|
/// 管理后台登录:验证账号密码并生成令牌。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -85,6 +90,78 @@ public sealed class AdminAuthService(
|
|||||||
return BuildProfile(user);
|
return BuildProfile(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定用户的权限概览(校验当前租户)。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserPermissionDto?> GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||||
|
var user = await userRepository.FindByIdAsync(userId, cancellationToken);
|
||||||
|
if (user == null || user.TenantId != tenantId)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserPermissionDto
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
TenantId = user.TenantId,
|
||||||
|
MerchantId = user.MerchantId,
|
||||||
|
Account = user.Account,
|
||||||
|
DisplayName = user.DisplayName,
|
||||||
|
Roles = user.Roles,
|
||||||
|
Permissions = user.Permissions,
|
||||||
|
CreatedAt = user.CreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按租户分页查询用户权限概览。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<PagedResult<UserPermissionDto>> SearchUserPermissionsAsync(
|
||||||
|
string? keyword,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
string? sortBy,
|
||||||
|
bool sortDescending,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||||
|
var users = await userRepository.SearchAsync(tenantId, keyword, cancellationToken);
|
||||||
|
|
||||||
|
var sorted = sortBy?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"account" => sortDescending
|
||||||
|
? users.OrderByDescending(x => x.Account)
|
||||||
|
: users.OrderBy(x => x.Account),
|
||||||
|
"displayname" => sortDescending
|
||||||
|
? users.OrderByDescending(x => x.DisplayName)
|
||||||
|
: users.OrderBy(x => x.DisplayName),
|
||||||
|
_ => sortDescending
|
||||||
|
? users.OrderByDescending(x => x.CreatedAt)
|
||||||
|
: users.OrderBy(x => x.CreatedAt)
|
||||||
|
};
|
||||||
|
|
||||||
|
var paged = sorted
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var items = paged.Select(user => new UserPermissionDto
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
TenantId = user.TenantId,
|
||||||
|
MerchantId = user.MerchantId,
|
||||||
|
Account = user.Account,
|
||||||
|
DisplayName = user.DisplayName,
|
||||||
|
Roles = user.Roles,
|
||||||
|
Permissions = user.Permissions,
|
||||||
|
CreatedAt = user.CreatedAt
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new PagedResult<UserPermissionDto>(items, page, pageSize, users.Count);
|
||||||
|
}
|
||||||
|
|
||||||
private static CurrentUserProfile BuildProfile(IdentityUser user)
|
private static CurrentUserProfile BuildProfile(IdentityUser user)
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TakeoutSaaS.Domain.Identity.Entities;
|
using TakeoutSaaS.Domain.Identity.Entities;
|
||||||
@@ -19,4 +20,12 @@ public interface IIdentityUserRepository
|
|||||||
/// 根据 ID 获取后台用户。
|
/// 根据 ID 获取后台用户。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default);
|
Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按租户与关键字查询后台用户列表(仅读)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="keyword">可选关键字(账号/名称)。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
Task<IReadOnlyList<IdentityUser>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -18,4 +19,19 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
|||||||
|
|
||||||
public Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default)
|
public Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default)
|
||||||
=> dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
=> dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<IdentityUser>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var query = dbContext.IdentityUsers
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
var normalized = keyword.Trim();
|
||||||
|
query = query.Where(x => x.Account.Contains(normalized) || x.DisplayName.Contains(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user