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());