< Summary

Information
Class: Anichron.API.Endpoints.AuthEndpoints
Assembly: Anichron.API
File(s): /home/runner/work/anichron/anichron/src/Anichron.API/Endpoints/AuthEndpoints.cs
Tag: 228_36821334185
Line coverage
68%
Covered lines: 28
Uncovered lines: 13
Coverable lines: 41
Total lines: 94
Line coverage: 68.2%
Branch coverage
100%
Covered branches: 12
Total branches: 12
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
MapAuthEndpoints(...)100%210%
RegisterAsync()100%11100%
LoginWebAsync(...)100%11100%
LoginMobileAsync(...)100%11100%
HandleLoginAsync()100%11100%
RefreshAsync()100%66100%
LogoutAsync()100%66100%
PasswordResetRequest()100%210%
PasswordResetConfirm()100%210%

File(s)

/home/runner/work/anichron/anichron/src/Anichron.API/Endpoints/AuthEndpoints.cs

#LineLine coverage
 1using Anichron.API.Infrastructure;
 2using Anichron.API.Services;
 3using Anichron.API.Settings;
 4using Microsoft.AspNetCore.Mvc;
 5using Microsoft.Extensions.Options;
 6
 7namespace Anichron.API.Endpoints;
 8
 9public static class AuthEndpoints
 10{
 11    public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder app)
 012    {
 013        var group = app.MapGroup(ApiPaths.Auth.Group).WithTags("Auth");
 14
 015        group.MapPost(ApiPaths.Auth.Register, RegisterAsync).AllowAnonymous().RequireRateLimiting(AuthRateLimitPolicies.
 016        group.MapPost(ApiPaths.Auth.Login, LoginWebAsync).AllowAnonymous().RequireRateLimiting(AuthRateLimitPolicies.Sen
 017        group.MapPost(ApiPaths.Auth.LoginMobile, LoginMobileAsync).AllowAnonymous().RequireRateLimiting(AuthRateLimitPol
 018        group.MapPost(ApiPaths.Auth.Refresh, RefreshAsync).AllowAnonymous().RequireRateLimiting(AuthRateLimitPolicies.Re
 019        group.MapPost(ApiPaths.Auth.Logout, LogoutAsync).RequireAuthorization();
 020        group.MapPost(ApiPaths.Auth.PasswordResetRequest, PasswordResetRequest).AllowAnonymous().RequireRateLimiting(Aut
 021        group.MapPost(ApiPaths.Auth.PasswordResetConfirm, PasswordResetConfirm).AllowAnonymous().RequireRateLimiting(Aut
 22
 023        return app;
 024    }
 25
 26    internal static async Task<IResult> RegisterAsync(
 27        RegisterRequest request,
 28        IAuthService auth,
 29        IAuthResponseMapper mapper,
 30        HttpContext http,
 31        IOptions<PasswordPolicy> passwordPolicy,
 32        IOptions<UsernamePolicy> usernamePolicy,
 33        CancellationToken ct)
 234    {
 235        var result = await auth.RegisterAsync(request.Username, request.Email, request.Password, request.InviteToken, ct
 236        return mapper.GetRegistrationResult(result, http, passwordPolicy.Value, usernamePolicy.Value);
 237    }
 38
 39    internal static Task<IResult> LoginWebAsync(
 40        LoginRequest request, IAuthService auth, IAuthResponseMapper mapper, HttpContext http, CancellationToken ct)
 241        => HandleLoginAsync(request, auth, mapper, http, setCookie: true, ct);
 42
 43    internal static Task<IResult> LoginMobileAsync(
 44        LoginRequest request, IAuthService auth, IAuthResponseMapper mapper, HttpContext http, CancellationToken ct)
 245        => HandleLoginAsync(request, auth, mapper, http, setCookie: false, ct);
 46
 47    private static async Task<IResult> HandleLoginAsync(
 48        LoginRequest request, IAuthService auth, IAuthResponseMapper mapper, HttpContext http, bool setCookie, Cancellat
 449    {
 450        var result = await auth.LoginAsync(request.UsernameOrEmail, request.Password, ct);
 451        return mapper.GetLoginResult(result, http, setCookie);
 452    }
 53
 54    internal static async Task<IResult> RefreshAsync(
 55        HttpContext http,
 56        IAuthService auth,
 57        IAuthResponseMapper mapper,
 58        // [FromBody] is explicit here because the request is nullable —
 59        // web clients send no body (token arrives via cookie), mobile clients send it in the body.
 60        [FromBody] RefreshRequest? request,
 61        CancellationToken ct)
 362    {
 363        var rawToken = http.Request.Cookies[AuthMessages.RefreshTokenCookieName] ?? request?.RefreshToken;
 364        if (rawToken is null)
 165        {
 166            return Results.Json(
 167                data: new { error = AuthMessages.RefreshTokenRequired },
 168                statusCode: StatusCodes.Status401Unauthorized);
 69        }
 70
 271        var result = await auth.RefreshAsync(rawToken, ct);
 272        var setCookie = http.Request.Cookies.ContainsKey(AuthMessages.RefreshTokenCookieName);
 273        return mapper.GetRefreshResult(result, http, setCookie);
 374    }
 75
 76    internal static async Task<IResult> LogoutAsync(
 77        HttpContext http, IAuthService auth, IAuthResponseMapper mapper, [FromBody] RefreshRequest? request, Cancellatio
 378    {
 379        var rawToken = http.Request.Cookies[AuthMessages.RefreshTokenCookieName] ?? request?.RefreshToken;
 380        mapper.ClearRefreshCookie(http);
 381        if (rawToken is not null)
 282            await auth.RevokeAsync(rawToken, ct);
 383        return Results.NoContent();
 384    }
 85
 86    // Not yet implemented — planned for Epic 8 (email notifications with deep links).
 087    private static IResult PasswordResetRequest() => Results.StatusCode(StatusCodes.Status501NotImplemented);
 88
 089    private static IResult PasswordResetConfirm() => Results.StatusCode(StatusCodes.Status501NotImplemented);
 90}
 91
 92public sealed record RegisterRequest(string Username, string Email, string Password, string InviteToken);
 93public sealed record LoginRequest(string UsernameOrEmail, string Password);
 94public sealed record RefreshRequest(string RefreshToken);