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
@@ -50,13 +50,13 @@ public static class ConfigurationProgramExtensions
public static void AddServices(this IServiceCollection services) public static void AddServices(this IServiceCollection services)
{ {
services.AddSingleton<IPasswordHasher, PasswordHasher>(); services.AddSingleton<IPasswordHasher, PasswordHasher>();
services.AddScoped<IJwtTokenHasher, JwtTokenHasher>(); services.AddSingleton<IUsernameValidator, UsernameValidator>();
services.AddSingleton<IJwtTokenHasher, JwtTokenHasher>();
services.AddScoped<IJwtService, JwtService>(); services.AddScoped<IJwtService, JwtService>();
services.AddScoped<IAccountService, AuthService>(); services.AddScoped<IAccountService, AuthService>();
services.AddScoped<IUsersAdministration, UsersService>(); services.AddScoped<IUsersAdministration, UsersService>();
services.AddScoped<IInvitesService, InvitesService>(); services.AddScoped<IInvitesService, InvitesService>();
services.AddScoped<IInvitationGenerator, InvitationGenerator>(); services.AddScoped<IInvitationGenerator, InvitationGenerator>();
services.AddScoped<IUsernameValidator, UsernameValidator>();
services.AddScoped<ISynchingService, SynchingService>(); services.AddScoped<ISynchingService, SynchingService>();
// Friends services // Friends services
@@ -1,8 +1,10 @@
using Govor.Application.Interfaces; using Govor.Application.Interfaces;
using Govor.Contracts.Responses.Admins; using Govor.Contracts.Responses.Admins;
using Govor.Core.Infrastructure.Extensions;
using Govor.Core.Models.Users; using Govor.Core.Models.Users;
using Govor.Data.Repositories.Exceptions; using Govor.Data.Repositories.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers.AdminStuff; namespace Govor.API.Controllers.AdminStuff;
@@ -10,13 +12,16 @@ namespace Govor.API.Controllers.AdminStuff;
[ApiController] [ApiController]
[Route("api/admin/[controller]")] [Route("api/admin/[controller]")]
[Authorize(Roles = "Admin")] //[Authorize(Roles = "Admin")]
public class UsersController : Controller public class UsersController : Controller
{ {
private readonly ILogger<UsersController> _logger; private readonly ILogger<UsersController> _logger;
private readonly IUsersAdministration _users; private readonly IUsersAdministration _users;
public UsersController(ILogger<UsersController> logger, IUsersAdministration users, IInvitationGenerator invitationGenerator)
public UsersController(ILogger<UsersController> logger,
IUsersAdministration users,
IInvitationGenerator invitationGenerator)
{ {
_logger = logger; _logger = logger;
_users = users; _users = users;
@@ -61,6 +66,19 @@ public class UsersController : Controller
} }
} }
[HttpGet("user/{id:guid}/setpassword/{password}")]
public async Task<IActionResult> SetNewPassword(Guid id, string password)
{
try
{
await _users.SetPasswordAsync(id, password);
return Ok();
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
private List<UserResponse> BuildUserDtos(IEnumerable<User> users) => users.Select(user => new UserResponse private List<UserResponse> BuildUserDtos(IEnumerable<User> users) => users.Select(user => new UserResponse
{ {
+3
View File
@@ -16,6 +16,9 @@ var services = builder.Services;
builder.AddLogger();// Serilog builder.AddLogger();// Serilog
builder.Configuration.AddJsonFile("configs/ban_usernames.json", optional: false, reloadOnChange: true);
#if DEBUG #if DEBUG
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
//builder.Configuration.AddJsonFile("appsettings.Development.json", optional: false, reloadOnChange: true); //builder.Configuration.AddJsonFile("appsettings.Development.json", optional: false, reloadOnChange: true);
+1 -1
View File
@@ -6,7 +6,7 @@
} }
}, },
"ConnectionStrings": { "ConnectionStrings": {
"GovorDbContext": "Host=46.19.68.182;Port=5432;Database=default_db;Username=main;Password=fP6%,WzF$S?IUI;" "GovorDbContext": "Host=46.19.68.182;Port=5432;Database=default_db;Username=main;Password=1M^rf!1JKL:y,w;"
}, },
"UseMySql": false, "UseMySql": false,
"AllowedHosts": "*", "AllowedHosts": "*",
+1 -1
View File
@@ -6,7 +6,7 @@
} }
}, },
"ConnectionStrings": { "ConnectionStrings": {
"GovorDbContext": "Host=46.19.68.182;Port=5432;Database=default_db;Username=main;Password=fP6%,WzF$S?IUI;" "GovorDbContext": "Host=46.19.68.182;Port=5432;Database=default_db;Username=main;Password=1M^rf!1JKL:y,w;"
}, },
"UseMySql": false, "UseMySql": false,
"AllowedHosts": "*", "AllowedHosts": "*",
+180
View File
@@ -0,0 +1,180 @@
{
"UsernameModeration": {
"BlockedExact": [
"говор",
"админ",
"администратор",
"модератор",
"модер",
"поддержка",
"служба",
"службаподдержки",
"система",
"сервер",
"разработчик",
"команда",
"официальный",
"владелец",
"гость",
"аноним",
"тест",
"пользователь",
"овнер",
"govor",
"govoradmin",
"govorteam",
"admin",
"administrator",
"root",
"system",
"support",
"moderator",
"staff",
"team",
"official",
"owner",
"server",
"api",
"service",
"null",
"undefined",
"guest",
"test"
],
"BlockedContains": [
"гитлер",
"наци",
"нацизм",
"фашист",
"фашизм",
"рейх",
"сс",
"гестапо",
"свинорез",
"свинарез",
"серборез",
"сербарез",
"навальный",
"байден",
"трамп",
"террор",
"террорист",
"терроризм",
"исис",
"наркотик",
"наркота",
"кокаин",
"героин",
"мет",
"метамфетамин",
"лсд",
"марихуана",
"конопля",
"травка",
"опиум",
"клад",
"секс",
"порно",
"порнуха",
"хентай",
"эротика",
"яой",
"хуй",
"хуе",
"хуи",
"хер",
"пизд",
"еба",
"ебл",
"ебан",
"еблон",
"бля",
"бляд",
"сук",
"мраз",
"мроз",
"гандон",
"гондон",
"гондан",
"ххх",
"чмо",
"лох",
"лопух",
"гниль",
"говно",
"гавн",
"хохол",
"дегенерат",
"дегинират",
"дегенират",
"дигенират",
"hitler",
"nazi",
"nazism",
"fascist",
"fascism",
"reich",
"gestapo",
"terror",
"terrorist",
"isis",
"1488",
"drug",
"drugs",
"cocaine",
"heroin",
"meth",
"lsd",
"weed",
"marijuana",
"opium",
"porn",
"sex",
"xxx",
"fuck",
"shit",
"bitch",
"asshole",
"dick",
"pussy",
"bastard",
"whore"
],
"Reserved": [
"логин",
"вход",
"регистрация",
"профиль",
"настройки",
"чат",
"чаты",
"сообщения",
"друзья",
"login",
"register",
"profile",
"settings",
"chat",
"messages",
"friends",
"about",
"privacy",
"terms"
]
}
}
@@ -1,5 +1,6 @@
using Govor.Application.Exceptions.AuthService; using Govor.Application.Exceptions.AuthService;
using Govor.Application.Infrastructure.Validators; using Govor.Application.Infrastructure.Validators;
using Microsoft.Extensions.Configuration;
namespace Govor.Application.Tests.Infrastructure.Validators; namespace Govor.Application.Tests.Infrastructure.Validators;
@@ -11,7 +12,23 @@ public class UsernameValidatorTests
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
_validator = new UsernameValidator(); var configData = new Dictionary<string, string?>
{
["UsernameModeration:BlockedExact:0"] = "админ",
["UsernameModeration:BlockedExact:1"] = "модератор",
["UsernameModeration:BlockedContains:0"] = "гитлер",
["UsernameModeration:BlockedContains:1"] = "наци",
["UsernameModeration:Reserved:0"] = "логин",
["UsernameModeration:Reserved:1"] = "регистрация"
};
var config = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();
_validator = new UsernameValidator(config);
} }
[TestCase("Иван")] [TestCase("Иван")]
@@ -34,9 +51,45 @@ public class UsernameValidatorTests
Assert.Throws<InvalidUsernameException>(() => _validator.Validate(username)); Assert.Throws<InvalidUsernameException>(() => _validator.Validate(username));
} }
[TestCase("Ааааааа")]
[TestCase("Бbbbb")]
public void Validate_RepeatingCharacters_ShouldThrow(string username)
{
Assert.Throws<InvalidUsernameException>(() => _validator.Validate(username));
}
[TestCase("Админ")]
[TestCase("Модератор")]
public void Validate_BlockedExact_ShouldThrow(string username)
{
Assert.Throws<InvalidUsernameException>(() => _validator.Validate(username));
}
[TestCase("Гитлер123")]
[TestCase("Нацист")]
public void Validate_BlockedContains_ShouldThrow(string username)
{
Assert.Throws<InvalidUsernameException>(() => _validator.Validate(username));
}
[TestCase("Логин")]
[TestCase("Регистрация")]
public void Validate_ReservedNames_ShouldThrow(string username)
{
Assert.Throws<InvalidUsernameException>(() => _validator.Validate(username));
}
[TestCase("Г1тлер")]
public void Validate_Normalization_ShouldDetectBlockedWord(string username)
{
Assert.Throws<InvalidUsernameException>(() => _validator.Validate(username));
}
[TestCase("Иван", ExpectedResult = true)] [TestCase("Иван", ExpectedResult = true)]
[TestCase("1234", ExpectedResult = false)] [TestCase("1234", ExpectedResult = false)]
public bool TryValidate_ShouldReturnTrueRegardlessOfInput(string username) [TestCase("Админ", ExpectedResult = false)]
[TestCase("Гитлер123", ExpectedResult = false)]
public bool TryValidate_ShouldReturnExpectedResult(string username)
{ {
return _validator.TryValidate(username); return _validator.TryValidate(username);
} }
@@ -89,15 +89,7 @@ public class JwtTokenHasherTests
{ {
// Arrange // Arrange
var emptyConfig = new ConfigurationBuilder().Build(); var emptyConfig = new ConfigurationBuilder().Build();
var hasherWithDefaultSecret = new JwtTokenHasher(emptyConfig); // Act & Assert
Assert.Throws<InvalidOperationException>(() => new JwtTokenHasher(emptyConfig));
string token = _fixture.Create<string>();
// Act
string hash = hasherWithDefaultSecret.HashToken(token);
var result = hasherWithDefaultSecret.VerifyToken(token, hash);
// Assert
Assert.That(result, Is.True);
} }
} }
@@ -16,6 +16,7 @@
<PackageReference Include="FirebaseAdmin" Version="3.4.0" /> <PackageReference Include="FirebaseAdmin" Version="3.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" 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="Microsoft.IdentityModel.Tokens" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
</ItemGroup> </ItemGroup>
@@ -1,4 +1,5 @@
using Govor.Application.Interfaces; using Govor.Application.Interfaces;
using Govor.Core.Infrastructure.Extensions;
using Govor.Core.Models.Users; using Govor.Core.Models.Users;
using Govor.Core.Repositories.Users; using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions; using Govor.Data.Repositories.Exceptions;
@@ -8,10 +9,12 @@ namespace Govor.Application.Infrastructure.AdminsStuff;
public class UsersService : IUsersAdministration public class UsersService : IUsersAdministration
{ {
private readonly IUsersRepository _usersRepository; private readonly IUsersRepository _usersRepository;
private readonly IPasswordHasher _passwordHasher;
public UsersService(IUsersRepository usersRepository) public UsersService(IUsersRepository usersRepository, IPasswordHasher passwordHasher)
{ {
_usersRepository = usersRepository; _usersRepository = usersRepository;
_passwordHasher = passwordHasher;
} }
public async Task<List<User>> GetAllUsersAsync() 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) public async Task<User> GetUserById(Guid userId)
{ {
var result = await _usersRepository.FindByIdAsync(userId); var result = await _usersRepository.FindByIdAsync(userId);
@@ -2,6 +2,7 @@ using System.Text.RegularExpressions;
using Govor.Application.Exceptions.AuthService; using Govor.Application.Exceptions.AuthService;
using Govor.Application.Interfaces.Authentication; using Govor.Application.Interfaces.Authentication;
using Govor.Core.Infrastructure.Validators; using Govor.Core.Infrastructure.Validators;
using Microsoft.Extensions.Configuration;
namespace Govor.Application.Infrastructure.Validators; namespace Govor.Application.Infrastructure.Validators;
@@ -9,6 +10,33 @@ public class UsernameValidator : IUsernameValidator
{ {
private readonly Regex _usernameRegex = new(@"^[А-Яа-яЁё]+[А-Яа-яЁё0-9]*$", RegexOptions.Compiled); 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) public void Validate(string username)
{ {
if(username.Length < UserValidator.MIN_LENGHT_OF_NAME || username.Length > UserValidator.MAX_LENGHT_OF_NAME) 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)) if (!_usernameRegex.IsMatch(username))
throw new InvalidUsernameException("The username must be in Cyrillic and start with a letter."); 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) public bool TryValidate(string username)
@@ -31,4 +76,15 @@ public class UsernameValidator : IUsernameValidator
} }
} }
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<List<User>> GetAllUsersAsync();
Task<User> GetUserById(Guid userId); Task<User> GetUserById(Guid userId);
Task SetPasswordAsync(Guid userId, string password);
} }
@@ -7,26 +7,35 @@ namespace Govor.Application.Services.Authentication;
public class JwtTokenHasher : IJwtTokenHasher public class JwtTokenHasher : IJwtTokenHasher
{ {
private readonly string _pepper; private readonly byte[] _pepperBytes;
public JwtTokenHasher(IConfiguration config) 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) public string HashToken(string token)
{ {
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_pepper)); using var hmac = new HMACSHA256(_pepperBytes);
var bytes = Encoding.UTF8.GetBytes(token);
var hash = hmac.ComputeHash(bytes); var tokenBytes = Encoding.UTF8.GetBytes(token);
var hash = hmac.ComputeHash(tokenBytes);
return Convert.ToBase64String(hash); return Convert.ToBase64String(hash);
} }
public bool VerifyToken(string token, string storedHash) public bool VerifyToken(string token, string storedHash)
{ {
var currentHashBytes = Encoding.UTF8.GetBytes(HashToken(token)); using var hmac = new HMACSHA256(_pepperBytes);
var storedHashBytes = Encoding.UTF8.GetBytes(storedHash);
return CryptographicOperations.FixedTimeEquals(currentHashBytes, storedHashBytes); var tokenBytes = Encoding.UTF8.GetBytes(token);
var computedHash = hmac.ComputeHash(tokenBytes);
var storedHashBytes = Convert.FromBase64String(storedHash);
return CryptographicOperations.FixedTimeEquals(computedHash, storedHashBytes);
} }
} }