was added new rules to usernames

This commit is contained in:
Artemy
2026-03-07 21:19:48 +07:00
parent ecb9f4581d
commit 08270e0350
13 changed files with 363 additions and 31 deletions
@@ -16,6 +16,7 @@
<PackageReference Include="FirebaseAdmin" Version="3.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="11.0.0-preview.1.26104.118" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
</ItemGroup>
@@ -1,4 +1,5 @@
using Govor.Application.Interfaces;
using Govor.Core.Infrastructure.Extensions;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions;
@@ -8,10 +9,12 @@ namespace Govor.Application.Infrastructure.AdminsStuff;
public class UsersService : IUsersAdministration
{
private readonly IUsersRepository _usersRepository;
public UsersService(IUsersRepository usersRepository)
private readonly IPasswordHasher _passwordHasher;
public UsersService(IUsersRepository usersRepository, IPasswordHasher passwordHasher)
{
_usersRepository = usersRepository;
_passwordHasher = passwordHasher;
}
public async Task<List<User>> GetAllUsersAsync()
@@ -27,6 +30,22 @@ public class UsersService : IUsersAdministration
}
}
public async Task SetPasswordAsync(Guid userId, string password)
{
try
{
var user = await _usersRepository.FindByIdAsync(userId);
user.PasswordHash = _passwordHasher.Hash(password);
await _usersRepository.UpdateAsync(user);
}
catch (NotFoundException ex)
{
throw new NotFoundException(ex.Message);
}
}
public async Task<User> GetUserById(Guid userId)
{
var result = await _usersRepository.FindByIdAsync(userId);
@@ -2,6 +2,7 @@ using System.Text.RegularExpressions;
using Govor.Application.Exceptions.AuthService;
using Govor.Application.Interfaces.Authentication;
using Govor.Core.Infrastructure.Validators;
using Microsoft.Extensions.Configuration;
namespace Govor.Application.Infrastructure.Validators;
@@ -9,6 +10,33 @@ public class UsernameValidator : IUsernameValidator
{
private readonly Regex _usernameRegex = new(@"^[А-Яа-яЁё]+[А-Яа-яЁё0-9]*$", RegexOptions.Compiled);
private readonly HashSet<string> _blockedExact;
private readonly List<string> _blockedContains;
private readonly HashSet<string> _reserved;
public UsernameValidator(IConfiguration config)
{
_blockedExact = config.GetSection("UsernameModeration:BlockedExact")
.Get<string[]>()?
.Select(Normalize)
.ToHashSet()
?? throw new InvalidOperationException("BlockedExact not set");
_blockedContains = config
.GetSection("UsernameModeration:BlockedContains")
.Get<string[]>()?
.Select(Normalize)
.ToList()
?? throw new InvalidOperationException("BlockedContains not set");
_reserved = config
.GetSection("UsernameModeration:Reserved")
.Get<string[]>()?
.Select(Normalize)
.ToHashSet()
?? throw new InvalidOperationException("Reserved not set");
}
public void Validate(string username)
{
if(username.Length < UserValidator.MIN_LENGHT_OF_NAME || username.Length > UserValidator.MAX_LENGHT_OF_NAME)
@@ -16,6 +44,23 @@ public class UsernameValidator : IUsernameValidator
if (!_usernameRegex.IsMatch(username))
throw new InvalidUsernameException("The username must be in Cyrillic and start with a letter.");
if (Regex.IsMatch(username, @"(.)\1{4,}"))
throw new InvalidUsernameException("Too many repeating characters.");
var normalized = Normalize(username);
if (_reserved.Contains(normalized))
throw new InvalidUsernameException("This username is reserved.");
if (_blockedExact.Contains(normalized))
throw new InvalidUsernameException("This username is not allowed.");
foreach (var banned in _blockedContains)
{
if (normalized.Contains(banned))
throw new InvalidUsernameException("Username contains prohibited content.");
}
}
public bool TryValidate(string username)
@@ -30,5 +75,16 @@ public class UsernameValidator : IUsernameValidator
return false;
}
}
private static string Normalize(string username)
{
return username
.ToLower()
.Replace("0", "о")
.Replace("1", "и")
.Replace("3", "е")
.Replace("4", "а")
.Replace("6", "б")
.Replace("8", "в");
}
}
@@ -6,4 +6,5 @@ public interface IUsersAdministration
{
Task<List<User>> GetAllUsersAsync();
Task<User> GetUserById(Guid userId);
Task SetPasswordAsync(Guid userId, string password);
}
@@ -7,26 +7,35 @@ namespace Govor.Application.Services.Authentication;
public class JwtTokenHasher : IJwtTokenHasher
{
private readonly string _pepper;
private readonly byte[] _pepperBytes;
public JwtTokenHasher(IConfiguration config)
{
_pepper = config["EncryptionOption:Secret"] ?? "D1fault%Lxng%Randxm^Secret^Key(123!";
var pepper = config["EncryptionOption:Secret"]
?? throw new InvalidOperationException("Pepper is missing");
_pepperBytes = Encoding.UTF8.GetBytes(pepper);
}
public string HashToken(string token)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_pepper));
var bytes = Encoding.UTF8.GetBytes(token);
var hash = hmac.ComputeHash(bytes);
using var hmac = new HMACSHA256(_pepperBytes);
var tokenBytes = Encoding.UTF8.GetBytes(token);
var hash = hmac.ComputeHash(tokenBytes);
return Convert.ToBase64String(hash);
}
public bool VerifyToken(string token, string storedHash)
{
var currentHashBytes = Encoding.UTF8.GetBytes(HashToken(token));
var storedHashBytes = Encoding.UTF8.GetBytes(storedHash);
return CryptographicOperations.FixedTimeEquals(currentHashBytes, storedHashBytes);
using var hmac = new HMACSHA256(_pepperBytes);
var tokenBytes = Encoding.UTF8.GetBytes(token);
var computedHash = hmac.ComputeHash(tokenBytes);
var storedHashBytes = Convert.FromBase64String(storedHash);
return CryptographicOperations.FixedTimeEquals(computedHash, storedHashBytes);
}
}