From 2c4e50b1d6723cb4217230aca82d959fe2282481 Mon Sep 17 00:00:00 2001 From: Darsicl Date: Tue, 3 Mar 2026 16:12:29 +0100 Subject: [PATCH 1/6] feat: create google login --- .../DTO/Auth/LoginWithGoogleDTO.cs | 8 +++ .../LoginWithGoogle/LoginWithGoogleCommand.cs | 8 +++ .../LoginWithGoogleCommandValidation.cs | 14 +++++ .../LoginWithGoogleDTOValidator.cs | 18 ++++++ .../LoginWithGoogle/LoginWithGoogleHandler.cs | 57 +++++++++++++++++++ .../Controllers/AuthController.cs | 49 +++++++++++++++- .../Extensions/ServiceCollectionExtensions.cs | 9 ++- .../Streetcode.Auth.WebApi.csproj | 2 + .../Streetcode.Auth.WebApi/appsettings.json | 8 ++- 9 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 Streetcode/Streetcode.Auth.BLL/DTO/Auth/LoginWithGoogleDTO.cs create mode 100644 Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleCommand.cs create mode 100644 Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleCommandValidation.cs create mode 100644 Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleDTOValidator.cs create mode 100644 Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs diff --git a/Streetcode/Streetcode.Auth.BLL/DTO/Auth/LoginWithGoogleDTO.cs b/Streetcode/Streetcode.Auth.BLL/DTO/Auth/LoginWithGoogleDTO.cs new file mode 100644 index 0000000..315ff98 --- /dev/null +++ b/Streetcode/Streetcode.Auth.BLL/DTO/Auth/LoginWithGoogleDTO.cs @@ -0,0 +1,8 @@ +namespace Streetcode.Auth.BLL.DTO.Auth +{ + public class LoginWithGoogleDTO + { + public string Email { get; set; } + public string Name { get; set; } + } +} diff --git a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleCommand.cs b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleCommand.cs new file mode 100644 index 0000000..e6b9008 --- /dev/null +++ b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleCommand.cs @@ -0,0 +1,8 @@ +using FluentResults; +using MediatR; +using Streetcode.Auth.BLL.DTO.Auth; + +namespace Streetcode.Auth.BLL.MediatR.LoginWithGoogle +{ + public record LoginWithGoogleCommand(LoginWithGoogleDTO LoginGoogle) : IRequest>; +} diff --git a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleCommandValidation.cs b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleCommandValidation.cs new file mode 100644 index 0000000..41d0eed --- /dev/null +++ b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleCommandValidation.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Streetcode.Auth.BLL.MediatR.Login; + +namespace Streetcode.Auth.BLL.MediatR.LoginWithGoogle +{ + public class LoginWithGoogleCommandValidation : AbstractValidator + { + public LoginWithGoogleCommandValidation() + { + RuleFor(x => x.LoginGoogle) + .SetValidator(new LoginWithGoogleDTOValidator()); + } + } +} diff --git a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleDTOValidator.cs b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleDTOValidator.cs new file mode 100644 index 0000000..7013342 --- /dev/null +++ b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleDTOValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using Streetcode.Auth.BLL.DTO.Auth; + +namespace Streetcode.Auth.BLL.MediatR.LoginWithGoogle +{ + public class LoginWithGoogleDTOValidator : AbstractValidator + { + public LoginWithGoogleDTOValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage("Email is required.") + .EmailAddress().WithMessage("Invalid email format."); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required."); + } + } +} diff --git a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs new file mode 100644 index 0000000..2743744 --- /dev/null +++ b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs @@ -0,0 +1,57 @@ +using AutoMapper; +using FluentResults; +using MassTransit; +using MediatR; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Streetcode.Auth.BLL.DTO.Auth; +using Streetcode.Auth.BLL.DTO.Users; +using Streetcode.Auth.BLL.Interfaces; +using Streetcode.Auth.BLL.MediatR.Login; +using Streetcode.Auth.DAL.Entities; +using Streetcode.Shared.DTO.Events; +using Streetcode.Shared.Enums; + +namespace Streetcode.Auth.BLL.MediatR.LoginWithGoogle +{ + public class LoginWithGoogleHandler : IRequestHandler> + { + private readonly ILogger _logger; + private readonly ITokenService _tokenService; + private readonly UserManager _userManager; + private readonly IMapper _mapper; + private readonly IConfiguration _configuration; + private readonly IPublishEndpoint _publishEndpoint; + + public LoginWithGoogleHandler(ILogger logger, ITokenService tokenService, UserManager userManager, IMapper mapper, IConfiguration configuration) + { + _logger = logger; + _tokenService = tokenService; + _userManager = userManager; + _mapper = mapper; + _configuration = configuration; + } + + public async Task> Handle(LoginWithGoogleCommand request, CancellationToken cancellationToken) + { + var user = await _userManager.FindByEmailAsync(request.LoginGoogle.Email); + + if (user == null) + { + return Result.Fail(new Error("UserNotFoundRegistrationRequired")); + } + + var (accessToken, refreshToken) = await _tokenService.GenerateTokensAsync(user); + + var responseDto = new TokenResponseDTO + { + AccessToken = accessToken, + AccessTokenExpiresAt = DateTime.UtcNow.AddMinutes(double.Parse(_configuration["Jwt:ExpireMinutes"] !)), + User = _mapper.Map(user) + }; + + return Result.Ok((responseDto, refreshToken.Token)); + } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.Auth.WebApi/Controllers/AuthController.cs b/Streetcode/Streetcode.Auth.WebApi/Controllers/AuthController.cs index 5798ccd..3897150 100644 --- a/Streetcode/Streetcode.Auth.WebApi/Controllers/AuthController.cs +++ b/Streetcode/Streetcode.Auth.WebApi/Controllers/AuthController.cs @@ -1,7 +1,12 @@ -using MediatR; +using System.Security.Claims; +using MediatR; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Mvc; using Streetcode.Auth.BLL.DTO.Auth; using Streetcode.Auth.BLL.MediatR.Login; +using Streetcode.Auth.BLL.MediatR.LoginWithGoogle; using Streetcode.Auth.BLL.MediatR.Logout; using Streetcode.Auth.BLL.MediatR.RefreshToken; using Streetcode.Auth.BLL.MediatR.Register; @@ -76,6 +81,48 @@ public async Task RefreshToken() return Ok(responseDto); } + [HttpGet("login-google")] + public IActionResult LoginGoogle() + { + var redirectUrl = Url.Action("GoogleCallback"); + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + + return Challenge(properties, GoogleDefaults.AuthenticationScheme); + } + + [HttpGet("google-callback")] + public async Task GoogleCallback() + { + var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + if (!result.Succeeded) + { + return BadRequest(new { error = "Google authentication failed" }); + } + + var email = result.Principal.FindFirstValue(ClaimTypes.Email); + var name = result.Principal.FindFirstValue(ClaimTypes.Name); + + var command = new LoginWithGoogleCommand(new LoginWithGoogleDTO() + { + Email = email, + Name = name + }); + + var loginResult = await _mediator.Send(command); + + if (loginResult.IsFailed) + { + return Unauthorized(loginResult.Errors.Select(e => e.Message)); + } + + var (responseDto, refreshToken) = loginResult.Value; + + _cookieService.SetRefreshTokenCookie(Response, refreshToken); + + return Ok(responseDto); + } + [HttpPost("logout")] public async Task Logout() { diff --git a/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs b/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs index 65e4dec..cf9f77c 100644 --- a/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs +++ b/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System.Reflection; using System.Text; using FluentValidation; +using Hangfire; using MassTransit; using MediatR; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -18,8 +19,6 @@ using Streetcode.Auth.DAL.Repositories.Realizations; using Streetcode.Auth.WebApi.Services.Interfaces; using Streetcode.Auth.WebApi.Services.Realizations; -using Hangfire; -using Hangfire.SqlServer; namespace Streetcode.Auth.WebApi.Extensions; @@ -66,6 +65,7 @@ public static void AddIdentityServices(this IServiceCollection services, IConfig options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) + .AddCookie() .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters @@ -78,6 +78,11 @@ public static void AddIdentityServices(this IServiceCollection services, IConfig ValidAudience = jwtOptions.Audience, IssuerSigningKey = new SymmetricSecurityKey(key) }; + }) + .AddGoogle(options => + { + options.ClientId = configuration["Authentication:Google:ClientId"]; + options.ClientSecret = configuration["Authentication:Google:ClientSecret"]; }); } diff --git a/Streetcode/Streetcode.Auth.WebApi/Streetcode.Auth.WebApi.csproj b/Streetcode/Streetcode.Auth.WebApi/Streetcode.Auth.WebApi.csproj index abf3452..dc40224 100644 --- a/Streetcode/Streetcode.Auth.WebApi/Streetcode.Auth.WebApi.csproj +++ b/Streetcode/Streetcode.Auth.WebApi/Streetcode.Auth.WebApi.csproj @@ -11,6 +11,8 @@ + + diff --git a/Streetcode/Streetcode.Auth.WebApi/appsettings.json b/Streetcode/Streetcode.Auth.WebApi/appsettings.json index 1ad7e6d..aed5298 100644 --- a/Streetcode/Streetcode.Auth.WebApi/appsettings.json +++ b/Streetcode/Streetcode.Auth.WebApi/appsettings.json @@ -29,5 +29,11 @@ "Admin": { "Email": "admin@streetcode.com", "Password": "SomePassword" + }, + "Authentication": { + "Google": { + "ClientId": "clientID", + "ClientSecret": "clientSecret" + } } -} \ No newline at end of file + } \ No newline at end of file From c9e86e009cb176534506e35f378f150c443e7161 Mon Sep 17 00:00:00 2001 From: Darsicl Date: Tue, 3 Mar 2026 16:22:33 +0100 Subject: [PATCH 2/6] test: create test for loggin with google --- .../LoginWithGoogleDTOValidatorTests.cs | 84 +++++++++++++ .../LoginWithGoogleHandlerTests.cs | 111 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleDTOValidatorTests.cs create mode 100644 Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleHandlerTests.cs diff --git a/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleDTOValidatorTests.cs b/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleDTOValidatorTests.cs new file mode 100644 index 0000000..57895d5 --- /dev/null +++ b/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleDTOValidatorTests.cs @@ -0,0 +1,84 @@ +namespace Streetcode.Auth.XUnitTest.LoginWithGoogle +{ + using FluentValidation.TestHelper; + using Streetcode.Auth.BLL.DTO.Auth; + using Streetcode.Auth.BLL.MediatR.LoginWithGoogle; + using Xunit; + + public class LoginWithGoogleDTOValidatorTests + { + private readonly LoginWithGoogleDTOValidator validator; + + public LoginWithGoogleDTOValidatorTests() + { + this.validator = new LoginWithGoogleDTOValidator(); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void ShouldHaveError_WhenEmailIsEmpty(string email) + { + // Arrange + var model = new LoginWithGoogleDTO { Email = email }; + + // Act + var result = this.validator.TestValidate(model); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.Email) + .WithErrorMessage("Email is required."); + } + + [Theory] + [InlineData("plainaddress")] + [InlineData("#@%^%#$@#$@#.com")] + [InlineData("@example.com")] + [InlineData("email.example.com")] + public void ShouldHaveError_WhenEmailIsInvalidFormat(string email) + { + // Arrange + var model = new LoginWithGoogleDTO { Email = email }; + + // Act + var result = this.validator.TestValidate(model); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.Email) + .WithErrorMessage("Invalid email format."); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void ShouldHaveError_WhenNameIsEmpty(string name) + { + // Arrange + var model = new LoginWithGoogleDTO { Name = name, Email = "test@gmail.com" }; + + // Act + var result = this.validator.TestValidate(model); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.Name) + .WithErrorMessage("Name is required."); + } + + [Fact] + public void ShouldNotHaveError_WhenDTOIsValid() + { + // Arrange + var model = new LoginWithGoogleDTO + { + Email = "google.user@gmail.com", + Name = "Ivan Ivanov", + }; + + // Act + var result = this.validator.TestValidate(model); + + // Assert + result.ShouldNotHaveAnyValidationErrors(); + } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleHandlerTests.cs b/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleHandlerTests.cs new file mode 100644 index 0000000..114e79c --- /dev/null +++ b/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleHandlerTests.cs @@ -0,0 +1,111 @@ +namespace Streetcode.Auth.XUnitTest.LoginWithGoogle +{ + using AutoMapper; + using FluentAssertions; + using Microsoft.AspNetCore.Identity; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Logging; + using Moq; + using Streetcode.Auth.BLL.DTO.Auth; + using Streetcode.Auth.BLL.DTO.Users; + using Streetcode.Auth.BLL.Interfaces; + using Streetcode.Auth.BLL.Mapping; + using Streetcode.Auth.BLL.MediatR.LoginWithGoogle; + using Streetcode.Auth.DAL.Entities; + using Xunit; + + public class LoginWithGoogleHandlerTests + { + private const string Email = "google-user@gmail.com"; + private const string AccessToken = "google_access_token"; + private const string RefreshTokenStr = "google_refresh_token"; + + private readonly Mock> userManagerMock; + private readonly Mock tokenServiceMock; + private readonly Mock configMock; + private readonly Mock> loggerMock; + private readonly IMapper mapper; + private readonly LoginWithGoogleHandler handler; + + public LoginWithGoogleHandlerTests() + { + this.userManagerMock = this.CreateUserManagerMock(); + this.tokenServiceMock = new Mock(); + this.configMock = new Mock(); + this.loggerMock = new Mock>(); + + var configuration = new MapperConfiguration(cfg => + { + cfg.AddProfile(new AuthProfile()); + }); + this.mapper = configuration.CreateMapper(); + + this.handler = new LoginWithGoogleHandler( + this.loggerMock.Object, + this.tokenServiceMock.Object, + this.userManagerMock.Object, + this.mapper, + this.configMock.Object); + } + + [Fact] + public async Task Handle_ShouldReturnSuccess_WhenUserExists() + { + // Arrange + var user = new ApplicationUser { Id = "google-id-123", Email = Email, UserName = Email }; + var refreshTokenEntity = new RefreshToken { Token = RefreshTokenStr, UserId = user.Id }; + + this.userManagerMock + .Setup(m => m.FindByEmailAsync(Email)) + .ReturnsAsync(user); + + this.tokenServiceMock + .Setup(s => s.GenerateTokensAsync(user)) + .ReturnsAsync((AccessToken, refreshTokenEntity)); + + this.configMock.Setup(c => c["Jwt:ExpireMinutes"]).Returns("60"); + + var command = new LoginWithGoogleCommand(new LoginWithGoogleDTO { Email = Email, Name = "Google User" }); + + // Act + var result = await handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + var (responseDto, refreshToken) = result.Value; + + refreshToken.Should().Be(RefreshTokenStr); + responseDto.AccessToken.Should().Be(AccessToken); + responseDto.User.Email.Should().Be(Email); + + this.tokenServiceMock.Verify(s => s.GenerateTokensAsync(user), Times.Once); + } + + [Fact] + public async Task Handle_ShouldReturnFail_WhenUserDoesNotExist() + { + // Arrange + this.userManagerMock + .Setup(m => m.FindByEmailAsync(Email)) + .ReturnsAsync((ApplicationUser?)null); + + var command = new LoginWithGoogleCommand(new LoginWithGoogleDTO { Email = Email, Name = "New User" }); + + // Act + var result = await handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailed.Should().BeTrue(); + result.Errors.Should().ContainSingle(e => e.Message == "UserNotFoundRegistrationRequired"); + + this.tokenServiceMock.Verify(s => s.GenerateTokensAsync(It.IsAny()), Times.Never); + } + + private Mock> CreateUserManagerMock() + { + var store = new Mock>(); + return new Mock>( + store.Object, null, null, null, null, null, null, null, null); + } + } +} \ No newline at end of file From c06097c2d8ae5691553d8f05cd375fe808e4a11f Mon Sep 17 00:00:00 2001 From: Darsicl Date: Wed, 4 Mar 2026 11:45:09 +0100 Subject: [PATCH 3/6] feat: add lazy register by Google --- .../LoginWithGoogle/LoginWithGoogleHandler.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs index 2743744..9654698 100644 --- a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs +++ b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs @@ -39,7 +39,25 @@ public LoginWithGoogleHandler(ILogger logger, ITokenServ if (user == null) { - return Result.Fail(new Error("UserNotFoundRegistrationRequired")); + user = _mapper.Map(request.LoginGoogle); + + var createResult = await _userManager.CreateAsync(user); + + if (!createResult.Succeeded) + { + return Result.Fail(createResult.Errors.Select(e => e.Description)); + } + + await _userManager.AddToRoleAsync(user, nameof(UserRole.User)); + + await _publishEndpoint.Publish( + new UserRegisteredEvent + { + UserId = user.Id, + Email = user.Email, + Role = UserRole.User + }, + cancellationToken); } var (accessToken, refreshToken) = await _tokenService.GenerateTokensAsync(user); From 522d04f26f050e544412bfd419ca78b80218f349 Mon Sep 17 00:00:00 2001 From: Darsicl Date: Wed, 4 Mar 2026 12:21:45 +0100 Subject: [PATCH 4/6] feat: add mappin for LoginWith Google --- .../Mapping/AuthProfile.cs | 6 ++++ .../Extensions/ServiceCollectionExtensions.cs | 21 ++++++++---- Streetcode/docker-compose.yml | 34 +++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 Streetcode/docker-compose.yml diff --git a/Streetcode/Streetcode.Auth.BLL/Mapping/AuthProfile.cs b/Streetcode/Streetcode.Auth.BLL/Mapping/AuthProfile.cs index 9d15b06..517d264 100644 --- a/Streetcode/Streetcode.Auth.BLL/Mapping/AuthProfile.cs +++ b/Streetcode/Streetcode.Auth.BLL/Mapping/AuthProfile.cs @@ -13,6 +13,12 @@ public AuthProfile() .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Email)); CreateMap(); + + CreateMap() + .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Email)) + .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email)) + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.EmailConfirmed, opt => opt.MapFrom(_ => true)); } } } diff --git a/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs b/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs index cf9f77c..7c6572a 100644 --- a/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs +++ b/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs @@ -1,9 +1,9 @@ -using System.Reflection; -using System.Text; -using FluentValidation; +using FluentValidation; using Hangfire; +using Hangfire.SqlServer; using MassTransit; using MediatR; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -19,6 +19,8 @@ using Streetcode.Auth.DAL.Repositories.Realizations; using Streetcode.Auth.WebApi.Services.Interfaces; using Streetcode.Auth.WebApi.Services.Realizations; +using System.Reflection; +using System.Text; namespace Streetcode.Auth.WebApi.Extensions; @@ -65,7 +67,7 @@ public static void AddIdentityServices(this IServiceCollection services, IConfig options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) - .AddCookie() + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters @@ -81,8 +83,10 @@ public static void AddIdentityServices(this IServiceCollection services, IConfig }) .AddGoogle(options => { - options.ClientId = configuration["Authentication:Google:ClientId"]; - options.ClientSecret = configuration["Authentication:Google:ClientSecret"]; + options.ClientId = configuration["Authentication:Google:ClientId"]; + options.ClientSecret = configuration["Authentication:Google:ClientSecret"]; + + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; }); } @@ -186,7 +190,10 @@ public static void AddHangfireServices(this IServiceCollection services, IConfig services.AddHangfire(config => { - config.UseSqlServerStorage(connectionString); + config.UseSqlServerStorage(connectionString, new SqlServerStorageOptions + { + PrepareSchemaIfNecessary = true + }); }); services.AddHangfireServer(); diff --git a/Streetcode/docker-compose.yml b/Streetcode/docker-compose.yml new file mode 100644 index 0000000..bf2c125 --- /dev/null +++ b/Streetcode/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + rabbitmq: + image: rabbitmq:3-management + container_name: streetcode-rabbitmq + restart: always + ports: + - "5672:5672" + - "15672:15672" + environment: + - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS} + + redis: + image: redis:latest + container_name: streetcode-redis + restart: always + ports: + - "6379:6379" + + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: streetcode-main-db + ports: + - "1433:1433" + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=${DB_PASSWORD} + healthcheck: + test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-U", "sa", "-P", "${DB_PASSWORD}", "-Q", "SELECT 1"] + interval: 10s + timeout: 3s + retries: 10 \ No newline at end of file From 17fb85857d2387e3de75f6957ae9646d1b127675 Mon Sep 17 00:00:00 2001 From: Darsicl Date: Wed, 4 Mar 2026 12:52:47 +0100 Subject: [PATCH 5/6] fix: add neccessary property surname to LoginWithGoogleLogic --- .../DTO/Auth/LoginWithGoogleDTO.cs | 1 + .../Mapping/AuthProfile.cs | 1 + .../LoginWithGoogleDTOValidator.cs | 3 + .../LoginWithGoogle/LoginWithGoogleHandler.cs | 5 +- .../Controllers/AuthController.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../LoginWithGoogleDTOValidatorTests.cs | 34 +++++++---- .../LoginWithGoogleHandlerTests.cs | 59 ++++++++++++++++--- 8 files changed, 85 insertions(+), 23 deletions(-) diff --git a/Streetcode/Streetcode.Auth.BLL/DTO/Auth/LoginWithGoogleDTO.cs b/Streetcode/Streetcode.Auth.BLL/DTO/Auth/LoginWithGoogleDTO.cs index 315ff98..38521e6 100644 --- a/Streetcode/Streetcode.Auth.BLL/DTO/Auth/LoginWithGoogleDTO.cs +++ b/Streetcode/Streetcode.Auth.BLL/DTO/Auth/LoginWithGoogleDTO.cs @@ -4,5 +4,6 @@ public class LoginWithGoogleDTO { public string Email { get; set; } public string Name { get; set; } + public string Surname { get; set; } } } diff --git a/Streetcode/Streetcode.Auth.BLL/Mapping/AuthProfile.cs b/Streetcode/Streetcode.Auth.BLL/Mapping/AuthProfile.cs index 517d264..92c8d43 100644 --- a/Streetcode/Streetcode.Auth.BLL/Mapping/AuthProfile.cs +++ b/Streetcode/Streetcode.Auth.BLL/Mapping/AuthProfile.cs @@ -18,6 +18,7 @@ public AuthProfile() .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Email)) .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email)) .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.Surname, opt => opt.MapFrom(src => src.Surname)) .ForMember(dest => dest.EmailConfirmed, opt => opt.MapFrom(_ => true)); } } diff --git a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleDTOValidator.cs b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleDTOValidator.cs index 7013342..4b52702 100644 --- a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleDTOValidator.cs +++ b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleDTOValidator.cs @@ -13,6 +13,9 @@ public LoginWithGoogleDTOValidator() RuleFor(x => x.Name) .NotEmpty().WithMessage("Name is required."); + + RuleFor(x => x.Surname) + .NotEmpty().WithMessage("Surname is required."); } } } diff --git a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs index 9654698..c23b1aa 100644 --- a/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs +++ b/Streetcode/Streetcode.Auth.BLL/MediatR/LoginWithGoogle/LoginWithGoogleHandler.cs @@ -24,13 +24,14 @@ public class LoginWithGoogleHandler : IRequestHandler logger, ITokenService tokenService, UserManager userManager, IMapper mapper, IConfiguration configuration) + public LoginWithGoogleHandler(ILogger logger, ITokenService tokenService, UserManager userManager, IMapper mapper, IConfiguration configuration, IPublishEndpoint publishEndpoint) { _logger = logger; _tokenService = tokenService; _userManager = userManager; _mapper = mapper; _configuration = configuration; + _publishEndpoint = publishEndpoint; } public async Task> Handle(LoginWithGoogleCommand request, CancellationToken cancellationToken) @@ -55,6 +56,8 @@ await _publishEndpoint.Publish( { UserId = user.Id, Email = user.Email, + Name = user.Name, + Surname = user.Surname, Role = UserRole.User }, cancellationToken); diff --git a/Streetcode/Streetcode.Auth.WebApi/Controllers/AuthController.cs b/Streetcode/Streetcode.Auth.WebApi/Controllers/AuthController.cs index 3897150..c87c36d 100644 --- a/Streetcode/Streetcode.Auth.WebApi/Controllers/AuthController.cs +++ b/Streetcode/Streetcode.Auth.WebApi/Controllers/AuthController.cs @@ -102,11 +102,13 @@ public async Task GoogleCallback() var email = result.Principal.FindFirstValue(ClaimTypes.Email); var name = result.Principal.FindFirstValue(ClaimTypes.Name); + var surname = result.Principal.FindFirstValue(ClaimTypes.Surname); var command = new LoginWithGoogleCommand(new LoginWithGoogleDTO() { Email = email, - Name = name + Name = name, + Surname = surname }); var loginResult = await _mediator.Send(command); diff --git a/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs b/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs index 7c6572a..d385800 100644 --- a/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs +++ b/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs @@ -20,6 +20,7 @@ using Streetcode.Auth.WebApi.Services.Interfaces; using Streetcode.Auth.WebApi.Services.Realizations; using System.Reflection; +using System.Security.Claims; using System.Text; namespace Streetcode.Auth.WebApi.Extensions; diff --git a/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleDTOValidatorTests.cs b/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleDTOValidatorTests.cs index 57895d5..b304461 100644 --- a/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleDTOValidatorTests.cs +++ b/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleDTOValidatorTests.cs @@ -19,13 +19,9 @@ public LoginWithGoogleDTOValidatorTests() [InlineData(null)] public void ShouldHaveError_WhenEmailIsEmpty(string email) { - // Arrange var model = new LoginWithGoogleDTO { Email = email }; - - // Act var result = this.validator.TestValidate(model); - // Assert result.ShouldHaveValidationErrorFor(x => x.Email) .WithErrorMessage("Email is required."); } @@ -37,13 +33,9 @@ public void ShouldHaveError_WhenEmailIsEmpty(string email) [InlineData("email.example.com")] public void ShouldHaveError_WhenEmailIsInvalidFormat(string email) { - // Arrange var model = new LoginWithGoogleDTO { Email = email }; - - // Act var result = this.validator.TestValidate(model); - // Assert result.ShouldHaveValidationErrorFor(x => x.Email) .WithErrorMessage("Invalid email format."); } @@ -52,16 +44,33 @@ public void ShouldHaveError_WhenEmailIsInvalidFormat(string email) [InlineData("")] [InlineData(null)] public void ShouldHaveError_WhenNameIsEmpty(string name) + { + var model = new LoginWithGoogleDTO { Name = name, Email = "test@gmail.com", Surname = "Test" }; + var result = this.validator.TestValidate(model); + + result.ShouldHaveValidationErrorFor(x => x.Name) + .WithErrorMessage("Name is required."); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void ShouldHaveError_WhenSurnameIsEmpty(string surname) { // Arrange - var model = new LoginWithGoogleDTO { Name = name, Email = "test@gmail.com" }; + var model = new LoginWithGoogleDTO + { + Surname = surname, + Email = "test@gmail.com", + Name = "John" + }; // Act var result = this.validator.TestValidate(model); // Assert - result.ShouldHaveValidationErrorFor(x => x.Name) - .WithErrorMessage("Name is required."); + result.ShouldHaveValidationErrorFor(x => x.Surname) + .WithErrorMessage("Surname is required."); } [Fact] @@ -71,7 +80,8 @@ public void ShouldNotHaveError_WhenDTOIsValid() var model = new LoginWithGoogleDTO { Email = "google.user@gmail.com", - Name = "Ivan Ivanov", + Name = "John", + Surname = "Smith" }; // Act diff --git a/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleHandlerTests.cs b/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleHandlerTests.cs index 114e79c..6975902 100644 --- a/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleHandlerTests.cs +++ b/Streetcode/Streetcode.Auth.XUnitTest/LoginWithGoogle/LoginWithGoogleHandlerTests.cs @@ -2,6 +2,7 @@ { using AutoMapper; using FluentAssertions; + using MassTransit; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -12,11 +13,14 @@ using Streetcode.Auth.BLL.Mapping; using Streetcode.Auth.BLL.MediatR.LoginWithGoogle; using Streetcode.Auth.DAL.Entities; + using Streetcode.Shared.DTO.Events; using Xunit; public class LoginWithGoogleHandlerTests { private const string Email = "google-user@gmail.com"; + private const string Name = "John"; + private const string Surname = "Smith"; private const string AccessToken = "google_access_token"; private const string RefreshTokenStr = "google_refresh_token"; @@ -24,6 +28,7 @@ public class LoginWithGoogleHandlerTests private readonly Mock tokenServiceMock; private readonly Mock configMock; private readonly Mock> loggerMock; + private readonly Mock publishEndpointMock; private readonly IMapper mapper; private readonly LoginWithGoogleHandler handler; @@ -33,6 +38,7 @@ public LoginWithGoogleHandlerTests() this.tokenServiceMock = new Mock(); this.configMock = new Mock(); this.loggerMock = new Mock>(); + this.publishEndpointMock = new Mock(); var configuration = new MapperConfiguration(cfg => { @@ -45,14 +51,22 @@ public LoginWithGoogleHandlerTests() this.tokenServiceMock.Object, this.userManagerMock.Object, this.mapper, - this.configMock.Object); + this.configMock.Object, + this.publishEndpointMock.Object); } [Fact] public async Task Handle_ShouldReturnSuccess_WhenUserExists() { // Arrange - var user = new ApplicationUser { Id = "google-id-123", Email = Email, UserName = Email }; + var user = new ApplicationUser + { + Id = "google-id-123", + Email = Email, + UserName = Email, + Name = Name, + Surname = Surname + }; var refreshTokenEntity = new RefreshToken { Token = RefreshTokenStr, UserId = user.Id }; this.userManagerMock @@ -65,7 +79,12 @@ public async Task Handle_ShouldReturnSuccess_WhenUserExists() this.configMock.Setup(c => c["Jwt:ExpireMinutes"]).Returns("60"); - var command = new LoginWithGoogleCommand(new LoginWithGoogleDTO { Email = Email, Name = "Google User" }); + var command = new LoginWithGoogleCommand(new LoginWithGoogleDTO + { + Email = Email, + Name = Name, + Surname = Surname + }); // Act var result = await handler.Handle(command, CancellationToken.None); @@ -77,28 +96,50 @@ public async Task Handle_ShouldReturnSuccess_WhenUserExists() refreshToken.Should().Be(RefreshTokenStr); responseDto.AccessToken.Should().Be(AccessToken); responseDto.User.Email.Should().Be(Email); + responseDto.User.Surname.Should().Be(Surname); this.tokenServiceMock.Verify(s => s.GenerateTokensAsync(user), Times.Once); } [Fact] - public async Task Handle_ShouldReturnFail_WhenUserDoesNotExist() + public async Task Handle_ShouldCreateUserAndReturnSuccess_WhenUserDoesNotExist() { // Arrange this.userManagerMock .Setup(m => m.FindByEmailAsync(Email)) .ReturnsAsync((ApplicationUser?)null); - var command = new LoginWithGoogleCommand(new LoginWithGoogleDTO { Email = Email, Name = "New User" }); + this.userManagerMock + .Setup(m => m.CreateAsync(It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + this.userManagerMock + .Setup(m => m.AddToRoleAsync(It.IsAny(), "User")) + .ReturnsAsync(IdentityResult.Success); + + var user = new ApplicationUser { Id = "new-id", Email = Email }; + var refreshTokenEntity = new RefreshToken { Token = RefreshTokenStr, UserId = user.Id }; + + this.tokenServiceMock + .Setup(s => s.GenerateTokensAsync(It.IsAny())) + .ReturnsAsync((AccessToken, refreshTokenEntity)); + + this.configMock.Setup(c => c["Jwt:ExpireMinutes"]).Returns("60"); + + var command = new LoginWithGoogleCommand(new LoginWithGoogleDTO + { + Email = Email, + Name = "New", + Surname = "User" + }); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert - result.IsFailed.Should().BeTrue(); - result.Errors.Should().ContainSingle(e => e.Message == "UserNotFoundRegistrationRequired"); - - this.tokenServiceMock.Verify(s => s.GenerateTokensAsync(It.IsAny()), Times.Never); + result.IsSuccess.Should().BeTrue(); + this.userManagerMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); + this.publishEndpointMock.Verify(p => p.Publish(It.IsAny(), It.IsAny()), Times.Once); } private Mock> CreateUserManagerMock() From 1f310b5447bd0dc530a2d55d20250f3f5549024b Mon Sep 17 00:00:00 2001 From: Darsicl Date: Wed, 4 Mar 2026 13:27:10 +0100 Subject: [PATCH 6/6] fix: change on correct branch and fix build --- .../Extensions/ServiceCollectionExtensions.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs b/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs index d385800..0cd641f 100644 --- a/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs +++ b/Streetcode/Streetcode.Auth.WebApi/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ -using FluentValidation; +using System.Reflection; +using System.Text; +using FluentValidation; using Hangfire; using Hangfire.SqlServer; using MassTransit; @@ -19,9 +21,6 @@ using Streetcode.Auth.DAL.Repositories.Realizations; using Streetcode.Auth.WebApi.Services.Interfaces; using Streetcode.Auth.WebApi.Services.Realizations; -using System.Reflection; -using System.Security.Claims; -using System.Text; namespace Streetcode.Auth.WebApi.Extensions;