fix(cicd): 删除 Endpoints/ 小写重复目录,统一 PascalCase

小写目录是大写目录内容完全一致的冗余副本,Linux 构建 duplicate items
This commit is contained in:
xiangning 2026-07-17 13:33:17 +08:00
parent 1ab9621803
commit d41c45c74d
329 changed files with 0 additions and 7086 deletions

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Buckets;
/// <summary>创建存储桶请求。携带待创建存储桶的名称。</summary>
public class CreateBucketRequest
{
public string Name { get; init; } = default!;
}

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Buckets;
/// <summary>删除存储桶请求。以查询参数传入存储桶名称。</summary>
public class DeleteBucketRequest
{
[QueryParam] public string Name { get; init; } = default!;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Files;
/// <summary>删除文件请求。以查询参数指定对象键。</summary>
public class DeleteFileRequest
{
[QueryParam] public string ObjectKey { get; init; } = default!;
}

View File

@ -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();
}
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Files;
/// <summary>下载文件请求。以查询参数指定对象键。</summary>
public class DownloadFileRequest
{
[QueryParam] public string ObjectKey { get; init; } = default!;
}

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Files;
/// <summary>文件内容请求。以查询参数指定对象键(bucket 由系统设置统一管理)。</summary>
public class GetFileContentRequest
{
[QueryParam] public string ObjectKey { get; init; } = default!;
}

View File

@ -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();
}
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Files;
/// <summary>Office/PDF 文档预览请求。以查询参数指定对象键。</summary>
public class GetOfficePreviewRequest
{
[QueryParam] public string ObjectKey { get; init; } = default!;
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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!;
}

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Folders;
/// <summary>删除文件夹请求(路由取 Id)。</summary>
public class DeleteFolderRequest
{
[RouteParam] public string Id { get; init; } = default!;
}

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Folders;
/// <summary>获取文件夹请求(路由取 Id)。</summary>
public class GetFolderRequest
{
[RouteParam] public string Id { get; init; } = default!;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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!;
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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!;
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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!;
}

View File

@ -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!;
}

View File

@ -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);
}
}

View File

@ -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!;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Settings;
/// <summary>更新默认存储桶请求。</summary>
public class UpdateDefaultBucketRequest
{
public string DefaultBucket { get; init; } = default!;
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Shares;
/// <summary>删除分享请求(路由取 Id)。</summary>
public class DeleteShareRequest
{
[RouteParam] public string Id { get; init; } = default!;
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -1,7 +0,0 @@
namespace BuildFlow.Host.Endpoints.File.Shares;
/// <summary>查询分享信息请求(路由取 Token)。</summary>
public class GetShareInfoRequest
{
[RouteParam] public string Token { get; init; } = default!;
}

View File

@ -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;
}
}

View File

@ -1,6 +0,0 @@
namespace BuildFlow.Host.Endpoints.IM.Chat;
/// <summary>
/// 批量添加群成员请求体。
/// </summary>
public record AddGroupMemberEndpointRequest(Guid GroupId, List<Guid> UserIds);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -1,6 +0,0 @@
namespace BuildFlow.Host.Endpoints.IM.Chat;
/// <summary>
/// 解散群组请求体(路由参数绑定)。
/// </summary>
public record DeleteGroupEndpointRequest(Guid GroupId);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,4 +0,0 @@
namespace BuildFlow.Host.Endpoints.IM.Chat;
/// <summary>查询群详情请求。GroupId 由路由参数绑定。</summary>
public record GetGroupEndpointRequest(Guid GroupId);

View File

@ -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);
}
}

View File

@ -1,6 +0,0 @@
namespace BuildFlow.Host.Endpoints.IM.Chat;
/// <summary>
/// 群成员列表查询请求体(路由参数绑定)。
/// </summary>
public record GetGroupMembersEndpointRequest(Guid GroupId);

View File

@ -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);
}
}

View File

@ -1,6 +0,0 @@
namespace BuildFlow.Host.Endpoints.IM.Chat;
/// <summary>
/// 历史消息查询请求体。含游标与分页大小。
/// </summary>
public record GetMessageHistoryRequest(Guid ConversationId, string? BeforeId, int Limit = 50);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,4 +0,0 @@
namespace BuildFlow.Host.Endpoints.IM.Chat;
/// <summary>撤回消息请求。ConversationId 与 MessageId 由路由参数绑定。</summary>
public record RecallMessageEndpointRequest(Guid ConversationId, string MessageId);

View File

@ -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;
}
}

View File

@ -1,6 +0,0 @@
namespace BuildFlow.Host.Endpoints.IM.Chat;
/// <summary>
/// 移除群成员请求体(路由参数绑定)。
/// </summary>
public record RemoveGroupMemberEndpointRequest(Guid GroupId, Guid TargetUserId);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,6 +0,0 @@
namespace BuildFlow.Host.Endpoints.IM.Chat;
/// <summary>
/// 设置会话备注名请求体。Remark 为空或 null 表示清除备注。
/// </summary>
public record SetConversationRemarkEndpointRequest(string? Remark);

View File

@ -1,6 +0,0 @@
namespace BuildFlow.Host.Endpoints.IM.Chat;
/// <summary>
/// 设置会话置顶/免打扰状态的请求体。isPinned 与 isMuted 不同时使用,视端点语义而定。
/// </summary>
public record SetConversationStateRequest(bool Value);

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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 个字符");
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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 个字符");
}
}

View File

@ -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>

View File

@ -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 不能为空");
}
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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 个字符");
});
}
}

View File

@ -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