From be51351f742f5f3eb1cdc60ae85c6ffb36c07615 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 18 Aug 2026 15:43:55 +0530 Subject: [PATCH] Add user and role management features --- Distribution/Features/Role/IRoleRepository.cs | 8 ++ Distribution/Features/Role/RoleController.cs | 28 ++++ Distribution/Features/Role/RoleDTOs.cs | 5 + Distribution/Features/Role/RoleRepository.cs | 19 +++ Distribution/Features/Role/RoleService.cs | 26 ++++ Distribution/Features/User/IUserRepository.cs | 18 +++ Distribution/Features/User/UserController.cs | 63 +++++++++ Distribution/Features/User/UserDTOs.cs | 28 ++++ Distribution/Features/User/UserRepository.cs | 61 ++++++++ Distribution/Features/User/UserService.cs | 130 ++++++++++++++++++ 10 files changed, 386 insertions(+) create mode 100644 Distribution/Features/Role/IRoleRepository.cs create mode 100644 Distribution/Features/Role/RoleController.cs create mode 100644 Distribution/Features/Role/RoleDTOs.cs create mode 100644 Distribution/Features/Role/RoleRepository.cs create mode 100644 Distribution/Features/Role/RoleService.cs create mode 100644 Distribution/Features/User/IUserRepository.cs create mode 100644 Distribution/Features/User/UserController.cs create mode 100644 Distribution/Features/User/UserDTOs.cs create mode 100644 Distribution/Features/User/UserRepository.cs create mode 100644 Distribution/Features/User/UserService.cs diff --git a/Distribution/Features/Role/IRoleRepository.cs b/Distribution/Features/Role/IRoleRepository.cs new file mode 100644 index 0000000..adcdbcf --- /dev/null +++ b/Distribution/Features/Role/IRoleRepository.cs @@ -0,0 +1,8 @@ +namespace Distribution.Features.Role; + +public interface IRoleRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(); + Task> GetPermissionsAsync(Guid roleId); +} diff --git a/Distribution/Features/Role/RoleController.cs b/Distribution/Features/Role/RoleController.cs new file mode 100644 index 0000000..ce5f7e1 --- /dev/null +++ b/Distribution/Features/Role/RoleController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Distribution.Features.Role; + +[ApiController] +[Route("api/roles")] +[Authorize] +public class RoleController(RoleService service) : ControllerBase +{ + [HttpGet] + public async Task GetAll() => + Ok(await service.GetAllAsync()); + + [HttpGet("{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpGet("{id:guid}/permissions")] + public async Task GetPermissions(Guid id) + { + var result = await service.GetPermissionsAsync(id); + return result is null ? NotFound() : Ok(result); + } +} diff --git a/Distribution/Features/Role/RoleDTOs.cs b/Distribution/Features/Role/RoleDTOs.cs new file mode 100644 index 0000000..6a00443 --- /dev/null +++ b/Distribution/Features/Role/RoleDTOs.cs @@ -0,0 +1,5 @@ +namespace Distribution.Features.Role; + +public record RoleResponse(Guid Id, string Name, int Level, bool IsActive, DateTime CreatedAt); + +public record RolePermissionResponse(Guid PermissionId, string Code, string Description); diff --git a/Distribution/Features/Role/RoleRepository.cs b/Distribution/Features/Role/RoleRepository.cs new file mode 100644 index 0000000..45a0907 --- /dev/null +++ b/Distribution/Features/Role/RoleRepository.cs @@ -0,0 +1,19 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Role; + +public class RoleRepository(AppDBContext context) : IRoleRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.Roles.FindAsync(id); + + public async Task> GetAllAsync() => + await context.Roles.ToListAsync(); + + public async Task> GetPermissionsAsync(Guid roleId) => + await context.RolePermissions + .Include(rp => rp.Permission) + .Where(rp => rp.FK_RoleId == roleId && rp.IsActive) + .ToListAsync(); +} diff --git a/Distribution/Features/Role/RoleService.cs b/Distribution/Features/Role/RoleService.cs new file mode 100644 index 0000000..ca25972 --- /dev/null +++ b/Distribution/Features/Role/RoleService.cs @@ -0,0 +1,26 @@ +namespace Distribution.Features.Role; + +public class RoleService(IRoleRepository repo) +{ + public async Task> GetAllAsync() => + (await repo.GetAllAsync()) + .Where(r => r.IsActive) + .Select(r => new RoleResponse(r.Id, r.Name, r.Level, r.IsActive, r.CreatedAt)) + .ToList(); + + public async Task GetByIdAsync(Guid id) + { + var role = await repo.GetByIdAsync(id); + if (role is null || !role.IsActive) return null; + return new RoleResponse(role.Id, role.Name, role.Level, role.IsActive, role.CreatedAt); + } + + public async Task?> GetPermissionsAsync(Guid roleId) + { + var role = await repo.GetByIdAsync(roleId); + if (role is null || !role.IsActive) return null; + return (await repo.GetPermissionsAsync(roleId)) + .Select(rp => new RolePermissionResponse(rp.FK_PermissionId, rp.Permission.Code, rp.Permission.Description)) + .ToList(); + } +} diff --git a/Distribution/Features/User/IUserRepository.cs b/Distribution/Features/User/IUserRepository.cs new file mode 100644 index 0000000..e8b4f3b --- /dev/null +++ b/Distribution/Features/User/IUserRepository.cs @@ -0,0 +1,18 @@ +namespace Distribution.Features.User; + +public interface IUserRepository +{ + Task GetByIdAsync(Guid id); + Task GetByUsernameAsync(string username); + Task> GetAllAsync(); + Task AddAsync(Models.User entity); + void Update(Models.User entity); + void Remove(Models.User entity); + + Task> GetRegionScopesAsync(Guid userId); + Task> GetRouteScopesAsync(Guid userId); + Task> GetStoreScopesAsync(Guid userId); + Task ReplaceRegionScopesAsync(Guid userId, IEnumerable regionIds); + Task ReplaceRouteScopesAsync(Guid userId, IEnumerable routeIds); + Task ReplaceStoreScopesAsync(Guid userId, IEnumerable storeIds); +} diff --git a/Distribution/Features/User/UserController.cs b/Distribution/Features/User/UserController.cs new file mode 100644 index 0000000..f035f11 --- /dev/null +++ b/Distribution/Features/User/UserController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Distribution.Features.User; + +[ApiController] +[Route("api/users")] +[Authorize] +public class UserController(UserService service) : ControllerBase +{ + [HttpGet] + [Authorize(Policy = "users.view")] + public async Task GetAll() => + Ok(await service.GetAllAsync()); + + [HttpGet("{id:guid}")] + [Authorize(Policy = "users.view")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpPost] + [Authorize(Policy = "users.create")] + public async Task Create(CreateUserRequest request, CancellationToken ct) + { + var result = await service.CreateAsync(request, ct); + return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); + } + + [HttpPut("{id:guid}")] + [Authorize(Policy = "users.update")] + public async Task Update(Guid id, UpdateUserRequest request, CancellationToken ct) + { + var result = await service.UpdateAsync(id, request, ct); + return result is null ? NotFound() : Ok(result); + } + + [HttpPut("{id:guid}/role")] + [Authorize(Policy = "users.assign_role")] + public async Task AssignRole(Guid id, AssignUserRoleRequest request, CancellationToken ct) + { + var result = await service.AssignRoleAsync(id, request, ct); + return result is null ? NotFound() : Ok(result); + } + + [HttpPut("{id:guid}/scope")] + [Authorize(Policy = "users.assign_scope")] + public async Task AssignScope(Guid id, AssignUserScopeRequest request, CancellationToken ct) + { + var updated = await service.AssignScopeAsync(id, request, ct); + return updated ? NoContent() : NotFound(); + } + + [HttpDelete("{id:guid}")] + [Authorize(Policy = "users.deactivate")] + public async Task Deactivate(Guid id, CancellationToken ct) + { + var deactivated = await service.DeactivateAsync(id, ct); + return deactivated ? NoContent() : NotFound(); + } +} diff --git a/Distribution/Features/User/UserDTOs.cs b/Distribution/Features/User/UserDTOs.cs new file mode 100644 index 0000000..185d43d --- /dev/null +++ b/Distribution/Features/User/UserDTOs.cs @@ -0,0 +1,28 @@ +namespace Distribution.Features.User; + +public record CreateUserRequest( + string Username, + string Password, + string FullName, + Guid RoleId, + Guid? ReportsToUserId); + +public record UpdateUserRequest(string FullName); + +public record AssignUserRoleRequest(Guid RoleId); + +public record AssignUserScopeRequest( + List RegionIds, + List RouteIds, + List StoreIds); + +public record UserResponse( + Guid Id, + string Username, + string FullName, + Guid RoleId, + string RoleName, + Guid? ReportsToUserId, + bool IsActive, + DateTime CreatedAt, + DateTime? UpdatedAt); diff --git a/Distribution/Features/User/UserRepository.cs b/Distribution/Features/User/UserRepository.cs new file mode 100644 index 0000000..26e22d2 --- /dev/null +++ b/Distribution/Features/User/UserRepository.cs @@ -0,0 +1,61 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.User; + +public class UserRepository(AppDBContext context) : IUserRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.Users.Include(u => u.Role).FirstOrDefaultAsync(u => u.Id == id); + + public async Task GetByUsernameAsync(string username) => + await context.Users.Include(u => u.Role).FirstOrDefaultAsync(u => u.Username == username); + + public async Task> GetAllAsync() => + await context.Users.Include(u => u.Role).ToListAsync(); + + public async Task AddAsync(Models.User entity) => + await context.Users.AddAsync(entity); + + public void Update(Models.User entity) => + context.Users.Update(entity); + + public void Remove(Models.User entity) + { + entity.IsActive = false; + context.Users.Update(entity); + } + + public async Task> GetRegionScopesAsync(Guid userId) => + await context.UserRegionScopes.Where(s => s.FK_UserId == userId && s.IsActive).ToListAsync(); + + public async Task> GetRouteScopesAsync(Guid userId) => + await context.UserRouteScopes.Where(s => s.FK_UserId == userId && s.IsActive).ToListAsync(); + + public async Task> GetStoreScopesAsync(Guid userId) => + await context.UserStoreScopes.Where(s => s.FK_UserId == userId && s.IsActive).ToListAsync(); + + public async Task ReplaceRegionScopesAsync(Guid userId, IEnumerable regionIds) + { + var existing = await context.UserRegionScopes.Where(s => s.FK_UserId == userId).ToListAsync(); + context.UserRegionScopes.RemoveRange(existing); + await context.UserRegionScopes.AddRangeAsync( + regionIds.Select(id => new Models.UserRegionScope { FK_UserId = userId, FK_RegionId = id })); + } + + public async Task ReplaceRouteScopesAsync(Guid userId, IEnumerable routeIds) + { + var existing = await context.UserRouteScopes.Where(s => s.FK_UserId == userId).ToListAsync(); + context.UserRouteScopes.RemoveRange(existing); + await context.UserRouteScopes.AddRangeAsync( + routeIds.Select(id => new Models.UserRouteScope { FK_UserId = userId, FK_RouteId = id })); + } + + public async Task ReplaceStoreScopesAsync(Guid userId, IEnumerable storeIds) + { + var existing = await context.UserStoreScopes.Where(s => s.FK_UserId == userId).ToListAsync(); + context.UserStoreScopes.RemoveRange(existing); + await context.UserStoreScopes.AddRangeAsync( + storeIds.Select(id => new Models.UserStoreScope { FK_UserId = userId, FK_StoreId = id })); + } +} diff --git a/Distribution/Features/User/UserService.cs b/Distribution/Features/User/UserService.cs new file mode 100644 index 0000000..1bce64e --- /dev/null +++ b/Distribution/Features/User/UserService.cs @@ -0,0 +1,130 @@ +using Distribution.Infra.UoW; +using Microsoft.AspNetCore.Identity; + +namespace Distribution.Features.User; + +public class UserService(IUserRepository repo, IUnitOfWork uow) +{ + private readonly PasswordHasher _hasher = new(); + + private static UserResponse ToResponse(Models.User u) => + new(u.Id, u.Username, u.FullName, u.FK_RoleId, u.Role?.Name ?? string.Empty, + u.FK_ReportsToUserId, u.IsActive, u.CreatedAt, u.UpdatedAt); + + public async Task> GetAllAsync() => + (await repo.GetAllAsync()) + .Where(u => u.IsActive) + .Select(ToResponse) + .ToList(); + + public async Task GetByIdAsync(Guid id) + { + var user = await repo.GetByIdAsync(id); + if (user is null || !user.IsActive) return null; + return ToResponse(user); + } + + public async Task CreateAsync(CreateUserRequest request, CancellationToken ct = default) + { + var user = new Models.User + { + Username = request.Username, + FullName = request.FullName, + FK_RoleId = request.RoleId, + FK_ReportsToUserId = request.ReportsToUserId + }; + user.PasswordHash = _hasher.HashPassword(user, request.Password); + + await uow.BeginAsync(ct); + try + { + await repo.AddAsync(user); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return ToResponse(user); + } + + public async Task UpdateAsync(Guid id, UpdateUserRequest request, CancellationToken ct = default) + { + var user = await repo.GetByIdAsync(id); + if (user is null || !user.IsActive) return null; + user.FullName = request.FullName; + user.UpdatedAt = DateTime.Now; + await uow.BeginAsync(ct); + try + { + repo.Update(user); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return ToResponse(user); + } + + public async Task AssignRoleAsync(Guid id, AssignUserRoleRequest request, CancellationToken ct = default) + { + var user = await repo.GetByIdAsync(id); + if (user is null || !user.IsActive) return null; + user.FK_RoleId = request.RoleId; + user.UpdatedAt = DateTime.Now; + await uow.BeginAsync(ct); + try + { + repo.Update(user); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return ToResponse(user); + } + + public async Task AssignScopeAsync(Guid id, AssignUserScopeRequest request, CancellationToken ct = default) + { + var user = await repo.GetByIdAsync(id); + if (user is null || !user.IsActive) return false; + + await uow.BeginAsync(ct); + try + { + await repo.ReplaceRegionScopesAsync(id, request.RegionIds); + await repo.ReplaceRouteScopesAsync(id, request.RouteIds); + await repo.ReplaceStoreScopesAsync(id, request.StoreIds); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return true; + } + + public async Task DeactivateAsync(Guid id, CancellationToken ct = default) + { + var user = await repo.GetByIdAsync(id); + if (user is null || !user.IsActive) return false; + await uow.BeginAsync(ct); + try + { + repo.Remove(user); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return true; + } +}