Skip to content
Merged
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
4 changes: 2 additions & 2 deletions server/StudySharp.API/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace StudySharp.API.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
[Route("api/account")]
public class AccountController : ControllerBase
{
private readonly IMediator _mediator;
Expand Down Expand Up @@ -63,7 +63,7 @@ public async Task<OperationResult> Logout()
public async Task<OperationResult<RefreshTokenResponse>> RefreshToken([FromBody] RefreshTokenRequest refreshTokenRequest)
{
var accessToken = await HttpContext.GetTokenAsync("Bearer", "access_token");
var userName = User.Identity.Name;
var userName = User.Identity?.Name;
var refreshTokenCommand = new RefreshTokenCommand
{
AccessToken = accessToken,
Expand Down
6 changes: 5 additions & 1 deletion server/StudySharp.API/Responses/Auth/RefreshTokenResponse.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
namespace StudySharp.API.Responses.Auth
{
public class RefreshTokenResponse : LoginResponse
public sealed class RefreshTokenResponse
{
public string UserName { get; set; }
public string Role { get; set; }
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
}
2 changes: 2 additions & 0 deletions server/StudySharp.API/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
endpoints.MapControllers();
})
.EnsureDbMigrated<StudySharpDbContext>();

StudySharpDbContextSeedData.InitializeAsync(app.ApplicationServices, Configuration);
}
}
}
9 changes: 7 additions & 2 deletions server/StudySharp.API/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"ConnectionStrings": {
"Default": "User ID=shrjmebswieehw;Password=7494580e3ea3832fc3944caf4e7c172867a35ddf48ba8607755190c3fadfd674;Host=ec2-34-240-75-196.eu-west-1.compute.amazonaws.com;Port=5432;Database=dcaqfc0mb9rvaj;Pooling=true;sslmode=Require;TrustServerCertificate=true;"
"Default": "User ID=shrjmebswieehw;Password=7494580e3ea3832fc3944caf4e7c172867a35ddf48ba8607755190c3fadfd674;Host=ec2-34-240-75-196.eu-west-1.compute.amazonaws.com;Port=5432;Database=dcaqfc0mb9rvaj;Pooling=true;sslmode=Require;TrustServerCertificate=true;",
"Local": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=StudySharp"
},
"Logging": {
"LogLevel": {
Expand All @@ -17,8 +18,12 @@
"accessTokenExpiration": 20,
"refreshTokenExpiration": 60
},
"EmailConfig":{
"EmailConfig": {
"Name": "StudySharp",
"From": "kotohomka@gmail.com"
},
"AdminCredentials": {
"UserName": "barcat75@gmail.com",
"Password": "HuskTheBest75_"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
using System.Threading.Tasks;
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using StudySharp.Domain.Constants;
using StudySharp.Domain.General;
using StudySharp.Domain.Models;
using StudySharp.Domain.Validators;
using StudySharp.Domain.ValidationRules;
using StudySharp.DomainServices;

namespace StudySharp.ApplicationServices.Commands
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Identity;
using StudySharp.ApplicationServices.JwtAuthService;
using StudySharp.ApplicationServices.JwtAuthService.ResultModels;
using StudySharp.ApplicationServices.ValidationRules.Auth;
using StudySharp.Domain.Constants;
using StudySharp.Domain.General;
using StudySharp.DomainServices;
Expand All @@ -18,24 +20,36 @@ public sealed class LoginCommand : IRequest<OperationResult<LoginResult>>
public string Password { get; set; }
}

public class LoginCommandValidator : AbstractValidator<LoginCommand>
{
public LoginCommandValidator(ILoginRules rules)
{
RuleFor(_ => _.Email)
.NotEmpty()
.WithMessage(ErrorConstants.InvalidCredentials)
.MustAsync(rules.UserIsRegistered)
.WithMessage(_ => ErrorConstants.InvalidCredentials);

RuleFor(_ => _.Password)
.NotEmpty()
.WithMessage(ErrorConstants.InvalidCredentials);
}
}

public sealed class LoginCommandHandler : IRequestHandler<LoginCommand, OperationResult<LoginResult>>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IJwtService _jwtService;

public LoginCommandHandler(UserManager<ApplicationUser> userManager, IJwtService jwtAuthManager)
public LoginCommandHandler(UserManager<ApplicationUser> userManager, IJwtService jwtService)
{
_userManager = userManager;
_jwtService = jwtAuthManager;
_jwtService = jwtService;
}

public async Task<OperationResult<LoginResult>> Handle(LoginCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByNameAsync(request.Email);
if (user == null)
{
return OperationResult.Fail<LoginResult>(ErrorConstants.InvalidCredentials);
}

var isSucceeded = await _userManager.CheckPasswordAsync(user, request.Password);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Identity;
using StudySharp.ApplicationServices.JwtAuthService;
using StudySharp.ApplicationServices.ValidationRules.Auth;
using StudySharp.Domain.Constants;
using StudySharp.Domain.General;
using StudySharp.DomainServices;
Expand All @@ -14,24 +16,32 @@ public sealed class LogoutCommand : IRequest<OperationResult>
public string UserName { get; set; }
}

public class LogoutCommandValidator : AbstractValidator<LogoutCommand>
{
public LogoutCommandValidator(ILogoutRules rules)
{
RuleFor(_ => _.UserName)
.NotEmpty()
.WithMessage(string.Format(ErrorConstants.FieldIsRequired, nameof(ApplicationUser.UserName)))
.MustAsync(rules.UserIsRegistered)
.WithMessage(_ => string.Format(ErrorConstants.EntityNotFound, "User", nameof(ApplicationUser.Email), _.UserName));
}
}

public sealed class LogoutCommandHandler : IRequestHandler<LogoutCommand, OperationResult>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IJwtService _jwtService;

public LogoutCommandHandler(UserManager<ApplicationUser> userManager, IJwtService jwtAuthManager)
public LogoutCommandHandler(UserManager<ApplicationUser> userManager, IJwtService jwtService)
{
_userManager = userManager;
_jwtService = jwtAuthManager;
_jwtService = jwtService;
}

public async Task<OperationResult> Handle(LogoutCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByNameAsync(request.UserName);
if (user == null)
{
return OperationResult.Fail(string.Format(ErrorConstants.EntityNotFound, "User", "Email", request.UserName));
}

_jwtService.RemoveRefreshTokenByUserName(user.UserName);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using StudySharp.ApplicationServices.JwtAuthService;
using StudySharp.ApplicationServices.JwtAuthService.ResultModels;
using StudySharp.ApplicationServices.ValidationRules.Auth;
using StudySharp.Domain.Constants;
using StudySharp.Domain.General;
using StudySharp.DomainServices;
Expand All @@ -19,14 +21,34 @@ public sealed class RefreshTokenCommand : IRequest<OperationResult<RefreshTokenR
public string RefreshToken { get; set; }
}

public sealed class RefsreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, OperationResult<RefreshTokenResult>>
public class RefreshTokenCommandValidator : AbstractValidator<RefreshTokenCommand>
{
public RefreshTokenCommandValidator(IRefreshTokenRules rules)
{
RuleFor(_ => _.UserName)
.NotEmpty()
.WithMessage(string.Format(ErrorConstants.FieldIsRequired, nameof(ApplicationUser.Email)))
.MustAsync(rules.UserIsRegistered)
.WithMessage(_ => string.Format(ErrorConstants.EntityNotFound, "User", nameof(ApplicationUser.Email), _.UserName));

RuleFor(_ => _.AccessToken)
.NotEmpty()
.WithMessage(string.Format(ErrorConstants.FieldIsRequired, nameof(RefreshTokenCommand.AccessToken)));

RuleFor(_ => _.RefreshToken)
.NotEmpty()
.WithMessage(string.Format(ErrorConstants.FieldIsRequired, nameof(RefreshTokenCommand.RefreshToken)));
}
}

public sealed class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, OperationResult<RefreshTokenResult>>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IJwtService _jwtService;

public RefsreshTokenCommandHandler(IJwtService jwtAuthManager, UserManager<ApplicationUser> userManager)
public RefreshTokenCommandHandler(IJwtService jwtService, UserManager<ApplicationUser> userManager)
{
_jwtService = jwtAuthManager;
_jwtService = jwtService;
_userManager = userManager;
}

Expand All @@ -35,15 +57,6 @@ public async Task<OperationResult<RefreshTokenResult>> Handle(RefreshTokenComman
try
{
var user = await _userManager.FindByNameAsync(request.UserName);
if (user == null)
{
return OperationResult.Fail<RefreshTokenResult>(string.Format(ErrorConstants.EntityNotFound, "User", "Email", request.UserName));
}

if (string.IsNullOrEmpty(request.RefreshToken))
{
return OperationResult.Fail<RefreshTokenResult>(ErrorConstants.InvalidToken);
}

var jwtResult = _jwtService.Refresh(request.RefreshToken, request.AccessToken, DateTime.Now);
return OperationResult.Ok(new RefreshTokenResult
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Identity;
using StudySharp.ApplicationServices.ValidationRules.Auth;
using StudySharp.Domain.Constants;
using StudySharp.Domain.General;
using StudySharp.DomainServices;
Expand All @@ -16,6 +18,28 @@ public sealed class RegisterNewUserCommand : IRequest<OperationResult>
public string ConfirmPassword { get; set; }
}

public class RegisterNewUserCommandValidator : AbstractValidator<RegisterNewUserCommand>
{
public RegisterNewUserCommandValidator(IRegisterNewUserValidationRules rules)
{
RuleFor(_ => _.Email)
.NotEmpty()
.WithMessage(string.Format(ErrorConstants.FieldIsRequired, nameof(RegisterNewUserCommand.Email)))
.MustAsync(rules.UserNameIsUnique)
.WithMessage(_ => string.Format(ErrorConstants.EntityAlreadyExists, "User", nameof(RegisterNewUserCommand.Email), _.Email));

RuleFor(_ => _.Password)
.NotEmpty()
.WithMessage(string.Format(ErrorConstants.FieldIsRequired, nameof(RegisterNewUserCommand.Password)));

RuleFor(_ => _.ConfirmPassword)
.NotEmpty()
.WithMessage(string.Format(ErrorConstants.FieldIsRequired, "Confirm Password"))
.Must((registerNewUserCommand, confirmPassword) => registerNewUserCommand.Password.Trim().Equals(confirmPassword.Trim()))
.WithMessage(ErrorConstants.PasswordsDoNotMatch);
}
}

public sealed class RegisterNewUserCommandHandler : IRequestHandler<RegisterNewUserCommand, OperationResult>
{
private readonly UserManager<ApplicationUser> _userManager;
Expand All @@ -27,17 +51,6 @@ public RegisterNewUserCommandHandler(UserManager<ApplicationUser> userManager)

public async Task<OperationResult> Handle(RegisterNewUserCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByNameAsync(request.Email);
if (user != null)
{
return OperationResult.Fail(string.Format(ErrorConstants.EntityAlreadyExists, "User", nameof(request.Email), request.Email));
}

if (!request.Password.Trim().Equals(request.ConfirmPassword.Trim()))
{
return OperationResult.Fail(ErrorConstants.PasswordsDoNotMatch);
}

var newUser = new ApplicationUser
{
UserName = request.Email,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

namespace StudySharp.ApplicationServices.JwtAuthService
{
public static class JwtAuthServiceExtensions
{
public static IServiceCollection AddJwtAuthService(this IServiceCollection services, IConfiguration configuration)
{
var jwtTokenConfig = configuration.GetSection(JwtTokenConfig.JwtTokenConfigSection).Get<JwtTokenConfig>();
services.Configure<JwtTokenConfig>(options => configuration.GetSection(JwtTokenConfig.JwtTokenConfigSection).Bind(options));
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtTokenConfig.Issuer,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtTokenConfig.Secret)),
ValidAudience = jwtTokenConfig.Audience,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
};
});
services.AddSingleton<IJwtService, JwtService>();
services.AddHostedService<JwtRefreshTokenCache>();

return services;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ private void DoWork(object state)

public Task StopAsync(CancellationToken stoppingToken)
{
_timer?.Change(Timeout.Infinite, 0);
_timer.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}

public void Dispose()
{
_timer?.Dispose();
_timer.Dispose();
GC.SuppressFinalize(this);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace StudySharp.ApplicationServices.JwtAuthService
Expand All @@ -28,11 +29,11 @@ public class JwtService : IJwtService
private readonly JwtTokenConfig _jwtTokenConfig;
private readonly byte[] _secret;

public JwtService(JwtTokenConfig jwtTokenConfig)
public JwtService(IOptions<JwtTokenConfig> jwtTokenConfigOptions)
{
_jwtTokenConfig = jwtTokenConfig;
_jwtTokenConfig = jwtTokenConfigOptions.Value;
_usersRefreshTokens = new ConcurrentDictionary<string, RefreshToken>();
_secret = Encoding.ASCII.GetBytes(jwtTokenConfig.Secret);
_secret = Encoding.ASCII.GetBytes(jwtTokenConfigOptions.Value.Secret);
}

// optional: clean up expired refresh tokens
Expand Down Expand Up @@ -122,7 +123,7 @@ public JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime n
ValidAudience = _jwtTokenConfig.Audience,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1),
ClockSkew = TimeSpan.Zero,
},
out var validatedToken);
return (principal, validatedToken as JwtSecurityToken);
Expand Down
Loading