Инициализировать репозиторий «Дейл»

Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
Rustam Khalimov
2026-09-11 02:50:17 +03:00
commit 9e07568ddd
1402 changed files with 177470 additions and 0 deletions
@@ -0,0 +1,57 @@
using Deal.Api.Configuration;
using Deal.Api.Http;
using Deal.Modules.Tenants.Application;
using Microsoft.Extensions.Options;
// Имя конфигурационного типа совпадает с Microsoft.AspNetCore.Http.CookieOptions — фиксируем алиасом.
using OperatorCookieOptions = Deal.Api.Configuration.OperatorCookieOptions;
namespace Deal.Api.Middleware;
/// <summary>
/// Middleware операторской сессии: читает httpOnly-куку deal_operator_session, разрешает сессию через
/// OperatorAuthService и наполняет <c>HttpContext.Items["CurrentOperator"]</c> (Ruling 1 этапа 7).
/// </summary>
/// <remarks>
/// Зеркало <see cref="SessionMiddleware"/> для операторов: отдельная кука и отдельный ключ Items —
/// операторская сессия не может подменить тенантную и наоборот (разные имена куки, разные middleware).
/// Tenant-контекст (ITenantContext/CurrentUser) middleware не трогает — оператор не принадлежит тенанту.
/// Middleware НЕ отвечает 401 сама (pass-through): ручки /api/operator/*, требующие оператора, проверяют
/// <c>GetCurrentOperator()</c> и выставляют 401. OperatorAuthService — scoped, поэтому на запрос
/// создаётся собственный scope через RequestServices (как в SessionMiddleware).
/// </remarks>
public sealed class OperatorSessionMiddleware
{
private readonly RequestDelegate _next;
private readonly IOptionsMonitor<OperatorCookieOptions> _cookieOptions;
public OperatorSessionMiddleware(
RequestDelegate next,
IOptionsMonitor<OperatorCookieOptions> cookieOptions)
{
_next = next;
_cookieOptions = cookieOptions;
}
/// <summary>
/// Обрабатывает запрос: разрешает операторскую сессию по куке и наполняет контекст.
/// </summary>
/// <param name="context">Контекст запроса.</param>
public async Task InvokeAsync(HttpContext context)
{
var cookieName = _cookieOptions.CurrentValue.Name;
if (context.Request.Cookies.TryGetValue(cookieName, out var rawToken)
&& !string.IsNullOrWhiteSpace(rawToken))
{
// OperatorAuthService scoped: создаём scope на запрос через RequestServices.
await using var scope = context.RequestServices.CreateAsyncScope();
var operatorAuthService = scope.ServiceProvider.GetRequiredService<OperatorAuthService>();
var identity = await operatorAuthService.ResolveSessionAsync(rawToken, context.RequestAborted);
if (identity is not null)
{
context.SetCurrentOperator(new CurrentOperator(identity.Id, identity.Login, identity.Status));
}
}
await _next(context);
}
}