ref: project structure
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record CreateRoleCommand(SaveRoleRequest Request) : IRequest<RoleDto>;
|
||||
|
||||
public class CreateRoleHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateRoleCommand, RoleDto>
|
||||
{
|
||||
public async Task<RoleDto> Handle(CreateRoleCommand command, CancellationToken ct)
|
||||
{
|
||||
var name = command.Request.Name.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new BadRequestException("Role name is required");
|
||||
}
|
||||
|
||||
if (await uow.Roles.ExistsByNameAsync(name, null, ct))
|
||||
{
|
||||
throw new BadRequestException("Role name already exists");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var role = new Role
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
IsSystem = false,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
Permissions = RolePermissionBuilder.Build(command.Request.Permissions),
|
||||
};
|
||||
|
||||
uow.Roles.Add(role);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<RoleDto>(role);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record DeleteRoleCommand(Guid Id) : IRequest;
|
||||
|
||||
public class DeleteRoleHandler(IUnitOfWork uow) : IRequestHandler<DeleteRoleCommand>
|
||||
{
|
||||
public async Task Handle(DeleteRoleCommand command, CancellationToken ct)
|
||||
{
|
||||
var role = await uow.Roles.GetByIdAsync(command.Id, ct)
|
||||
?? throw new NotFoundException("Role not found");
|
||||
|
||||
if (role.IsSystem)
|
||||
{
|
||||
throw new BadRequestException("Cannot delete a system role");
|
||||
}
|
||||
|
||||
if (await uow.Users.ExistsByRoleIdAsync(command.Id, ct))
|
||||
{
|
||||
throw new BadRequestException("Cannot delete a role that is assigned to accounts");
|
||||
}
|
||||
|
||||
uow.Roles.Remove(role);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record UpdateRoleCommand(Guid Id, SaveRoleRequest Request) : IRequest<RoleDto>;
|
||||
|
||||
public class UpdateRoleHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateRoleCommand, RoleDto>
|
||||
{
|
||||
public async Task<RoleDto> Handle(UpdateRoleCommand command, CancellationToken ct)
|
||||
{
|
||||
var role = await uow.Roles.GetByIdWithPermissionsAsync(command.Id, ct)
|
||||
?? throw new NotFoundException("Role not found");
|
||||
|
||||
var name = command.Request.Name.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new BadRequestException("Role name is required");
|
||||
}
|
||||
|
||||
if (await uow.Roles.ExistsByNameAsync(name, command.Id, ct))
|
||||
{
|
||||
throw new BadRequestException("Role name already exists");
|
||||
}
|
||||
|
||||
role.Name = name;
|
||||
role.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
uow.Roles.RemovePermissions(role.Permissions.ToList());
|
||||
role.Permissions = RolePermissionBuilder.Build(command.Request.Permissions);
|
||||
foreach (var p in role.Permissions)
|
||||
{
|
||||
p.RoleId = role.Id;
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<RoleDto>(role);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public enum PermissionAction
|
||||
{
|
||||
View,
|
||||
Create,
|
||||
Edit,
|
||||
Delete,
|
||||
}
|
||||
|
||||
public class MenuItemDto
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public bool CanView { get; set; }
|
||||
public bool CanCreate { get; set; }
|
||||
public bool CanEdit { get; set; }
|
||||
public bool CanDelete { get; set; }
|
||||
}
|
||||
|
||||
public class PermissionEntryDto
|
||||
{
|
||||
public string Screen { get; set; } = string.Empty;
|
||||
public bool CanView { get; set; }
|
||||
public bool CanCreate { get; set; }
|
||||
public bool CanEdit { get; set; }
|
||||
public bool CanDelete { get; set; }
|
||||
}
|
||||
|
||||
public class RoleDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsSystem { get; set; }
|
||||
public List<PermissionEntryDto> Permissions { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SaveRoleRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<PermissionEntryDto> Permissions { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public interface IPermissionService
|
||||
{
|
||||
Task<List<MenuItemDto>> GetMenuAsync(Guid userId, CancellationToken ct = default);
|
||||
Task EnsureAsync(Guid userId, string screen, PermissionAction action, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public class PermissionService(IUnitOfWork uow) : IPermissionService
|
||||
{
|
||||
public async Task<List<MenuItemDto>> GetMenuAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var roleIds = await uow.Users.GetRoleIdsAsync(userId, ct);
|
||||
var permissions = await uow.Roles.GetPermissionsForRolesAsync(roleIds, ct);
|
||||
|
||||
var merged = permissions.GroupBy(p => p.Screen).ToDictionary(
|
||||
g => g.Key,
|
||||
g => new
|
||||
{
|
||||
CanView = g.Any(p => p.CanView),
|
||||
CanCreate = g.Any(p => p.CanCreate),
|
||||
CanEdit = g.Any(p => p.CanEdit),
|
||||
CanDelete = g.Any(p => p.CanDelete),
|
||||
}
|
||||
);
|
||||
|
||||
return ScreenCatalog.Screens.Select(s =>
|
||||
{
|
||||
merged.TryGetValue(s.Key, out var p);
|
||||
return new MenuItemDto
|
||||
{
|
||||
Key = s.Key,
|
||||
Label = s.Label,
|
||||
Path = s.Path,
|
||||
CanView = p?.CanView ?? false,
|
||||
CanCreate = p?.CanCreate ?? false,
|
||||
CanEdit = p?.CanEdit ?? false,
|
||||
CanDelete = p?.CanDelete ?? false,
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task EnsureAsync(Guid userId, string screen, PermissionAction action, CancellationToken ct = default)
|
||||
{
|
||||
var roleIds = await uow.Users.GetRoleIdsAsync(userId, ct);
|
||||
var permissions = await uow.Roles.GetPermissionsForRolesAsync(roleIds, ct);
|
||||
var screenPermissions = permissions.Where(p => p.Screen == screen).ToList();
|
||||
|
||||
var allowed = action switch
|
||||
{
|
||||
PermissionAction.View => screenPermissions.Any(p => p.CanView),
|
||||
PermissionAction.Create => screenPermissions.Any(p => p.CanCreate),
|
||||
PermissionAction.Edit => screenPermissions.Any(p => p.CanEdit),
|
||||
PermissionAction.Delete => screenPermissions.Any(p => p.CanDelete),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (!allowed)
|
||||
{
|
||||
throw new ForbiddenException($"Not permitted to {action} on {screen}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record GetRoleQuery(Guid Id) : IRequest<RoleDto>;
|
||||
|
||||
public class GetRoleHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<GetRoleQuery, RoleDto>
|
||||
{
|
||||
public async Task<RoleDto> Handle(GetRoleQuery query, CancellationToken ct)
|
||||
{
|
||||
var role = await uow.Roles.GetByIdWithPermissionsAsync(query.Id, ct)
|
||||
?? throw new NotFoundException("Role not found");
|
||||
return mapper.Map<RoleDto>(role);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record GetRolesQuery(int Page, int PageSize) : IRequest<PagedResult<RoleDto>>;
|
||||
|
||||
public class GetRolesHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<GetRolesQuery, PagedResult<RoleDto>>
|
||||
{
|
||||
public async Task<PagedResult<RoleDto>> Handle(GetRolesQuery query, CancellationToken ct)
|
||||
{
|
||||
var roles = await uow.Roles.GetAllWithPermissionsAsync(query.Page, query.PageSize, ct);
|
||||
return new PagedResult<RoleDto>
|
||||
{
|
||||
Items = mapper.Map<List<RoleDto>>(roles.Items),
|
||||
TotalCount = roles.TotalCount,
|
||||
Page = roles.Page,
|
||||
PageSize = roles.PageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
internal static class RolePermissionBuilder
|
||||
{
|
||||
public static List<RolePermission> Build(List<PermissionEntryDto> entries)
|
||||
{
|
||||
var byScreen = entries.Where(e => ScreenCatalog.Keys.Contains(e.Screen)).ToDictionary(e => e.Screen);
|
||||
|
||||
return ScreenCatalog.Keys.Select(key =>
|
||||
{
|
||||
byScreen.TryGetValue(key, out var entry);
|
||||
return new RolePermission
|
||||
{
|
||||
Screen = key,
|
||||
CanView = entry?.CanView ?? false,
|
||||
CanCreate = entry?.CanCreate ?? false,
|
||||
CanEdit = entry?.CanEdit ?? false,
|
||||
CanDelete = entry?.CanDelete ?? false,
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record ScreenDefinition(string Key, string Label, string Path);
|
||||
|
||||
public static class ScreenCatalog
|
||||
{
|
||||
public static readonly IReadOnlyList<ScreenDefinition> Screens =
|
||||
[
|
||||
new("dashboard", "Dashboard", "/"),
|
||||
new("projects", "Projects", "/projects"),
|
||||
new("tasks", "Tasks", "/tasks"),
|
||||
new("documents", "Documents", "/documents"),
|
||||
new("users", "Users", "/users"),
|
||||
new("permissions", "Permissions", "/settings/permission"),
|
||||
new("masterdata", "Master Data", "/settings/masterdata"),
|
||||
];
|
||||
|
||||
public static readonly IReadOnlySet<string> Keys = Screens.Select(s => s.Key).ToHashSet();
|
||||
}
|
||||
Reference in New Issue
Block a user