fix: change permission
This commit is contained in:
@@ -54,7 +54,7 @@ Two independent mechanisms — don't cross-wire them:
|
|||||||
- PostgreSQL via `Npgsql.EntityFrameworkCore.PostgreSQL` 10. Connection string key is `ConnectionStrings:Default` (see [Mws.Api/appsettings.json](Mws.Api/appsettings.json)); fallback to `localhost:5432` dev creds `mws/mws`. Override via env if you must.
|
- PostgreSQL via `Npgsql.EntityFrameworkCore.PostgreSQL` 10. Connection string key is `ConnectionStrings:Default` (see [Mws.Api/appsettings.json](Mws.Api/appsettings.json)); fallback to `localhost:5432` dev creds `mws/mws`. Override via env if you must.
|
||||||
- Enum columns are stored as `varchar(20)` (not ints) — preserve that for any new enum.
|
- Enum columns are stored as `varchar(20)` (not ints) — preserve that for any new enum.
|
||||||
- Self-referencing `Document.ParentId` uses `Restrict` to block accidental cycles. Cascade delete is implemented in `DeleteDocumentHandler` via a manual descendant walk (`DocumentAccess.DeleteDescendantsAsync`); do not switch the FK to cascade.
|
- Self-referencing `Document.ParentId` uses `Restrict` to block accidental cycles. Cascade delete is implemented in `DeleteDocumentHandler` via a manual descendant walk (`DocumentAccess.DeleteDescendantsAsync`); do not switch the FK to cascade.
|
||||||
- `DbSeeder` ([Mws.Infrastructure/Persistence/DbSeeder.cs](Mws.Infrastructure/Persistence/DbSeeder.cs)) seeds two system roles (`Admin` — full CRUD on every screen; `Member` — CRUD on everything except Users/Permissions/Master Data, no delete) plus admin/alice/bob (password: `password`, admin → Admin role, alice/bob → Member) and one demo project on first boot. Skips if any users exist. System roles (`Role.IsSystem = true`) can't be deleted via `DeleteRoleCommand`.
|
- `DbSeeder` ([Mws.Infrastructure/Persistence/DbSeeder.cs](Mws.Infrastructure/Persistence/DbSeeder.cs)) seeds two system roles (`Admin` — full CRUD on every screen; `Member` — view-only on Projects, CRUD on Tasks/Documents (no delete), and no access to Users/Permissions/Master Data) plus admin/alice/bob (password: `password`, admin → Admin role, alice/bob → Member) and one demo project on first boot. System roles are re-synced to this canonical permission matrix on every startup (`SyncSystemRoleAsync`), so edits made to them through the Roles UI do not persist across restarts. Skips user/project seeding if any users exist. System roles (`Role.IsSystem = true`) can't be deleted via `DeleteRoleCommand`.
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.application.Permissions;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.domain.Projects;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
|
|
||||||
public record ArchiveProjectCommand(Guid UserId, Guid ProjectId) : IRequest;
|
public record ArchiveProjectCommand(Guid UserId, Guid ProjectId) : IRequest;
|
||||||
|
|
||||||
public class ArchiveProjectHandler(IUnitOfWork uow) : IRequestHandler<ArchiveProjectCommand>
|
public class ArchiveProjectHandler(IUnitOfWork uow, IPermissionService permissions) : IRequestHandler<ArchiveProjectCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(ArchiveProjectCommand command, CancellationToken ct)
|
public async Task Handle(ArchiveProjectCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var project = await ProjectAccess.GetForUserOrThrowAsync(uow, command.UserId, command.ProjectId, ct);
|
await permissions.EnsureAsync(command.UserId, "projects", PermissionAction.Delete, ct);
|
||||||
|
|
||||||
if (await uow.Projects.GetMemberRoleAsync(command.ProjectId, command.UserId, ct) != MemberRole.Owner)
|
var project = await ProjectAccess.GetForUserOrThrowAsync(uow, command.UserId, command.ProjectId, ct);
|
||||||
{
|
|
||||||
throw new ForbiddenException("Only the project owner can archive the project");
|
|
||||||
}
|
|
||||||
|
|
||||||
project.Status = ProjectStatus.Archived;
|
project.Status = ProjectStatus.Archived;
|
||||||
project.UpdatedAt = DateTime.UtcNow;
|
project.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.application.Permissions;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.domain.Projects;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
|
|
||||||
public record CreateProjectCommand(Guid UserId, CreateProjectRequest Request) : IRequest<ProjectDto>;
|
public record CreateProjectCommand(Guid UserId, CreateProjectRequest Request) : IRequest<ProjectDto>;
|
||||||
|
|
||||||
public class CreateProjectHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateProjectCommand, ProjectDto>
|
public class CreateProjectHandler(IUnitOfWork uow, IMapper mapper, IPermissionService permissions) : IRequestHandler<CreateProjectCommand, ProjectDto>
|
||||||
{
|
{
|
||||||
public async Task<ProjectDto> Handle(CreateProjectCommand command, CancellationToken ct)
|
public async Task<ProjectDto> Handle(CreateProjectCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
await permissions.EnsureAsync(command.UserId, "projects", PermissionAction.Create, ct);
|
||||||
|
|
||||||
var request = command.Request;
|
var request = command.Request;
|
||||||
if (string.IsNullOrWhiteSpace(request.Name))
|
if (string.IsNullOrWhiteSpace(request.Name))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
|
|
||||||
public record UpdateProjectCommand(Guid UserId, Guid ProjectId, UpdateProjectRequest Request) : IRequest<ProjectDto>;
|
public record UpdateProjectCommand(Guid UserId, Guid ProjectId, UpdateProjectRequest Request) : IRequest<ProjectDto>;
|
||||||
|
|
||||||
public class UpdateProjectHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateProjectCommand, ProjectDto>
|
public class UpdateProjectHandler(IUnitOfWork uow, IMapper mapper, IPermissionService permissions) : IRequestHandler<UpdateProjectCommand, ProjectDto>
|
||||||
{
|
{
|
||||||
public async Task<ProjectDto> Handle(UpdateProjectCommand command, CancellationToken ct)
|
public async Task<ProjectDto> Handle(UpdateProjectCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var project = await ProjectAccess.GetForUserOrThrowAsync(uow, command.UserId, command.ProjectId, ct);
|
await permissions.EnsureAsync(command.UserId, "projects", PermissionAction.Edit, ct);
|
||||||
|
|
||||||
if (MemberRole.Owner != await uow.Projects.GetMemberRoleAsync(command.ProjectId, command.UserId, ct))
|
var project = await ProjectAccess.GetForUserOrThrowAsync(uow, command.UserId, command.ProjectId, ct);
|
||||||
{
|
|
||||||
throw new ForbiddenException("Only the project owner can update the project");
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = command.Request;
|
var request = command.Request;
|
||||||
if (string.IsNullOrWhiteSpace(request.Name))
|
if (string.IsNullOrWhiteSpace(request.Name))
|
||||||
|
|||||||
@@ -134,29 +134,58 @@ public static class DbSeeder
|
|||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
if (!await db.Roles.AnyAsync(r => r.Name == "Admin"))
|
await SyncSystemRoleAsync(
|
||||||
|
db,
|
||||||
|
"Admin",
|
||||||
|
_ => (View: true, Create: true, Edit: true, Delete: true),
|
||||||
|
now);
|
||||||
|
|
||||||
|
await SyncSystemRoleAsync(
|
||||||
|
db,
|
||||||
|
"Member",
|
||||||
|
screen => screen.Key switch
|
||||||
{
|
{
|
||||||
var adminRole = new Role { Id = Guid.NewGuid(), Name = "Admin", IsSystem = true, CreatedAt = now, UpdatedAt = now };
|
"users" or "permissions" or "masterdata" => (View: false, Create: false, Edit: false, Delete: false),
|
||||||
adminRole.Permissions = ScreenCatalog.Screens.Select(s => new RolePermission
|
"projects" => (View: true, Create: false, Edit: false, Delete: false),
|
||||||
{
|
_ => (View: true, Create: true, Edit: true, Delete: false),
|
||||||
RoleId = adminRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = true,
|
},
|
||||||
}).ToList();
|
now);
|
||||||
db.Roles.Add(adminRole);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await db.Roles.AnyAsync(r => r.Name == "Member"))
|
private static async Task SyncSystemRoleAsync(
|
||||||
|
AppDbContext db,
|
||||||
|
string name,
|
||||||
|
Func<ScreenDefinition, (bool View, bool Create, bool Edit, bool Delete)> rule,
|
||||||
|
DateTime now)
|
||||||
{
|
{
|
||||||
var memberRole = new Role { Id = Guid.NewGuid(), Name = "Member", IsSystem = true, CreatedAt = now, UpdatedAt = now };
|
var role = await db.Roles
|
||||||
memberRole.Permissions = ScreenCatalog.Screens
|
.Include(r => r.Permissions)
|
||||||
.Where(s => s.Key is not ("users" or "permissions" or "masterdata"))
|
.FirstOrDefaultAsync(r => r.Name == name);
|
||||||
.Select(s => new RolePermission
|
|
||||||
|
if (role is null)
|
||||||
{
|
{
|
||||||
RoleId = memberRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = false,
|
role = new Role { Id = Guid.NewGuid(), Name = name, IsSystem = true, CreatedAt = now, UpdatedAt = now };
|
||||||
}).ToList();
|
db.Roles.Add(role);
|
||||||
db.Roles.Add(memberRole);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var byScreen = role.Permissions.ToDictionary(p => p.Screen);
|
||||||
|
foreach (var screen in ScreenCatalog.Screens)
|
||||||
|
{
|
||||||
|
if (!byScreen.TryGetValue(screen.Key, out var permission))
|
||||||
|
{
|
||||||
|
permission = new RolePermission { RoleId = role.Id, Screen = screen.Key };
|
||||||
|
role.Permissions.Add(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
var (view, create, edit, delete) = rule(screen);
|
||||||
|
permission.CanView = view;
|
||||||
|
permission.CanCreate = create;
|
||||||
|
permission.CanEdit = edit;
|
||||||
|
permission.CanDelete = delete;
|
||||||
|
}
|
||||||
|
|
||||||
|
role.UpdatedAt = now;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<User> EnsureUserAsync(AppDbContext db, IPasswordHasher passwordHasher, string username, string displayName, string roleName)
|
private static async Task<User> EnsureUserAsync(AppDbContext db, IPasswordHasher passwordHasher, string username, string displayName, string roleName)
|
||||||
|
|||||||
Reference in New Issue
Block a user