Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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