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,194 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Hua.Todo.Application.CloudSync.Auth;
|
||||
using Hua.Todo.Application.CloudSync.Models;
|
||||
using Hua.Todo.Application.CloudSync.Services;
|
||||
|
||||
namespace Hua.Todo.Application.CloudSync;
|
||||
|
||||
/// <summary>
|
||||
/// 云同步服务端 API 路由注册扩展。
|
||||
/// </summary>
|
||||
public static class CloudSyncEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 映射云同步相关 API 端点。
|
||||
/// </summary>
|
||||
/// <param name="app">Web 应用。</param>
|
||||
/// <returns>Web 应用。</returns>
|
||||
public static WebApplication MapCloudSyncEndpoints(this WebApplication app)
|
||||
{
|
||||
var auth = app.MapGroup("/auth").WithTags("CloudSync - Auth");
|
||||
auth.MapPost("/bootstrap", BootstrapAdminAsync).AllowAnonymous();
|
||||
auth.MapPost("/login", LoginAsync).AllowAnonymous();
|
||||
auth.MapPost("/step-up", StepUpAsync).AllowAnonymous();
|
||||
|
||||
var tasks = app.MapGroup("/tasks").WithTags("CloudSync - Tasks");
|
||||
tasks.MapGet("/", GetTasksAsync).AllowAnonymous();
|
||||
|
||||
var sync = app.MapGroup("/sync").WithTags("CloudSync - Sync");
|
||||
sync.MapPost("/", SyncAsync).AllowAnonymous();
|
||||
|
||||
var security = app.MapGroup("/security").WithTags("CloudSync - Security");
|
||||
security.MapGet("/policy", GetPolicyAsync).AllowAnonymous();
|
||||
security.MapPut("/policy", UpdatePolicyAsync).AllowAnonymous();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<IResult> BootstrapAdminAsync(
|
||||
BootstrapAdminRequest request,
|
||||
CloudAuthService authService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.Password))
|
||||
{
|
||||
return CloudApiErrors.BadRequest("UserName and Password are required.");
|
||||
}
|
||||
|
||||
var ok = await authService.BootstrapAdminAsync(request.UserName, request.Password, cancellationToken);
|
||||
if (!ok)
|
||||
{
|
||||
return CloudApiErrors.Forbidden("Bootstrap is not allowed (already initialized or invalid input).");
|
||||
}
|
||||
|
||||
return Results.Ok();
|
||||
}
|
||||
|
||||
private static async Task<IResult> LoginAsync(
|
||||
LoginRequest request,
|
||||
CloudAuthService authService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.Password))
|
||||
{
|
||||
return CloudApiErrors.BadRequest("UserName and Password are required.");
|
||||
}
|
||||
|
||||
var response = await authService.LoginAsync(request.UserName, request.Password, TimeSpan.FromDays(7), cancellationToken);
|
||||
if (response == null)
|
||||
{
|
||||
return CloudApiErrors.Unauthorized("Invalid credentials.");
|
||||
}
|
||||
|
||||
return Results.Json(response);
|
||||
}
|
||||
|
||||
private static async Task<IResult> StepUpAsync(
|
||||
StepUpRequest request,
|
||||
CloudAuthService authService,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sessionId = httpContext.User.GetSessionId();
|
||||
if (sessionId == null)
|
||||
{
|
||||
return CloudApiErrors.Unauthorized();
|
||||
}
|
||||
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.Password))
|
||||
{
|
||||
return CloudApiErrors.BadRequest("Password is required.");
|
||||
}
|
||||
|
||||
var expiresAt = await authService.StepUpAsync(sessionId.Value, request.Password, TimeSpan.FromMinutes(5), cancellationToken);
|
||||
if (!expiresAt.HasValue)
|
||||
{
|
||||
return CloudApiErrors.Unauthorized("Invalid credentials or session expired.");
|
||||
}
|
||||
|
||||
return Results.Json(new StepUpResponse { StepUpExpiresAtUtc = expiresAt.Value });
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetTasksAsync(
|
||||
CloudTaskSyncService taskService,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = httpContext.User.GetUserId();
|
||||
if (userId == null)
|
||||
{
|
||||
return CloudApiErrors.Unauthorized();
|
||||
}
|
||||
|
||||
if (!httpContext.User.HasPermission(CloudPermissions.TasksRead))
|
||||
{
|
||||
return CloudApiErrors.Forbidden();
|
||||
}
|
||||
|
||||
var tasks = await taskService.GetTasksAsync(userId.Value, cancellationToken);
|
||||
return Results.Json(tasks);
|
||||
}
|
||||
|
||||
private static async Task<IResult> SyncAsync(
|
||||
SyncRequest request,
|
||||
CloudTaskSyncService taskService,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = httpContext.User.GetUserId();
|
||||
if (userId == null)
|
||||
{
|
||||
return CloudApiErrors.Unauthorized();
|
||||
}
|
||||
|
||||
if (!httpContext.User.HasPermission(CloudPermissions.SyncWrite))
|
||||
{
|
||||
return CloudApiErrors.Forbidden();
|
||||
}
|
||||
|
||||
if (!httpContext.User.HasStepUp())
|
||||
{
|
||||
return CloudApiErrors.SecondFactorRequired();
|
||||
}
|
||||
|
||||
var response = await taskService.SyncAsync(userId.Value, request, cancellationToken);
|
||||
return Results.Json(response);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetPolicyAsync(
|
||||
SecurityPolicyService policyService,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = httpContext.User.GetUserId();
|
||||
if (userId == null)
|
||||
{
|
||||
return CloudApiErrors.Unauthorized();
|
||||
}
|
||||
|
||||
if (!httpContext.User.HasPermission(CloudPermissions.PolicyRead))
|
||||
{
|
||||
return CloudApiErrors.Forbidden();
|
||||
}
|
||||
|
||||
var policy = await policyService.GetPolicyAsync(userId.Value, cancellationToken);
|
||||
return Results.Json(policy);
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdatePolicyAsync(
|
||||
UpdateSecurityPolicyRequest request,
|
||||
SecurityPolicyService policyService,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = httpContext.User.GetUserId();
|
||||
if (userId == null)
|
||||
{
|
||||
return CloudApiErrors.Unauthorized();
|
||||
}
|
||||
|
||||
if (!httpContext.User.HasPermission(CloudPermissions.PolicyWrite))
|
||||
{
|
||||
return CloudApiErrors.Forbidden();
|
||||
}
|
||||
|
||||
if (!httpContext.User.HasStepUp())
|
||||
{
|
||||
return CloudApiErrors.SecondFactorRequired();
|
||||
}
|
||||
|
||||
var policy = await policyService.UpdatePolicyAsync(userId.Value, request.AllowPersist, request.AllowSync, cancellationToken);
|
||||
return Results.Json(policy);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user