< Summary

Information
Class: Api.Auth.Endpoints.RegisterEndpoints.RegisterDTOValidator
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Auth/Endpoints/RegisterEndpoints.cs
Line coverage
100%
Covered lines: 13
Uncovered lines: 0
Coverable lines: 13
Total lines: 128
Line coverage: 100%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
<-ctor()100%11100%
<-ctor()100%22100%

File(s)

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

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik <alper@alper-celik.dev>
 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
 23using SharpGrip.FluentValidation.AutoValidation.Endpoints.Extensions;
 24
 25namespace Api.Auth.Endpoints;
 26
 27public static class RegisterEndpoints
 28{
 29
 30    // see https://www.rfc-editor.org/rfc/rfc9106.html#name-recommendations
 31    const int ARGON2ID_ITER = 3;
 32    const int ARGON2ID_MEM_BYTES = 64 * 1024 * 1024;
 33    static bool _adminCreated = false;
 34
 35    public static void Map(IEndpointRouteBuilder route)
 36    {
 37        route.MapPost("register", PostRegister).AddFluentValidationAutoValidation();
 38        route.MapGet("register_info", GetRegisterInfo);
 39    }
 40
 41    private static async Task<bool> CanAdminRegister(PGContext db)
 42    {
 43        if (_adminCreated)
 44        {
 45            return false;
 46        }
 47
 48        _adminCreated = db.Users.Any(u => u.Admin == true);
 49        return !_adminCreated;
 50    }
 51
 52    [AllowAnonymous]
 53    private static async Task<Results<
 54        Ok<LoginUtils.LoginResultDTO>,
 55        Conflict,
 56        BadRequest
 57        >>
 58        PostRegister(
 59            HttpContext ctx,
 60            [FromServices] PGContext db,
 61            [FromHeader(Name = "user-agent")] string? userAgent,
 62            [FromBody] RegisterDTO dto
 63            )
 64    {
 65        userAgent ??= "unknown";
 66        var password_bytes = Encoding.UTF8.GetBytes(dto.Password.Normalize());
 67        var hash_chars = new char[Argon2id.HashSize];
 68        Argon2id.ComputeHash(hash_chars, password_bytes, ARGON2ID_ITER, ARGON2ID_MEM_BYTES);
 69        string hash = new(hash_chars);
 70
 71        var user = new User()
 72        {
 73            Id = Guid.CreateVersion7(),
 74            Email = dto.Email,
 75            PasswordHash = hash,
 76            Admin = (await CanAdminRegister(db)) && dto.AdminRegistration
 77        };
 78
 79        try
 80        {
 81            await db.Users.AddAsync(user);
 82            string token = await LoginUtils.CreateUserSession(user.Id, userAgent, db);
 83            await db.SaveChangesAsync();
 84            return LoginUtils.LogUserIn(ctx, token);
 85        }
 86        catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx &&
 87                                            pgEx.SqlState == PostgresErrorCodes.UniqueViolation)
 88        {
 89            return TypedResults.Conflict();
 90        }
 91    }
 92
 93    [AllowAnonymous]
 94    private static async Task<Ok<RegisterInfo>> GetRegisterInfo([FromServices] PGContext db) => TypedResults.Ok(new Regi
 95         CanRegisterAsAdmin: await CanAdminRegister(db)
 96        ));
 97
 98
 99    public record RegisterInfo(bool CanRegisterAsAdmin);
 100
 101    public record RegisterDTO
 102    {
 103        public required string Email { get; set; }
 104        public required string Password { get; set; }
 105        public required bool AdminRegistration { get; set; }
 106    }
 107
 108    public class RegisterDTOValidator : AbstractValidator<RegisterDTO>
 109    {
 1110        public RegisterDTOValidator(PGContext db)
 1111        {
 1112            RuleFor(r => r.Email)
 1113                .Must(e => new EmailAddressAttribute().IsValid(e))
 1114                .WithMessage("Email is invalid");
 115
 1116            RuleFor(r => r.Email)
 1117                .MustAsync(async (e, ct) =>
 1118                        !await db.Users
 1119                        .Where(u => u.Email == e)
 1120                        .AnyAsync(ct))
 1121                .WithMessage("Email is already used");
 122
 123
 1124            RuleFor(r => r.AdminRegistration).MustAsync(async (adr, ct) => !adr || await CanAdminRegister(db)).WithMessa
 125
 1126        }
 127    }
 128}