SA-14 Updated User model structure and Services

This commit is contained in:
shchoholiev 2023-10-16 03:22:49 +00:00
parent 4102312fe9
commit 39bed12f30
12 changed files with 249 additions and 208 deletions

View File

@ -14,6 +14,12 @@
// "protocol": "https" // "protocol": "https"
// } // }
// } // }
// Container is not working on M1 Mac
// "runArgs": [
// "--platform=linux/amd64"
// ],
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [

View File

@ -14,7 +14,7 @@ public class AccessMutation
[Service] IUserManager userManager) [Service] IUserManager userManager)
=> userManager.AccessGuestAsync(guest, cancellationToken); => userManager.AccessGuestAsync(guest, cancellationToken);
public Task<TokensModel> RefreshUserTokenAsync(TokensModel model, CancellationToken cancellationToken, public Task<TokensModel> RefreshAccessTokenAsync(TokensModel model, CancellationToken cancellationToken,
[Service] ITokensService tokensService) [Service] IUserManager userManager)
=> tokensService.RefreshUserAsync(model, cancellationToken); => userManager.RefreshAccessTokenAsync(model, cancellationToken);
} }

View File

@ -1,22 +1,14 @@
using ShoppingAssistantApi.Application.IServices.Identity; using HotChocolate.Authorization;
using ShoppingAssistantApi.Application.IServices; using ShoppingAssistantApi.Application.IServices;
using ShoppingAssistantApi.Application.Models.CreateDtos; using ShoppingAssistantApi.Application.Models.CreateDtos;
using ShoppingAssistantApi.Application.Models.Dtos; using ShoppingAssistantApi.Application.Models.Dtos;
using ShoppingAssistantApi.Application.Models.Identity;
namespace ShoppingAssistantApi.Api.Mutations; namespace ShoppingAssistantApi.Api.Mutations;
[ExtendObjectType(OperationTypeNames.Mutation)] [ExtendObjectType(OperationTypeNames.Mutation)]
public class RolesMutation public class RolesMutation
{ {
public Task<TokensModel> AddToRoleAsync(string roleName, string id, CancellationToken cancellationToken, [Authorize]
[Service] IUserManager userManager)
=> userManager.AddToRoleAsync(roleName, id, cancellationToken);
public Task<TokensModel> RemoveFromRoleAsync(string roleName, string id, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.RemoveFromRoleAsync(roleName, id, cancellationToken);
public Task<RoleDto> AddRole(RoleCreateDto roleDto, CancellationToken cancellationToken, public Task<RoleDto> AddRole(RoleCreateDto roleDto, CancellationToken cancellationToken,
[Service] IRolesService rolesService) [Service] IRolesService rolesService)
=> rolesService.AddRoleAsync(roleDto, cancellationToken); => rolesService.AddRoleAsync(roleDto, cancellationToken);

View File

@ -14,7 +14,17 @@ public class UsersMutation
=> userManager.UpdateAsync(userDto, cancellationToken); => userManager.UpdateAsync(userDto, cancellationToken);
[Authorize] [Authorize]
public Task<UpdateUserModel> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken, public Task<UserDto> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken,
[Service] IUserManager userManager) [Service] IUserManager userManager)
=> userManager.UpdateUserByAdminAsync(id, userDto, cancellationToken); => userManager.UpdateUserByAdminAsync(id, userDto, cancellationToken);
[Authorize]
public Task<UserDto> AddToRoleAsync(string roleName, string userId, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.AddToRoleAsync(roleName, userId, cancellationToken);
[Authorize]
public Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken,
[Service] IUserManager userManager)
=> userManager.RemoveFromRoleAsync(roleName, userId, cancellationToken);
} }

View File

@ -1,5 +1,4 @@
using ShoppingAssistantApi.Application.Models.Identity; using System.Security.Claims;
using System.Security.Claims;
namespace ShoppingAssistantApi.Application.IServices.Identity; namespace ShoppingAssistantApi.Application.IServices.Identity;
@ -9,5 +8,5 @@ public interface ITokensService
string GenerateRefreshToken(); string GenerateRefreshToken();
Task<TokensModel> RefreshUserAsync(TokensModel tokensModel, CancellationToken cancellationToken); ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
} }

View File

@ -10,11 +10,13 @@ public interface IUserManager
Task<TokensModel> LoginAsync(AccessUserModel login, CancellationToken cancellationToken); Task<TokensModel> LoginAsync(AccessUserModel login, CancellationToken cancellationToken);
Task<TokensModel> AddToRoleAsync(string roleName, string id, CancellationToken cancellationToken); Task<UserDto> AddToRoleAsync(string roleName, string userId, CancellationToken cancellationToken);
Task<TokensModel> RemoveFromRoleAsync(string roleName, string id, CancellationToken cancellationToken); Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken);
Task<UpdateUserModel> UpdateAsync(UserDto userDto, CancellationToken cancellationToken); Task<UpdateUserModel> UpdateAsync(UserDto userDto, CancellationToken cancellationToken);
Task<UpdateUserModel> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken); Task<UserDto> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken);
Task<TokensModel> RefreshAccessTokenAsync(TokensModel tokensModel, CancellationToken cancellationToken);
} }

View File

@ -13,8 +13,4 @@ public class User : EntityBase
public string? Email { get; set; } public string? Email { get; set; }
public string? PasswordHash { get; set; } public string? PasswordHash { get; set; }
public string RefreshToken { get; set; }
public DateTime RefreshTokenExpiryDate { get; set; }
} }

View File

@ -5,10 +5,7 @@ using System.Text;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson;
using ShoppingAssistantApi.Application.IRepositories;
using ShoppingAssistantApi.Application.IServices.Identity; using ShoppingAssistantApi.Application.IServices.Identity;
using ShoppingAssistantApi.Application.Models.Identity;
namespace ShoppingAssistantApi.Infrastructure.Services.Identity; namespace ShoppingAssistantApi.Infrastructure.Services.Identity;
@ -16,50 +13,16 @@ public class TokensService : ITokensService
{ {
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly IUsersRepository _usersRepository;
private readonly ILogger _logger; private readonly ILogger _logger;
public TokensService(IConfiguration configuration, IUsersRepository usersRepository, public TokensService(
IConfiguration configuration,
ILogger<TokensService> logger) ILogger<TokensService> logger)
{ {
this._configuration = configuration; this._configuration = configuration;
this._usersRepository = usersRepository;
this._logger = logger; this._logger = logger;
} }
public async Task<TokensModel> RefreshUserAsync(TokensModel tokensModel, CancellationToken cancellationToken)
{
var principal = this.GetPrincipalFromExpiredToken(tokensModel.AccessToken);
var userId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
if (!ObjectId.TryParse(userId, out var objectId))
{
throw new InvalidDataException("Provided id is invalid.");
}
var user = await this._usersRepository.GetUserAsync(objectId, cancellationToken);
if (user == null || user?.RefreshToken != tokensModel.RefreshToken
|| user?.RefreshTokenExpiryDate <= DateTime.UtcNow)
{
throw new SecurityTokenExpiredException();
}
var newAccessToken = this.GenerateAccessToken(principal.Claims);
var newRefreshToken = this.GenerateRefreshToken();
user.RefreshToken = newRefreshToken;
user.RefreshTokenExpiryDate = DateTime.UtcNow.AddDays(30);
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
this._logger.LogInformation($"Refreshed user tokens.");
return new TokensModel
{
AccessToken = newAccessToken,
RefreshToken = newRefreshToken
};
}
public string GenerateAccessToken(IEnumerable<Claim> claims) public string GenerateAccessToken(IEnumerable<Claim> claims)
{ {
var tokenOptions = GetTokenOptions(claims); var tokenOptions = GetTokenOptions(claims);
@ -73,8 +36,7 @@ public class TokensService : ITokensService
public string GenerateRefreshToken() public string GenerateRefreshToken()
{ {
var randomNumber = new byte[32]; var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create()) using var rng = RandomNumberGenerator.Create();
{
rng.GetBytes(randomNumber); rng.GetBytes(randomNumber);
var refreshToken = Convert.ToBase64String(randomNumber); var refreshToken = Convert.ToBase64String(randomNumber);
@ -82,9 +44,8 @@ public class TokensService : ITokensService
return refreshToken; return refreshToken;
} }
}
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{ {
var tokenValidationParameters = new TokenValidationParameters var tokenValidationParameters = new TokenValidationParameters
{ {
@ -96,11 +57,10 @@ public class TokensService : ITokensService
ValidateLifetime = false ValidateLifetime = false
}; };
var tokenHandler = new JwtSecurityTokenHandler(); var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken; var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken; var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, if (jwtSecurityToken == null
StringComparison.InvariantCultureIgnoreCase)) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token"); throw new SecurityTokenException("Invalid token");
this._logger.LogInformation($"Returned data from expired access token."); this._logger.LogInformation($"Returned data from expired access token.");
@ -117,7 +77,7 @@ public class TokensService : ITokensService
var tokenOptions = new JwtSecurityToken( var tokenOptions = new JwtSecurityToken(
issuer: _configuration.GetValue<string>("JsonWebTokenKeys:ValidIssuer"), issuer: _configuration.GetValue<string>("JsonWebTokenKeys:ValidIssuer"),
audience: _configuration.GetValue<string>("JsonWebTokenKeys:ValidAudience"), audience: _configuration.GetValue<string>("JsonWebTokenKeys:ValidAudience"),
expires: DateTime.UtcNow.AddMinutes(5), expires: DateTime.UtcNow.AddMinutes(15),
claims: claims, claims: claims,
signingCredentials: signinCredentials signingCredentials: signinCredentials
); );

View File

@ -1,7 +1,6 @@
using AutoMapper; using AutoMapper;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.Win32;
using MongoDB.Bson; using MongoDB.Bson;
using ShoppingAssistantApi.Application.Exceptions; using ShoppingAssistantApi.Application.Exceptions;
using ShoppingAssistantApi.Application.GlobalInstances; using ShoppingAssistantApi.Application.GlobalInstances;
@ -14,23 +13,32 @@ using ShoppingAssistantApi.Domain.Entities;
using System.Security.Claims; using System.Security.Claims;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace ShoppingAssistantApi.Infrastructure.Services.Identity; namespace ShoppingAssistantApi.Infrastructure.Services.Identity;
public class UserManager : IUserManager
public class UserManager : ServiceBase, IUserManager
{ {
private readonly IUsersRepository _usersRepository; private readonly IUsersRepository _usersRepository;
private readonly ILogger _logger;
private readonly IPasswordHasher _passwordHasher; private readonly IPasswordHasher _passwordHasher;
private readonly ITokensService _tokensService; private readonly ITokensService _tokensService;
private readonly IMapper _mapper;
private readonly IRolesRepository _rolesRepository; private readonly IRolesRepository _rolesRepository;
public UserManager(IUsersRepository usersRepository, ILogger<UserManager> logger, IPasswordHasher passwordHasher, ITokensService tokensService, IMapper mapper, IRolesRepository rolesRepository) private readonly IRefreshTokensRepository _refreshTokensRepository;
private readonly IMapper _mapper;
private readonly ILogger _logger;
public UserManager(
IUsersRepository usersRepository,
IPasswordHasher passwordHasher,
ITokensService tokensService,
IRolesRepository rolesRepository,
IRefreshTokensRepository refreshTokensRepository,
IMapper mapper,
ILogger<UserManager> logger)
{ {
this._usersRepository = usersRepository; this._usersRepository = usersRepository;
this._logger = logger; this._logger = logger;
@ -38,15 +46,16 @@ public class UserManager : IUserManager
this._tokensService = tokensService; this._tokensService = tokensService;
this._mapper = mapper; this._mapper = mapper;
this._rolesRepository = rolesRepository; this._rolesRepository = rolesRepository;
this._refreshTokensRepository = refreshTokensRepository;
} }
public async Task<TokensModel> LoginAsync(AccessUserModel login, CancellationToken cancellationToken) public async Task<TokensModel> LoginAsync(AccessUserModel login, CancellationToken cancellationToken)
{ {
var user = login.Email != null _logger.LogInformation($"Logging in user with email: {login.Email} and phone: {login.Phone}.");
? await this._usersRepository.GetUserAsync(x => x.Email == login.Email, cancellationToken)
: await this._usersRepository.GetUserAsync(x => x.Phone == login.Phone, cancellationToken);
var user = string.IsNullOrEmpty(login.Phone)
? await this._usersRepository.GetUserAsync(u => u.Email == login.Email, cancellationToken)
: await this._usersRepository.GetUserAsync(u => u.Phone == login.Phone, cancellationToken);
if (user == null) if (user == null)
{ {
throw new EntityNotFoundException<User>(); throw new EntityNotFoundException<User>();
@ -57,197 +66,216 @@ public class UserManager : IUserManager
throw new InvalidDataException("Invalid password!"); throw new InvalidDataException("Invalid password!");
} }
user.RefreshToken = this.GetRefreshToken(); var refreshToken = await AddRefreshToken(user.Id, cancellationToken);
user.RefreshTokenExpiryDate = DateTime.UtcNow.AddDays(30);
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var tokens = this.GetUserTokens(user);
this._logger.LogInformation($"Logged in user with email: {login.Email}."); var tokens = this.GetUserTokens(user, refreshToken);
this._logger.LogInformation($"Logged in user with email: {login.Email} and phone: {login.Phone}.");
return tokens; return tokens;
} }
public async Task<TokensModel> AccessGuestAsync(AccessGuestModel guest, CancellationToken cancellationToken) public async Task<TokensModel> AccessGuestAsync(AccessGuestModel guest, CancellationToken cancellationToken)
{ {
_logger.LogInformation($"Logging in / Registering guest with guest id: {guest.GuestId}.");
var user = await this._usersRepository.GetUserAsync(x => x.GuestId == guest.GuestId, cancellationToken); var user = await this._usersRepository.GetUserAsync(x => x.GuestId == guest.GuestId, cancellationToken);
if (user != null) if (user == null)
{ {
user.RefreshToken = this.GetRefreshToken();
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var userTokens = this.GetUserTokens(user);
this._logger.LogInformation($"Logged in guest with guest id: {guest.GuestId}.");
return userTokens;
}
var role = await this._rolesRepository.GetRoleAsync(r => r.Name == "Guest", cancellationToken); var role = await this._rolesRepository.GetRoleAsync(r => r.Name == "Guest", cancellationToken);
user = new User
var newUser = new User
{ {
GuestId = guest.GuestId, GuestId = guest.GuestId,
Roles = new List<Role> { role }, Roles = new List<Role> { role },
RefreshToken = this.GetRefreshToken(),
RefreshTokenExpiryDate = DateTime.Now.AddDays(30),
CreatedDateUtc = DateTime.UtcNow, CreatedDateUtc = DateTime.UtcNow,
LastModifiedDateUtc = DateTime.UtcNow CreatedById = ObjectId.Empty // Default value for all new users
}; };
await this._usersRepository.AddAsync(newUser, cancellationToken); await this._usersRepository.AddAsync(user, cancellationToken);
var tokens = this.GetUserTokens(newUser);
this._logger.LogInformation($"Created guest with guest id: {guest.GuestId}."); this._logger.LogInformation($"Created guest with guest id: {guest.GuestId}.");
}
var refreshToken = await AddRefreshToken(user.Id, cancellationToken);
var tokens = this.GetUserTokens(user, refreshToken);
this._logger.LogInformation($"Logged in guest with guest id: {guest.GuestId}.");
return tokens; return tokens;
} }
public async Task<TokensModel> AddToRoleAsync(string roleName, string id, CancellationToken cancellationToken) public async Task<TokensModel> RefreshAccessTokenAsync(TokensModel tokensModel, CancellationToken cancellationToken)
{ {
_logger.LogInformation($"Refreshing access token.");
var principal = _tokensService.GetPrincipalFromExpiredToken(tokensModel.AccessToken);
var userId = ParseObjectId(principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value);
var refreshTokenModel = await this._refreshTokensRepository
.GetOneAsync(r =>
r.Token == tokensModel.RefreshToken
&& r.CreatedById == userId
&& r.IsDeleted == false, cancellationToken);
if (refreshTokenModel == null || refreshTokenModel.ExpiryDateUTC < DateTime.UtcNow)
{
throw new SecurityTokenExpiredException();
}
var refreshToken = refreshTokenModel.Token;
// Update Refresh token if it expires in less than 7 days to keep user constantly logged in if he uses the app
if (refreshTokenModel.ExpiryDateUTC.AddDays(-7) < DateTime.UtcNow)
{
await _refreshTokensRepository.DeleteAsync(refreshTokenModel, cancellationToken);
var newRefreshToken = await AddRefreshToken(userId, cancellationToken);
refreshToken = newRefreshToken.Token;
}
var tokens = new TokensModel
{
AccessToken = _tokensService.GenerateAccessToken(principal.Claims),
RefreshToken = refreshToken
};
this._logger.LogInformation($"Refreshed access token.");
return tokens;
}
public async Task<UserDto> AddToRoleAsync(string roleName, string userId, CancellationToken cancellationToken)
{
_logger.LogInformation($"Adding Role: {roleName} to User with Id: {userId}.");
var role = await this._rolesRepository.GetRoleAsync(r => r.Name == roleName, cancellationToken); var role = await this._rolesRepository.GetRoleAsync(r => r.Name == roleName, cancellationToken);
if (role == null) if (role == null)
{ {
throw new EntityNotFoundException<Role>(); throw new EntityNotFoundException<Role>();
} }
if (!ObjectId.TryParse(id, out var objectId)) var userObjectId = ParseObjectId(userId);
{ var user = await this._usersRepository.GetUserAsync(userObjectId, cancellationToken);
throw new InvalidDataException("Provided id is invalid.");
}
var user = await this._usersRepository.GetUserAsync(objectId, cancellationToken);
if (user == null) if (user == null)
{ {
throw new EntityNotFoundException<User>(); throw new EntityNotFoundException<User>();
} }
user.Roles.Add(role); user.Roles.Add(role);
await this._usersRepository.UpdateUserAsync(user, cancellationToken); var updatedUser = await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var tokens = this.GetUserTokens(user); var userDto = this._mapper.Map<UserDto>(updatedUser);
this._logger.LogInformation($"Added role {roleName} to user with id: {id}."); this._logger.LogInformation($"Added Role: {roleName} to User with Id: {userId}.");
return tokens; return userDto;
} }
public async Task<TokensModel> RemoveFromRoleAsync(string roleName, string id, CancellationToken cancellationToken) public async Task<UserDto> RemoveFromRoleAsync(string roleName, string userId, CancellationToken cancellationToken)
{ {
_logger.LogInformation($"Removing Role: {roleName} from User with Id: {userId}.");
var role = await this._rolesRepository.GetRoleAsync(r => r.Name == roleName, cancellationToken); var role = await this._rolesRepository.GetRoleAsync(r => r.Name == roleName, cancellationToken);
if (role == null) if (role == null)
{ {
throw new EntityNotFoundException<Role>(); throw new EntityNotFoundException<Role>();
} }
if (!ObjectId.TryParse(id, out var objectId)) var userObjectId = ParseObjectId(userId);
{ var user = await this._usersRepository.GetUserAsync(userObjectId, cancellationToken);
throw new InvalidDataException("Provided id is invalid.");
}
var user = await this._usersRepository.GetUserAsync(objectId, cancellationToken);
if (user == null) if (user == null)
{ {
throw new EntityNotFoundException<User>(); throw new EntityNotFoundException<User>();
} }
var deletedRole = user.Roles.Find(x => x.Name == role.Name); var deletedRole = user.Roles.Find(x => x.Name == role.Name);
user.Roles.Remove(deletedRole); user.Roles.Remove(deletedRole);
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var tokens = this.GetUserTokens(user);
this._logger.LogInformation($"Added role {roleName} to user with id: {id}."); var updatedUser = await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var userDto = this._mapper.Map<UserDto>(updatedUser);
return tokens; this._logger.LogInformation($"Removed Role: {roleName} from User with Id: {userId}.");
return userDto;
} }
public async Task<UpdateUserModel> UpdateAsync(UserDto userDto, CancellationToken cancellationToken) public async Task<UpdateUserModel> UpdateAsync(UserDto userDto, CancellationToken cancellationToken)
{ {
if (userDto.Email != null) ValidateEmail(userDto.Email); _logger.LogInformation($"Updating user with id: {GlobalUser.Id}.");
if (userDto.Phone != null) ValidateNumber(userDto.Phone);
if (userDto.Roles.Any(x => x.Name == "Guest") && !userDto.Roles.Any(x => x.Name == "User"))
{
if (userDto.Password != null && (userDto.Email != null || userDto.Phone != null))
{
var roleEntity = await this._rolesRepository.GetRoleAsync(x => x.Name == "User", cancellationToken);
var roleDto = this._mapper.Map<RoleDto>(roleEntity);
userDto.Roles.Add(roleDto);
}
}
var user = await this._usersRepository.GetUserAsync(x => x.Id == GlobalUser.Id, cancellationToken); var user = await this._usersRepository.GetUserAsync(x => x.Id == GlobalUser.Id, cancellationToken);
if (user == null) if (user == null)
{ {
throw new EntityNotFoundException<User>(); throw new EntityNotFoundException<User>();
} }
if (userDto.Roles.Any(x => x.Name == "User") && userDto.Email != null) await ValidateUserAsync(userDto, user, cancellationToken);
{
if (await this._usersRepository.GetUserAsync(x => x.Email == userDto.Email, cancellationToken) != null)
{
throw new EntityAlreadyExistsException<User>("email", userDto.Email);
}
}
if (userDto.Roles.Any(x => x.Name == "User") && userDto.Phone != null)
{
if (await this._usersRepository.GetUserAsync(x => x.Phone == userDto.Phone, cancellationToken) != null)
{
throw new EntityAlreadyExistsException<User>("phone", userDto.Phone);
}
}
this._mapper.Map(userDto, user); this._mapper.Map(userDto, user);
if (!userDto.Password.IsNullOrEmpty()) if (!string.IsNullOrEmpty(userDto.Password))
{ {
user.PasswordHash = this._passwordHasher.Hash(userDto.Password); user.PasswordHash = this._passwordHasher.Hash(userDto.Password);
} }
user.RefreshToken = this.GetRefreshToken(); await CheckAndUpgradeToUserAsync(user, cancellationToken);
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var tokens = this.GetUserTokens(user); var updatedUser = await this._usersRepository.UpdateUserAsync(user, cancellationToken);
this._logger.LogInformation($"Update user with id: {GlobalUser.Id.ToString()}."); var refreshToken = await AddRefreshToken(user.Id, cancellationToken);
var tokens = this.GetUserTokens(user, refreshToken);
return new UpdateUserModel() { Tokens = tokens, User = this._mapper.Map<UserDto>(user) }; var updatedUserDto = this._mapper.Map<UserDto>(updatedUser);
this._logger.LogInformation($"Update user with id: {GlobalUser.Id}.");
return new UpdateUserModel()
{
Tokens = tokens,
User = updatedUserDto
};
} }
public async Task<UpdateUserModel> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken) public async Task<UserDto> UpdateUserByAdminAsync(string id, UserDto userDto, CancellationToken cancellationToken)
{ {
if (!ObjectId.TryParse(id, out var objectId)) _logger.LogInformation($"Admin updating User with Id: {id}.");
{
throw new InvalidDataException("Provided id is invalid.");
}
var user = await this._usersRepository.GetUserAsync(objectId, cancellationToken);
var userObjectId = ParseObjectId(id);
var user = await this._usersRepository.GetUserAsync(userObjectId, cancellationToken);
if (user == null) if (user == null)
{ {
throw new EntityNotFoundException<User>(); throw new EntityNotFoundException<User>();
} }
await ValidateUserAsync(userDto, user, cancellationToken);
this._mapper.Map(userDto, user); this._mapper.Map(userDto, user);
var updatedUser = await this._usersRepository.UpdateUserAsync(user, cancellationToken);
user.RefreshToken = this.GetRefreshToken(); var updatedUserDto = this._mapper.Map<UserDto>(updatedUser);
await this._usersRepository.UpdateUserAsync(user, cancellationToken);
var tokens = this.GetUserTokens(user); this._logger.LogInformation($"Admin updated User with Id: {id}.");
this._logger.LogInformation($"Update user with id: {id}."); return updatedUserDto;
return new UpdateUserModel() { Tokens = tokens, User = this._mapper.Map<UserDto>(user) };
} }
private string GetRefreshToken() private async Task<RefreshToken> AddRefreshToken(ObjectId userId, CancellationToken cancellationToken)
{ {
var refreshToken = this._tokensService.GenerateRefreshToken(); _logger.LogInformation($"Adding new refresh token for user with Id : {userId}.");
this._logger.LogInformation($"Returned new refresh token."); var refreshToken = new RefreshToken
{
Token = _tokensService.GenerateRefreshToken(),
ExpiryDateUTC = DateTime.UtcNow.AddDays(30),
CreatedById = userId,
CreatedDateUtc = DateTime.UtcNow
};
await this._refreshTokensRepository.AddAsync(refreshToken, cancellationToken);
this._logger.LogInformation($"Added new refresh token.");
return refreshToken; return refreshToken;
} }
private TokensModel GetUserTokens(User user) private TokensModel GetUserTokens(User user, RefreshToken refreshToken)
{ {
var claims = this.GetClaims(user); var claims = this.GetClaims(user);
var accessToken = this._tokensService.GenerateAccessToken(claims); var accessToken = this._tokensService.GenerateAccessToken(claims);
@ -257,7 +285,7 @@ public class UserManager : IUserManager
return new TokensModel return new TokensModel
{ {
AccessToken = accessToken, AccessToken = accessToken,
RefreshToken = user.RefreshToken, RefreshToken = refreshToken.Token,
}; };
} }
@ -265,21 +293,56 @@ public class UserManager : IUserManager
{ {
var claims = new List<Claim>() var claims = new List<Claim>()
{ {
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new (ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email ?? string.Empty), new (ClaimTypes.Email, user.Email ?? string.Empty),
new Claim(ClaimTypes.MobilePhone, user.Phone ?? string.Empty), new (ClaimTypes.MobilePhone, user.Phone ?? string.Empty),
}; };
foreach (var role in user.Roles) foreach (var role in user.Roles)
{ {
claims.Add(new Claim(ClaimTypes.Role, role.Name)); claims.Add(new (ClaimTypes.Role, role.Name));
} }
this._logger.LogInformation($"Returned claims for user with id: {user.Id.ToString()}."); this._logger.LogInformation($"Returned claims for User with Id: {user.Id}.");
return claims; return claims;
} }
private async Task CheckAndUpgradeToUserAsync(User user, CancellationToken cancellationToken)
{
if (user.Roles.Any(x => x.Name == "Guest") && !user.Roles.Any(x => x.Name == "User"))
{
if (!string.IsNullOrEmpty(user.PasswordHash) && (!string.IsNullOrEmpty(user.Email) || !string.IsNullOrEmpty(user.Phone)))
{
var role = await this._rolesRepository.GetRoleAsync(x => x.Name == "User", cancellationToken);
user.Roles.Add(role);
}
}
}
private async Task ValidateUserAsync(UserDto userDto, User user, CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(userDto.Email))
{
ValidateEmail(userDto.Email);
if (userDto.Email != user.Email
&& await this._usersRepository.ExistsAsync(x => x.Email == userDto.Email, cancellationToken))
{
throw new EntityAlreadyExistsException<User>("email", userDto.Email);
}
}
if (!string.IsNullOrEmpty(userDto.Phone))
{
ValidatePhone(userDto.Phone);
if (userDto.Phone != user.Phone
&& await this._usersRepository.ExistsAsync(x => x.Phone == userDto.Phone, cancellationToken))
{
throw new EntityAlreadyExistsException<User>("phone", userDto.Phone);
}
}
}
private void ValidateEmail(string email) private void ValidateEmail(string email)
{ {
string regex = @"^[^@\s]+@[^@\s]+\.[^@\s]+$"; string regex = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
@ -290,7 +353,7 @@ public class UserManager : IUserManager
} }
} }
private void ValidateNumber(string phone) private void ValidatePhone(string phone)
{ {
string regex = @"^\+[0-9]{1,15}$"; string regex = @"^\+[0-9]{1,15}$";

View File

@ -33,14 +33,16 @@ public class RolesService : IRolesService
entity.CreatedDateUtc = DateTime.UtcNow; entity.CreatedDateUtc = DateTime.UtcNow;
entity.LastModifiedDateUtc = DateTime.UtcNow; entity.LastModifiedDateUtc = DateTime.UtcNow;
await this._repository.AddAsync(entity, cancellationToken); await this._repository.AddAsync(entity, cancellationToken);
return this._mapper.Map<RoleDto>(entity); return this._mapper.Map<RoleDto>(entity);
} }
public async Task<PagedList<RoleDto>> GetRolesPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken) public async Task<PagedList<RoleDto>> GetRolesPageAsync(int pageNumber, int pageSize, CancellationToken cancellationToken)
{ {
var entities = await this._repository.GetPageAsync(pageNumber, pageSize, cancellationToken); var entities = await this._repository.GetPageAsync(pageNumber, pageSize, cancellationToken);
var dtos = this._mapper.Map<List<RoleDto>>(entities);
var count = await this._repository.GetTotalCountAsync(); var count = await this._repository.GetTotalCountAsync();
var dtos = this._mapper.Map<List<RoleDto>>(entities);
return new PagedList<RoleDto>(dtos, pageNumber, pageSize, count); return new PagedList<RoleDto>(dtos, pageNumber, pageSize, count);
} }
} }

View File

@ -0,0 +1,15 @@
namespace ShoppingAssistantApi.Infrastructure.Services;
using MongoDB.Bson;
public abstract class ServiceBase
{
public ObjectId ParseObjectId(string? id)
{
if (ObjectId.TryParse(id, out ObjectId objectId))
{
return objectId;
}
throw new InvalidDataException("Provided id cannot be parsed to a MongoDb ObjectId.");
}
}

View File

@ -27,8 +27,6 @@ public class UsersRepository : BaseRepository<User>, IUsersRepository
var updateDefinition = Builders<User>.Update var updateDefinition = Builders<User>.Update
.Set(u => u.Email, user.Email) .Set(u => u.Email, user.Email)
.Set(u => u.Phone, user.Phone) .Set(u => u.Phone, user.Phone)
.Set(u => u.RefreshToken, user.RefreshToken)
.Set(u => u.RefreshTokenExpiryDate, user.RefreshTokenExpiryDate)
.Set(u => u.GuestId, user.GuestId) .Set(u => u.GuestId, user.GuestId)
.Set(u => u.Roles, user.Roles) .Set(u => u.Roles, user.Roles)
.Set(u => u.PasswordHash, user.PasswordHash) .Set(u => u.PasswordHash, user.PasswordHash)
@ -42,7 +40,5 @@ public class UsersRepository : BaseRepository<User>, IUsersRepository
return await this._collection.FindOneAndUpdateAsync( return await this._collection.FindOneAndUpdateAsync(
Builders<User>.Filter.Eq(u => u.Id, user.Id), updateDefinition, options, cancellationToken); Builders<User>.Filter.Eq(u => u.Id, user.Id), updateDefinition, options, cancellationToken);
} }
} }