Вынести условия-предикаты в extension-методы
HasUser из 13 endpoint-файлов сведён в AuthHelpers.HasUser; 15 приватных предикатов заменены extension-методами с удалением дублирующих приватных методов: IsCommunicationFailure, IsTransportFailure, IsPrivateEndpoint, IsConfigured, IsTrue, IsExpired, IsFailedLogin/IsSuccessfulLogin, HasChanges, HasBudget, HasAnyTerm, IsCurrencyLetter, IsEmojiCodePoint, ContainsFooterHint, IsTypeLabel.
This commit is contained in:
@@ -61,7 +61,7 @@ public static class AiSuggestEndpoints
|
||||
// «похожие колонки уже есть» — за адаптером LocalColumnSuggester (Ruling 3).
|
||||
private static async Task<IResult> SuggestColumnsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public static class AiSuggestEndpoints
|
||||
// Ответ — результат порта 1:1: {ok:true, keywords:[…]} (≤60) либо {ok:false, reason} (HTTP 200).
|
||||
private static async Task<IResult> SuggestKeywordsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -93,8 +93,4 @@ public static class AiSuggestEndpoints
|
||||
SuggestKeywordsResultDto result = await suggester.SuggestKeywordsAsync(ct);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public static class CardDetailsEndpoints
|
||||
// POST /api/cards: ручное создание «локальной» карточки. Ответ — созданная карточка.
|
||||
private static async Task<IResult> CreateCardAsync(CreateCardRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ public static class CardDetailsEndpoints
|
||||
// POST /api/cards/take {cardId}: «взять в работу» — перенос карточки в planned. Ответ — карточка.
|
||||
private static async Task<IResult> TakeCardAsync(TakeCardRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -141,7 +141,7 @@ public static class CardDetailsEndpoints
|
||||
// POST /api/cards/clear-rejected: полная очистка терминальной стадии «Отклонено».
|
||||
private static async Task<IResult> ClearRejectedAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ public static class CardDetailsEndpoints
|
||||
// (budget:null, stack:null) не теряется типизированным биндингом. Ответ — обновлённая карточка.
|
||||
private static async Task<IResult> PatchCardAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -177,7 +177,7 @@ public static class CardDetailsEndpoints
|
||||
// POST /api/cards/{cardId}/links {name?,url}: добавить ссылку. Ответ — карточка.
|
||||
private static async Task<IResult> AddLinkAsync(string cardId, CardLinkRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -201,7 +201,7 @@ public static class CardDetailsEndpoints
|
||||
// DELETE /api/cards/{cardId}/links/{linkId}: удалить ссылку. Ответ — карточка.
|
||||
private static async Task<IResult> RemoveLinkAsync(string cardId, string linkId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -218,7 +218,7 @@ public static class CardDetailsEndpoints
|
||||
// имя/ContentType/поток/длина → CardsService.AddFileAsync. Ранний null — гонка (404).
|
||||
private static async Task<IResult> UploadFilesAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -258,7 +258,7 @@ public static class CardDetailsEndpoints
|
||||
// с Content-Length/Content-Type из дескриптора; Content-Disposition attachment, имя без кавычек.
|
||||
private static async Task<IResult> DownloadFileAsync(string cardId, string fileId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -307,7 +307,7 @@ public static class CardDetailsEndpoints
|
||||
// DELETE /api/cards/{cardId}/files/{fileId}: открепить файл. Ответ — карточка.
|
||||
private static async Task<IResult> RemoveFileAsync(string cardId, string fileId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -322,7 +322,7 @@ public static class CardDetailsEndpoints
|
||||
// POST /api/cards/{cardId}/reminder {at: epoch-ms}: установить напоминание. Ответ — карточка.
|
||||
private static async Task<IResult> SetReminderAsync(string cardId, ReminderSetRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -347,7 +347,7 @@ public static class CardDetailsEndpoints
|
||||
// DELETE /api/cards/{cardId}/reminder: снять напоминание. Ответ — карточка.
|
||||
private static async Task<IResult> ClearReminderAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -361,7 +361,7 @@ public static class CardDetailsEndpoints
|
||||
// POST /api/cards/{cardId}/reminder/snooze: «напомнить позже» (now + 24 ч). Ответ — карточка.
|
||||
private static async Task<IResult> SnoozeReminderAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -424,9 +424,4 @@ public static class CardDetailsEndpoints
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса.
|
||||
// context: Контекст запроса.
|
||||
// Возвращает: True — сессия есть.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ public static class CardsEndpoints
|
||||
// Параметр col принят как алиас containerId (совместимость со старым фронтом).
|
||||
private static async Task<IResult> ListCardsAsync(string? containerId, string? col, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -108,7 +108,7 @@ public static class CardsEndpoints
|
||||
// GET /api/cards/counts: плоская wire-форма счётчиков {new, <col>:{count,new}, learning, ml, ai} (L161–163, §4.1 L257).
|
||||
private static async Task<IResult> CountsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -132,7 +132,7 @@ public static class CardsEndpoints
|
||||
// GET /api/cards/{cardId}: одна карточка; 404 «Карточка не найдена» (L166–168).
|
||||
private static async Task<IResult> GetCardAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -147,7 +147,7 @@ public static class CardsEndpoints
|
||||
// POST /api/cards/mark-all-seen: снять «новое» со всех карточек (L177–180); ответ {ok:true}.
|
||||
private static async Task<IResult> MarkAllSeenAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ public static class CardsEndpoints
|
||||
// POST /api/cards/mark-col-seen: снять «новое» с колонки (L187–191); ответ {ok:true}.
|
||||
private static async Task<IResult> MarkColSeenAsync(MarkColBody body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -184,7 +184,7 @@ public static class CardsEndpoints
|
||||
// несуществующем контейнере, 404 — карточки нет.
|
||||
private static async Task<IResult> MoveAsync(string cardId, MoveBody body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -214,7 +214,7 @@ public static class CardsEndpoints
|
||||
// POST /api/cards/{cardId}/trash: в корзину + обучение ML spam (L203–207); ответ {ok:true}; 404.
|
||||
private static async Task<IResult> TrashAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -234,7 +234,7 @@ public static class CardsEndpoints
|
||||
// POST /api/cards/{cardId}/restore: возврат из архив/корзины на канбан (L210–214); ответ {ok, col}; 404.
|
||||
private static async Task<IResult> RestoreAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -254,7 +254,7 @@ public static class CardsEndpoints
|
||||
// DELETE /api/cards/{cardId}: удалить навсегда (Cards + комментарии; L217–221); ответ {ok:true}; 404.
|
||||
private static async Task<IResult> DeleteAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -274,7 +274,7 @@ public static class CardsEndpoints
|
||||
// POST /api/cards/clear-col {col}: очистить корзину/архив (L228–235); ответ {ok, cleared}; 400.
|
||||
private static async Task<IResult> ClearColAsync(ClearColBody body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -289,7 +289,7 @@ public static class CardsEndpoints
|
||||
// POST /api/cards/{cardId}/comments {text}: добавить комментарий (L238–242); ответ {comments}; 400 «Пустой комментарий»; 404.
|
||||
private static async Task<IResult> AddCommentAsync(string cardId, CommentBody body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -321,7 +321,7 @@ public static class CardsEndpoints
|
||||
// ct: Токен отмены.
|
||||
private static async Task<IResult> ReclassifyAsync(ReclassifyBody? body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -339,7 +339,7 @@ public static class CardsEndpoints
|
||||
// ct: Токен отмены.
|
||||
private static async Task<IResult> ReclassifyOneAsync(string cardId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -416,7 +416,7 @@ public static class CardsEndpoints
|
||||
// GET /api/search?q=: поиск карточек (FTS + LIKE, Ruling 6/Task 12; dashboard_routes L254–256). Ответ {leads, messages: []}.
|
||||
private static async Task<IResult> SearchAsync(string? q, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -440,8 +440,4 @@ public static class CardsEndpoints
|
||||
ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>();
|
||||
return await containers.GetAsync(col, ct) is not null;
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public static class ContainersEndpoints
|
||||
// GET /api/containers?space=: список контейнеров пространства (или всех) со счётчиками.
|
||||
private static async Task<IResult> ListContainersAsync(string? space, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -74,7 +74,7 @@ public static class ContainersEndpoints
|
||||
// POST /api/containers: создать контейнер; ответ {id}.
|
||||
private static async Task<IResult> CreateContainerAsync(ContainerCreateRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -105,7 +105,7 @@ public static class ContainersEndpoints
|
||||
// PATCH /api/containers/{id}: частичное обновление; ответ {id}; 404 «Контейнер не найден».
|
||||
private static async Task<IResult> PatchContainerAsync(string containerId, JsonElement body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -165,7 +165,7 @@ public static class ContainersEndpoints
|
||||
// POST /api/containers/{id}/accept: принять ИИ-предложение (suggested=false).
|
||||
private static async Task<IResult> AcceptSuggestedAsync(string containerId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -185,7 +185,7 @@ public static class ContainersEndpoints
|
||||
// DELETE /api/containers/{id}: удалить контейнер; карточки → «Неразобранное» новыми.
|
||||
private static async Task<IResult> DeleteContainerAsync(string containerId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -201,7 +201,7 @@ public static class ContainersEndpoints
|
||||
// POST /api/containers/reorder: порядок контейнеров пространства; ответ {ok:true}.
|
||||
private static async Task<IResult> ReorderContainersAsync(OrderBody body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -219,7 +219,7 @@ public static class ContainersEndpoints
|
||||
// GET /api/containers/state: свёрнутость/ширина всех колонок (colState).
|
||||
private static async Task<IResult> GetColumnsStateAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -239,7 +239,7 @@ public static class ContainersEndpoints
|
||||
// PATCH /api/containers/{id}/state: merge патча в состояние колонки; ответ — состояние этой колонки.
|
||||
private static async Task<IResult> PatchColumnStateAsync(string containerId, ColStateBody body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -296,9 +296,4 @@ public static class ContainersEndpoints
|
||||
|
||||
return wire;
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса.
|
||||
// context: Контекст запроса.
|
||||
// Возвращает: True — сессия есть.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public static class DiscoveryEndpoints
|
||||
// GET /api/discovery/tasks: список задач, старые первыми (list_tasks L141–143).
|
||||
private static async Task<IResult> ListTasksAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -140,7 +140,7 @@ public static class DiscoveryEndpoints
|
||||
// POST /api/discovery/tasks: создать задачу поиска (create_task L146–151; дефолты — в сервисе).
|
||||
private static async Task<IResult> CreateTaskAsync(DiscoveryTaskCreateBody body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ public static class DiscoveryEndpoints
|
||||
// PATCH /api/discovery/tasks/{task_id}: обновить задачу (patch_task L154–161; 404/400).
|
||||
private static async Task<IResult> PatchTaskAsync(string task_id, DiscoveryTaskPatchBody body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -180,7 +180,7 @@ public static class DiscoveryEndpoints
|
||||
// DELETE /api/discovery/tasks/{task_id}: удалить задачу с кандидатами и логом (delete_task L164–168).
|
||||
private static async Task<IResult> DeleteTaskAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -193,7 +193,7 @@ public static class DiscoveryEndpoints
|
||||
// POST /api/discovery/tasks/{task_id}/start: запуск поиска (start_task L171–179; пустые ключи → 400).
|
||||
private static async Task<IResult> StartTaskAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -213,7 +213,7 @@ public static class DiscoveryEndpoints
|
||||
// POST /api/discovery/tasks/{task_id}/pause: пауза поиска (pause_task L181–187).
|
||||
private static async Task<IResult> PauseTaskAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -230,7 +230,7 @@ public static class DiscoveryEndpoints
|
||||
// Локальный режим (LocalAiTools, UseLocal=true) — NotSupportedException → та же мягкая ветка с текстом причины.
|
||||
private static async Task<IResult> GenerateKeywordsAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -277,7 +277,7 @@ public static class DiscoveryEndpoints
|
||||
// new|review|joined|rejected (list_candidates L216–224; невалидный статус — пустой список).
|
||||
private static async Task<IResult> ListCandidatesAsync(string task_id, string? status, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -299,7 +299,7 @@ public static class DiscoveryEndpoints
|
||||
// mark_joined(auto:false). Ошибка Telegram → 400 с текстом причины.
|
||||
private static async Task<IResult> JoinCandidateAsync(string dialog_id, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -355,7 +355,7 @@ public static class DiscoveryEndpoints
|
||||
// (reject_candidate L253–264; уже вступившего — нельзя, 400).
|
||||
private static async Task<IResult> RejectCandidateAsync(string dialog_id, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -386,7 +386,7 @@ public static class DiscoveryEndpoints
|
||||
// GET /api/discovery/blacklist: чёрный список источников (list_blacklist L269–271).
|
||||
private static async Task<IResult> ListBlacklistAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -399,7 +399,7 @@ public static class DiscoveryEndpoints
|
||||
// DELETE /api/discovery/blacklist/{dialog_id}: снять источник с чёрного списка (remove_blacklist L274–277).
|
||||
private static async Task<IResult> RemoveBlacklistAsync(string dialog_id, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -412,7 +412,7 @@ public static class DiscoveryEndpoints
|
||||
// GET /api/discovery/tasks/{task_id}/log: лог задачи (task_log L282–285), события от новых к старым.
|
||||
private static async Task<IResult> TaskLogAsync(string task_id, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -533,8 +533,4 @@ public static class DiscoveryEndpoints
|
||||
AutoJoin = body.AutoJoin,
|
||||
};
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public static class FilterTesterEndpoints
|
||||
// POST /api/admin/check-message: этап-1 правила + этап-2 (skipped) для тестера (dashboard_routes.py L267–284).
|
||||
private static async Task<IResult> CheckAsync(CheckMessageRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -72,8 +72,4 @@ public static class FilterTesterEndpoints
|
||||
passed = true,
|
||||
});
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public static class MlEndpoints
|
||||
// GET /api/ml/status: статус ML-сервиса + локальная статистика (ml_routes.py L66–75).
|
||||
private static async Task<IResult> StatusAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -90,7 +90,7 @@ public static class MlEndpoints
|
||||
// POST /api/ml/reset: полный сброс модели + очистка очереди обучения (ml_routes.py L78–81).
|
||||
private static async Task<IResult> ResetAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ public static class MlEndpoints
|
||||
// POST /api/ml/predict: проверка ML на тексте (ml_routes.py L84–90).
|
||||
private static async Task<IResult> PredictAsync(MlPredictRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public static class MlEndpoints
|
||||
// POST /api/ml/candidates: последние сообщения канала + мнение ML (ml_routes.py L112–134).
|
||||
private static async Task<IResult> CandidatesAsync(MlCandidatesRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -150,7 +150,7 @@ public static class MlEndpoints
|
||||
// POST /api/ml/apply: ручное решение по сообщению (ml_routes.py L137–171).
|
||||
private static async Task<IResult> ApplyAsync(MlApplyRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -175,8 +175,4 @@ public static class MlEndpoints
|
||||
leadId = result.LeadId,
|
||||
});
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public static class PipelineEndpoints
|
||||
// GET /api/pipeline/stats: сводка вкладки {queue:{new,ai,total}, rejected} (processing_routes.py L17–20, stats L315–320).
|
||||
private static async Task<IResult> StatsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ public static class PipelineEndpoints
|
||||
// rejected_count L201–202. limit — дефолт 100, clamp 1..500 делает сервис (ListQueueAsync).
|
||||
private static async Task<IResult> QueueAsync(int? limit, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -108,7 +108,7 @@ public static class PipelineEndpoints
|
||||
// первыми; offset ≥ 0, limit 1..500 (clamp в сервисе), значения эхом в ответе {items,total,offset,limit}.
|
||||
private static async Task<IResult> RejectedAsync(string? q, int? offset, int? limit, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -121,7 +121,7 @@ public static class PipelineEndpoints
|
||||
// POST /api/pipeline/rejected/clear: полная безвозвратная очистка отсева (processing_routes.py L45–49, clear_all L120–125).
|
||||
private static async Task<IResult> ClearAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public static class PipelineEndpoints
|
||||
// Прототип не проверяет наличие записи — 404 не шлём (план Task 9 L444; Ruling 10 «always ok»).
|
||||
private static async Task<IResult> DeleteAsync(string rejId, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -151,7 +151,7 @@ public static class PipelineEndpoints
|
||||
// прототипом); записи нет — 404 «Запись не найдена» (текст 404 — слой эндпоинтов).
|
||||
private static async Task<IResult> ReturnAsync(string rejId, ReturnReasonRequest body, HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -167,8 +167,4 @@ public static class PipelineEndpoints
|
||||
? EndpointResults.BadRequest(result.Error)
|
||||
: Results.Ok(new { id = result.Id, returned = result.Returned, returnedAt = result.ReturnedAtMs });
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public static class RatesEndpoints
|
||||
// GET /api/rates: текущий кэш курсов тенанта (+ ленивый фоновый refresh при необходимости).
|
||||
private static async Task<IResult> GetRatesAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ public static class RatesEndpoints
|
||||
// POST /api/rates/refresh: принудительное обновление; ответ {ok, rates} (1:1 settings_routes.py L229–232).
|
||||
private static async Task<IResult> RefreshRatesAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -82,8 +82,4 @@ public static class RatesEndpoints
|
||||
RatesDto current = await ratesService.GetAsync(ct);
|
||||
return Results.Ok(new { ok, rates = current });
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -1,132 +1,128 @@
|
||||
using System.Text.Json;
|
||||
using Deal.Api;
|
||||
using Deal.Api.Http;
|
||||
using Deal.Modules.Settings.Application;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Tenants.Application;
|
||||
|
||||
namespace Deal.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP-эндпоинты настроек тенанта: GET/PATCH /api/settings (api-map §3.4 L146–147, §4.6).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// GET — публичный снимок настроек (дефолты + переопределения, маски секретов, providers — Ruling 3);
|
||||
/// PATCH — произвольный JSON-объект публичных полей §4.6, ответ — полный снимок после применения
|
||||
/// (фронт затирает локальный state ответом — store.js). Оба эндпоинта требуют сессию:
|
||||
/// 401 {"detail":"Требуется авторизация"} (Ruling 10). Мягкая семантика: невалидное поле PATCH
|
||||
/// просто не применяется; жёсткая ошибка — только тело не JSON-объект (400).
|
||||
/// Побочные эффекты прототипа L186–192: PATCH с полем rateSource запускает фоновое
|
||||
/// обновление кэша курсов (<see cref="RatesRefreshScheduler"/>, Ruling 6); пересчёт карточек при смене
|
||||
/// targetCurrency/conversionOn выполняет сам SettingsService через порт <see cref="IRatesChangedListener"/>
|
||||
/// (реализация — ConversionRecomputer модуля Kanban, Ruling 7, Task 12).
|
||||
/// <para>
|
||||
/// SettingsService резолвится из RequestServices ВНУТРИ обработчика после проверки сессии, а не
|
||||
/// параметром эндпоинта: DI-биндинг параметров выполняется до тела обработчика, а зависимость
|
||||
/// сервиса — scoped TenantDbContext, опции которого строятся по tenant-контексту запроса
|
||||
/// (без сессии контекст не разрешим — ошибка конфигурации). Так запрос без сессии получает 401,
|
||||
/// а не 500 при резолве.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class SettingsEndpoints
|
||||
{
|
||||
private const string ApiGroupPrefix = "/api";
|
||||
private const string SettingsPath = "/settings";
|
||||
private const string SettingsOpenApiTag = "settings";
|
||||
private const string InvalidBodyDetail = "Тело запроса должно быть JSON-объектом";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует GET/PATCH /api/settings.
|
||||
/// </summary>
|
||||
/// <param name="app">Построитель маршрутов приложения.</param>
|
||||
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
||||
public static IEndpointRouteBuilder MapSettingsEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(ApiGroupPrefix).WithTags(SettingsOpenApiTag);
|
||||
|
||||
group.MapGet(SettingsPath, GetSettingsAsync);
|
||||
group.MapPatch(SettingsPath, PatchSettingsAsync);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
// GET /api/settings: публичный снимок настроек текущего тенанта.
|
||||
private static async Task<IResult> GetSettingsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
SettingsService settingsService = context.RequestServices.GetRequiredService<SettingsService>();
|
||||
return Results.Ok(await settingsService.GetPublicAsync(ct));
|
||||
}
|
||||
|
||||
// PATCH /api/settings: частичное обновление настроек; ответ — полный снимок после применения.
|
||||
private static async Task<IResult> PatchSettingsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
// Произвольный JSON-объект: ключи тела — публичные ключи §4.6 (как их шлёт фронт).
|
||||
Dictionary<string, JsonElement>? body;
|
||||
try
|
||||
{
|
||||
body = await JsonSerializer.DeserializeAsync<Dictionary<string, JsonElement>>(
|
||||
context.Request.Body,
|
||||
options: null,
|
||||
cancellationToken: ct);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Не-JSON или не-объект целиком — ошибка запроса: 400 + detail
|
||||
// (в прототипе FastAPI на такое тело — 422).
|
||||
return EndpointResults.BadRequest(InvalidBodyDetail);
|
||||
}
|
||||
|
||||
if (body is null)
|
||||
{
|
||||
return EndpointResults.BadRequest(InvalidBodyDetail);
|
||||
}
|
||||
|
||||
SettingsService settingsService = context.RequestServices.GetRequiredService<SettingsService>();
|
||||
PublicSettingsDto result = await settingsService.ApplyPatchAsync(body, ct);
|
||||
|
||||
// Аудит сохранения настроек (этап 10, T1): только имена полей — значения (в т.ч. секреты) не пишутся.
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.SettingsUpdated, new { fields = body.Keys }, ct);
|
||||
|
||||
// Смена источника курсов в PATCH (settings_routes.py L188–189) — фоновое обновление кэша
|
||||
// курсов (Ruling 6, Task 8). RefreshAsync читает уже сохранённую настройку rateSource.
|
||||
if (ShouldScheduleRatesRefresh(body))
|
||||
{
|
||||
context.RequestServices.GetRequiredService<RatesRefreshScheduler>().Schedule();
|
||||
}
|
||||
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
// Запускать ли фоновый refresh курсов после PATCH (семантика if body.get("rateSource") L188).
|
||||
// body: Тело PATCH — публичные ключи §4.6.
|
||||
// Возвращает: True — поле rateSource передано «правдивым» значением (не null/пустая строка).
|
||||
private static bool ShouldScheduleRatesRefresh(Dictionary<string, JsonElement> body)
|
||||
{
|
||||
if (!body.TryGetValue(SettingsKeys.RateSource, out JsonElement element))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// JSON-булево/число в python «правдивы» и запускают refresh; пустая строка/null — нет.
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => !string.IsNullOrEmpty(element.GetString()),
|
||||
JsonValueKind.Null => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
using System.Text.Json;
|
||||
using Deal.Api;
|
||||
using Deal.Api.Http;
|
||||
using Deal.Modules.Settings.Application;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Tenants.Application;
|
||||
|
||||
namespace Deal.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP-эндпоинты настроек тенанта: GET/PATCH /api/settings (api-map §3.4 L146–147, §4.6).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// GET — публичный снимок настроек (дефолты + переопределения, маски секретов, providers — Ruling 3);
|
||||
/// PATCH — произвольный JSON-объект публичных полей §4.6, ответ — полный снимок после применения
|
||||
/// (фронт затирает локальный state ответом — store.js). Оба эндпоинта требуют сессию:
|
||||
/// 401 {"detail":"Требуется авторизация"} (Ruling 10). Мягкая семантика: невалидное поле PATCH
|
||||
/// просто не применяется; жёсткая ошибка — только тело не JSON-объект (400).
|
||||
/// Побочные эффекты прототипа L186–192: PATCH с полем rateSource запускает фоновое
|
||||
/// обновление кэша курсов (<see cref="RatesRefreshScheduler"/>, Ruling 6); пересчёт карточек при смене
|
||||
/// targetCurrency/conversionOn выполняет сам SettingsService через порт <see cref="IRatesChangedListener"/>
|
||||
/// (реализация — ConversionRecomputer модуля Kanban, Ruling 7, Task 12).
|
||||
/// <para>
|
||||
/// SettingsService резолвится из RequestServices ВНУТРИ обработчика после проверки сессии, а не
|
||||
/// параметром эндпоинта: DI-биндинг параметров выполняется до тела обработчика, а зависимость
|
||||
/// сервиса — scoped TenantDbContext, опции которого строятся по tenant-контексту запроса
|
||||
/// (без сессии контекст не разрешим — ошибка конфигурации). Так запрос без сессии получает 401,
|
||||
/// а не 500 при резолве.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class SettingsEndpoints
|
||||
{
|
||||
private const string ApiGroupPrefix = "/api";
|
||||
private const string SettingsPath = "/settings";
|
||||
private const string SettingsOpenApiTag = "settings";
|
||||
private const string InvalidBodyDetail = "Тело запроса должно быть JSON-объектом";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует GET/PATCH /api/settings.
|
||||
/// </summary>
|
||||
/// <param name="app">Построитель маршрутов приложения.</param>
|
||||
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
||||
public static IEndpointRouteBuilder MapSettingsEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(ApiGroupPrefix).WithTags(SettingsOpenApiTag);
|
||||
|
||||
group.MapGet(SettingsPath, GetSettingsAsync);
|
||||
group.MapPatch(SettingsPath, PatchSettingsAsync);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
// GET /api/settings: публичный снимок настроек текущего тенанта.
|
||||
private static async Task<IResult> GetSettingsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
SettingsService settingsService = context.RequestServices.GetRequiredService<SettingsService>();
|
||||
return Results.Ok(await settingsService.GetPublicAsync(ct));
|
||||
}
|
||||
|
||||
// PATCH /api/settings: частичное обновление настроек; ответ — полный снимок после применения.
|
||||
private static async Task<IResult> PatchSettingsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
// Произвольный JSON-объект: ключи тела — публичные ключи §4.6 (как их шлёт фронт).
|
||||
Dictionary<string, JsonElement>? body;
|
||||
try
|
||||
{
|
||||
body = await JsonSerializer.DeserializeAsync<Dictionary<string, JsonElement>>(
|
||||
context.Request.Body,
|
||||
options: null,
|
||||
cancellationToken: ct);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Не-JSON или не-объект целиком — ошибка запроса: 400 + detail
|
||||
// (в прототипе FastAPI на такое тело — 422).
|
||||
return EndpointResults.BadRequest(InvalidBodyDetail);
|
||||
}
|
||||
|
||||
if (body is null)
|
||||
{
|
||||
return EndpointResults.BadRequest(InvalidBodyDetail);
|
||||
}
|
||||
|
||||
SettingsService settingsService = context.RequestServices.GetRequiredService<SettingsService>();
|
||||
PublicSettingsDto result = await settingsService.ApplyPatchAsync(body, ct);
|
||||
|
||||
// Аудит сохранения настроек (этап 10, T1): только имена полей — значения (в т.ч. секреты) не пишутся.
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.SettingsUpdated, new { fields = body.Keys }, ct);
|
||||
|
||||
// Смена источника курсов в PATCH (settings_routes.py L188–189) — фоновое обновление кэша
|
||||
// курсов (Ruling 6, Task 8). RefreshAsync читает уже сохранённую настройку rateSource.
|
||||
if (ShouldScheduleRatesRefresh(body))
|
||||
{
|
||||
context.RequestServices.GetRequiredService<RatesRefreshScheduler>().Schedule();
|
||||
}
|
||||
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
// Запускать ли фоновый refresh курсов после PATCH (семантика if body.get("rateSource") L188).
|
||||
// body: Тело PATCH — публичные ключи §4.6.
|
||||
// Возвращает: True — поле rateSource передано «правдивым» значением (не null/пустая строка).
|
||||
private static bool ShouldScheduleRatesRefresh(Dictionary<string, JsonElement> body)
|
||||
{
|
||||
if (!body.TryGetValue(SettingsKeys.RateSource, out JsonElement element))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// JSON-булево/число в python «правдивы» и запускают refresh; пустая строка/null — нет.
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => !string.IsNullOrEmpty(element.GetString()),
|
||||
JsonValueKind.Null => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public static class StorageEndpoints
|
||||
// PumpOnceAsync (сбой не роняет тик) → SSE new_card по созданным карточкам → queue. Формы — 1:1 с прототипом.
|
||||
private static async Task<IResult> AdminTickAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -75,7 +75,7 @@ public static class StorageEndpoints
|
||||
// is_ready() → ready) — кнопка Settings фронта показывает ошибку по ready (store.js L1883–1889).
|
||||
private static async Task<IResult> FtsRebuildAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -84,8 +84,4 @@ public static class StorageEndpoints
|
||||
bool ok = await fts.RebuildAsync(ct);
|
||||
return Results.Ok(new { ok, ready = ok });
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,7 @@ public static class TelegramQrImageEndpoint
|
||||
// GET /api/tg/qr-image: SVG QR-кода фазы входа «qr» (tg_routes.py L30–39).
|
||||
private static async Task<IResult> QrImageAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
if (!context.HasUser())
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
@@ -81,8 +81,4 @@ public static class TelegramQrImageEndpoint
|
||||
context.Response.Headers.ContentDisposition = "inline";
|
||||
return Results.Text(svg, SvgMediaType);
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,13 @@ public static class AuthHelpers
|
||||
public static CurrentUser? GetCurrentUser(this HttpContext context) =>
|
||||
context.Items[CurrentUserItemKey] as CurrentUser;
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, разрешена ли для запроса пользовательская сессия.
|
||||
/// </summary>
|
||||
/// <param name="context">Контекст запроса.</param>
|
||||
/// <returns>True — текущий пользователь установлен.</returns>
|
||||
public static bool HasUser(this HttpContext context) => context.GetCurrentUser() is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Кладёт оператора в <c>HttpContext.Items</c>.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user