Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9d005be
feat: create microservice
Darsicl Feb 23, 2026
9566399
feat: create entity, hadlers, dbcontext, mapping, dtos, validation, s…
Darsicl Feb 25, 2026
1218d73
feat: finish emailservice, created docker-compose
Darsicl Feb 25, 2026
2f46d9c
feat: create appsetings
Darsicl Feb 25, 2026
88efed7
feat: crate rabbitmq consumer
Darsicl Feb 26, 2026
037d6b5
fix: prepared for migration
Darsicl Feb 26, 2026
636ddf3
fix: fix appsettings
Darsicl Feb 26, 2026
ba87c76
fix: fix problem with migration
Darsicl Feb 27, 2026
e9491a3
refactor: removed unusing files
Darsicl Feb 27, 2026
d3ddcc5
feat: create endpoint for email
Darsicl Feb 27, 2026
1df68e0
fix: change email endpoint, on masstransit publisher
Darsicl Mar 1, 2026
d4cbea5
fix: fix consumer setting for masstransit
Darsicl Mar 1, 2026
9490658
feat: remove old logic for email
Darsicl Mar 1, 2026
8f44931
fix: fix handfire configuration with sql
Darsicl Mar 2, 2026
1c7c496
test: implement test for mediatr email
Darsicl Mar 2, 2026
55303d2
refactor: rename feedbackEntity in Email Entity
Darsicl Mar 2, 2026
21c2a4b
refactor: rename feedbackMethods into Email
Darsicl Mar 2, 2026
5083966
refactor: preparing for db update
Darsicl Mar 2, 2026
06470cc
refactor: renamed test from feedback to email
Darsicl Mar 2, 2026
1653f03
refactor: renamed main api endpoint from feedback to email
Darsicl Mar 2, 2026
a359cff
Merge branch 'dev' into tast/SSAD-93/Create_EmailMicroservice
Darsicl Mar 2, 2026
eb121c8
refactor: fix spacing
Darsicl Mar 2, 2026
40d650e
chore: stop tracking dockerpassword.env
Darsicl Mar 2, 2026
17c9520
fix: change order of migration down build
Darsicl Mar 2, 2026
9f155a2
chore: stop tracking dockerpassword.env
Darsicl Mar 2, 2026
ffa66c5
fix: replace hardcoder RabbitMQ configuration
Darsicl Mar 2, 2026
77e7b88
fix: add construction try-catch for emailService
Darsicl Mar 2, 2026
1e75cb9
fix: changes access to hangfire, for differnt enviroment
Darsicl Mar 2, 2026
0af405b
fix: change attribute validation on 1000 from 100 like entity allow
Darsicl Mar 2, 2026
898a44c
fix: finish rename in bll and test
Darsicl Mar 2, 2026
65f6736
fix: fix inmemory version different
Darsicl Mar 2, 2026
80f11a2
fix: fix validator settings into test
Darsicl Mar 2, 2026
10a4efe
feat: create google login
Darsicl Mar 3, 2026
508c2ec
test: create test for loggin with google
Darsicl Mar 3, 2026
7ef1568
feat: add lazy register by Google
Darsicl Mar 4, 2026
b9766e0
feat: add mappin for LoginWith Google
Darsicl Mar 4, 2026
bd33967
fix: add neccessary property surname to LoginWithGoogleLogic
Darsicl Mar 4, 2026
0fdf16e
fix: reorder usings
Darsicl Mar 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Streetcode/Streetcode.Auth.BLL/DTO/Auth/LoginWithGoogleDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Streetcode.Auth.BLL.DTO.Auth
{
public class LoginWithGoogleDTO
{
public string Email { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
}
}
7 changes: 7 additions & 0 deletions Streetcode/Streetcode.Auth.BLL/Mapping/AuthProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ public AuthProfile()
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Email));

CreateMap<ApplicationUser, UserDTO>();

CreateMap<LoginWithGoogleDTO, ApplicationUser>()
.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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Result<(TokenResponseDTO Response, string RefreshToken)>>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using FluentValidation;
using Streetcode.Auth.BLL.MediatR.Login;

namespace Streetcode.Auth.BLL.MediatR.LoginWithGoogle
{
public class LoginWithGoogleCommandValidation : AbstractValidator<LoginWithGoogleCommand>
{
public LoginWithGoogleCommandValidation()
{
RuleFor(x => x.LoginGoogle)
.SetValidator(new LoginWithGoogleDTOValidator());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using FluentValidation;
using Streetcode.Auth.BLL.DTO.Auth;

namespace Streetcode.Auth.BLL.MediatR.LoginWithGoogle
{
public class LoginWithGoogleDTOValidator : AbstractValidator<LoginWithGoogleDTO>
{
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.");

RuleFor(x => x.Surname)
.NotEmpty().WithMessage("Surname is required.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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<LoginWithGoogleCommand, Result<(TokenResponseDTO, string)>>
{
private readonly ILogger<LoginWithGoogleHandler> _logger;
private readonly ITokenService _tokenService;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IMapper _mapper;
private readonly IConfiguration _configuration;
private readonly IPublishEndpoint _publishEndpoint;

public LoginWithGoogleHandler(ILogger<LoginWithGoogleHandler> logger, ITokenService tokenService, UserManager<ApplicationUser> userManager, IMapper mapper, IConfiguration configuration, IPublishEndpoint publishEndpoint)
{
_logger = logger;
_tokenService = tokenService;
_userManager = userManager;
_mapper = mapper;
_configuration = configuration;
_publishEndpoint = publishEndpoint;
}

public async Task<Result<(TokenResponseDTO, string)>> Handle(LoginWithGoogleCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByEmailAsync(request.LoginGoogle.Email);

if (user == null)
{
user = _mapper.Map<ApplicationUser>(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,
Name = user.Name,
Surname = user.Surname,
Role = UserRole.User
},
cancellationToken);
}

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<UserDTO>(user)
};

return Result.Ok((responseDto, refreshToken.Token));
}
}
}
51 changes: 50 additions & 1 deletion Streetcode/Streetcode.Auth.WebApi/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -76,6 +81,50 @@ public async Task<IActionResult> 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<IActionResult> 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 surname = result.Principal.FindFirstValue(ClaimTypes.Surname);

var command = new LoginWithGoogleCommand(new LoginWithGoogleDTO()
{
Email = email,
Name = name,
Surname = surname
});

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<IActionResult> Logout()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Reflection;
using System.Text;
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;
Expand All @@ -18,8 +21,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;

Expand Down Expand Up @@ -66,6 +67,7 @@ public static void AddIdentityServices(this IServiceCollection services, IConfig
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
Expand All @@ -78,6 +80,13 @@ 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"];

options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
}

Expand Down Expand Up @@ -181,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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.23" />
<PackageReference Include="Hangfire.SqlServer" Version="1.8.23" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.3.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.23" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.23" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.23" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.23">
Expand Down
8 changes: 7 additions & 1 deletion Streetcode/Streetcode.Auth.WebApi/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,11 @@
"Admin": {
"Email": "admin@streetcode.com",
"Password": "SomePassword"
},
"Authentication": {
"Google": {
"ClientId": "clientID",
"ClientSecret": "clientSecret"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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)
{
var model = new LoginWithGoogleDTO { Email = email };
var result = this.validator.TestValidate(model);

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)
{
var model = new LoginWithGoogleDTO { Email = email };
var result = this.validator.TestValidate(model);

result.ShouldHaveValidationErrorFor(x => x.Email)
.WithErrorMessage("Invalid email format.");
}

[Theory]
[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
{
Surname = surname,
Email = "test@gmail.com",
Name = "John"
};

// Act
var result = this.validator.TestValidate(model);

// Assert
result.ShouldHaveValidationErrorFor(x => x.Surname)
.WithErrorMessage("Surname is required.");
}

[Fact]
public void ShouldNotHaveError_WhenDTOIsValid()
{
// Arrange
var model = new LoginWithGoogleDTO
{
Email = "google.user@gmail.com",
Name = "John",
Surname = "Smith"
};

// Act
var result = this.validator.TestValidate(model);

// Assert
result.ShouldNotHaveAnyValidationErrors();
}
}
}
Loading
Loading