feat: 引入 CloudSync 核心能力并新增 Avalonia 桌面端与发布脚本
- 后端:新增 CloudSync 认证/权限/端点/服务与 DTO - 数据:新增用户/会话/安全策略实体与 EF Core migrations - 前端:新增云同步设置 UI、客户端与本地存储;Vite 支持 maui 构建输出到 wwwroot - 桌面端:新增 Avalonia 项目、内置 WebServer、托盘与 Windows 全局热键 - 发布/构建:新增 Windows/Linux 发布脚本与统一入口;调整 MAUI 资源与安装包配置 - 文档:同步更新 README/docs 与协作规则
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user