Удалены <remarks>, <summary> сжаты до короткой фразы, вырезаны ссылки на Task/Ruling/этап/python/прототип; //-комментарии со ссылками на процесс удалены; то же в .proto. Правила обновлены в docs/spec/Код-стайл-Дейл.md. Строк комментариев 27210 -> ~19100.
99 lines
4.9 KiB
C#
99 lines
4.9 KiB
C#
using Deal.Api.Services;
|
|
using Deal.Modules.Tenants.Application.Models;
|
|
using Deal.Modules.Tenants.Application.Services;
|
|
|
|
namespace Deal.Api.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Публичный эндпоинт активации инвайта
|
|
/// </summary>
|
|
public static class JoinEndpoint
|
|
{
|
|
// Путь ручки (вне группы /api/operator — публичная).
|
|
private const string JoinPath = "/api/join";
|
|
|
|
// OpenAPI-тег.
|
|
private const string JoinOpenApiTag = "join";
|
|
|
|
// Текст 400: приглашение с таким кодом не найдено.
|
|
private const string InviteNotFoundDetail = "Приглашение не найдено";
|
|
|
|
private const string InviteExpiredDetail = "Срок действия приглашения истёк";
|
|
|
|
// Текст 400: приглашение уже активировано (повторная активация тем же кодом).
|
|
private const string InviteUsedDetail = "Приглашение уже использовано";
|
|
|
|
// Текст 400: приглашение отозвано оператором.
|
|
private const string InviteRevokedDetail = "Приглашение отозвано";
|
|
|
|
private const string EmailMismatchDetail = "Email не совпадает с приглашением";
|
|
|
|
private const string EmailTakenDetail = "Этот email уже зарегистрирован";
|
|
|
|
private const string PasswordTooShortDetail = "Пароль слишком короткий (минимум 8 символов)";
|
|
|
|
// Текст 400: целевой тенант инвайта не существует (Security review).
|
|
private const string TenantNotFoundDetail = "Тенант приглашения не найден";
|
|
|
|
// Текст 400: целевой тенант инвайта приостановлен (Security review).
|
|
private const string TenantSuspendedDetail = "Тенант приглашения приостановлен";
|
|
|
|
/// <summary>
|
|
/// Регистрирует POST /api/join.
|
|
/// </summary>
|
|
/// <param name="app">Построитель маршрутов приложения.</param>
|
|
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
|
public static IEndpointRouteBuilder MapJoinEndpoint(this IEndpointRouteBuilder app)
|
|
{
|
|
app.MapPost(JoinPath, JoinAsync).WithTags(JoinOpenApiTag);
|
|
return app;
|
|
}
|
|
|
|
private static async Task<IResult> JoinAsync(
|
|
JoinRequest body,
|
|
JoinService joinService,
|
|
AuditService auditService,
|
|
HttpContext context,
|
|
CancellationToken ct)
|
|
{
|
|
var result = await joinService.ActivateAsync(body.Code, body.Email, body.Name, body.Password, ct);
|
|
if (!result.Ok || result.Login is null || result.UserId is null || result.TenantId is null)
|
|
{
|
|
return EndpointResults.BadRequest(DetailFor(result.Error));
|
|
}
|
|
|
|
await auditService.AppendAsync(new AuditRecordDto(
|
|
AuditEvents.InviteJoined,
|
|
AuditActorTypes.Tenant,
|
|
ActorId: result.UserId,
|
|
TenantId: result.TenantId,
|
|
Ip: ClientIp(context),
|
|
// Код инвайта — capability-токен: в аудит пишется только SHA-256-хэш (Security review).
|
|
DetailJson: AuditService.ToDetailJson(new { email = result.Login, codeHash = SessionTokens.HashToken(body.Code?.Trim() ?? string.Empty) })), ct);
|
|
|
|
return Results.Ok(new { ok = true, login = result.Login });
|
|
}
|
|
|
|
// Фиксированный текст 400 по коду ошибки JoinService (все отказы активации — 400).
|
|
// error: Код ошибки JoinResultDto.
|
|
// Возвращает: Текст детали ошибки.
|
|
private static string DetailFor(string? error) =>
|
|
error switch
|
|
{
|
|
JoinResultDto.ErrorExpired => InviteExpiredDetail,
|
|
JoinResultDto.ErrorUsed => InviteUsedDetail,
|
|
JoinResultDto.ErrorRevoked => InviteRevokedDetail,
|
|
JoinResultDto.ErrorEmailMismatch => EmailMismatchDetail,
|
|
JoinResultDto.ErrorEmailTaken => EmailTakenDetail,
|
|
JoinResultDto.ErrorPasswordTooShort => PasswordTooShortDetail,
|
|
JoinResultDto.ErrorTenantNotFound => TenantNotFoundDetail,
|
|
JoinResultDto.ErrorTenantSuspended => TenantSuspendedDetail,
|
|
_ => InviteNotFoundDetail,
|
|
};
|
|
|
|
// IP-адрес клиента для аудита (без порта; null, если недоступен).
|
|
// context: Контекст запроса.
|
|
// Возвращает: Строковое представление IP или null.
|
|
private static string? ClientIp(HttpContext context) => context.Connection.RemoteIpAddress?.ToString();
|
|
}
|