< Summary

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

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Email()100%11100%

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>
 315public 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)]
 24public sealed class PartnerAuthController(
 25    IPartnerAuthService auth,
 26    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    {
 34        Response.Headers.CacheControl = "no-store";
 35        var name = await auth.LookupNameByEmailAsync(request.Email ?? string.Empty, ct);
 36        return Ok(new { name });
 37    }
 38
 39    /// <summary>Отправляет код входа на почту. Для неизвестной почты отвечает так же (202), письмо не уходит.</summary>
 40    [HttpPost("code/request")]
 41    [AllowAnonymous]
 42    public async Task<IActionResult> RequestCode([FromBody] PartnerEmailRequest request, CancellationToken ct)
 43    {
 44        var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
 45        var result = await auth.RequestCodeAsync(request.Email ?? string.Empty, ip, ct);
 46
 47        switch (result.Status)
 48        {
 49            case PartnerCodeRequestStatus.Throttled:
 50                Response.Headers.RetryAfter = result.RetryAfterSeconds.ToString();
 51                return StatusCode(StatusCodes.Status429TooManyRequests, new
 52                {
 53                    error = $"Код уже отправлен. Повторить можно через {result.RetryAfterSeconds} с.",
 54                    retryAfterSeconds = result.RetryAfterSeconds
 55                });
 56            case PartnerCodeRequestStatus.Locked:
 57                return StatusCode(StatusCodes.Status429TooManyRequests, new
 58                {
 59                    error = "Слишком много неверных кодов. Повторите позже.",
 60                    lockedUntilUtc = result.LockedUntilUtc
 61                });
 62            case PartnerCodeRequestStatus.MailFailed:
 63                return StatusCode(StatusCodes.Status503ServiceUnavailable, new { error = "Не удалось отправить письмо. П
 64            default:
 65                return Accepted();
 66        }
 67    }
 68
 69    [HttpPost("code/verify")]
 70    [AllowAnonymous]
 71    public async Task<IActionResult> VerifyCode([FromBody] PartnerCodeVerifyRequest request, CancellationToken ct)
 72    {
 73        var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
 74        var result = await auth.SignInWithCodeAsync(request.Email ?? string.Empty, request.Code ?? string.Empty, ip, ct)
 75
 76        switch (result.Status)
 77        {
 78            case PartnerLoginStatus.Success:
 79                await HttpContext.SignInAsync(
 80                    PartnerOfficeExtensions.PartnerCookieScheme,
 81                    PartnerOfficeExtensions.CreatePartnerPrincipal(result.Partner!));
 82                return Ok(new { result.Partner!.Id, result.Partner.Name, result.Partner.Email });
 83            case PartnerLoginStatus.Locked:
 84                return StatusCode(StatusCodes.Status429TooManyRequests, new
 85                {
 86                    error = "Слишком много неверных кодов. Повторите позже.",
 87                    lockedUntilUtc = result.LockedUntilUtc
 88                });
 89            default:
 90                // Отключённого партнёра и неверный код не различаем: ответ не должен раскрывать состояние учётной запис
 91                return Unauthorized(new { error = "Неверный или просроченный код." });
 92        }
 93    }
 94
 95    [HttpPost("logout")]
 96    [Authorize(AuthenticationSchemes = PartnerOfficeExtensions.PartnerCookieScheme)]
 97    public async Task<IActionResult> Logout()
 98    {
 99        await HttpContext.SignOutAsync(PartnerOfficeExtensions.PartnerCookieScheme);
 100        return NoContent();
 101    }
 102
 103    [HttpGet("me")]
 104    [Authorize(AuthenticationSchemes = PartnerOfficeExtensions.PartnerCookieScheme)]
 105    public async Task<IActionResult> Me(CancellationToken ct)
 106    {
 107        if (!User.TryGetPartnerId(out var id)) return Unauthorized();
 108        var partner = await management.GetAsync(id, ct);
 109        return partner is null
 110            ? Unauthorized()
 111            : Ok(new { partner.Id, partner.Name, partner.Email, partner.Permissions });
 112    }
 113}

Methods/Properties

get_Email()