From ec44347bbe287113f901a847e85eb8a0d458ce3c Mon Sep 17 00:00:00 2001 From: Artemy <109195690+stalcker2288969@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:36:56 +0700 Subject: [PATCH] Add username validation and initial friends feature Introduced a UsernameValidator with Cyrillic and length checks, integrated into AuthService and registration flow. Added InvalidUsernameException and related interface. Updated User model to support friend requests, added FriendsController (stub), and configured Friendship entity in EF Core. Adjusted UserValidator max name length and removed navigation properties from PrivateChat. --- .../Authentication/AuthServiceTests.cs | 5 +- .../Validators/UsernameValidatorTests.cs | 43 ++++++++++++++ Govor.API/Controllers/AuthController.cs | 8 ++- Govor.API/Controllers/FriendsController.cs | 56 +++++++++++++++++++ .../ConfigurationProgramExtensions.cs | 2 + .../AuthService/InvalidUsernameException.cs | 8 +++ .../Authentication/IUsernameValidator.cs | 8 +++ Govor.Application/Services/AuthService.cs | 9 ++- .../Validators/UsernameValidator.cs | 35 ++++++++++++ .../Validators/UserValidator.cs | 2 +- Govor.Core/Models/PrivateChat.cs | 2 - Govor.Core/Models/User.cs | 2 + .../Configurations/FriendshipConfiguration.cs | 26 +++++++++ Govor.Data/GovorDbContext.cs | 1 + 14 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 Govor.API.Tests/UnitTests/Services/Validators/UsernameValidatorTests.cs create mode 100644 Govor.API/Controllers/FriendsController.cs create mode 100644 Govor.Application/Exceptions/AuthService/InvalidUsernameException.cs create mode 100644 Govor.Application/Interfaces/Authentication/IUsernameValidator.cs create mode 100644 Govor.Application/Validators/UsernameValidator.cs create mode 100644 Govor.Data/Configurations/FriendshipConfiguration.cs diff --git a/Govor.API.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs b/Govor.API.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs index 86d6990..3abea94 100644 --- a/Govor.API.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs +++ b/Govor.API.Tests/UnitTests/Services/Authentication/AuthServiceTests.cs @@ -19,6 +19,7 @@ public class AuthServiceTests private Mock _jwtServiceMock; private Mock _usersRepositoryMock; private Mock _adminsRepositoryMock; + private Mock _usernameValidatorMock; private IAccountService _accountService; @@ -38,12 +39,14 @@ public class AuthServiceTests _passwordHasherMock = new Mock(); _jwtServiceMock = new Mock(); _adminsRepositoryMock = new Mock(); + _usernameValidatorMock = new Mock(); _accountService = new AuthService( _usersRepositoryMock.Object, _jwtServiceMock.Object, _passwordHasherMock.Object, - _adminsRepositoryMock.Object + _adminsRepositoryMock.Object, + _usernameValidatorMock.Object ); } diff --git a/Govor.API.Tests/UnitTests/Services/Validators/UsernameValidatorTests.cs b/Govor.API.Tests/UnitTests/Services/Validators/UsernameValidatorTests.cs new file mode 100644 index 0000000..510baba --- /dev/null +++ b/Govor.API.Tests/UnitTests/Services/Validators/UsernameValidatorTests.cs @@ -0,0 +1,43 @@ +using Govor.Application.Validators; // или другой namespace +using Govor.Application.Exceptions.AuthService; + +namespace Govor.API.Tests.UnitTests.Services.Validators; + +[TestFixture] +public class UsernameValidatorTests +{ + private UsernameValidator _validator; + + [SetUp] + public void SetUp() + { + _validator = new UsernameValidator(); + } + + [TestCase("Иван")] + [TestCase("Алексей")] + [TestCase("Ёжик")] + public void Validate_ValidUsernames_ShouldNotThrow(string username) + { + Assert.DoesNotThrow(() => _validator.Validate(username)); + } + + [TestCase("Ivan")] // не кириллица + [TestCase("123Иван")] // начинается не с буквы + [TestCase("Иван123")] // содержит цифры + [TestCase("!@#$")] // спецсимволы + [TestCase("")] // пусто + [TestCase("И")] // меньше минимума + [TestCase("ИванИванИванИванИванИванИванИванИванИванИванИванИван")] // больше максимума (44 символа) + public void Validate_InvalidUsernames_ShouldThrow(string username) + { + Assert.Throws(() => _validator.Validate(username)); + } + + [TestCase("Иван", ExpectedResult = true)] + [TestCase("1234", ExpectedResult = false)] + public bool TryValidate_ShouldReturnTrueRegardlessOfInput(string username) + { + return _validator.TryValidate(username); + } +} \ No newline at end of file diff --git a/Govor.API/Controllers/AuthController.cs b/Govor.API/Controllers/AuthController.cs index aff2159..34badb2 100644 --- a/Govor.API/Controllers/AuthController.cs +++ b/Govor.API/Controllers/AuthController.cs @@ -35,7 +35,8 @@ public class AuthController : Controller var invite = _invitesService.Validate(registrationRequest.InviteLink); - var token = await _accountService.RegistrationAsync(registrationRequest.Name, registrationRequest.Password, invite); + var token = await _accountService.RegistrationAsync(registrationRequest.Name, registrationRequest.Password, + invite); _logger.LogInformation($"Register request for {registrationRequest.Name}"); return Ok(new { token }); } @@ -49,6 +50,11 @@ public class AuthController : Controller _logger.LogWarning(ex, $"Invite link invalid: {registrationRequest.InviteLink}"); return BadRequest("Invite link invalid."); } + catch (InvalidUsernameException ex) + { + _logger.LogWarning(ex, $"Invalid username: {registrationRequest.Name}"); + return BadRequest($"Invalid username: {ex.Message}"); + } catch (Exception ex) { _logger.LogError(ex, "Unexpected error during registration for user {Name}", registrationRequest.Name); diff --git a/Govor.API/Controllers/FriendsController.cs b/Govor.API/Controllers/FriendsController.cs new file mode 100644 index 0000000..a221c8e --- /dev/null +++ b/Govor.API/Controllers/FriendsController.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Govor.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(Roles = "User,Admin")] +public class FriendsController : Controller +{ + private readonly ILogger _logger; + + [HttpGet("search")] + public async Task Search([FromBody] string query) + { + return BadRequest("Not a valid request"); + } + + [HttpPost("request")] + public async Task SendRequest([FromBody] Guid targetUserId) + { + return BadRequest("Not a valid request"); + } + + [HttpGet("requests")] + public async Task GetIncomingRequests() + { + return BadRequest("Not a valid request"); + } + + [HttpPost("accept")] + public async Task AcceptFriend([FromBody] Guid requesterId) + { + return BadRequest("Not a valid request"); + } + + [HttpGet] + public async Task GetFriends() + { + return BadRequest("Not a valid request"); + } + + private Guid GetCurrentUserId() + { + var userIdClaim = HttpContext.User?.FindFirst("userID")?.Value; + _logger.LogInformation("Claims: {Claims}", string.Join(", ", HttpContext.User?.Claims.Select(c => $"{c.Type}: {c.Value}") ?? new string[0])); + + if (string.IsNullOrEmpty(userIdClaim)) + { + _logger.LogError("No userID claim found"); + return Guid.Empty; + } + + return Guid.TryParse(userIdClaim, out var userId) ? userId : Guid.Empty; + } +} \ No newline at end of file diff --git a/Govor.API/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Extensions/ConfigurationProgramExtensions.cs index df1a872..1435914 100644 --- a/Govor.API/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Extensions/ConfigurationProgramExtensions.cs @@ -4,6 +4,7 @@ using Govor.Application.Interfaces; using Govor.Application.Interfaces.AdminsStuff; using Govor.Application.Interfaces.Authentication; using Govor.Application.Services; +using Govor.Application.Validators; using Govor.Core.Infrastructure.Extensions; using Govor.Core.Infrastructure.Validators; using Govor.Core.Models; @@ -28,6 +29,7 @@ public static class ConfigurationProgramExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(sp => { diff --git a/Govor.Application/Exceptions/AuthService/InvalidUsernameException.cs b/Govor.Application/Exceptions/AuthService/InvalidUsernameException.cs new file mode 100644 index 0000000..9824b86 --- /dev/null +++ b/Govor.Application/Exceptions/AuthService/InvalidUsernameException.cs @@ -0,0 +1,8 @@ +using Govor.Core; + +namespace Govor.Application.Exceptions.AuthService; + +public class InvalidUsernameException(string message) : GovorCoreException(message) +{ + +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/Authentication/IUsernameValidator.cs b/Govor.Application/Interfaces/Authentication/IUsernameValidator.cs new file mode 100644 index 0000000..74fd6de --- /dev/null +++ b/Govor.Application/Interfaces/Authentication/IUsernameValidator.cs @@ -0,0 +1,8 @@ +using Govor.Core.Infrastructure.Validators; + +namespace Govor.Application.Interfaces.Authentication; + +public interface IUsernameValidator : IObjectValidator +{ + +} \ No newline at end of file diff --git a/Govor.Application/Services/AuthService.cs b/Govor.Application/Services/AuthService.cs index 673cb06..8b2d0b1 100644 --- a/Govor.Application/Services/AuthService.cs +++ b/Govor.Application/Services/AuthService.cs @@ -1,9 +1,11 @@ +using System.Text.RegularExpressions; using Govor.API.Services.Authentication.Interfaces; using Govor.Application.Exceptions.AuthService; using Govor.Core.Infrastructure.Extensions; using Govor.Core.Models; using Govor.Core.Repositories.Users; using Govor.Application.Interfaces.Authentication; +using Govor.Core.Infrastructure.Validators; using Govor.Core.Repositories.Admins; namespace Govor.Application.Services; @@ -14,21 +16,26 @@ public class AuthService : IAccountService private readonly IJwtService _jwtService; private readonly IUsersRepository _usersRepository; private readonly IAdminsRepository _adminsRepository; + private readonly IUsernameValidator _usernameValidator; public AuthService(IUsersRepository usersRepository, IJwtService jwtService, IPasswordHasher passwordHasher, - IAdminsRepository adminsRepository + IAdminsRepository adminsRepository, + IUsernameValidator usernameValidator ) { _usersRepository = usersRepository; _jwtService = jwtService; _passwordHasher = passwordHasher; _adminsRepository = adminsRepository; + _usernameValidator = usernameValidator; } public async Task RegistrationAsync(string name, string password, Invitation invitation) { + _usernameValidator.Validate(name); + if (await _usersRepository.ExistsUsernameAsync(name)) throw new UserAlreadyExistException(name); diff --git a/Govor.Application/Validators/UsernameValidator.cs b/Govor.Application/Validators/UsernameValidator.cs new file mode 100644 index 0000000..3d0420d --- /dev/null +++ b/Govor.Application/Validators/UsernameValidator.cs @@ -0,0 +1,35 @@ +using System.Linq.Expressions; +using System.Text.RegularExpressions; +using Govor.Application.Exceptions.AuthService; +using Govor.Application.Interfaces.Authentication; +using Govor.Core.Infrastructure.Validators; + +namespace Govor.Application.Validators; + +public class UsernameValidator : IUsernameValidator +{ + private readonly Regex _usernameRegex = new(@"^[А-Яа-яЁё]+$", RegexOptions.Compiled); + + public void Validate(string username) + { + if(username.Length < UserValidator.MIN_LENGHT_OF_NAME || username.Length > UserValidator.MAX_LENGHT_OF_NAME) + throw new InvalidUsernameException($"Username must be between {UserValidator.MIN_LENGHT_OF_NAME} and {UserValidator.MAX_LENGHT_OF_NAME} characters."); + + if (!_usernameRegex.IsMatch(username)) + throw new InvalidUsernameException("The username must be in Cyrillic and start with a letter."); + } + + public bool TryValidate(string username) + { + try + { + Validate(username); + return true; + } + catch + { + return false; + } + } + +} \ No newline at end of file diff --git a/Govor.Core/Infrastructure/Validators/UserValidator.cs b/Govor.Core/Infrastructure/Validators/UserValidator.cs index 5c51616..6bcc609 100644 --- a/Govor.Core/Infrastructure/Validators/UserValidator.cs +++ b/Govor.Core/Infrastructure/Validators/UserValidator.cs @@ -6,7 +6,7 @@ namespace Govor.Core.Infrastructure.Validators; public class UserValidator : IObjectValidator { public const int MIN_LENGHT_OF_NAME = 4; - public const int MAX_LENGHT_OF_NAME = 50; + public const int MAX_LENGHT_OF_NAME = 44; public void Validate(User user) { diff --git a/Govor.Core/Models/PrivateChat.cs b/Govor.Core/Models/PrivateChat.cs index 3dce79b..82ef1e5 100644 --- a/Govor.Core/Models/PrivateChat.cs +++ b/Govor.Core/Models/PrivateChat.cs @@ -5,7 +5,5 @@ public class PrivateChat public Guid Id { get; set; } public Guid UserAId { get; set; } public Guid UserBId { get; set; } - public User UserA { get; set; } - public User UserB { get; set; } public List Messages { get; set; } = new List(); } \ No newline at end of file diff --git a/Govor.Core/Models/User.cs b/Govor.Core/Models/User.cs index 56e441d..7d407a8 100644 --- a/Govor.Core/Models/User.cs +++ b/Govor.Core/Models/User.cs @@ -13,4 +13,6 @@ public class User public DateTime WasOnline {get; set;} public Guid InviteId {get; set;} public Invitation? Invite { get; set; } + public List SentFriendRequests { get; set; } = new(); + public List ReceivedFriendRequests { get; set; } = new(); } \ No newline at end of file diff --git a/Govor.Data/Configurations/FriendshipConfiguration.cs b/Govor.Data/Configurations/FriendshipConfiguration.cs new file mode 100644 index 0000000..1d829cd --- /dev/null +++ b/Govor.Data/Configurations/FriendshipConfiguration.cs @@ -0,0 +1,26 @@ +using Govor.Core.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Govor.Data.Configurations; + +public class FriendshipConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(f => f.Id); + + builder.HasOne(f => f.Requester) + .WithMany(u => u.SentFriendRequests) + .HasForeignKey(f => f.RequesterId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasOne(f => f.Addressee) + .WithMany(u => u.ReceivedFriendRequests) + .HasForeignKey(f => f.AddresseeId) + .OnDelete(DeleteBehavior.Restrict); + + builder.Property(f => f.Status) + .IsRequired(); + } +} \ No newline at end of file diff --git a/Govor.Data/GovorDbContext.cs b/Govor.Data/GovorDbContext.cs index 2ce583e..dea43e1 100644 --- a/Govor.Data/GovorDbContext.cs +++ b/Govor.Data/GovorDbContext.cs @@ -25,6 +25,7 @@ public class GovorDbContext(DbContextOptions options) : DbContex protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.ApplyConfiguration(new FriendshipConfiguration()); modelBuilder.ApplyConfiguration(new UserConfiguration()); modelBuilder.ApplyConfiguration(new InvitationConfiguration()); modelBuilder.ApplyConfiguration(new AdminConfiguration());