7a4c516a20
- 后端:新增 CloudSync 认证/权限/端点/服务与 DTO - 数据:新增用户/会话/安全策略实体与 EF Core migrations - 前端:新增云同步设置 UI、客户端与本地存储;Vite 支持 maui 构建输出到 wwwroot - 桌面端:新增 Avalonia 项目、内置 WebServer、托盘与 Windows 全局热键 - 发布/构建:新增 Windows/Linux 发布脚本与统一入口;调整 MAUI 资源与安装包配置 - 文档:同步更新 README/docs 与协作规则
195 lines
6.4 KiB
C#
195 lines
6.4 KiB
C#
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);
|
|
}
|
|
}
|