< Summary

Information
Class: OzonOrdersWeb.Extensions.PartnerOfficeExtensions
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Extensions/PartnerOfficeExtensions.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 53
Coverable lines: 53
Total lines: 93
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 12
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddPartnerOffice(...)0%620%
CreatePartnerPrincipal(...)100%210%
TryGetPartnerId(...)100%210%
ValidatePartnerSessionAsync()0%110100%
Reject(...)100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Extensions/PartnerOfficeExtensions.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.AspNetCore.Authentication;
 3using Microsoft.AspNetCore.Authentication.Cookies;
 4using OzonDomains.Models.Partners;
 5using Services.PartnerServices;
 6
 7namespace OzonOrdersWeb.Extensions;
 8
 9/// <summary>Регистрация кабинета партнёров PartnerOffice: сервисы и отдельная cookie-схема партнёра.</summary>
 10public static class PartnerOfficeExtensions
 11{
 12    public const string PartnerCookieScheme = "PartnerCookie";
 13    public const string PartnerRole = "Partner";
 14    public const string PartnerStampClaim = "partner_stamp";
 15    public const string SwaggerDocName = "partner";
 16
 17    public static IServiceCollection AddPartnerOffice(this IServiceCollection services, bool isProd)
 18    {
 019        services.AddOptions<PartnerOfficeOptions>().Configure<IConfiguration>((options, configuration) =>
 020            configuration.GetSection(PartnerOfficeOptions.SectionName).Bind(options));
 021        services.AddTransient<IPartnerLoginMailer, PartnerLoginMailer>();
 022        services.AddScoped<IPartnerAuditService, PartnerAuditService>();
 023        services.AddScoped<IPartnerAuthService, PartnerAuthService>();
 024        services.AddScoped<IPartnerManagementService, PartnerManagementService>();
 025        services.AddScoped<IPartnerOrdersService, PartnerOrdersService>();
 026        services.AddScoped<IPartnerPermissionService, PartnerPermissionService>();
 027        services.AddScoped<OzonOrdersWeb.WebServices.Partners.IPartnerOrderActionsService, OzonOrdersWeb.WebServices.Par
 28
 29        // Отдельная схема: партнёр не должен попадать в Identity сотрудников Studio2 и наоборот.
 030        services.AddAuthentication().AddCookie(PartnerCookieScheme, options =>
 031        {
 032            options.Cookie.Name = ".PartnerOffice.Auth";
 033            options.Cookie.HttpOnly = true;
 034            options.Cookie.SecurePolicy = isProd ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
 035            options.Cookie.SameSite = SameSiteMode.Strict;
 036            options.ExpireTimeSpan = TimeSpan.FromHours(12);
 037            options.SlidingExpiration = true;
 038
 039            // API без редиректов: SPA сама показывает форму входа.
 040            options.Events.OnRedirectToLogin = ctx =>
 041            {
 042                ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
 043                return Task.CompletedTask;
 044            };
 045            options.Events.OnRedirectToAccessDenied = ctx =>
 046            {
 047                ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
 048                return Task.CompletedTask;
 049            };
 050            options.Events.OnValidatePrincipal = ValidatePartnerSessionAsync;
 051        });
 52
 053        return services;
 54    }
 55
 56    public static ClaimsPrincipal CreatePartnerPrincipal(Partner partner)
 57    {
 058        var identity = new ClaimsIdentity(
 059            [
 060                new Claim(ClaimTypes.NameIdentifier, partner.Id.ToString()),
 061                new Claim(ClaimTypes.Name, partner.Login),
 062                new Claim(ClaimTypes.Role, PartnerRole),
 063                new Claim(PartnerStampClaim, partner.SecurityStamp)
 064            ],
 065            PartnerCookieScheme);
 066        return new ClaimsPrincipal(identity);
 67    }
 68
 69    public static bool TryGetPartnerId(this ClaimsPrincipal user, out long partnerId) =>
 070        long.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier), out partnerId);
 71
 72    /// <summary>Сессия живёт, пока партнёр существует, активен и не менялся пароль или статус блокировки.</summary>
 73    private static async Task ValidatePartnerSessionAsync(CookieValidatePrincipalContext context)
 74    {
 075        var principal = context.Principal;
 076        var stamp = principal?.FindFirstValue(PartnerStampClaim);
 077        if (principal is null || stamp is null || !principal.TryGetPartnerId(out var partnerId))
 78        {
 079            await Reject(context);
 080            return;
 81        }
 82
 083        var auth = context.HttpContext.RequestServices.GetRequiredService<IPartnerAuthService>();
 084        var partner = await auth.ValidateSessionAsync(partnerId, stamp, context.HttpContext.RequestAborted);
 085        if (partner is null) await Reject(context);
 086    }
 87
 88    private static Task Reject(CookieValidatePrincipalContext context)
 89    {
 090        context.RejectPrincipal();
 091        return context.HttpContext.SignOutAsync(PartnerCookieScheme);
 92    }
 93}