< Summary

Information
Class: Api.Auth.Endpoints.RegisterEndpoints
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Auth/Endpoints/RegisterEndpoints.cs
Line coverage
24%
Covered lines: 11
Uncovered lines: 34
Coverable lines: 45
Total lines: 115
Line coverage: 24.4%
Branch coverage
40%
Covered branches: 4
Total branches: 10
Branch coverage: 40%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Map(...)100%44100%
CanAdminRegister()100%11100%
PostRegister()0%620%
GetRegisterInfo()100%11100%
.ctor(...)100%11100%
.ctor(...)0%2040%
<-ctor()100%210%
<-ctor()0%620%

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)
 133    {
 134        route.MapPost("register", PostRegister);
 135        route.MapGet("register_info", GetRegisterInfo);
 136    }
 37
 38    private static async Task<bool> CanAdminRegister()
 139    {
 140        return true;
 141    }
 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            )
 053    {
 054        var password_bytes = Encoding.UTF8.GetBytes(dto.Password.Normalize());
 055        var hash_chars = new char[Argon2id.HashSize];
 056        Argon2id.ComputeHash(hash_chars, password_bytes, ARGON2ID_ITER, ARGON2ID_MEM_BYTES);
 057        string hash = hash_chars.ToString()!;
 58
 059        var user = new User()
 060        {
 061            Id = Guid.CreateVersion7(),
 062            Email = dto.Email,
 063            PasswordHash = hash,
 064        };
 65
 66        try
 067        {
 068            await db.Users.AddAsync(user);
 069            string token = await LoginUtils.CreateUserSession(user.Id, userAgent, db);
 070            await db.SaveChangesAsync();
 071            return TypedResults.Ok(token);
 72        }
 073        catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx &&
 074                                            pgEx.SqlState == PostgresErrorCodes.UniqueViolation)
 075        {
 076            return TypedResults.Conflict();
 77        }
 078    }
 79
 80    [AllowAnonymous]
 181    private static async Task<Ok<RegisterInfo>> GetRegisterInfo() => TypedResults.Ok(new RegisterInfo(
 182         CanRegisterAsAdmin: await CanAdminRegister()
 183        ));
 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    {
 097        public RegisterDTOValidator(PGContext ctx)
 098        {
 099            RuleFor(r => r.Email)
 0100                .Must(e => new EmailAddressAttribute().IsValid(e))
 0101                .WithMessage("Email is invalid");
 102
 0103            RuleFor(r => r.Email)
 0104                .MustAsync(async (e, ct) =>
 0105                        !await ctx.Users
 0106                        .Where(u => u.Email == e)
 0107                        .AnyAsync(ct))
 0108                .WithMessage("Email is already used");
 109
 110
 0111            RuleFor(r => r.AdminRegistration).MustAsync(async (adr, ct) => !adr || await CanAdminRegister()).WithMessage
 112
 0113        }
 114    }
 115}