285 lines
12 KiB
C#
285 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
|
using TakeoutSaaS.Application.Identity.Contracts;
|
|
using TakeoutSaaS.Domain.Identity.Entities;
|
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
|
|
|
namespace TakeoutSaaS.Application.Identity.Services;
|
|
|
|
/// <summary>
|
|
/// 管理后台认证服务实现。
|
|
/// </summary>
|
|
public sealed class AdminAuthService(
|
|
IIdentityUserRepository userRepository,
|
|
IUserRoleRepository userRoleRepository,
|
|
IRoleRepository roleRepository,
|
|
IPermissionRepository permissionRepository,
|
|
IRolePermissionRepository rolePermissionRepository,
|
|
IPasswordHasher<IdentityUser> passwordHasher,
|
|
IJwtTokenService jwtTokenService,
|
|
IRefreshTokenStore refreshTokenStore,
|
|
ITenantProvider tenantProvider) : IAdminAuthService
|
|
{
|
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
|
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
|
|
private readonly IRoleRepository _roleRepository = roleRepository;
|
|
private readonly IPermissionRepository _permissionRepository = permissionRepository;
|
|
private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository;
|
|
|
|
/// <summary>
|
|
/// 管理后台登录:验证账号密码并生成令牌。
|
|
/// </summary>
|
|
/// <param name="request">登录请求</param>
|
|
/// <param name="cancellationToken">取消令牌</param>
|
|
/// <returns>令牌响应</returns>
|
|
/// <exception cref="BusinessException">账号或密码错误时抛出</exception>
|
|
public async Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
// 1. 根据账号查找用户
|
|
var user = await userRepository.FindByAccountAsync(request.Account, cancellationToken)
|
|
?? throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
|
|
|
|
// 2. 验证密码(使用 ASP.NET Core Identity 的密码哈希器)
|
|
var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password);
|
|
if (result == PasswordVerificationResult.Failed)
|
|
{
|
|
throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
|
|
}
|
|
|
|
// 3. 构建用户档案并生成令牌
|
|
var profile = await BuildProfileAsync(user, cancellationToken);
|
|
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。
|
|
/// </summary>
|
|
/// <param name="request">刷新令牌请求</param>
|
|
/// <param name="cancellationToken">取消令牌</param>
|
|
/// <returns>新的令牌响应</returns>
|
|
/// <exception cref="BusinessException">刷新令牌无效、已过期或用户不存在时抛出</exception>
|
|
public async Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
// 1. 验证刷新令牌(检查是否存在、是否过期、是否已撤销)
|
|
var descriptor = await refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken);
|
|
if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked)
|
|
{
|
|
throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期");
|
|
}
|
|
|
|
// 2. 根据用户 ID 查找用户
|
|
var user = await userRepository.FindByIdAsync(descriptor.UserId, cancellationToken)
|
|
?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在");
|
|
|
|
// 3. 撤销旧刷新令牌(防止重复使用)
|
|
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
|
|
|
|
// 4. 生成新的令牌对
|
|
var profile = await BuildProfileAsync(user, cancellationToken);
|
|
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取用户档案。
|
|
/// </summary>
|
|
/// <param name="userId">用户 ID</param>
|
|
/// <param name="cancellationToken">取消令牌</param>
|
|
/// <returns>用户档案</returns>
|
|
/// <exception cref="BusinessException">用户不存在时抛出</exception>
|
|
public async Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default)
|
|
{
|
|
var user = await userRepository.FindByIdAsync(userId, cancellationToken)
|
|
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
|
|
|
return await BuildProfileAsync(user, cancellationToken);
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
|
|
var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
|
|
var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
|
|
|
|
return new UserPermissionDto
|
|
{
|
|
UserId = user.Id,
|
|
TenantId = user.TenantId,
|
|
MerchantId = user.MerchantId,
|
|
Account = user.Account,
|
|
DisplayName = user.DisplayName,
|
|
Roles = roleCodes,
|
|
Permissions = permissionCodes,
|
|
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 resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken);
|
|
var items = paged.Select(user => new UserPermissionDto
|
|
{
|
|
UserId = user.Id,
|
|
TenantId = user.TenantId,
|
|
MerchantId = user.MerchantId,
|
|
Account = user.Account,
|
|
DisplayName = user.DisplayName,
|
|
Roles = resolved[user.Id].roles,
|
|
Permissions = resolved[user.Id].permissions,
|
|
CreatedAt = user.CreatedAt
|
|
}).ToList();
|
|
|
|
return new PagedResult<UserPermissionDto>(items, page, pageSize, users.Count);
|
|
}
|
|
|
|
private async Task<CurrentUserProfile> BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken)
|
|
{
|
|
var tenantId = user.TenantId;
|
|
var roles = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
|
|
var permissions = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
|
|
|
|
return new CurrentUserProfile
|
|
{
|
|
UserId = user.Id,
|
|
Account = user.Account,
|
|
DisplayName = user.DisplayName,
|
|
TenantId = user.TenantId,
|
|
MerchantId = user.MerchantId,
|
|
Roles = roles,
|
|
Permissions = permissions,
|
|
Avatar = user.Avatar
|
|
};
|
|
}
|
|
|
|
private async Task<string[]> ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
|
{
|
|
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
|
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
|
if (roleIds.Length == 0)
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
|
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
|
}
|
|
|
|
private async Task<string[]> ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
|
{
|
|
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
|
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
|
if (roleIds.Length == 0)
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
|
|
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
|
if (permissionIds.Length == 0)
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
|
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
|
}
|
|
|
|
private async Task<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
|
|
long tenantId,
|
|
IReadOnlyCollection<IdentityUser> users,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userIds = users.Select(x => x.Id).ToArray();
|
|
var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
|
|
var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray();
|
|
|
|
var roles = roleIds.Length == 0
|
|
? Array.Empty<Role>()
|
|
: await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
|
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
|
|
|
|
var rolePermissions = roleIds.Length == 0
|
|
? Array.Empty<RolePermission>()
|
|
: await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
|
|
|
|
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
|
var permissions = permissionIds.Length == 0
|
|
? Array.Empty<Permission>()
|
|
: await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
|
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
|
|
|
var rolePermissionsLookup = rolePermissions
|
|
.GroupBy(rp => rp.RoleId)
|
|
.ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer<long>.Default);
|
|
|
|
var result = new Dictionary<long, (string[] roles, string[] permissions)>();
|
|
foreach (var userId in userIds)
|
|
{
|
|
var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray();
|
|
var roleCodes = rolesForUser
|
|
.Select(rid => roleCodeMap.GetValueOrDefault(rid))
|
|
.Where(c => !string.IsNullOrWhiteSpace(c))
|
|
.Select(c => c!)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
|
|
var permissionCodes = rolesForUser
|
|
.SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty<long>())
|
|
.Select(pid => permissionCodeMap.GetValueOrDefault(pid))
|
|
.Where(code => !string.IsNullOrWhiteSpace(code))
|
|
.Select(code => code!)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
|
|
result[userId] = (roleCodes, permissionCodes);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|