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