Add user manager page (#22)
Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using GameIdeas.Shared.Dto;
|
||||
using GameIdeas.Shared.Constants;
|
||||
using GameIdeas.Shared.Dto;
|
||||
using GameIdeas.WebAPI.Exceptions;
|
||||
using GameIdeas.WebAPI.Services.Users;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GameIdeas.WebAPI.Controllers;
|
||||
@@ -8,7 +10,8 @@ namespace GameIdeas.WebAPI.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class UserController(
|
||||
IUserService userService,
|
||||
IUserReadService userReadService,
|
||||
IUserWriteService userWriteService,
|
||||
ILoggerFactory loggerFactory) : Controller
|
||||
{
|
||||
private readonly ILogger<UserController> logger = loggerFactory.CreateLogger<UserController>();
|
||||
@@ -18,7 +21,7 @@ public class UserController(
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(await userService.Login(model));
|
||||
return Ok(await userReadService.Login(model));
|
||||
}
|
||||
catch (UserInvalidException e)
|
||||
{
|
||||
@@ -36,4 +39,82 @@ public class UserController(
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||
[HttpGet("Roles")]
|
||||
public async Task<ActionResult<IEnumerable<RoleDto>>> GetRoles()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(await userReadService.GetRoles());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Internal error while get roles");
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<UserListDto>> GetUsers([FromQuery] UserFilterDto filter)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(await userReadService.GetUsers(filter));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Internal error while get users");
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||
[HttpPost("Create")]
|
||||
public async Task<ActionResult<IdDto>> CreateUser([FromBody] UserDto user)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = new IdDto() { Id = await userWriteService.CreateUser(user) };
|
||||
return Created("/Create", id);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Internal error while create user");
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||
[HttpPut("Update/{userId}")]
|
||||
public async Task<ActionResult<IdDto>> UpdateUser(string userId, [FromBody] UserDto user)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = new IdDto() { Id = await userWriteService.UpdateUser(userId, user) };
|
||||
return Created("/Update", id);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Internal error while update user");
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||
[HttpDelete("Delete/{userId}")]
|
||||
public async Task<ActionResult<IdDto>> DeleteUser(string userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = new IdDto() { Id = await userWriteService.DeleteUser(userId) };
|
||||
return Created("/Delete", id);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Internal error while delete user");
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"RequestFailedStatusFormat": "Erreur lors de la réponse, code {0}",
|
||||
"ErrorFetchCategories": "Erreur lors de la récupération des catégories",
|
||||
"PlaceholderAdd": "Ajouter un nouveau",
|
||||
"ErrorCreateGame": "Erreur lors de la Création d'un jeu",
|
||||
"ErrorCreateGame": "Erreur lors de la création d'un jeu",
|
||||
"InvalidTitle": "Le titre est incorrect",
|
||||
"InvalidInterest": "L'interêt est incorrect",
|
||||
"Unknown": "Inconnu",
|
||||
@@ -49,5 +49,15 @@
|
||||
"InvalidToken": "Le token JWT est invalide",
|
||||
"UserUnauthorized": "Utilisateur non authorisé",
|
||||
"UserLoginFailed": "Authentification de l'utilisateur échoué",
|
||||
"UserLogoutFailed": "Déconnection de l'utilisateur échoué"
|
||||
"UserLogoutFailed": "Déconnection de l'utilisateur échoué",
|
||||
"Roles": "Rôles",
|
||||
"ErrorFetchUsers": "Erreur lors de la récupération des utilisateurs",
|
||||
"ErrorFetchRoles": "Erreur lors de la récupération des rôles",
|
||||
"MissingField": "Un champs est manquant",
|
||||
"ErrorCreateUser": "Erreur lors de la création d'un utilisateur",
|
||||
"ErrorUpdateUser": "Erreur lors de la mise à jour d'un utilisateur",
|
||||
"ErrorDeleteUser": "Erreur lors de la suppression d'un utilisateur",
|
||||
"Cancel": "Annuler",
|
||||
"Confirm": "Confirmer",
|
||||
"ConfirmDeleteDescription": "Êtes-vous sur de vouloir supprimer cet élément ?"
|
||||
}
|
||||
@@ -19,13 +19,13 @@ namespace GameIdeas.WebAPI.Migrations
|
||||
{
|
||||
GlobalConstants.ADMINISTRATOR_ID.ToString(),
|
||||
GlobalConstants.ADMINISTRATOR,
|
||||
GlobalConstants.ADMINISTRATOR.Normalize(),
|
||||
GlobalConstants.ADMINISTRATOR_NORMALIZED,
|
||||
Guid.NewGuid().ToString()
|
||||
},
|
||||
{
|
||||
GlobalConstants.MEMBER_ID.ToString(),
|
||||
GlobalConstants.MEMBER,
|
||||
GlobalConstants.MEMBER.Normalize(),
|
||||
GlobalConstants.MEMBER_NORMALIZED,
|
||||
Guid.NewGuid().ToString()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using AutoMapper;
|
||||
using GameIdeas.Shared.Dto;
|
||||
using GameIdeas.Shared.Model;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace GameIdeas.WebAPI.Profiles;
|
||||
|
||||
public class UserProfile : Profile
|
||||
{
|
||||
public UserProfile()
|
||||
{
|
||||
CreateMap<IdentityRole, RoleDto>()
|
||||
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
|
||||
.ForMember(d => d.Name, o => o.MapFrom(s => s.Name));
|
||||
|
||||
CreateMap<User, UserDto>()
|
||||
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
|
||||
.ForMember(d => d.Username, o => o.MapFrom(s => s.UserName));
|
||||
}
|
||||
}
|
||||
@@ -59,12 +59,24 @@ services.AddAuthentication(options =>
|
||||
};
|
||||
});
|
||||
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
// Default Password settings.
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Password.RequiredLength = 6;
|
||||
options.Password.RequiredUniqueChars = 1;
|
||||
});
|
||||
|
||||
services.AddAuthorization();
|
||||
|
||||
services.AddSingleton<TranslationService>();
|
||||
services.AddSingleton<Translations>();
|
||||
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
services.AddScoped<IUserReadService, UserReadService>();
|
||||
services.AddScoped<IUserWriteService, UserWriteService>();
|
||||
services.AddScoped<IGameReadService, GameReadService>();
|
||||
services.AddScoped<IGameWriteService, GameWriteService>();
|
||||
services.AddScoped<ICategoryService, CategoryService>();
|
||||
|
||||
@@ -72,7 +72,7 @@ public class GameReadService(GameIdeasContext context, IMapper mapper, ICategory
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFilter(ref IQueryable<Game> query, GameFilterDto filter)
|
||||
private static void ApplyFilter(ref IQueryable<Game> query, GameFilterDto filter)
|
||||
{
|
||||
if (filter.PlatformIds != null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using GameIdeas.Shared.Dto;
|
||||
|
||||
namespace GameIdeas.WebAPI.Services.Users;
|
||||
|
||||
public interface IUserReadService
|
||||
{
|
||||
Task<TokenDto> Login(UserDto user);
|
||||
Task<IEnumerable<RoleDto>> GetRoles();
|
||||
Task<UserListDto> GetUsers(UserFilterDto filter);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using GameIdeas.Shared.Dto;
|
||||
|
||||
namespace GameIdeas.WebAPI.Services.Users;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task<TokenDto> Login(UserDto user);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using GameIdeas.Shared.Dto;
|
||||
|
||||
namespace GameIdeas.WebAPI.Services.Users;
|
||||
|
||||
public interface IUserWriteService
|
||||
{
|
||||
Task<string> CreateUser(UserDto user);
|
||||
Task<string> UpdateUser(string userId, UserDto user);
|
||||
Task<string> DeleteUser(string userId);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using AutoMapper;
|
||||
using GameIdeas.Resources;
|
||||
using GameIdeas.Shared.Constants;
|
||||
using GameIdeas.Shared.Dto;
|
||||
using GameIdeas.Shared.Model;
|
||||
using GameIdeas.WebAPI.Context;
|
||||
using GameIdeas.WebAPI.Exceptions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace GameIdeas.WebAPI.Services.Users;
|
||||
|
||||
public class UserReadService(
|
||||
UserManager<User> userManager,
|
||||
GameIdeasContext context,
|
||||
IMapper mapper) : IUserReadService
|
||||
{
|
||||
public async Task<IEnumerable<RoleDto>> GetRoles()
|
||||
{
|
||||
var roles = await context.Roles
|
||||
.ToListAsync();
|
||||
|
||||
return mapper.Map<IEnumerable<RoleDto>>(roles);
|
||||
}
|
||||
|
||||
public async Task<UserListDto> GetUsers(UserFilterDto filter)
|
||||
{
|
||||
var users = await context.Users
|
||||
.OrderBy(u => u.UserName)
|
||||
.ToListAsync();
|
||||
|
||||
var count = users.Count;
|
||||
ApplyStaticFilter(ref users, filter);
|
||||
|
||||
var usersDto = mapper.Map<IEnumerable<UserDto>>(users);
|
||||
usersDto = await ApplyRoles(usersDto, filter);
|
||||
|
||||
return new UserListDto
|
||||
{
|
||||
Users = usersDto,
|
||||
UsersCount = count
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<UserDto>> ApplyRoles(IEnumerable<UserDto> users, UserFilterDto filter)
|
||||
{
|
||||
var userRolesQuery = context.UserRoles
|
||||
.Where(ur => users.Select(u => u.Id).Contains(ur.UserId))
|
||||
.AsQueryable();
|
||||
|
||||
var rolesQuery = context.Roles.AsQueryable();
|
||||
|
||||
if (filter.RoleIds != null)
|
||||
{
|
||||
userRolesQuery = userRolesQuery
|
||||
.Where(ur => filter.RoleIds.Contains(ur.RoleId));
|
||||
|
||||
rolesQuery = rolesQuery.Where(role => filter.RoleIds.Contains(role.Id));
|
||||
}
|
||||
|
||||
var roles = await rolesQuery.ToListAsync();
|
||||
var userRoles = await userRolesQuery.ToListAsync();
|
||||
users = users.Where(user => userRoles.Select(ur => ur.UserId).Contains(user.Id));
|
||||
|
||||
foreach (var user in users)
|
||||
{
|
||||
var currentRoleId = userRoles.FirstOrDefault(ur => ur.UserId == user.Id)?.RoleId;
|
||||
|
||||
if (currentRoleId == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentRole = roles.FirstOrDefault(r => r.Id == currentRoleId);
|
||||
user.Role = mapper.Map<RoleDto>(currentRole);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
private static void ApplyStaticFilter(ref List<User> users, UserFilterDto filter)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(filter.Name))
|
||||
{
|
||||
var keywords = filter.Name?
|
||||
.Split([' '], StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(k => k.Trim())
|
||||
.ToArray() ?? [];
|
||||
|
||||
users = users
|
||||
.Where(user => keywords.All(
|
||||
kw => user.UserName?.Contains(kw, StringComparison.OrdinalIgnoreCase) ?? true
|
||||
))
|
||||
.OrderBy(user => keywords.Min(kw =>
|
||||
user.UserName?.IndexOf(kw, StringComparison.OrdinalIgnoreCase)
|
||||
))
|
||||
.ThenBy(user => user.UserName?.Length)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TokenDto> Login(UserDto userDto)
|
||||
{
|
||||
if (userDto.Username == null || userDto.Password == null)
|
||||
throw new UserInvalidException(ResourcesKey.UserArgumentsNull);
|
||||
|
||||
var user = await userManager.FindByNameAsync(userDto.Username);
|
||||
|
||||
if (user == null || !await userManager.CheckPasswordAsync(user, userDto.Password))
|
||||
{
|
||||
throw new UserUnauthorizedException(ResourcesKey.UserUnauthorized);
|
||||
}
|
||||
|
||||
List<Claim> authClaims =
|
||||
[
|
||||
new Claim(ClaimTypes.Name, user.UserName ?? string.Empty),
|
||||
new Claim(ClaimTypes.Sid, user.Id),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
];
|
||||
|
||||
authClaims.AddRange((await userManager.GetRolesAsync(user))
|
||||
.Select(r => new Claim(ClaimTypes.Role, r)));
|
||||
|
||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY")
|
||||
?? throw new ArgumentNullException(message: ResourcesKey.InvalidToken, null);
|
||||
|
||||
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: Environment.GetEnvironmentVariable("JWT_ISSUER"),
|
||||
audience: Environment.GetEnvironmentVariable("JWT_AUDIENCE"),
|
||||
expires: DateTime.Now.AddHours(GlobalConstants.JWT_DURATION_HOUR),
|
||||
claims: authClaims,
|
||||
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
|
||||
);
|
||||
|
||||
return new TokenDto
|
||||
{
|
||||
Token = new JwtSecurityTokenHandler().WriteToken(token),
|
||||
Expiration = token.ValidTo
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
using GameIdeas.Resources;
|
||||
using GameIdeas.Shared.Constants;
|
||||
using GameIdeas.Shared.Dto;
|
||||
using GameIdeas.Shared.Model;
|
||||
using GameIdeas.WebAPI.Exceptions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace GameIdeas.WebAPI.Services.Users;
|
||||
|
||||
public class UserService(UserManager<User> userManager) : IUserService
|
||||
{
|
||||
public async Task<TokenDto> Login(UserDto userDto)
|
||||
{
|
||||
if (userDto.Username == null || userDto.Password == null)
|
||||
throw new UserInvalidException(ResourcesKey.UserArgumentsNull);
|
||||
|
||||
var user = await userManager.FindByNameAsync(userDto.Username);
|
||||
|
||||
if (user == null || !await userManager.CheckPasswordAsync(user, userDto.Password))
|
||||
{
|
||||
throw new UserUnauthorizedException(ResourcesKey.UserUnauthorized);
|
||||
}
|
||||
|
||||
List<Claim> authClaims =
|
||||
[
|
||||
new Claim(ClaimTypes.Name, user.UserName ?? string.Empty),
|
||||
new Claim(ClaimTypes.Sid, user.Id),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
];
|
||||
|
||||
authClaims.AddRange((await userManager.GetRolesAsync(user))
|
||||
.Select(r => new Claim(ClaimTypes.Role, r)));
|
||||
|
||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY")
|
||||
?? throw new ArgumentNullException(message: ResourcesKey.InvalidToken, null);
|
||||
|
||||
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: Environment.GetEnvironmentVariable("JWT_ISSUER"),
|
||||
audience: Environment.GetEnvironmentVariable("JWT_AUDIENCE"),
|
||||
expires: DateTime.Now.AddHours(GlobalConstants.JWT_DURATION_HOUR),
|
||||
claims: authClaims,
|
||||
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
|
||||
);
|
||||
|
||||
return new TokenDto
|
||||
{
|
||||
Token = new JwtSecurityTokenHandler().WriteToken(token),
|
||||
Expiration = token.ValidTo
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using AutoMapper;
|
||||
using GameIdeas.Resources;
|
||||
using GameIdeas.Shared.Dto;
|
||||
using GameIdeas.Shared.Model;
|
||||
using GameIdeas.WebAPI.Exceptions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace GameIdeas.WebAPI.Services.Users;
|
||||
|
||||
public class UserWriteService(
|
||||
UserManager<User> userManager) : IUserWriteService
|
||||
{
|
||||
public async Task<string> CreateUser(UserDto user)
|
||||
{
|
||||
if (user.Username == null ||
|
||||
user.Password == null ||
|
||||
user.Role == null)
|
||||
{
|
||||
throw new UserInvalidException(ResourcesKey.MissingField);
|
||||
}
|
||||
|
||||
User userToCreate = new() { UserName = user.Username };
|
||||
|
||||
var result = await userManager.CreateAsync(userToCreate, user.Password);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await userManager.AddToRoleAsync(userToCreate, user.Role.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UserInvalidException(string.Join("; ", result.Errors.Select(e => $"{e.Code} {e.Description}")));
|
||||
}
|
||||
|
||||
return userToCreate.Id;
|
||||
}
|
||||
|
||||
public async Task<string> DeleteUser(string userId)
|
||||
{
|
||||
if (userId == null)
|
||||
{
|
||||
throw new ArgumentNullException(ResourcesKey.MissingField);
|
||||
}
|
||||
|
||||
var user = await userManager.FindByIdAsync(userId)
|
||||
?? throw new UserInvalidException("User not found");
|
||||
|
||||
await userManager.DeleteAsync(user);
|
||||
return userId;
|
||||
}
|
||||
|
||||
public async Task<string> UpdateUser(string userId, UserDto user)
|
||||
{
|
||||
if (userId == null)
|
||||
{
|
||||
throw new ArgumentNullException(ResourcesKey.MissingField);
|
||||
}
|
||||
|
||||
var userToUpdate = await userManager.FindByIdAsync(userId)
|
||||
?? throw new UserInvalidException("User not found");
|
||||
|
||||
if (user.Username != null)
|
||||
{
|
||||
userToUpdate.UserName = user.Username;
|
||||
await userManager.UpdateAsync(userToUpdate);
|
||||
}
|
||||
|
||||
if (user.Password != null)
|
||||
{
|
||||
await userManager.RemovePasswordAsync(userToUpdate);
|
||||
await userManager.AddPasswordAsync(userToUpdate, user.Password);
|
||||
}
|
||||
|
||||
if (user.Role != null)
|
||||
{
|
||||
var roles = await userManager.GetRolesAsync(userToUpdate);
|
||||
await userManager.RemoveFromRolesAsync(userToUpdate, roles);
|
||||
await userManager.AddToRoleAsync(userToUpdate, user.Role.Name);
|
||||
}
|
||||
|
||||
return userToUpdate.Id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user