< Summary

Information
Class: Api.Auth.Mutations.RegisterInputValidator
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Auth/Mutations/RegisterMutation.cs
Line coverage
100%
Covered lines: 16
Uncovered lines: 0
Coverable lines: 16
Total lines: 94
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/Mutations/RegisterMutation.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;
 7using Api.Auth.Models;
 8using Api.Auth.Utils;
 9using Api.Database;
 10using Api.Database.Utils;
 11using Api.Utils;
 12using FluentValidation;
 13using Geralt;
 14using HotChocolate.Authorization;
 15using Microsoft.EntityFrameworkCore;
 16
 17namespace Api.Auth.Mutations;
 18
 19public record RegisterInput(
 20    string Email,
 21    string Password,
 22    bool AdminRegistration,
 23    string ClientName = "unknown"
 24);
 25
 26[MutationType]
 27public static partial class RegisterMutations
 28{
 29    [AllowAnonymous]
 30    public static async Task<LoginPayload> RegisterMutation(
 31        [Service] PGContext db,
 32        [Service] IEFTransactionDIAccessorService txAccessor,
 33        [Service] IValidator<RegisterInput> validator,
 34        RegisterInput input,
 35        CancellationToken ct
 36    )
 37    {
 38        await validator.ValidateOrThrowAsync(input, ct);
 39
 40        var tx = await txAccessor.BeginOrGetTransactionAsync();
 41
 42        var password_bytes = Encoding.UTF8.GetBytes(input.Password.Normalize());
 43        var hash_chars = new char[Argon2id.HashSize];
 44        Argon2id.ComputeHash(
 45            hash_chars,
 46            password_bytes,
 47            AuthUtils.ARGON2ID_ITER,
 48            AuthUtils.ARGON2ID_MEM_BYTES
 49        );
 50        string hash = new([.. hash_chars.Where(c => c != (char)byte.MinValue)]);
 51
 52        var now = Now();
 53        var user = new UserEF()
 54        {
 55            Id = Guid.CreateVersion7().WithPostfix(UserEF.IdPostfix),
 56            MetadataAddedAt = now,
 57            MetadataUpdatedAt = now,
 58
 59            Email = input.Email.NormalizeEmail(),
 60            PasswordHash = hash,
 61            Admin = (await AuthUtils.CanAdminRegister(db)) && input.AdminRegistration,
 62        };
 63
 64        await db.Users.AddAsync(user);
 65        string token = await AuthUtils.CreateUserSession(user.Id, input.ClientName, db);
 66        await db.SaveChangesAsync();
 67        await tx.CommitAsync();
 68
 69        return new(Queries.UserMapper.ToDto(user), token);
 70    }
 71}
 72
 73public class RegisterInputValidator : AbstractValidator<RegisterInput>
 74{
 175    public RegisterInputValidator(PGContext db, IEFTransactionDIAccessorService tx)
 176    {
 177        RuleFor(w => w).BeginTransaction(tx);
 78
 179        RuleFor(r => r.Email)
 180            .Must(e => new EmailAddressAttribute().IsValid(e.NormalizeEmail()))
 181            .WithMessage("Email is invalid");
 82
 183        RuleFor(r => r.Email)
 184            .MustAsync(
 185                async (e, ct) =>
 186                    !await db.Users.Where(u => u.Email == e.NormalizeEmail()).AnyAsync(ct)
 187            )
 188            .WithMessage("Email is already used");
 89
 190        RuleFor(r => r.AdminRegistration)
 191            .MustAsync(async (adr, ct) => !adr || await AuthUtils.CanAdminRegister(db))
 192            .WithMessage("Can't Register As Admin");
 193    }
 94}