< Summary

Information
Class: OzonOrdersWeb.Api.Partner.PartnerAuthController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Api/Partner/PartnerAuthController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 47
Coverable lines: 47
Total lines: 113
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 24
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
Lookup()0%620%
RequestCode()0%7280%
VerifyCode()0%110100%
Logout()100%210%
Me()0%2040%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Api/Partner/PartnerAuthController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authentication;
 2using Microsoft.AspNetCore.Authorization;
 3using Microsoft.AspNetCore.Mvc;
 4using Microsoft.AspNetCore.RateLimiting;
 5using OzonOrdersWeb.Authorization;
 6using OzonOrdersWeb.Extensions;
 7using Services.PartnerServices;
 8
 9namespace OzonOrdersWeb.Api.Partner;
 10
 11/// <summary>Почта партнёра. Пустое или неверное значение отвечает так же, как неизвестная почта: ответ не подсказывает,
 12public sealed record PartnerEmailRequest(string? Email);
 13
 14/// <summary>Почта и код из письма (код входа или код из приглашения; пробелы в коде допустимы).</summary>
 15public sealed record PartnerCodeVerifyRequest(string? Email, string? Code);
 16
 17/// <summary>Вход и выход партнёра в кабинет PartnerOffice. Отдельная cookie-схема, не связанная с Identity сотрудников.
 18[ApiController]
 19[ApiExplorerSettings(GroupName = PartnerOfficeExtensions.SwaggerDocName)]
 20[ServiceEndpoint]
 21[IgnoreAntiforgeryToken]
 22[Route("partner-api/auth")]
 23[EnableRateLimiting(AuthorizationExtensions.AuthRateLimitPolicy)]
 024public sealed class PartnerAuthController(
 025    IPartnerAuthService auth,
 026    IPartnerManagementService management) : ControllerBase
 27{
 28    /// <summary>Название организации по почте. Всегда 200 и один формат ответа: для чужой почты name = null.</summary>
 29    [HttpPost("lookup")]
 30    [AllowAnonymous]
 31    [EnableRateLimiting(AuthorizationExtensions.PartnerLookupRateLimitPolicy)]
 32    public async Task<IActionResult> Lookup([FromBody] PartnerEmailRequest request, CancellationToken ct)
 33    {
 034        Response.Headers.CacheControl = "no-store";
 035        var name = await auth.LookupNameByEmailAsync(request.Email ?? string.Empty, ct);
 036        return Ok(new { name });
 037    }
 38
 39    /// <summary>Отправляет код входа на почту. Для неизвестной почты отвечает так же (202), письмо не уходит.</summary>
 40    [HttpPost("code/request")]
 41    [AllowAnonymous]
 42    public async Task<IActionResult> RequestCode([FromBody] PartnerEmailRequest request, CancellationToken ct)
 43    {
 044        var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
 045        var result = await auth.RequestCodeAsync(request.Email ?? string.Empty, ip, ct);
 46
 047        switch (result.Status)
 48        {
 49            case PartnerCodeRequestStatus.Throttled:
 050                Response.Headers.RetryAfter = result.RetryAfterSeconds.ToString();
 051                return StatusCode(StatusCodes.Status429TooManyRequests, new
 052                {
 053                    error = $"Код уже отправлен. Повторить можно через {result.RetryAfterSeconds} с.",
 054                    retryAfterSeconds = result.RetryAfterSeconds
 055                });
 56            case PartnerCodeRequestStatus.Locked:
 057                return StatusCode(StatusCodes.Status429TooManyRequests, new
 058                {
 059                    error = "Слишком много неверных кодов. Повторите позже.",
 060                    lockedUntilUtc = result.LockedUntilUtc
 061                });
 62            case PartnerCodeRequestStatus.MailFailed:
 063                return StatusCode(StatusCodes.Status503ServiceUnavailable, new { error = "Не удалось отправить письмо. П
 64            default:
 065                return Accepted();
 66        }
 067    }
 68
 69    [HttpPost("code/verify")]
 70    [AllowAnonymous]
 71    public async Task<IActionResult> VerifyCode([FromBody] PartnerCodeVerifyRequest request, CancellationToken ct)
 72    {
 073        var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
 074        var result = await auth.SignInWithCodeAsync(request.Email ?? string.Empty, request.Code ?? string.Empty, ip, ct)
 75
 076        switch (result.Status)
 77        {
 78            case PartnerLoginStatus.Success:
 079                await HttpContext.SignInAsync(
 080                    PartnerOfficeExtensions.PartnerCookieScheme,
 081                    PartnerOfficeExtensions.CreatePartnerPrincipal(result.Partner!));
 082                return Ok(new { result.Partner!.Id, result.Partner.Name, result.Partner.Email });
 83            case PartnerLoginStatus.Locked:
 084                return StatusCode(StatusCodes.Status429TooManyRequests, new
 085                {
 086                    error = "Слишком много неверных кодов. Повторите позже.",
 087                    lockedUntilUtc = result.LockedUntilUtc
 088                });
 89            default:
 90                // Отключённого партнёра и неверный код не различаем: ответ не должен раскрывать состояние учётной запис
 091                return Unauthorized(new { error = "Неверный или просроченный код." });
 92        }
 093    }
 94
 95    [HttpPost("logout")]
 96    [Authorize(AuthenticationSchemes = PartnerOfficeExtensions.PartnerCookieScheme)]
 97    public async Task<IActionResult> Logout()
 98    {
 099        await HttpContext.SignOutAsync(PartnerOfficeExtensions.PartnerCookieScheme);
 0100        return NoContent();
 0101    }
 102
 103    [HttpGet("me")]
 104    [Authorize(AuthenticationSchemes = PartnerOfficeExtensions.PartnerCookieScheme)]
 105    public async Task<IActionResult> Me(CancellationToken ct)
 106    {
 0107        if (!User.TryGetPartnerId(out var id)) return Unauthorized();
 0108        var partner = await management.GetAsync(id, ct);
 0109        return partner is null
 0110            ? Unauthorized()
 0111            : Ok(new { partner.Id, partner.Name, partner.Email, partner.Permissions });
 0112    }
 113}