< Summary

Information
Class: Api.Auth.Endpoints.RegisterEndpoints.RegisterInfo
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Auth/Endpoints/RegisterEndpoints.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 115
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
.ctor(...)100%11100%

File(s)

/home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Auth/Endpoints/RegisterEndpoints.cs

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik <[email protected]>
 2//
 3// SPDX-License-Identifier: AGPL-3.0-or-later
 4
 5using System.ComponentModel.DataAnnotations;
 6using System.Text;
 7
 8using Api.Auth.Models;
 9using Api.Auth.Utils;
 10using Api.Database;
 11
 12using FluentValidation;
 13
 14using Geralt;
 15
 16using Microsoft.AspNetCore.Authorization;
 17using Microsoft.AspNetCore.Http.HttpResults;
 18using Microsoft.AspNetCore.Mvc;
 19using Microsoft.EntityFrameworkCore;
 20
 21using Npgsql;
 22
 23namespace Api.Auth.Endpoints;
 24
 25public static class RegisterEndpoints
 26{
 27
 28    // see https://www.rfc-editor.org/rfc/rfc9106.html#name-recommendations
 29    const int ARGON2ID_ITER = 3;
 30    const int ARGON2ID_MEM_BYTES = 64 * 1024 * 1024;
 31
 32    public static void Map(IEndpointRouteBuilder route)
 33    {
 34        route.MapPost("register", PostRegister);
 35        route.MapGet("register_info", GetRegisterInfo);
 36    }
 37
 38    private static async Task<bool> CanAdminRegister()
 39    {
 40        return true;
 41    }
 42
 43    [AllowAnonymous]
 44    private static async Task<Results<
 45        Ok<string>,
 46        Conflict
 47        >>
 48        PostRegister(
 49            [FromServices] PGContext db,
 50            [FromHeader(Name = "user-agent")] string userAgent,
 51            [FromBody] RegisterDTO dto
 52            )
 53    {
 54        var password_bytes = Encoding.UTF8.GetBytes(dto.Password.Normalize());
 55        var hash_chars = new char[Argon2id.HashSize];
 56        Argon2id.ComputeHash(hash_chars, password_bytes, ARGON2ID_ITER, ARGON2ID_MEM_BYTES);
 57        string hash = hash_chars.ToString()!;
 58
 59        var user = new User()
 60        {
 61            Id = Guid.CreateVersion7(),
 62            Email = dto.Email,
 63            PasswordHash = hash,
 64        };
 65
 66        try
 67        {
 68            await db.Users.AddAsync(user);
 69            string token = await LoginUtils.CreateUserSession(user.Id, userAgent, db);
 70            await db.SaveChangesAsync();
 71            return TypedResults.Ok(token);
 72        }
 73        catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx &&
 74                                            pgEx.SqlState == PostgresErrorCodes.UniqueViolation)
 75        {
 76            return TypedResults.Conflict();
 77        }
 78    }
 79
 80    [AllowAnonymous]
 81    private static async Task<Ok<RegisterInfo>> GetRegisterInfo() => TypedResults.Ok(new RegisterInfo(
 82         CanRegisterAsAdmin: await CanAdminRegister()
 83        ));
 84
 85
 186    public record RegisterInfo(bool CanRegisterAsAdmin);
 87
 88    public record RegisterDTO
 89    {
 90        public required string Email { get; set; }
 91        public required string Password { get; set; }
 92        public required bool AdminRegistration { get; set; }
 93    }
 94
 95    private class RegisterDTOValidator : AbstractValidator<RegisterDTO>
 96    {
 97        public RegisterDTOValidator(PGContext ctx)
 98        {
 99            RuleFor(r => r.Email)
 100                .Must(e => new EmailAddressAttribute().IsValid(e))
 101                .WithMessage("Email is invalid");
 102
 103            RuleFor(r => r.Email)
 104                .MustAsync(async (e, ct) =>
 105                        !await ctx.Users
 106                        .Where(u => u.Email == e)
 107                        .AnyAsync(ct))
 108                .WithMessage("Email is already used");
 109
 110
 111            RuleFor(r => r.AdminRegistration).MustAsync(async (adr, ct) => !adr || await CanAdminRegister()).WithMessage
 112
 113        }
 114    }
 115}

Methods/Properties

.ctor(bool)