7a4c516a20
- 后端:新增 CloudSync 认证/权限/端点/服务与 DTO - 数据:新增用户/会话/安全策略实体与 EF Core migrations - 前端:新增云同步设置 UI、客户端与本地存储;Vite 支持 maui 构建输出到 wwwroot - 桌面端:新增 Avalonia 项目、内置 WebServer、托盘与 Windows 全局热键 - 发布/构建:新增 Windows/Linux 发布脚本与统一入口;调整 MAUI 资源与安装包配置 - 文档:同步更新 README/docs 与协作规则
189 lines
6.3 KiB
C#
189 lines
6.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 云同步认证服务(登录、初始化管理员、二次认证)。
|
|
/// </summary>
|
|
public class CloudAuthService
|
|
{
|
|
private readonly TodoDbContext _dbContext;
|
|
private readonly IPasswordHasher<UserEntity> _passwordHasher;
|
|
private readonly IRolePermissionMapper _rolePermissionMapper;
|
|
|
|
/// <summary>
|
|
/// 创建 <see cref="CloudAuthService"/>。
|
|
/// </summary>
|
|
/// <param name="dbContext">数据库上下文。</param>
|
|
/// <param name="passwordHasher">密码哈希器。</param>
|
|
/// <param name="rolePermissionMapper">角色权限映射器。</param>
|
|
public CloudAuthService(
|
|
TodoDbContext dbContext,
|
|
IPasswordHasher<UserEntity> passwordHasher,
|
|
IRolePermissionMapper rolePermissionMapper)
|
|
{
|
|
_dbContext = dbContext;
|
|
_passwordHasher = passwordHasher;
|
|
_rolePermissionMapper = rolePermissionMapper;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 初始化系统管理员账号(仅在系统尚无云用户时可用)。
|
|
/// </summary>
|
|
/// <param name="userName">用户名。</param>
|
|
/// <param name="password">密码。</param>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>是否初始化成功。</returns>
|
|
public async Task<bool> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 用户名密码登录并创建会话。
|
|
/// </summary>
|
|
/// <param name="userName">用户名。</param>
|
|
/// <param name="password">密码。</param>
|
|
/// <param name="sessionTtl">会话有效期。</param>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>登录响应;失败则返回 null。</returns>
|
|
public async Task<LoginResponse?> 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()
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 通过再输入口令提升会话权限(step-up)。
|
|
/// </summary>
|
|
/// <param name="sessionId">会话 ID。</param>
|
|
/// <param name="password">口令。</param>
|
|
/// <param name="stepUpTtl">二次认证有效期。</param>
|
|
/// <param name="cancellationToken">取消令牌。</param>
|
|
/// <returns>有效期截止时间;失败返回 null。</returns>
|
|
public async Task<DateTime?> 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;
|
|
}
|
|
}
|
|
|