using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Hua.Todo.Application.CloudSync.Auth; using Hua.Todo.Application.CloudSync.Models; using Hua.Todo.Application.Data; using Hua.Todo.Core.Entities; namespace Hua.Todo.Application.CloudSync.Services; /// /// 云同步认证服务(登录、初始化管理员、二次认证)。 /// public class CloudAuthService { private readonly TodoDbContext _dbContext; private readonly IPasswordHasher _passwordHasher; private readonly IRolePermissionMapper _rolePermissionMapper; /// /// 创建 。 /// /// 数据库上下文。 /// 密码哈希器。 /// 角色权限映射器。 public CloudAuthService( TodoDbContext dbContext, IPasswordHasher passwordHasher, IRolePermissionMapper rolePermissionMapper) { _dbContext = dbContext; _passwordHasher = passwordHasher; _rolePermissionMapper = rolePermissionMapper; } /// /// 初始化系统管理员账号(仅在系统尚无云用户时可用)。 /// /// 用户名。 /// 密码。 /// 取消令牌。 /// 是否初始化成功。 public async Task BootstrapAdminAsync(string userName, string password, CancellationToken cancellationToken) { var normalizedUserName = (userName ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(normalizedUserName) || string.IsNullOrWhiteSpace(password)) { return false; } var hasAnyCloudUser = await _dbContext.Users .AsNoTracking() .AnyAsync(u => u.Role != "local", cancellationToken); if (hasAnyCloudUser) { return false; } var exists = await _dbContext.Users .AsNoTracking() .AnyAsync(u => u.UserName == normalizedUserName, cancellationToken); if (exists) { return false; } var user = new UserEntity { Id = Guid.NewGuid(), UserName = normalizedUserName, Role = "admin" }; user.PasswordHash = _passwordHasher.HashPassword(user, password); _dbContext.Users.Add(user); _dbContext.SecurityPolicies.Add(new SecurityPolicyEntity { Id = Guid.NewGuid(), UserId = user.Id, AllowPersist = true }); await _dbContext.SaveChangesAsync(cancellationToken); return true; } /// /// 用户名密码登录并创建会话。 /// /// 用户名。 /// 密码。 /// 会话有效期。 /// 取消令牌。 /// 登录响应;失败则返回 null。 public async Task LoginAsync(string userName, string password, TimeSpan sessionTtl, CancellationToken cancellationToken) { var normalizedUserName = (userName ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(normalizedUserName) || string.IsNullOrWhiteSpace(password)) { return null; } var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.UserName == normalizedUserName, cancellationToken); if (user == null || user.Role == "local") { return null; } var verify = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); if (verify == PasswordVerificationResult.Failed) { return null; } var now = DateTime.UtcNow; var expiresAt = now.Add(sessionTtl); var session = new UserSessionEntity { Id = Guid.NewGuid(), UserId = user.Id, CreatedAtUtc = now, ExpiresAtUtc = expiresAt }; _dbContext.UserSessions.Add(session); var hasPolicy = await _dbContext.SecurityPolicies .AsNoTracking() .AnyAsync(p => p.UserId == user.Id, cancellationToken); if (!hasPolicy) { _dbContext.SecurityPolicies.Add(new SecurityPolicyEntity { Id = Guid.NewGuid(), UserId = user.Id, AllowPersist = true }); } await _dbContext.SaveChangesAsync(cancellationToken); return new LoginResponse { AccessToken = session.Id.ToString(), ExpiresAtUtc = expiresAt, UserId = user.Id, Role = user.Role, Permissions = _rolePermissionMapper.GetPermissions(user.Role).ToList() }; } /// /// 通过再输入口令提升会话权限(step-up)。 /// /// 会话 ID。 /// 口令。 /// 二次认证有效期。 /// 取消令牌。 /// 有效期截止时间;失败返回 null。 public async Task StepUpAsync(Guid sessionId, string password, TimeSpan stepUpTtl, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(password)) { return null; } var now = DateTime.UtcNow; var session = await _dbContext.UserSessions.FirstOrDefaultAsync(s => s.Id == sessionId, cancellationToken); if (session == null || session.ExpiresAtUtc <= now) { return null; } var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == session.UserId, cancellationToken); if (user == null) { return null; } var verify = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); if (verify == PasswordVerificationResult.Failed) { return null; } var stepUpExpiresAt = now.Add(stepUpTtl); session.StepUpExpiresAtUtc = stepUpExpiresAt; await _dbContext.SaveChangesAsync(cancellationToken); return stepUpExpiresAt; } }