fix(cicd): 删除 Endpoints/ 小写重复目录,统一 PascalCase
小写目录是大写目录内容完全一致的冗余副本,Linux 构建 duplicate items
This commit is contained in:
parent
1ab9621803
commit
d41c45c74d
@ -1,25 +0,0 @@
|
||||
using File.Application.Features.Buckets.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Buckets;
|
||||
|
||||
|
||||
/// <summary>创建存储桶端点。POST /api/buckets,按名称创建存储桶。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class CreateBucketEndpoint(IMediator mediator) : Endpoint<CreateBucketRequest, object>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/buckets");
|
||||
Permissions("bucket:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateBucketRequest req, CancellationToken ct)
|
||||
{
|
||||
var message = await mediator.Send(new CreateBucketCommand(req.Name), ct);
|
||||
await Send.OkAsync(new { message }, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Buckets;
|
||||
|
||||
/// <summary>创建存储桶请求。携带待创建存储桶的名称。</summary>
|
||||
public class CreateBucketRequest
|
||||
{
|
||||
public string Name { get; init; } = default!;
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using File.Application.Features.Buckets.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Buckets;
|
||||
|
||||
|
||||
/// <summary>删除存储桶端点。DELETE /api/buckets,按名称删除存储桶。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class DeleteBucketEndpoint(IMediator mediator) : Endpoint<DeleteBucketRequest, object>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/buckets");
|
||||
Permissions("bucket:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DeleteBucketRequest req, CancellationToken ct)
|
||||
{
|
||||
var message = await mediator.Send(new DeleteBucketCommand(req.Name), ct);
|
||||
await Send.OkAsync(new { message }, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Buckets;
|
||||
|
||||
/// <summary>删除存储桶请求。以查询参数传入存储桶名称。</summary>
|
||||
public class DeleteBucketRequest
|
||||
{
|
||||
[QueryParam] public string Name { get; init; } = default!;
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
using File.Application.Features.Buckets.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Buckets;
|
||||
|
||||
/// <summary>列出存储桶端点。GET /api/buckets,返回全部存储桶名称。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class ListBucketsEndpoint(IMediator mediator) : Endpoint<EmptyRequest, List<string>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/buckets");
|
||||
Permissions("bucket:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new ListBucketsQuery(), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using File.Application.Features.Files.DTOs;
|
||||
using File.Application.Features.Files.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>秒传预检端点。POST /api/files/check,按哈希查同用户已存在文件。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class CheckFileEndpoint(IMediator mediator) : Endpoint<CheckFileRequest, CheckFileResultDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/files/check");
|
||||
Permissions("file:upload");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CheckFileRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new CheckFileQuery(req.ContentHash, req.Size), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>秒传预检请求。前端上传前带 content_hash 与 size 查重。</summary>
|
||||
public class CheckFileRequest
|
||||
{
|
||||
/// <summary>文件内容 SHA-256 哈希(前端计算)。</summary>
|
||||
public string ContentHash { get; init; } = default!;
|
||||
/// <summary>文件大小(字节),防哈希碰撞。</summary>
|
||||
public long Size { get; init; }
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using File.Application.Features.Files.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
|
||||
/// <summary>删除文件端点。DELETE /api/files/delete,按对象键删除文件。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class DeleteFileEndpoint(IMediator mediator) : Endpoint<DeleteFileRequest, object>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/files/delete");
|
||||
Permissions("file:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DeleteFileRequest req, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteFileCommand(req.ObjectKey), ct);
|
||||
await Send.OkAsync(new { message = "deleted" }, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>删除文件请求。以查询参数指定对象键。</summary>
|
||||
public class DeleteFileRequest
|
||||
{
|
||||
[QueryParam] public string ObjectKey { get; init; } = default!;
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
using System.Net.Mime;
|
||||
using File.Application.Features.Files.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件端点。GET /api/files/download,按对象键流式返回文件内容。
|
||||
/// 直接把 S3 响应流透传到 HTTP 响应体,整文件不进内存;Content-Disposition 触发浏览器下载。
|
||||
/// 该路径已在 ApiResponseMiddlewareOptions.PassthroughPrefixes 注册,绕过响应包裹中间件的缓冲。
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class DownloadFileEndpoint(IMediator mediator) : Endpoint<DownloadFileRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/files/download");
|
||||
Permissions("file:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DownloadFileRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new DownloadFileQuery(req.ObjectKey), ct);
|
||||
|
||||
// 设置响应头:真实 ContentType + 触发下载的文件名
|
||||
HttpContext.Response.StatusCode = 200;
|
||||
HttpContext.Response.ContentType = result.ContentType;
|
||||
var disposition = new ContentDisposition { FileName = result.FileName, Inline = false };
|
||||
HttpContext.Response.Headers.ContentDisposition = disposition.ToString();
|
||||
|
||||
// 流式透传:S3 流 → HTTP 响应体,零内存拷贝
|
||||
try
|
||||
{
|
||||
await result.Stream.CopyToAsync(HttpContext.Response.Body, ct);
|
||||
await HttpContext.Response.Body.FlushAsync(ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await result.Stream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>下载文件请求。以查询参数指定对象键。</summary>
|
||||
public class DownloadFileRequest
|
||||
{
|
||||
[QueryParam] public string ObjectKey { get; init; } = default!;
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
using File.Application.Features.Files.DTOs;
|
||||
using File.Application.Features.Files.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
|
||||
/// <summary>文件内容端点。GET /api/files/content,返回文件文本内容。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class GetFileContentEndpoint(IMediator mediator) : Endpoint<GetFileContentRequest, FileContentResult>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/files/content");
|
||||
Permissions("file:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetFileContentRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new GetFileContentQuery(req.ObjectKey), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>文件内容请求。以查询参数指定对象键(bucket 由系统设置统一管理)。</summary>
|
||||
public class GetFileContentRequest
|
||||
{
|
||||
[QueryParam] public string ObjectKey { get; init; } = default!;
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
using System.Net.Mime;
|
||||
using File.Application.Features.Files.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>
|
||||
/// 文件预览端点。GET /api/files/preview,流式返回文件内容供内联展示(非下载)。
|
||||
/// 后端从 S3 拉流透传,前端永远不接触 S3 URL 和凭据。
|
||||
/// 鉴权:[Authorize] + JWT 中间件从 token query 参数读 JWT(img src / window.open 无法带 header)。
|
||||
/// 该路径在 ApiResponseMiddlewareOptions.PassthroughPrefixes 注册,绕过响应包裹中间件的缓冲。
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class GetFilePreviewEndpoint(IMediator mediator) : Endpoint<GetFilePreviewRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/files/preview");
|
||||
Permissions("file:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetFilePreviewRequest req, CancellationToken ct)
|
||||
{
|
||||
// 复用下载查询(返回 Stream + ContentType + FileName)
|
||||
var result = await mediator.Send(new DownloadFileQuery(req.ObjectKey), ct);
|
||||
|
||||
// inline = 浏览器内联展示(图片/PDF 直接渲染),而非触发下载
|
||||
HttpContext.Response.ContentType = result.ContentType;
|
||||
var disposition = new ContentDisposition { FileName = result.FileName, Inline = true };
|
||||
HttpContext.Response.Headers.ContentDisposition = disposition.ToString();
|
||||
|
||||
// 流式透传:S3 流 → HTTP 响应体
|
||||
try
|
||||
{
|
||||
await result.Stream.CopyToAsync(HttpContext.Response.Body, ct);
|
||||
await HttpContext.Response.Body.FlushAsync(ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await result.Stream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>
|
||||
/// 文件预览请求。以查询参数指定对象键。
|
||||
/// 鉴权三选一:Authorization header / Token 查询参数(JWT) / InternalToken+Expires(KKFileView 内部回拉)。
|
||||
/// </summary>
|
||||
public class GetFilePreviewRequest
|
||||
{
|
||||
[QueryParam] public string ObjectKey { get; init; } = default!;
|
||||
|
||||
/// <summary>JWT token(用于 img src / window.open 等无法带 Authorization header 的场景)。</summary>
|
||||
[QueryParam] public string? Token { get; init; }
|
||||
|
||||
/// <summary>KKFileView 内部回拉签名(HMAC),配合 Expires 使用。</summary>
|
||||
[QueryParam] public string? InternalToken { get; init; }
|
||||
|
||||
/// <summary>内部签名过期时间戳(Unix 秒)。</summary>
|
||||
[QueryParam] public long? Expires { get; init; }
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
using File.Application.Features.Files.DTOs;
|
||||
using File.Application.Features.Files.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>
|
||||
/// Office/PDF 文档在线预览端点。GET /api/files/office-preview,
|
||||
/// 返回 KKFileView 预览地址,前端 window.open 打开即可渲染 Office 文档。
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class GetOfficePreviewEndpoint(IMediator mediator) : Endpoint<GetOfficePreviewRequest, OfficePreviewResult>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/files/office-preview");
|
||||
Permissions("file:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetOfficePreviewRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new GetOfficePreviewQuery(req.ObjectKey), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>Office/PDF 文档预览请求。以查询参数指定对象键。</summary>
|
||||
public class GetOfficePreviewRequest
|
||||
{
|
||||
[QueryParam] public string ObjectKey { get; init; } = default!;
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
using File.Application.Features.Files.DTOs;
|
||||
using File.Application.Features.Files.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
|
||||
/// <summary>列举文件端点。GET /api/files/list,支持前缀与分页查询。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class ListFilesEndpoint(IMediator mediator) : Endpoint<ListFilesRequest, ListFilesResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/files/list");
|
||||
Permissions("file:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ListFilesRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new ListFilesQuery(req.Prefix, req.MaxKeys, req.ContinuationToken), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>列举文件请求。包含前缀、最大键数与续传令牌。</summary>
|
||||
public class ListFilesRequest
|
||||
{
|
||||
[QueryParam] public string? Prefix { get; init; }
|
||||
[QueryParam] public int? MaxKeys { get; init; }
|
||||
[QueryParam] public string? ContinuationToken { get; init; }
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
using File.Application.Features.Files.Commands;
|
||||
using File.Application.Features.Files.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
|
||||
/// <summary>上传文件端点。POST /api/files/upload,支持 multipart 文件上传。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class UploadFileEndpoint(IMediator mediator) : Endpoint<UploadFileRequest, UploadFileResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/files/upload");
|
||||
Permissions("file:upload");
|
||||
AllowFileUploads();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UploadFileRequest req, CancellationToken ct)
|
||||
{
|
||||
await using var stream = req.File.OpenReadStream();
|
||||
var result = await mediator.Send(new UploadFileCommand(req.ObjectKey,
|
||||
stream, req.File.ContentType, req.File.Length, req.ContentHash), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Files;
|
||||
|
||||
/// <summary>上传文件请求。包含 object_key 与文件数据。</summary>
|
||||
public class UploadFileRequest
|
||||
{
|
||||
public string ObjectKey { get; init; } = default!;
|
||||
public IFormFile File { get; init; } = default!;
|
||||
/// <summary>文件内容哈希(可选),秒传查重用。</summary>
|
||||
public string? ContentHash { get; init; }
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
using File.Application.Features.Folders.Commands;
|
||||
using File.Application.Features.Folders.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
|
||||
/// <summary>创建文件夹端点。POST /api/folders。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class CreateFolderEndpoint(IMediator mediator) : Endpoint<CreateFolderRequest, FolderDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/folders");
|
||||
Permissions("file:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateFolderRequest req, CancellationToken ct)
|
||||
{
|
||||
var parentId = string.IsNullOrEmpty(req.ParentId) ? (Guid?)null : Guid.Parse(req.ParentId);
|
||||
var result = await mediator.Send(new CreateFolderCommand(parentId, req.Name), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
/// <summary>创建文件夹请求体(父目录 Id 与名称)。</summary>
|
||||
public class CreateFolderRequest
|
||||
{
|
||||
public string? ParentId { get; init; }
|
||||
public string Name { get; init; } = default!;
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using File.Application.Features.Folders.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
|
||||
/// <summary>删除文件夹端点。DELETE /api/folders/{Id}。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class DeleteFolderEndpoint(IMediator mediator) : Endpoint<DeleteFolderRequest, object>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/folders/{Id}");
|
||||
Permissions("file:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DeleteFolderRequest req, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteFolderCommand(Guid.Parse(req.Id)), ct);
|
||||
await Send.OkAsync(new { message = "deleted" }, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
/// <summary>删除文件夹请求(路由取 Id)。</summary>
|
||||
public class DeleteFolderRequest
|
||||
{
|
||||
[RouteParam] public string Id { get; init; } = default!;
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using File.Application.Features.Folders.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
|
||||
/// <summary>获取文件夹详情端点。GET /api/folders/{Id}。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class GetFolderEndpoint(IMediator mediator) : Endpoint<GetFolderRequest, object>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/folders/{Id}");
|
||||
Permissions("file:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetFolderRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new GetFolderQuery(Guid.Parse(req.Id)), ct);
|
||||
await Send.OkAsync(result!, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
/// <summary>获取文件夹请求(路由取 Id)。</summary>
|
||||
public class GetFolderRequest
|
||||
{
|
||||
[RouteParam] public string Id { get; init; } = default!;
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using File.Application.Features.Folders.DTOs;
|
||||
using File.Application.Features.Folders.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
/// <summary>获取整棵文件夹树端点。GET /api/folders/tree。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class GetFolderTreeEndpoint(IMediator mediator) : Endpoint<EmptyRequest, List<FolderDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/folders/tree");
|
||||
Permissions("file:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new GetFolderTreeQuery(), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
using File.Application.Features.Folders.DTOs;
|
||||
using File.Application.Features.Folders.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
/// <summary>
|
||||
/// 查询根级文件端点。GET /api/files/root,
|
||||
/// 返回当前用户 FolderId=null 的所有 FileMeta,使根目录与子文件夹共享同一数据源(DB)。
|
||||
/// 注意:路径用 /files/root 而非 /folders/root/files,避免与 POST /folders/{FolderId}/files 模板冲突。
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class GetRootFilesEndpoint(IMediator mediator) : Endpoint<EmptyRequest, List<FileMetaDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/files/root");
|
||||
Permissions("file:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new GetRootFilesQuery(), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
using File.Application.Features.Folders.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
|
||||
/// <summary>移动文件到目标文件夹端点。POST /api/files/{Id}/move。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class MoveFileEndpoint(IMediator mediator) : Endpoint<MoveFileRequest, object>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/files/{Id}/move");
|
||||
Permissions("file:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(MoveFileRequest req, CancellationToken ct)
|
||||
{
|
||||
var targetId = string.IsNullOrEmpty(req.TargetFolderId) ? (Guid?)null : Guid.Parse(req.TargetFolderId);
|
||||
await mediator.Send(new MoveFileCommand(Guid.Parse(req.Id), targetId), ct);
|
||||
await Send.OkAsync(new { message = "moved" }, ct);
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
/// <summary>移动文件请求(文件 Id 与目标目录)。</summary>
|
||||
public class MoveFileRequest
|
||||
{
|
||||
[RouteParam] public string Id { get; init; } = default!;
|
||||
public string? TargetFolderId { get; init; }
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
using File.Application.Features.Folders.Commands;
|
||||
using File.Application.Features.Folders.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
|
||||
/// <summary>重命名文件夹端点。PUT /api/folders/{Id}。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class RenameFolderEndpoint(IMediator mediator) : Endpoint<RenameFolderRequest, FolderDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/folders/{Id}");
|
||||
Permissions("file:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(RenameFolderRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new RenameFolderCommand(Guid.Parse(req.Id), req.Name), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
/// <summary>重命名文件夹请求(路由 Id 与新名称)。</summary>
|
||||
public class RenameFolderRequest
|
||||
{
|
||||
[RouteParam] public string Id { get; init; } = default!;
|
||||
public string Name { get; init; } = default!;
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
using File.Application.Features.Folders.Commands;
|
||||
using File.Application.Features.Folders.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
|
||||
/// <summary>上传文件到指定文件夹端点。POST /api/folders/{FolderId}/files。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class UploadToFolderEndpoint(IMediator mediator) : Endpoint<UploadToFolderRequest, FileMetaDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/folders/{FolderId}/files");
|
||||
Permissions("file:upload");
|
||||
AllowFileUploads();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UploadToFolderRequest req, CancellationToken ct)
|
||||
{
|
||||
await using var stream = req.File.OpenReadStream();
|
||||
var result = await mediator.Send(new UploadToFolderCommand(Guid.Parse(req.FolderId),
|
||||
req.FileName, stream, req.File.ContentType, req.File.Length, req.ContentHash), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Folders;
|
||||
|
||||
/// <summary>上传文件到文件夹请求(含 multipart 文件流)。</summary>
|
||||
public class UploadToFolderRequest
|
||||
{
|
||||
[RouteParam] public string FolderId { get; init; } = default!;
|
||||
public string FileName { get; init; } = default!;
|
||||
public IFormFile File { get; init; } = default!;
|
||||
/// <summary>文件内容哈希(可选),秒传查重用。</summary>
|
||||
public string? ContentHash { get; init; }
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using File.Application.Features.Multipart.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
|
||||
/// <summary>中止分片上传端点。POST /api/files/multipart/abort。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class AbortMultipartEndpoint(IMediator mediator) : Endpoint<AbortMultipartRequest, object>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/files/multipart/abort");
|
||||
Permissions("file:upload");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(AbortMultipartRequest req, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new AbortMultipartCommand(req.ObjectKey, req.UploadId), ct);
|
||||
await Send.OkAsync(new { message = "aborted" }, ct);
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
/// <summary>中止分片上传请求(对象 key、UploadId)。</summary>
|
||||
public class AbortMultipartRequest
|
||||
{
|
||||
public string ObjectKey { get; init; } = default!;
|
||||
public string UploadId { get; init; } = default!;
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
using File.Application.Features.Multipart.Commands;
|
||||
using File.Application.Features.Multipart.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
|
||||
/// <summary>完成分片上传端点。POST /api/files/multipart/complete。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class CompleteMultipartEndpoint(IMediator mediator) : Endpoint<CompleteMultipartRequest, CompleteMultipartResult>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/files/multipart/complete");
|
||||
Permissions("file:upload");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CompleteMultipartRequest req, CancellationToken ct)
|
||||
{
|
||||
var parts = req.Parts.Select(p => new CompletedPartDto(p.PartNumber, p.ETag)).ToList();
|
||||
var result = await mediator.Send(new CompleteMultipartCommand(req.ObjectKey, req.UploadId, parts, req.FileName, req.Size), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
/// <summary>完成分片上传请求(含各分片信息列表)。</summary>
|
||||
public class CompleteMultipartRequest
|
||||
{
|
||||
public string ObjectKey { get; init; } = default!;
|
||||
public string UploadId { get; init; } = default!;
|
||||
public List<PartRequest> Parts { get; init; } = new();
|
||||
/// <summary>文件名(可选),完成后落 FileMeta 用,供秒传查重。</summary>
|
||||
public string? FileName { get; init; }
|
||||
/// <summary>文件总大小(可选),落 FileMeta 用。</summary>
|
||||
public long Size { get; init; }
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
using File.Application.Features.Multipart.Commands;
|
||||
using File.Application.Features.Multipart.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
|
||||
/// <summary>初始化分片上传端点。POST /api/files/multipart/init。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class InitMultipartEndpoint(IMediator mediator) : Endpoint<InitMultipartRequest, InitMultipartResult>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/files/multipart/init");
|
||||
Permissions("file:upload");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(InitMultipartRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new InitMultipartCommand(req.ObjectKey, req.ContentHash, req.TotalParts, req.Size), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
/// <summary>初始化分片上传请求。ContentHash/TotalParts 可选,断点续传时由前端附带。</summary>
|
||||
public class InitMultipartRequest
|
||||
{
|
||||
/// <summary>S3 对象 key(断点续传建议用内容哈希派生稳定值)。</summary>
|
||||
public string ObjectKey { get; init; } = default!;
|
||||
/// <summary>文件内容哈希(可选),仅落库记录用。</summary>
|
||||
public string? ContentHash { get; init; }
|
||||
/// <summary>预计分片总数(可选),进度参考。</summary>
|
||||
public int? TotalParts { get; init; }
|
||||
/// <summary>文件大小(可选),秒传判定用(防哈希碰撞)。</summary>
|
||||
public long? Size { get; init; }
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
using File.Application.Features.Multipart.DTOs;
|
||||
using File.Application.Features.Multipart.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
/// <summary>
|
||||
/// 查询分片上传已传分片清单端点。GET /api/files/multipart/parts?upload_id=xxx。
|
||||
/// 断点续传恢复点:前端续传前调用,据此跳过已传 partNumber。
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class ListPartsEndpoint(IMediator mediator) : Endpoint<ListPartsRequest, ListPartsResultDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/files/multipart/parts");
|
||||
Permissions("file:upload");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ListPartsRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new ListPartsQuery(req.UploadId), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
/// <summary>查询已传分片清单请求。upload_id 经 query string 传入。</summary>
|
||||
public class ListPartsRequest
|
||||
{
|
||||
/// <summary>分片上传会话的 S3 UploadId。</summary>
|
||||
[QueryParam] public string UploadId { get; init; } = default!;
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
/// <summary>分片上传的单片信息(CompleteMultipart 时用)。</summary>
|
||||
public class PartRequest
|
||||
{
|
||||
public int PartNumber { get; init; }
|
||||
public string ETag { get; init; } = default!;
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
using File.Application.Features.Multipart.Commands;
|
||||
using File.Application.Features.Multipart.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
|
||||
/// <summary>上传单个分片端点。PUT /api/files/multipart/part。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class UploadPartEndpoint(IMediator mediator) : Endpoint<UploadPartRequest, UploadPartResult>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/files/multipart/part");
|
||||
Permissions("file:upload");
|
||||
AllowFileUploads();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UploadPartRequest req, CancellationToken ct)
|
||||
{
|
||||
await using var stream = req.File.OpenReadStream();
|
||||
var result = await mediator.Send(new UploadPartCommand(req.ObjectKey,
|
||||
req.UploadId, req.PartNumber, stream), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Multipart;
|
||||
|
||||
/// <summary>上传单个分片请求(含 multipart 文件流)。</summary>
|
||||
public class UploadPartRequest
|
||||
{
|
||||
public string ObjectKey { get; init; } = default!;
|
||||
public string UploadId { get; init; } = default!;
|
||||
public int PartNumber { get; init; }
|
||||
public IFormFile File { get; init; } = default!;
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using File.Application.Features.Settings.DTOs;
|
||||
using File.Application.Features.Settings.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Settings;
|
||||
|
||||
/// <summary>查询文件系统设置端点。GET /api/file-settings,返回当前默认存储桶。</summary>
|
||||
[Authorize]
|
||||
public class GetFileSettingEndpoint(IMediator mediator) : Endpoint<EmptyRequest, FileSettingDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/file-settings");
|
||||
Permissions("bucket:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new GetFileSettingQuery(), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using File.Application.Features.Settings.Commands;
|
||||
using File.Application.Features.Settings.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Settings;
|
||||
|
||||
/// <summary>更新默认存储桶端点。PUT /api/file-settings/default-bucket。</summary>
|
||||
[Authorize]
|
||||
public class UpdateDefaultBucketEndpoint(IMediator mediator) : Endpoint<UpdateDefaultBucketRequest, FileSettingDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/file-settings/default-bucket");
|
||||
Permissions("bucket:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UpdateDefaultBucketRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new UpdateDefaultBucketCommand(req.DefaultBucket), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Settings;
|
||||
|
||||
/// <summary>更新默认存储桶请求。</summary>
|
||||
public class UpdateDefaultBucketRequest
|
||||
{
|
||||
public string DefaultBucket { get; init; } = default!;
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
using File.Application.Features.Shares.Commands;
|
||||
using File.Application.Features.Shares.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Shares;
|
||||
|
||||
|
||||
/// <summary>创建分享链接端点。POST /api/share。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class CreateShareEndpoint(IMediator mediator) : Endpoint<CreateShareRequest, ShareLinkDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/share");
|
||||
Permissions("file-share:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateShareRequest req, CancellationToken ct)
|
||||
{
|
||||
var expiresAt = string.IsNullOrEmpty(req.ExpiresAt)
|
||||
? (DateTime?)null
|
||||
: DateTime.Parse(req.ExpiresAt, null, System.Globalization.DateTimeStyles.RoundtripKind);
|
||||
var result = await mediator.Send(new CreateShareCommand(req.ResourceType,
|
||||
Guid.Parse(req.ResourceId), req.Password, expiresAt, req.MaxDownloads), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Shares;
|
||||
|
||||
/// <summary>创建分享请求(资源、密码、过期、下载上限)。</summary>
|
||||
public class CreateShareRequest
|
||||
{
|
||||
public string ResourceType { get; init; } = default!;
|
||||
public string ResourceId { get; init; } = default!;
|
||||
public string? Password { get; init; }
|
||||
public string? ExpiresAt { get; init; } // ISO 8601 字符串
|
||||
public int? MaxDownloads { get; init; }
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using File.Application.Features.Shares.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Shares;
|
||||
|
||||
|
||||
/// <summary>删除分享端点。DELETE /api/share/{Id}。</summary>
|
||||
[Authorize]
|
||||
/// <summary>
|
||||
/// undefined
|
||||
/// </summary>
|
||||
public class DeleteShareEndpoint(IMediator mediator) : Endpoint<DeleteShareRequest, object>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/share/{Id}");
|
||||
Permissions("file-share:write");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DeleteShareRequest req, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteShareCommand(Guid.Parse(req.Id)), ct);
|
||||
await Send.OkAsync(new { message = "deleted" }, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Shares;
|
||||
|
||||
/// <summary>删除分享请求(路由取 Id)。</summary>
|
||||
public class DeleteShareRequest
|
||||
{
|
||||
[RouteParam] public string Id { get; init; } = default!;
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using File.Application.Features.Shares.DTOs;
|
||||
using File.Application.Features.Shares.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Shares;
|
||||
|
||||
|
||||
/// <summary>下载分享端点(匿名访问)。POST /api/share/{Token}/download。</summary>
|
||||
public class DownloadShareEndpoint(IMediator mediator) : Endpoint<DownloadShareRequest, DownloadShareResult>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/share/{Token}/download");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DownloadShareRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new DownloadShareQuery(req.Token, req.Password), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Shares;
|
||||
|
||||
/// <summary>下载分享请求(Token 与可选密码)。</summary>
|
||||
public class DownloadShareRequest
|
||||
{
|
||||
[RouteParam] public string Token { get; init; } = default!;
|
||||
public string? Password { get; init; }
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
using File.Application.Features.Shares.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.File.Shares;
|
||||
|
||||
|
||||
/// <summary>查询分享信息端点(匿名访问)。GET /api/share/{Token}。</summary>
|
||||
public class GetShareInfoEndpoint(IMediator mediator) : Endpoint<GetShareInfoRequest, object>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/share/{Token}");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetShareInfoRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new GetShareInfoQuery(req.Token), ct);
|
||||
await Send.OkAsync(result!, ct);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.File.Shares;
|
||||
|
||||
/// <summary>查询分享信息请求(路由取 Token)。</summary>
|
||||
public class GetShareInfoRequest
|
||||
{
|
||||
[RouteParam] public string Token { get; init; } = default!;
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加群成员端点。POST /groups/{GroupId}/members。
|
||||
/// </summary>
|
||||
public class AddGroupMemberEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<AddGroupMemberEndpointRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/groups/{GroupId}/members");
|
||||
Permissions("im:group:manage");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(AddGroupMemberEndpointRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
await mediator.Send(new AddGroupMemberCommand(req.GroupId, userId, req.UserIds), ct);
|
||||
HttpContext.Response.StatusCode = 204;
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加群成员请求体。
|
||||
/// </summary>
|
||||
public record AddGroupMemberEndpointRequest(Guid GroupId, List<Guid> UserIds);
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using IM.Application.Chat.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 创建群组端点。POST /groups。
|
||||
/// </summary>
|
||||
public class CreateGroupEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<CreateGroupRequest, GroupDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/groups");
|
||||
Permissions("im:group:create");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateGroupRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var result = await mediator.Send(new CreateGroupCommand(req.Name, req.Avatar, req.Description, userId, req.MemberUserIds), ct);
|
||||
await Send.ResponseAsync(result, 201, ct);
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using IM.Application.Chat.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 创建或复用私聊会话。若当前用户与目标用户间已存在私聊会话则直接返回,保证幂等。
|
||||
/// </summary>
|
||||
public class CreatePrivateConversationEndpoint(
|
||||
IMediator mediator,
|
||||
ICurrentUserContext userContext) : Endpoint<CreatePrivateConversationRequest, ConversationDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/conversations/private");
|
||||
Permissions("im:chat");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreatePrivateConversationRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var result = await mediator.Send(
|
||||
new CreatePrivateConversationCommand(userId, req.TargetUserId), ct);
|
||||
await Send.ResponseAsync(result, 200, ct);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 解散群组端点。DELETE /groups/{GroupId}。
|
||||
/// </summary>
|
||||
public class DeleteGroupEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<DeleteGroupEndpointRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/groups/{GroupId}");
|
||||
Permissions("im:group:manage");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DeleteGroupEndpointRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
await mediator.Send(new DeleteGroupCommand(req.GroupId, userId), ct);
|
||||
HttpContext.Response.StatusCode = 204;
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 解散群组请求体(路由参数绑定)。
|
||||
/// </summary>
|
||||
public record DeleteGroupEndpointRequest(Guid GroupId);
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.DTOs;
|
||||
using IM.Application.Chat.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 查询用户会话列表端点。GET /conversations。
|
||||
/// </summary>
|
||||
public class GetConversationsEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest<List<ConversationDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/conversations");
|
||||
Permissions("im:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var result = await mediator.Send(new GetConversationsQuery(userId), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.DTOs;
|
||||
using IM.Application.Chat.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 查询单个群详情(含公告/成员数)。
|
||||
/// </summary>
|
||||
public class GetGroupEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<GetGroupEndpointRequest, GroupDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/groups/{GroupId}");
|
||||
Permissions("im:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetGroupEndpointRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var result = await mediator.Send(new GetGroupQuery(req.GroupId, userId), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>查询群详情请求。GroupId 由路由参数绑定。</summary>
|
||||
public record GetGroupEndpointRequest(Guid GroupId);
|
||||
@ -1,26 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.DTOs;
|
||||
using IM.Application.Chat.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 查询群成员列表端点。GET /groups/{GroupId}/members。
|
||||
/// </summary>
|
||||
public class GetGroupMembersEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<GetGroupMembersEndpointRequest, List<GroupMemberDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/groups/{GroupId}/members");
|
||||
Permissions("im:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetGroupMembersEndpointRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var result = await mediator.Send(new GetGroupMembersQuery(userId, req.GroupId), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 群成员列表查询请求体(路由参数绑定)。
|
||||
/// </summary>
|
||||
public record GetGroupMembersEndpointRequest(Guid GroupId);
|
||||
@ -1,27 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.DTOs;
|
||||
using IM.Application.Chat.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 查询历史消息端点。GET /conversations/{ConversationId}/messages。
|
||||
/// </summary>
|
||||
public class GetMessageHistoryEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<GetMessageHistoryRequest, MessageHistoryDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/conversations/{ConversationId}/messages");
|
||||
Permissions("im:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetMessageHistoryRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var result = await mediator.Send(
|
||||
new GetMessageHistoryQuery(userId, req.ConversationId, req.BeforeId, req.Limit), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 历史消息查询请求体。含游标与分页大小。
|
||||
/// </summary>
|
||||
public record GetMessageHistoryRequest(Guid ConversationId, string? BeforeId, int Limit = 50);
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.DTOs;
|
||||
using IM.Application.Chat.Queries;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 查询未读消息数端点。GET /conversations/unread。
|
||||
/// </summary>
|
||||
public class GetUnreadCountEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest<UnreadCountsDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/unread");
|
||||
Permissions("im:read");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var result = await mediator.Send(new GetUnreadCountQuery(userId), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 标记会话未读端点。POST /conversations/{ConversationId}/unread。
|
||||
/// </summary>
|
||||
public class MarkConversationUnreadEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/conversations/{ConversationId}/unread");
|
||||
Permissions("im:conversation:manage");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var conversationId = Route<Guid>("ConversationId");
|
||||
await mediator.Send(new MarkConversationUnreadCommand(conversationId, userId), ct);
|
||||
await Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 撤回消息 REST 端点(SignalR 的非实时兜底通道)。
|
||||
/// 权限与时间窗校验在 RecallMessageCommandHandler 内统一执行。
|
||||
/// </summary>
|
||||
public class RecallMessageEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<RecallMessageEndpointRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/conversations/{ConversationId}/messages/{MessageId}/recall");
|
||||
Permissions("im:chat");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(RecallMessageEndpointRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
await mediator.Send(new RecallMessageCommand(req.ConversationId, req.MessageId, userId), ct);
|
||||
await Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>撤回消息请求。ConversationId 与 MessageId 由路由参数绑定。</summary>
|
||||
public record RecallMessageEndpointRequest(Guid ConversationId, string MessageId);
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 移除群成员端点。DELETE /groups/{GroupId}/members/{UserId}。
|
||||
/// </summary>
|
||||
public class RemoveGroupMemberEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<RemoveGroupMemberEndpointRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/groups/{GroupId}/members/{TargetUserId}");
|
||||
Permissions("im:group:manage");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(RemoveGroupMemberEndpointRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
await mediator.Send(new RemoveGroupMemberCommand(req.GroupId, userId, req.TargetUserId), ct);
|
||||
HttpContext.Response.StatusCode = 204;
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 移除群成员请求体(路由参数绑定)。
|
||||
/// </summary>
|
||||
public record RemoveGroupMemberEndpointRequest(Guid GroupId, Guid TargetUserId);
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 设置会话免打扰端点。PUT /conversations/{ConversationId}/muted。
|
||||
/// </summary>
|
||||
public class SetConversationMutedEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<SetConversationStateRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/conversations/{ConversationId}/muted");
|
||||
Permissions("im:conversation:manage");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(SetConversationStateRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var conversationId = Route<Guid>("ConversationId");
|
||||
await mediator.Send(new SetConversationMutedCommand(conversationId, userId, req.Value), ct);
|
||||
await Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 设置会话置顶端点。PUT /conversations/{ConversationId}/pinned。
|
||||
/// </summary>
|
||||
public class SetConversationPinnedEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<SetConversationStateRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/conversations/{ConversationId}/pinned");
|
||||
Permissions("im:conversation:manage");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(SetConversationStateRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var conversationId = Route<Guid>("ConversationId");
|
||||
await mediator.Send(new SetConversationPinnedCommand(conversationId, userId, req.Value), ct);
|
||||
await Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 设置会话备注名端点。PUT /conversations/{ConversationId}/remark。
|
||||
/// </summary>
|
||||
public class SetConversationRemarkEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<SetConversationRemarkEndpointRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/conversations/{ConversationId}/remark");
|
||||
Permissions("im:conversation:manage");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(SetConversationRemarkEndpointRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var conversationId = Route<Guid>("ConversationId");
|
||||
await mediator.Send(new SetConversationRemarkCommand(conversationId, userId, req.Remark), ct);
|
||||
await Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 设置会话备注名请求体。Remark 为空或 null 表示清除备注。
|
||||
/// </summary>
|
||||
public record SetConversationRemarkEndpointRequest(string? Remark);
|
||||
@ -1,6 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 设置会话置顶/免打扰状态的请求体。isPinned 与 isMuted 不同时使用,视端点语义而定。
|
||||
/// </summary>
|
||||
public record SetConversationStateRequest(bool Value);
|
||||
@ -1,26 +0,0 @@
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using IM.Application.Chat.Commands;
|
||||
using IM.Application.Chat.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 更新群信息端点。PUT /groups/{GroupId}。
|
||||
/// </summary>
|
||||
public class UpdateGroupEndpoint(IMediator mediator, ICurrentUserContext userContext) : Endpoint<UpdateGroupEndpointRequest, GroupDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/groups/{GroupId}");
|
||||
Permissions("im:group:manage");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UpdateGroupEndpointRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserGuid();
|
||||
var result = await mediator.Send(new UpdateGroupCommand(req.GroupId, userId, req.Name, req.Avatar, req.Description, req.Announcement), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.IM.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// 更新群信息请求体。
|
||||
/// </summary>
|
||||
public record UpdateGroupEndpointRequest(Guid GroupId, string? Name, string? Avatar, string? Description, string? Announcement = null);
|
||||
@ -1,44 +0,0 @@
|
||||
using BuildFlow.Platform.Infrastructure.Persistence;
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 修改当前用户密码。验证旧密码后更新为新密码。
|
||||
/// POST /api/user/change-password
|
||||
/// </summary>
|
||||
public class ChangePasswordEndpoint(PlatformDbContext db, ICurrentUserContext userContext)
|
||||
: Endpoint<ChangePasswordRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/user/change-password");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ChangePasswordRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserId();
|
||||
|
||||
var user = await db.Users
|
||||
.FirstOrDefaultAsync(u => u.Id.ToString() == userId, ct)
|
||||
?? throw new UnauthorizedException("用户不存在");
|
||||
|
||||
// 验证旧密码
|
||||
if (!BCrypt.Net.BCrypt.Verify(req.OldPassword, user.PasswordHash))
|
||||
{
|
||||
throw new BusinessException("旧密码不正确");
|
||||
}
|
||||
|
||||
// 新密码不能与旧密码相同
|
||||
if (req.OldPassword == req.NewPassword)
|
||||
{
|
||||
throw new BusinessException("新密码不能与旧密码相同");
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.NewPassword);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
await Send.OkAsync(ct);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>修改密码请求。包含旧密码(用于二次校验)与新密码。</summary>
|
||||
public class ChangePasswordRequest
|
||||
{
|
||||
/// <summary>旧密码(登录态下二次校验,防止会话被劫持后改密)。</summary>
|
||||
public string OldPassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>新密码(明文传入,服务端 BCrypt 哈希后存储)。</summary>
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.Queries;
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户的权限码列表。前端据此渲染按钮/菜单的可见性(细粒度前端权限控制)。
|
||||
/// 鉴权通过 GetRequiredUserId 强制——未登录直接抛 401。
|
||||
/// </summary>
|
||||
public class GetAccessCodesEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest<List<string>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/auth/codes");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserId();
|
||||
var codes = await mediator.Send(new GetAccessCodesQuery(userId), ct);
|
||||
await Send.OkAsync(codes, ct);
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.Queries;
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前登录用户信息端点。前端登录后调用以填充用户菜单、权限、头像。
|
||||
/// GetRequiredUserId 在未登录时抛 UnauthorizedException,等同强制鉴权(无需显式 Permissions)。
|
||||
/// </summary>
|
||||
public class GetCurrentUserEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest<CurrentUserInfo>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/user/info");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserId();
|
||||
var result = await mediator.Send(new GetCurrentUserQuery(userId), ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.Commands;
|
||||
using BuildFlow.Platform.Application.Auth.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 登录端点。
|
||||
/// </summary>
|
||||
public class LoginEndpoint(IMediator mediator, IConfiguration config) : Endpoint<LoginRequest, TokenResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/auth/login");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(LoginRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new LoginCommand(req.Username, req.Password), ct);
|
||||
|
||||
// refresh_token 写入 HttpOnly Cookie:浏览器自动随请求带上,前端 JS 不可读,防 XSS
|
||||
HttpContext.Response.Cookies.Append("jwt", result.RefreshToken, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = config.GetValue<bool>("Cookie:Secure"),
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Expires = DateTimeOffset.UtcNow.AddDays(7),
|
||||
Path = "/"
|
||||
});
|
||||
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>登录请求校验。规则与 LoginCommandValidator 一致,HTTP 入口层提前拦截非法输入。</summary>
|
||||
@ -1,21 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 登录Request的请求校验器(FluentValidation)。
|
||||
/// </summary>
|
||||
public class LoginRequestValidator : Validator<LoginRequest>
|
||||
{
|
||||
public LoginRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("用户名不能为空")
|
||||
.MaximumLength(50).WithMessage("用户名长度不能超过 50 个字符")
|
||||
.Must(u => u == u.Trim()).WithMessage("用户名前后不能有空格");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("密码不能为空")
|
||||
.MaximumLength(100).WithMessage("密码长度不能超过 100 个字符");
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 登出接口:撤销 refresh token 并清除 jwt Cookie。
|
||||
/// 前端通过 baseRequestClient 调用(withCredentials: true 发送 Cookie)。
|
||||
/// </summary>
|
||||
public class LogoutEndpoint(IMediator mediator) : EndpointWithoutRequest<string>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/auth/logout");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
// 从 Cookie 中读取 refresh token
|
||||
var refreshToken = HttpContext.Request.Cookies["jwt"];
|
||||
|
||||
if (!string.IsNullOrEmpty(refreshToken))
|
||||
{
|
||||
await mediator.Send(new LogoutCommand(refreshToken), ct);
|
||||
}
|
||||
|
||||
// 清除 Cookie
|
||||
HttpContext.Response.Cookies.Delete("jwt");
|
||||
|
||||
await Send.OkAsync(string.Empty, ct);
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新令牌端点。从 HttpOnly Cookie 读 refresh_token(前端无法读取,只能由浏览器自动随请求带上),
|
||||
/// 换取新 access_token 以纯文本响应(被 ApiResponseMiddleware 列入 ExcludedPaths 不包裹)。
|
||||
/// ExcludeFromDescription 避免 Swagger 暴露此内部接口。
|
||||
/// </summary>
|
||||
public class RefreshTokenEndpoint(IMediator mediator, IConfiguration config) : EndpointWithoutRequest<string>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/auth/refresh");
|
||||
AllowAnonymous();
|
||||
Options(b => b.ExcludeFromDescription());
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var refreshToken = HttpContext.Request.Cookies["jwt"]
|
||||
?? throw new UnauthorizedException("缺少刷新令牌");
|
||||
|
||||
var result = await mediator.Send(new RefreshTokenCommand(refreshToken), ct);
|
||||
|
||||
// 新 refresh_token 写回 Cookie(Rotation:旧 token 已在 Handler 中作废)
|
||||
HttpContext.Response.Cookies.Append("jwt", result.RefreshToken, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = config.GetValue<bool>("Cookie:Secure"),
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Expires = DateTimeOffset.UtcNow.AddDays(7),
|
||||
Path = "/"
|
||||
});
|
||||
|
||||
// 用 FastEndpoints 的 Send.StringAsync 返回纯文本 access token。
|
||||
// 之前用 HttpContext.Response.WriteAsync 被 FastEndpoints 管线覆盖成 204 空响应。
|
||||
await Send.StringAsync(result.AccessToken, 200, "text/plain; charset=utf-8", ct);
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.Commands;
|
||||
using BuildFlow.Platform.Application.Auth.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 注册端点。
|
||||
/// </summary>
|
||||
public class RegisterEndpoint(IMediator mediator) : Endpoint<RegisterRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/auth/register");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(RegisterRequest req, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new RegisterCommand(req.Username, req.Email, req.Password), ct);
|
||||
await Send.OkAsync(true, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>自助注册请求校验。</summary>
|
||||
@ -1,24 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 注册Request的请求校验器(FluentValidation)。
|
||||
/// </summary>
|
||||
public class RegisterRequestValidator : Validator<RegisterRequest>
|
||||
{
|
||||
public RegisterRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("用户名不能为空")
|
||||
.Length(3, 50).WithMessage("用户名长度 3-50 个字符");
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("邮箱不能为空")
|
||||
.EmailAddress().WithMessage("邮箱格式不正确");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("密码不能为空")
|
||||
.Length(6, 100).WithMessage("密码长度 6-100 个字符");
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.Commands;
|
||||
using BuildFlow.Platform.Application.Auth.DTOs;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 吊销令牌端点。
|
||||
/// </summary>
|
||||
public class RevokeTokenEndpoint(IMediator mediator) : Endpoint<RevokeTokenRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/auth/revoke");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(RevokeTokenRequest req, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new RevokeTokenCommand(req.RefreshToken), ct);
|
||||
await Send.OkAsync(true, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>吊销刷新令牌请求校验。</summary>
|
||||
@ -1,15 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 吊销令牌Request的请求校验器(FluentValidation)。
|
||||
/// </summary>
|
||||
public class RevokeTokenRequestValidator : Validator<RevokeTokenRequest>
|
||||
{
|
||||
public RevokeTokenRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.RefreshToken)
|
||||
.NotEmpty().WithMessage("RefreshToken 不能为空");
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Auth.Commands;
|
||||
using BuildFlow.Platform.Application.Auth.Queries;
|
||||
using BuildFlow.Shared.Domain.Common;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前用户个人资料(姓名、头像、简介)。
|
||||
/// PUT /api/user/profile
|
||||
/// </summary>
|
||||
public class UpdateProfileEndpoint(IMediator mediator, ICurrentUserContext userContext)
|
||||
: Endpoint<UpdateProfileRequest, CurrentUserInfo>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/user/profile");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UpdateProfileRequest req, CancellationToken ct)
|
||||
{
|
||||
var userId = userContext.GetRequiredUserId();
|
||||
var result = await mediator.Send(new UpdateProfileCommand(
|
||||
UserId: userId,
|
||||
RealName: req.RealName,
|
||||
Avatar: req.Avatar,
|
||||
Description: req.Description), ct);
|
||||
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Auth;
|
||||
|
||||
/// <summary>更新个人资料请求。三个字段均可选,仅更新非 null 的字段。</summary>
|
||||
public class UpdateProfileRequest
|
||||
{
|
||||
/// <summary>真实姓名(可选)。</summary>
|
||||
public string? RealName { get; set; }
|
||||
|
||||
/// <summary>头像 URL(可选)。</summary>
|
||||
public string? Avatar { get; set; }
|
||||
|
||||
/// <summary>个人简介(可选)。</summary>
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Menus.Commands;
|
||||
using BuildFlow.Platform.Application.Menus.DTOs;
|
||||
using BuildFlow.Platform.Domain.Enums;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Menus;
|
||||
|
||||
/// <summary>
|
||||
/// 创建菜单端点。
|
||||
/// </summary>
|
||||
public class CreateMenuEndpoint(IMediator mediator) : Endpoint<CreateMenuRequest, MenuDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/menus");
|
||||
Permissions("menu:create");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateMenuRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new CreateMenuCommand
|
||||
{
|
||||
ParentId = req.ParentId,
|
||||
Name = req.Name,
|
||||
Path = req.Path,
|
||||
Component = req.Component,
|
||||
Redirect = req.Redirect,
|
||||
RenderType = MenuRuntimeFieldParser.ParseRenderType(req.RenderType) ?? MenuRenderType.Route,
|
||||
FormDefinitionId = req.FormDefinitionId,
|
||||
RuntimeViewMode = MenuRuntimeFieldParser.ParseRuntimeViewMode(req.RuntimeViewMode) ?? MenuRuntimeViewMode.List,
|
||||
Title = req.Title,
|
||||
Code = req.Code,
|
||||
Icon = req.Icon,
|
||||
ActiveIcon = req.ActiveIcon,
|
||||
Order = req.Order,
|
||||
HideInMenu = req.HideInMenu,
|
||||
KeepAlive = req.KeepAlive,
|
||||
AffixTab = req.AffixTab,
|
||||
Link = req.Link,
|
||||
NoBasicLayout = req.NoBasicLayout,
|
||||
MenuVisibleWithForbidden = req.MenuVisibleWithForbidden,
|
||||
Authority = req.Authority,
|
||||
}, ct);
|
||||
await Send.ResponseAsync(result, 201, ct);
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Menus.DTOs;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Menus;
|
||||
|
||||
/// <summary>
|
||||
/// 创建菜单请求校验。
|
||||
/// </summary>
|
||||
public class CreateMenuRequestValidator : Validator<CreateMenuRequest>
|
||||
{
|
||||
public CreateMenuRequestValidator()
|
||||
{
|
||||
// Name 可选:留空时由后端根据 Code 派生,无需用户填写。
|
||||
When(x => x.Name is not null, () =>
|
||||
{
|
||||
RuleFor(x => x.Name!).MaximumLength(100).WithMessage("菜单路由名称长度不能超过 100 个字符");
|
||||
});
|
||||
RuleFor(x => x.Path).NotEmpty().WithMessage("路由路径不能为空")
|
||||
.MaximumLength(200).WithMessage("路由路径长度不能超过 200 个字符");
|
||||
RuleFor(x => x.Title).NotEmpty().WithMessage("菜单标题不能为空")
|
||||
.MaximumLength(100).WithMessage("菜单标题长度不能超过 100 个字符");
|
||||
When(x => x.Component is not null, () =>
|
||||
{
|
||||
RuleFor(x => x.Component!).MaximumLength(200).WithMessage("组件路径长度不能超过 200 个字符");
|
||||
});
|
||||
When(x => x.Authority is not null, () =>
|
||||
{
|
||||
RuleFor(x => x.Authority!).MaximumLength(200).WithMessage("Authority 长度不能超过 200 个字符");
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using BuildFlow.Platform.Application.Menus.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildFlow.Host.Endpoints.Platform.Menus;
|
||||
|
||||
/// <summary>
|
||||
/// 删除菜单端点。
|
||||
/// </summary>
|
||||
public class DeleteMenuEndpoint(IMediator mediator) : Endpoint<DeleteMenuEndpointRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/menus/{Id}");
|
||||
Permissions("menu:delete");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DeleteMenuEndpointRequest req, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteMenuCommand(req.Id), ct);
|
||||
await Send.OkAsync(true, ct);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user