ref: project structure
This commit is contained in:
@@ -6,16 +6,16 @@ Backend for My Workspace (MWS) — ASP.NET Core 10 Web API with JWT auth, EF Cor
|
||||
|
||||
```bash
|
||||
# build everything
|
||||
dotnet build Mws.slnx
|
||||
dotnet build mws.backend.dotnet.sln
|
||||
|
||||
# run the API (auto-migrates + seeds on startup)
|
||||
dotnet run --project Mws.Api
|
||||
dotnet run --project mws.api
|
||||
|
||||
# add a migration (from repo root, after editing entities)
|
||||
dotnet ef migrations add <Name> --project Mws.Infrastructure --startup-project Mws.Api
|
||||
dotnet ef migrations add <Name> --project mws.infrastructure --startup-project mws.api
|
||||
|
||||
# apply migrations explicitly (otherwise the app does it on boot)
|
||||
dotnet ef database update --project Mws.Infrastructure --startup-project Mws.Api
|
||||
dotnet ef database update --project mws.infrastructure --startup-project mws.api
|
||||
|
||||
# run the docker image
|
||||
docker build -t mws.api .
|
||||
@@ -26,35 +26,35 @@ No test project exists. Add one under `Mws.Tests/` (xUnit) when tests are needed
|
||||
|
||||
## Architecture
|
||||
|
||||
Clean-lite, 4 projects in [Mws.slnx](Mws.slnx). Dependencies point inward only: `Api → Application ← Infrastructure`, both `Application` and `Infrastructure → Domain`.
|
||||
Clean-lite, 4 projects in [mws.backend.dotnet.sln](mws.backend.dotnet.sln). Dependencies point inward only: `Api → Application ← Infrastructure`, both `Application` and `Infrastructure → Domain`.
|
||||
|
||||
- **[Mws.Domain](Mws.Domain/)** — POCOs only. Entities (`User`, `Project`, `ProjectMember`, `TaskItem`, `Document`, `Role`, `RolePermission`) and enums (`ProjectStatus`, `MemberRole`, `TaskStatus`, `TaskPriority`, `DocumentType`). No EF attributes. Roles are data (`roles` table), not a hardcoded enum — `User.RoleId` FK's to `Role`.
|
||||
- **[Mws.Application](Mws.Application/)** — use cases. One folder per aggregate (`Auth/`, `Projects/`, `Tasks/`, `Documents/`, `Accounts/`, `Roles/`, `Permissions/`) plus shared `Common/` (DTO interfaces + exception types). Each service depends only on `IApplicationDbContext`, never on EF Core directly.
|
||||
- **[Mws.Domain](Mws.Domain/)** — POCOs only. Entities (`User`, `Project`, `ProjectMember`, `TaskItem`, `Document`, `Role`, `RolePermission`, `UserRole`, `MasterDataEntry`) and enums (`ProjectStatus`, `MemberRole`, `TaskStatus`, `TaskPriority`, `DocumentType`). No EF attributes. Roles are data (`roles` table), not a hardcoded enum — users link to roles through the `UserRole` join table (`user_roles`, many-to-many; `User.UserRoles`).
|
||||
- **[Mws.Application](Mws.Application/)** — use cases. One folder per module (`Auth/`, `Users/`, `Projects/`, `Tasks/`, `Documents/`, `MasterData/`, `Permissions/`) plus shared `Common/` (DTOs, repositories, exceptions). Module names match controllers/routes (`api/users`, `api/permissions`, `api/masterdata`). Use cases are MediatR command/query handlers; each depends on `IUnitOfWork` (via `Common/Repositories/`) and `IPermissionService`, never on EF Core directly.
|
||||
- **[Mws.Infrastructure](Mws.Infrastructure/)** — EF Core + auth wiring. `AppDbContext` (in [Mws.Infrastructure/Persistence/AppDbContext.cs](Mws.Infrastructure/Persistence/AppDbContext.cs)) configures all entities via fluent API and stores enums as strings. `Authentication/` contains `BcryptPasswordHasher` and `JwtTokenService`. Migrations live under `Migrations/`. `DependencyInjection.AddInfrastructure` is the single composition root.
|
||||
- **[Mws.Api](Mws.Api/)** — controllers, middleware, `Program.cs`. Thin: controllers resolve a service via DI, get the user id from `User.GetUserId()` ([Mws.Api/ClaimsExtensions.cs](Mws.Api/ClaimsExtensions.cs)), and delegate.
|
||||
|
||||
### Request flow
|
||||
|
||||
```
|
||||
HTTP → ApiExceptionMiddleware → JwtBearer auth → Controller → Application service
|
||||
→ IApplicationDbContext (→ AppDbContext) → PostgreSQL
|
||||
HTTP → ApiExceptionMiddleware → JwtBearer auth → Controller → MediatR handler
|
||||
→ IUnitOfWork (repos) → AppDbContext → PostgreSQL
|
||||
```
|
||||
|
||||
Exceptions in [Mws.Application/Common/Exceptions.cs](Mws.Application/Common/Exceptions.cs) (`BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`) are caught by [Mws.Api/Middleware/ApiExceptionMiddleware.cs](Mws.Api/Middleware/ApiExceptionMiddleware.cs) and mapped to 400/401/403/404. Anything else → 500 with a generic message (original logged). Use these exceptions in services instead of returning `Result<T>`.
|
||||
Exceptions in [Mws.Application/Common/Exceptions.cs](Mws.Application/Common/Exceptions.cs) (`BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`) are caught by [Mws.Api/Middleware/ApiExceptionMiddleware.cs](Mws.Api/Middleware/ApiExceptionMiddleware.cs) and mapped to 400/401/403/404. Anything else → 500 with a generic message (original logged). Use these exceptions in handlers instead of returning `Result<T>`.
|
||||
|
||||
### Authorization model
|
||||
|
||||
Two independent mechanisms — don't cross-wire them:
|
||||
|
||||
1. **Project membership** (unchanged). JWT carries `sub` = user `Guid`. Every protected project/task/document endpoint looks up `ProjectMember` to verify the caller belongs to the project — checks live in the application service (`GetProjectForUserAsync`, `EnsureMemberAccessAsync`, `GetMemberRoleAsync`). Mutating project settings requires `MemberRole.Owner`. Add a new check the same way.
|
||||
2. **Screen-level role permissions** (admin screens only: Accounts, Roles). `Role` is a DB entity (`roles` table), not an enum — a user's `User.RoleId` points to one. `RolePermission` is a per-(role, screen) CRUD matrix (`CanView`/`CanCreate`/`CanEdit`/`CanDelete`). `IPermissionService.EnsureAsync(userId, screen, action)` (in `Mws.Application/Permissions/PermissionService.cs`) is the gate — called at the top of `AccountService`/`RoleService` methods and inline in `RolesController`. There is **no** `[Authorize(Roles=...)]` JWT-claim-based gating — the JWT only carries `sub`/`name`; role/permission is always resolved fresh from the DB per request, so changing a role's permissions or a user's role takes effect immediately without re-login. `ScreenCatalog.cs` is the single source of truth for valid screen keys (`dashboard`, `projects`, `tasks`, `documents`, `accounts`, `roles`) — adding a new admin-gated screen means adding it there. `GET /api/menu` returns the CRUD matrix per screen for the caller's role so the frontend can render nav + gate buttons; it does not itself enforce anything server-side.
|
||||
1. **Project membership** (unchanged). JWT carries `sub` = user `Guid`. Every protected project/task/document endpoint looks up `ProjectMember` to verify the caller belongs to the project — checks live in the application handlers via `ProjectAccess`/`TaskAccess` helpers and `IProjectRepository` (`GetMemberRoleAsync`). Mutating project settings requires `MemberRole.Owner`. Add a new check the same way.
|
||||
2. **Screen-level role permissions** (admin screens only: Users, Permissions, Master Data). `Role` is a DB entity (`roles` table), not an enum — a user links to roles via the `UserRole` join table. `RolePermission` is a per-(role, screen) CRUD matrix (`CanView`/`CanCreate`/`CanEdit`/`CanDelete`). `IPermissionService.EnsureAsync(userId, screen, action)` (in `Mws.Application/Permissions/PermissionService.cs`) is the gate — called at the top of the users/roles/masterdata handlers. There is **no** `[Authorize(Roles=...)]` JWT-claim-based gating — the JWT only carries `sub`/`name`; role/permission is always resolved fresh from the DB per request, so changing a role's permissions or a user's roles takes effect immediately without re-login. `ScreenCatalog.cs` is the single source of truth for valid screen keys (`dashboard`, `projects`, `tasks`, `documents`, `users`, `permissions`, `masterdata`) — adding a new admin-gated screen means adding it there. `GET /api/menu` returns the CRUD matrix per screen for the caller's roles so the frontend can render nav + gate buttons; it does not itself enforce anything server-side.
|
||||
|
||||
### Database
|
||||
|
||||
- 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.
|
||||
- Self-referencing `Document.ParentId` uses `Restrict` to block accidental cycles. Cascade delete is implemented in `DocumentService.DeleteAsync` via a manual descendant walk; 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 Accounts/Roles, 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 `RoleService.DeleteRoleAsync`.
|
||||
- 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`.
|
||||
|
||||
### Config
|
||||
|
||||
@@ -67,7 +67,7 @@ Two independent mechanisms — don't cross-wire them:
|
||||
### Conventions
|
||||
|
||||
- `net10.0`, `<Nullable>enable</Nullable>`, `<ImplicitUsings>enable</ImplicitUsings>` on every project.
|
||||
- DTOs live next to the service that returns them (e.g. `ProjectDto` in `Projects/Contracts.cs`). Use `Contracts.cs` per folder; don't sprawl.
|
||||
- DTOs live next to the module that returns them (e.g. `ProjectDto` in `Projects/Contracts.cs`). Use `Contracts.cs` per folder; don't sprawl.
|
||||
- All async service methods take `CancellationToken ct = default` and forward it.
|
||||
- Enums serialized as strings globally (`JsonStringEnumConverter` in `Program.cs`).
|
||||
|
||||
|
||||
+8
-8
@@ -1,19 +1,19 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY Mws.slnx ./
|
||||
COPY Mws.Domain/*.csproj Mws.Domain/
|
||||
COPY Mws.Application/*.csproj Mws.Application/
|
||||
COPY Mws.Infrastructure/*.csproj Mws.Infrastructure/
|
||||
COPY Mws.Api/*.csproj Mws.Api/
|
||||
COPY mws.backend.dotnet.sln ./
|
||||
COPY mws.domain/*.csproj mws.domain/
|
||||
COPY mws.application/*.csproj mws.application/
|
||||
COPY mws.infrastructure/*.csproj mws.infrastructure/
|
||||
COPY mws.api/*.csproj mws.api/
|
||||
RUN dotnet restore
|
||||
|
||||
COPY app/ app/
|
||||
RUN dotnet publish app/Mws.Api -c Release -o /app/publish
|
||||
COPY . .
|
||||
RUN dotnet publish mws.api -c Release -o /app/publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "Mws.Api.dll"]
|
||||
ENTRYPOINT ["dotnet", "mws.api.dll"]
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
|
||||
namespace Mws.Api;
|
||||
namespace mws.backend.dotnet.api;
|
||||
|
||||
public static class ClaimsExtensions
|
||||
{
|
||||
@@ -1,19 +1,27 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Auth;
|
||||
using mws.backend.dotnet.application.Auth;
|
||||
using mws.backend.dotnet.application.Users;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
namespace mws.backend.dotnet.api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
[AllowAnonymous]
|
||||
public class AuthController(ISender sender) : ControllerBase
|
||||
{
|
||||
[HttpPost("login")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest request, CancellationToken ct)
|
||||
{
|
||||
var response = await sender.Send(new LoginCommand(request), ct);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("me")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<UserDto>> GetProfile(CancellationToken ct)
|
||||
{
|
||||
return Ok(await sender.Send(new GetProfileQuery(User.GetUserId()), ct));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Documents;
|
||||
using mws.backend.dotnet.application.Documents;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
namespace mws.backend.dotnet.api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
@@ -0,0 +1,50 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.MasterData;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
|
||||
namespace mws.backend.dotnet.api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/masterdata")]
|
||||
[Authorize]
|
||||
public class MasterDataController(ISender sender, IPermissionService permissions) : ControllerBase
|
||||
{
|
||||
[HttpGet("groups/{group}")]
|
||||
public async Task<ActionResult<List<MasterDataDto>>> GetByGroup(string group, CancellationToken ct)
|
||||
{
|
||||
return Ok(await sender.Send(new GetMasterDataByGroupQuery(group), ct));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<MasterDataDto>>> GetAll(
|
||||
[FromQuery] string? group, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "masterdata", PermissionAction.View, ct);
|
||||
return Ok(await sender.Send(new GetMasterDataListQuery(group, page, pageSize), ct));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<MasterDataDto>> Create([FromBody] SaveMasterDataRequest request, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "masterdata", PermissionAction.Create, ct);
|
||||
return Ok(await sender.Send(new CreateMasterDataCommand(request), ct));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<MasterDataDto>> Update(Guid id, [FromBody] SaveMasterDataRequest request, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "masterdata", PermissionAction.Edit, ct);
|
||||
return Ok(await sender.Send(new UpdateMasterDataCommand(id, request), ct));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "masterdata", PermissionAction.Delete, ct);
|
||||
await sender.Send(new DeleteMasterDataCommand(id), ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
|
||||
namespace mws.backend.dotnet.api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/settings/permission")]
|
||||
[Authorize]
|
||||
public class PermissionsController(ISender sender, IPermissionService permissions) : ControllerBase
|
||||
{
|
||||
[HttpGet("menu")]
|
||||
public async Task<ActionResult<List<MenuItemDto>>> GetMenu(CancellationToken ct)
|
||||
{
|
||||
return Ok(await permissions.GetMenuAsync(User.GetUserId(), ct));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<RoleDto>>> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.View, ct);
|
||||
return Ok(await sender.Send(new GetRolesQuery(page, pageSize), ct));
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<RoleDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.View, ct);
|
||||
return Ok(await sender.Send(new GetRoleQuery(id), ct));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<RoleDto>> Create([FromBody] SaveRoleRequest request, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Create, ct);
|
||||
return Ok(await sender.Send(new CreateRoleCommand(request), ct));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<RoleDto>> Update(Guid id, [FromBody] SaveRoleRequest request, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Edit, ct);
|
||||
return Ok(await sender.Send(new UpdateRoleCommand(id, request), ct));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Delete, ct);
|
||||
await sender.Send(new DeleteRoleCommand(id), ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -1,9 +1,10 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Projects;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
namespace mws.backend.dotnet.api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/projects/{projectId:guid}/members")]
|
||||
@@ -11,9 +12,10 @@ namespace Mws.Api.Controllers;
|
||||
public class ProjectMembersController(ISender sender) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<ProjectMemberDto>>> GetAll(Guid projectId, CancellationToken ct)
|
||||
public async Task<ActionResult<PagedResult<ProjectMemberDto>>> GetAll(
|
||||
Guid projectId, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
|
||||
{
|
||||
return Ok(await sender.Send(new GetProjectMembersQuery(User.GetUserId(), projectId), ct));
|
||||
return Ok(await sender.Send(new GetProjectMembersQuery(User.GetUserId(), projectId, page, pageSize), ct));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -1,9 +1,10 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Projects;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
namespace mws.backend.dotnet.api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/projects")]
|
||||
@@ -11,9 +12,9 @@ namespace Mws.Api.Controllers;
|
||||
public class ProjectsController(ISender sender) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<ProjectDto>>> GetAll(CancellationToken ct)
|
||||
public async Task<ActionResult<PagedResult<ProjectDto>>> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
|
||||
{
|
||||
return Ok(await sender.Send(new GetProjectsQuery(User.GetUserId()), ct));
|
||||
return Ok(await sender.Send(new GetProjectsQuery(User.GetUserId(), page, pageSize), ct));
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
@@ -1,12 +1,12 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Tasks;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Tasks;
|
||||
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
||||
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
namespace mws.backend.dotnet.api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
@@ -20,16 +20,18 @@ public class TasksController(ISender sender) : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("projects/{projectId:guid}/tasks")]
|
||||
public async Task<ActionResult<List<TaskDto>>> GetAll(
|
||||
public async Task<ActionResult<PagedResult<TaskDto>>> GetAll(
|
||||
Guid projectId,
|
||||
[FromQuery] string? status,
|
||||
[FromQuery] string? priority,
|
||||
[FromQuery] Guid? assigneeId,
|
||||
CancellationToken ct)
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var statusValue = ParseOptional<TaskStatus>(status);
|
||||
var priorityValue = ParseOptional<TaskPriority>(priority);
|
||||
return Ok(await sender.Send(new GetTasksQuery(User.GetUserId(), projectId, statusValue, priorityValue, assigneeId), ct));
|
||||
return Ok(await sender.Send(new GetTasksQuery(User.GetUserId(), projectId, statusValue, priorityValue, assigneeId, page, pageSize), ct));
|
||||
}
|
||||
|
||||
[HttpPost("projects/{projectId:guid}/tasks")]
|
||||
@@ -0,0 +1,161 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.application.Users;
|
||||
using mws.backend.dotnet.domain.Users;
|
||||
using mws.backend.dotnet.infrastructure.Persistence;
|
||||
|
||||
namespace mws.backend.dotnet.api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/users")]
|
||||
[Authorize]
|
||||
public class UsersController(ISender sender, AppDbContext db, IPermissionService permissions, IMapper mapper) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<UserDto>>> GetAll([FromQuery] string? q, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
|
||||
{
|
||||
return Ok(await sender.Send(new GetUsersQuery(User.GetUserId(), q, page, pageSize), ct));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<UserDto>> Create([FromBody] CreateUserRequest request, CancellationToken ct)
|
||||
{
|
||||
return Ok(await sender.Send(new CreateUserCommand(User.GetUserId(), request), ct));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<UserDto>> Update(Guid id, [FromBody] UpdateUserRequest request, CancellationToken ct)
|
||||
{
|
||||
return Ok(await sender.Send(new UpdateUserCommand(User.GetUserId(), id, request), ct));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await sender.Send(new DeleteUserCommand(User.GetUserId(), id), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/reset-password")]
|
||||
public async Task<IActionResult> ResetPassword(Guid id, [FromBody] ResetPasswordRequest request, CancellationToken ct)
|
||||
{
|
||||
await sender.Send(new ResetPasswordCommand(User.GetUserId(), id, request), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("list")]
|
||||
public async Task<ActionResult<List<UserDto>>> GetList([FromQuery] string? q, CancellationToken ct)
|
||||
{
|
||||
var query = db.Users.AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
var lower = q.Trim().ToLower();
|
||||
query = query.Where(u => u.Username.ToLower().Contains(lower) || u.DisplayName.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
var users = await query.OrderBy(u => u.DisplayName).ToListAsync(ct);
|
||||
return Ok(mapper.Map<List<UserDto>>(users));
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/roles")]
|
||||
public async Task<ActionResult<UserRoleDetailDto>> GetUserRoles(Guid id, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.View, ct);
|
||||
|
||||
var user = await db.Users
|
||||
.Include(u => u.UserRoles)
|
||||
.ThenInclude(ur => ur.Role)
|
||||
.FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
|
||||
if (user is null) return NotFound("User not found");
|
||||
|
||||
var allRoles = await db.Roles.Include(r => r.Permissions).OrderBy(r => r.Name).ToListAsync(ct);
|
||||
var assignedRoleIds = user.UserRoles.Select(ur => ur.RoleId).ToHashSet();
|
||||
|
||||
var assigned = allRoles.Where(r => assignedRoleIds.Contains(r.Id)).ToList();
|
||||
var unassigned = allRoles.Where(r => !assignedRoleIds.Contains(r.Id)).ToList();
|
||||
|
||||
return Ok(new UserRoleDetailDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
Username = user.Username,
|
||||
DisplayName = user.DisplayName,
|
||||
AssignedRoles = mapper.Map<List<RoleDto>>(assigned),
|
||||
UnassignedRoles = mapper.Map<List<RoleDto>>(unassigned)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/roles")]
|
||||
public async Task<IActionResult> AssignRole(Guid id, [FromBody] AssignRoleRequest req, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Edit, ct);
|
||||
|
||||
var user = await db.Users.Include(u => u.UserRoles).FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
if (user is null) return NotFound("User not found");
|
||||
|
||||
if (!user.UserRoles.Any(ur => ur.RoleId == req.RoleId))
|
||||
{
|
||||
user.UserRoles.Add(new UserRole { UserId = id, RoleId = req.RoleId });
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}/roles/{roleId:guid}")]
|
||||
public async Task<IActionResult> UnassignRole(Guid id, Guid roleId, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Edit, ct);
|
||||
|
||||
var ur = await db.UserRoles.FirstOrDefaultAsync(x => x.UserId == id && x.RoleId == roleId, ct);
|
||||
if (ur != null)
|
||||
{
|
||||
db.UserRoles.Remove(ur);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/permissions")]
|
||||
public async Task<ActionResult<List<MenuItemDto>>> GetUserPermissions(Guid id, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.View, ct);
|
||||
|
||||
var roleIds = await db.UserRoles.Where(ur => ur.UserId == id).Select(ur => ur.RoleId).ToListAsync(ct);
|
||||
var permissionsList = await db.RolePermissions.Where(p => roleIds.Contains(p.RoleId)).ToListAsync(ct);
|
||||
|
||||
var merged = permissionsList.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),
|
||||
}
|
||||
);
|
||||
|
||||
var res = 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();
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Api.Middleware;
|
||||
namespace mws.backend.dotnet.api.Middleware;
|
||||
|
||||
public class ApiExceptionMiddleware(RequestDelegate next, ILogger<ApiExceptionMiddleware> logger)
|
||||
{
|
||||
@@ -3,10 +3,10 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi;
|
||||
using Mws.Api.Middleware;
|
||||
using Mws.Application.Auth;
|
||||
using Mws.Infrastructure;
|
||||
using Mws.Infrastructure.Persistence;
|
||||
using mws.backend.dotnet.api.Middleware;
|
||||
using mws.backend.dotnet.application.Auth;
|
||||
using mws.backend.dotnet.infrastructure;
|
||||
using mws.backend.dotnet.infrastructure.Persistence;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>mws.backend.dotnet.api</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.11">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\application\application.csproj" />
|
||||
<ProjectReference Include="..\domain\domain.csproj" />
|
||||
<ProjectReference Include="..\infrastructure\infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=mws;Username=postgres;Password=Pa55w0rd"
|
||||
"Default": "Host=192.168.2.100;Port=5432;Database=mws;Username=postgres;Password=Pa55w0rd"
|
||||
},
|
||||
"Jwt": {
|
||||
"Secret": "mws-development-secret-key-change-me-in-production-123456",
|
||||
@@ -1,8 +1,9 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Users;
|
||||
|
||||
namespace Mws.Application.Auth;
|
||||
namespace mws.backend.dotnet.application.Auth;
|
||||
|
||||
public record LoginCommand(LoginRequest Request) : IRequest<LoginResponse>;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Mws.Application.Auth;
|
||||
using mws.backend.dotnet.application.Users;
|
||||
|
||||
namespace mws.backend.dotnet.application.Auth;
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
@@ -6,15 +8,6 @@ public class LoginRequest
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UserDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public Guid RoleId { get; set; }
|
||||
public string RoleName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
@@ -0,0 +1,23 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Users;
|
||||
|
||||
namespace mws.backend.dotnet.application.Auth;
|
||||
|
||||
public record GetProfileQuery(Guid UserId) : IRequest<UserDto>;
|
||||
|
||||
public class GetProfileHandler(IUnitOfWork uow, IMapper mapper)
|
||||
: IRequestHandler<GetProfileQuery, UserDto>
|
||||
{
|
||||
public async Task<UserDto> Handle(GetProfileQuery request, CancellationToken ct)
|
||||
{
|
||||
var user = await uow.Users.GetByIdWithRoleAsync(request.UserId, ct);
|
||||
if (user is null)
|
||||
{
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
return mapper.Map<UserDto>(user);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Application.Common;
|
||||
namespace mws.backend.dotnet.application.Common;
|
||||
|
||||
public class NotFoundException(string message) : Exception(message);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Mws.Application.Common.Repositories;
|
||||
using mws.backend.dotnet.application.Common.Repositories;
|
||||
|
||||
namespace Mws.Application.Common;
|
||||
namespace mws.backend.dotnet.application.Common;
|
||||
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
@@ -9,6 +9,7 @@ public interface IUnitOfWork
|
||||
ITaskRepository Tasks { get; }
|
||||
IDocumentRepository Documents { get; }
|
||||
IRoleRepository Roles { get; }
|
||||
IMasterDataRepository MasterData { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using AutoMapper;
|
||||
using mws.backend.dotnet.application.Documents;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.application.Projects;
|
||||
using mws.backend.dotnet.application.Tasks;
|
||||
using mws.backend.dotnet.application.Users;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
using mws.backend.dotnet.domain.MasterData;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
using mws.backend.dotnet.domain.Tasks;
|
||||
using mws.backend.dotnet.domain.Users;
|
||||
|
||||
namespace mws.backend.dotnet.application.Common;
|
||||
|
||||
public class MappingProfile : Profile
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<mws.backend.dotnet.domain.Projects.Project, ProjectDto>()
|
||||
.ForMember(d => d.CreatedByName, opt => opt.MapFrom(s => s.CreatedByUser == null ? null : s.CreatedByUser.DisplayName))
|
||||
.ForMember(d => d.UpdatedByName, opt => opt.MapFrom(s => s.UpdatedByUser == null ? null : s.UpdatedByUser.DisplayName));
|
||||
CreateMap<Role, RoleDto>();
|
||||
CreateMap<MasterDataEntry, MasterData.MasterDataDto>();
|
||||
CreateMap<RolePermission, PermissionEntryDto>();
|
||||
CreateMap<User, UserDto>()
|
||||
.ForMember(d => d.RoleId, opt => opt.MapFrom(s => s.UserRoles.Select(r => r.RoleId).FirstOrDefault()))
|
||||
.ForMember(d => d.RoleName, opt => opt.MapFrom(s => string.Join(", ", s.UserRoles.Select(r => r.Role.Name))));
|
||||
CreateMap<Document, DocumentDto>();
|
||||
CreateMap<Document, DocumentNodeDto>()
|
||||
.ForMember(d => d.Children, opt => opt.Ignore());
|
||||
CreateMap<TaskItem, TaskDto>()
|
||||
.ForMember(d => d.AssigneeName, opt => opt.MapFrom(s => s.Assignee == null ? null : s.Assignee.DisplayName));
|
||||
CreateMap<TaskItem, RecentTaskDto>()
|
||||
.ForMember(d => d.Status, opt => opt.MapFrom(s => s.Status.ToString()));
|
||||
CreateMap<Document, RecentDocumentDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace mws.backend.dotnet.application.Common;
|
||||
|
||||
public class PagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; init; } = [];
|
||||
public int TotalCount { get; init; }
|
||||
public int Page { get; init; }
|
||||
public int PageSize { get; init; }
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
using Mws.Domain.Documents;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
namespace mws.backend.dotnet.application.Common.Repositories;
|
||||
|
||||
public interface IDocumentRepository : IRepository<Document>
|
||||
{
|
||||
@@ -0,0 +1,12 @@
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.MasterData;
|
||||
|
||||
namespace mws.backend.dotnet.application.Common.Repositories;
|
||||
|
||||
public interface IMasterDataRepository : IRepository<MasterDataEntry>
|
||||
{
|
||||
Task<PagedResult<MasterDataEntry>> GetAllAsync(string? group, int page, int pageSize, CancellationToken ct = default);
|
||||
Task<List<MasterDataEntry>> GetActiveByGroupAsync(string group, CancellationToken ct = default);
|
||||
Task<MasterDataEntry?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
Task<bool> ExistsAsync(string group, string value, Guid? excludeId, CancellationToken ct = default);
|
||||
}
|
||||
+5
-4
@@ -1,12 +1,13 @@
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
namespace mws.backend.dotnet.application.Common.Repositories;
|
||||
|
||||
public interface IProjectRepository : IRepository<Project>
|
||||
{
|
||||
Task<Project?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
Task<Project?> GetForUserAsync(Guid userId, Guid projectId, CancellationToken ct = default);
|
||||
Task<List<Project>> GetForUserAsync(Guid userId, CancellationToken ct = default);
|
||||
Task<PagedResult<Project>> GetForUserAsync(Guid userId, int page, int pageSize, CancellationToken ct = default);
|
||||
Task<List<Project>> SearchForUserAsync(Guid userId, string? term, CancellationToken ct = default);
|
||||
Task<int> CountMembersAsync(Guid projectId, CancellationToken ct = default);
|
||||
|
||||
@@ -14,7 +15,7 @@ public interface IProjectRepository : IRepository<Project>
|
||||
Task<MemberRole?> GetMemberRoleAsync(Guid projectId, Guid userId, CancellationToken ct = default);
|
||||
Task<ProjectMember?> GetMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default);
|
||||
Task<ProjectMember?> GetMemberWithUserAsync(Guid projectId, Guid userId, CancellationToken ct = default);
|
||||
Task<List<ProjectMember>> GetMembersWithUserAsync(Guid projectId, CancellationToken ct = default);
|
||||
Task<PagedResult<ProjectMember>> GetMembersWithUserAsync(Guid projectId, int page, int pageSize, CancellationToken ct = default);
|
||||
Task<int> CountOwnersAsync(Guid projectId, CancellationToken ct = default);
|
||||
Task<List<Guid>> GetProjectIdsForUserAsync(Guid userId, CancellationToken ct = default);
|
||||
Task<List<Guid>> GetOwnedProjectIdsAsync(Guid userId, CancellationToken ct = default);
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
namespace mws.backend.dotnet.application.Common.Repositories;
|
||||
|
||||
public interface IRepository<in T> where T : class
|
||||
{
|
||||
+5
-3
@@ -1,14 +1,16 @@
|
||||
using Mws.Domain.Roles;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
namespace mws.backend.dotnet.application.Common.Repositories;
|
||||
|
||||
public interface IRoleRepository : IRepository<Role>
|
||||
{
|
||||
Task<List<Role>> GetAllWithPermissionsAsync(CancellationToken ct = default);
|
||||
Task<PagedResult<Role>> GetAllWithPermissionsAsync(int page, int pageSize, CancellationToken ct = default);
|
||||
Task<Role?> GetByIdWithPermissionsAsync(Guid id, CancellationToken ct = default);
|
||||
Task<Role?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
Task<bool> ExistsByNameAsync(string name, Guid? excludeId, CancellationToken ct = default);
|
||||
Task<RolePermission?> GetPermissionAsync(Guid roleId, string screen, CancellationToken ct = default);
|
||||
Task<Dictionary<string, RolePermission>> GetPermissionsAsync(Guid roleId, CancellationToken ct = default);
|
||||
Task<List<RolePermission>> GetPermissionsForRolesAsync(IEnumerable<Guid> roleIds, CancellationToken ct = default);
|
||||
void RemovePermissions(IEnumerable<RolePermission> permissions);
|
||||
}
|
||||
+7
-6
@@ -1,15 +1,16 @@
|
||||
using Mws.Domain.Tasks;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Tasks;
|
||||
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
||||
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
namespace mws.backend.dotnet.application.Common.Repositories;
|
||||
|
||||
public interface ITaskRepository : IRepository<TaskItem>
|
||||
{
|
||||
Task<TaskItem?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
Task<TaskItem?> GetWithAssigneeAsync(Guid id, CancellationToken ct = default);
|
||||
Task<List<TaskItem>> GetForProjectAsync(
|
||||
Guid projectId, TaskStatus? status, TaskPriority? priority, Guid? assigneeId, CancellationToken ct = default);
|
||||
Task<PagedResult<TaskItem>> GetForProjectAsync(
|
||||
Guid projectId, TaskStatus? status, TaskPriority? priority, Guid? assigneeId, int page, int pageSize, CancellationToken ct = default);
|
||||
Task<List<TaskItem>> SearchWithAssigneeAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default);
|
||||
Task<Dictionary<TaskStatus, int>> CountByStatusForProjectAsync(Guid projectId, CancellationToken ct = default);
|
||||
Task<List<TaskItem>> GetRecentForProjectAsync(Guid projectId, int take, CancellationToken ct = default);
|
||||
+5
-3
@@ -1,6 +1,7 @@
|
||||
using Mws.Domain.Users;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Users;
|
||||
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
namespace mws.backend.dotnet.application.Common.Repositories;
|
||||
|
||||
public interface IUserRepository : IRepository<User>
|
||||
{
|
||||
@@ -9,6 +10,7 @@ public interface IUserRepository : IRepository<User>
|
||||
Task<User?> GetByUsernameWithRoleAsync(string username, CancellationToken ct = default);
|
||||
Task<bool> ExistsByUsernameAsync(string username, CancellationToken ct = default);
|
||||
Task<bool> ExistsByRoleIdAsync(Guid roleId, CancellationToken ct = default);
|
||||
Task<Guid> GetRoleIdAsync(Guid userId, CancellationToken ct = default);
|
||||
Task<List<Guid>> GetRoleIdsAsync(Guid userId, CancellationToken ct = default);
|
||||
Task<List<User>> SearchWithRoleAsync(string? term, int? take, CancellationToken ct = default);
|
||||
Task<PagedResult<User>> SearchWithRolePagedAsync(string? term, int page, int pageSize, CancellationToken ct = default);
|
||||
}
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Domain.Documents;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
namespace mws.backend.dotnet.application.Documents;
|
||||
|
||||
public record CreateDocumentCommand(Guid UserId, Guid ProjectId, CreateDocumentRequest Request) : IRequest<DocumentDto>;
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
namespace mws.backend.dotnet.application.Documents;
|
||||
|
||||
public record DeleteDocumentCommand(Guid UserId, Guid DocumentId) : IRequest;
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Domain.Documents;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
namespace mws.backend.dotnet.application.Documents;
|
||||
|
||||
public record MoveDocumentCommand(Guid UserId, Guid DocumentId, Guid? NewParentId) : IRequest;
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Domain.Documents;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
namespace mws.backend.dotnet.application.Documents;
|
||||
|
||||
public record UpdateDocumentCommand(Guid UserId, Guid DocumentId, UpdateDocumentRequest Request) : IRequest<DocumentDto>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Mws.Domain.Documents;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
namespace mws.backend.dotnet.application.Documents;
|
||||
|
||||
public class CreateDocumentRequest
|
||||
{
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Domain.Documents;
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
namespace mws.backend.dotnet.application.Documents;
|
||||
|
||||
internal static class DocumentAccess
|
||||
{
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
namespace mws.backend.dotnet.application.Documents;
|
||||
|
||||
public record GetDocumentQuery(Guid UserId, Guid DocumentId) : IRequest<DocumentDto>;
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
namespace mws.backend.dotnet.application.Documents;
|
||||
|
||||
public record GetDocumentTreeQuery(Guid UserId, Guid ProjectId) : IRequest<List<DocumentNodeDto>>;
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
namespace mws.backend.dotnet.application.Documents;
|
||||
|
||||
public record SearchDocumentsQuery(Guid UserId, string Term) : IRequest<List<DocumentNodeDto>>;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.MasterData;
|
||||
|
||||
public record CreateMasterDataCommand(SaveMasterDataRequest Request) : IRequest<MasterDataDto>;
|
||||
|
||||
public class CreateMasterDataHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateMasterDataCommand, MasterDataDto>
|
||||
{
|
||||
public async Task<MasterDataDto> Handle(CreateMasterDataCommand command, CancellationToken ct)
|
||||
{
|
||||
var entry = await MasterDataValidation.ValidateAsync(uow, command.Request, null, ct);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
entry.Id = Guid.NewGuid();
|
||||
entry.CreatedAt = now;
|
||||
entry.UpdatedAt = now;
|
||||
|
||||
uow.MasterData.Add(entry);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<MasterDataDto>(entry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.MasterData;
|
||||
|
||||
public record DeleteMasterDataCommand(Guid Id) : IRequest;
|
||||
|
||||
public class DeleteMasterDataHandler(IUnitOfWork uow) : IRequestHandler<DeleteMasterDataCommand>
|
||||
{
|
||||
public async Task Handle(DeleteMasterDataCommand command, CancellationToken ct)
|
||||
{
|
||||
var entry = await uow.MasterData.GetByIdAsync(command.Id, ct)
|
||||
?? throw new NotFoundException("Master data entry not found");
|
||||
|
||||
uow.MasterData.Remove(entry);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.MasterData;
|
||||
|
||||
public record UpdateMasterDataCommand(Guid Id, SaveMasterDataRequest Request) : IRequest<MasterDataDto>;
|
||||
|
||||
public class UpdateMasterDataHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateMasterDataCommand, MasterDataDto>
|
||||
{
|
||||
public async Task<MasterDataDto> Handle(UpdateMasterDataCommand command, CancellationToken ct)
|
||||
{
|
||||
var existing = await uow.MasterData.GetByIdAsync(command.Id, ct)
|
||||
?? throw new NotFoundException("Master data entry not found");
|
||||
|
||||
var updated = await MasterDataValidation.ValidateAsync(uow, command.Request, command.Id, ct);
|
||||
|
||||
existing.Group = updated.Group;
|
||||
existing.Label = updated.Label;
|
||||
existing.Value = updated.Value;
|
||||
existing.SortOrder = updated.SortOrder;
|
||||
existing.IsActive = updated.IsActive;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<MasterDataDto>(existing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace mws.backend.dotnet.application.MasterData;
|
||||
|
||||
public class MasterDataDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Group { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class SaveMasterDataRequest
|
||||
{
|
||||
public string Group { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.MasterData;
|
||||
|
||||
namespace mws.backend.dotnet.application.MasterData;
|
||||
|
||||
internal static class MasterDataValidation
|
||||
{
|
||||
public static async Task<MasterDataEntry> ValidateAsync(IUnitOfWork uow, SaveMasterDataRequest request, Guid? excludeId, CancellationToken ct)
|
||||
{
|
||||
var group = request.Group.Trim();
|
||||
var label = request.Label.Trim();
|
||||
var value = request.Value.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(group) || string.IsNullOrWhiteSpace(label) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new BadRequestException("Group, label and value are required");
|
||||
}
|
||||
|
||||
if (await uow.MasterData.ExistsAsync(group, value, excludeId, ct))
|
||||
{
|
||||
throw new BadRequestException("An entry with this group and value already exists");
|
||||
}
|
||||
|
||||
return new MasterDataEntry
|
||||
{
|
||||
Group = group,
|
||||
Label = label,
|
||||
Value = value,
|
||||
SortOrder = request.SortOrder,
|
||||
IsActive = request.IsActive,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.MasterData;
|
||||
|
||||
public record GetMasterDataByGroupQuery(string Group) : IRequest<List<MasterDataDto>>;
|
||||
|
||||
public class GetMasterDataByGroupHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<GetMasterDataByGroupQuery, List<MasterDataDto>>
|
||||
{
|
||||
public async Task<List<MasterDataDto>> Handle(GetMasterDataByGroupQuery query, CancellationToken ct)
|
||||
{
|
||||
var entries = await uow.MasterData.GetActiveByGroupAsync(query.Group, ct);
|
||||
return mapper.Map<List<MasterDataDto>>(entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.MasterData;
|
||||
|
||||
public record GetMasterDataListQuery(string? Group, int Page, int PageSize) : IRequest<PagedResult<MasterDataDto>>;
|
||||
|
||||
public class GetMasterDataListHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<GetMasterDataListQuery, PagedResult<MasterDataDto>>
|
||||
{
|
||||
public async Task<PagedResult<MasterDataDto>> Handle(GetMasterDataListQuery query, CancellationToken ct)
|
||||
{
|
||||
var entries = await uow.MasterData.GetAllAsync(query.Group, query.Page, query.PageSize, ct);
|
||||
return new PagedResult<MasterDataDto>
|
||||
{
|
||||
Items = mapper.Map<List<MasterDataDto>>(entries.Items),
|
||||
TotalCount = entries.TotalCount,
|
||||
Page = entries.Page,
|
||||
PageSize = entries.PageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Roles;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace Mws.Application.Roles;
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record CreateRoleCommand(SaveRoleRequest Request) : IRequest<RoleDto>;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Roles;
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record DeleteRoleCommand(Guid Id) : IRequest;
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Roles;
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record UpdateRoleCommand(Guid Id, SaveRoleRequest Request) : IRequest<RoleDto>;
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
namespace Mws.Application.Roles;
|
||||
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
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Application.Permissions;
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public interface IPermissionService
|
||||
{
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Roles;
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record GetRoleQuery(Guid Id) : IRequest<RoleDto>;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Domain.Roles;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace Mws.Application.Roles;
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
internal static class RolePermissionBuilder
|
||||
{
|
||||
+4
-3
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Application.Permissions;
|
||||
namespace mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public record ScreenDefinition(string Key, string Label, string Path);
|
||||
|
||||
@@ -10,8 +10,9 @@ public static class ScreenCatalog
|
||||
new("projects", "Projects", "/projects"),
|
||||
new("tasks", "Tasks", "/tasks"),
|
||||
new("documents", "Documents", "/documents"),
|
||||
new("accounts", "Accounts", "/accounts"),
|
||||
new("roles", "Roles", "/roles"),
|
||||
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();
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record AddProjectMemberCommand(Guid UserId, Guid ProjectId, AddMemberRequest Request) : IRequest<ProjectMemberDto>;
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record ArchiveProjectCommand(Guid UserId, Guid ProjectId) : IRequest;
|
||||
|
||||
+5
-3
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record CreateProjectCommand(Guid UserId, CreateProjectRequest Request) : IRequest<ProjectDto>;
|
||||
|
||||
@@ -24,7 +24,9 @@ public class CreateProjectHandler(IUnitOfWork uow, IMapper mapper) : IRequestHan
|
||||
Name = request.Name.Trim(),
|
||||
Description = request.Description,
|
||||
Status = ProjectStatus.Active,
|
||||
CreatedBy = command.UserId,
|
||||
CreatedAt = now,
|
||||
UpdatedBy = command.UserId,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record RemoveProjectMemberCommand(Guid UserId, Guid ProjectId, Guid MemberUserId) : IRequest;
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record UpdateMemberDocumentPermissionsCommand(
|
||||
Guid UserId, Guid ProjectId, Guid MemberUserId, UpdateMemberDocumentPermissionsRequest Request) : IRequest<ProjectMemberDto>;
|
||||
+4
-3
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record UpdateProjectCommand(Guid UserId, Guid ProjectId, UpdateProjectRequest Request) : IRequest<ProjectDto>;
|
||||
|
||||
@@ -27,6 +27,7 @@ public class UpdateProjectHandler(IUnitOfWork uow, IMapper mapper) : IRequestHan
|
||||
project.Name = request.Name.Trim();
|
||||
project.Description = request.Description;
|
||||
project.Status = request.Status;
|
||||
project.UpdatedBy = command.UserId;
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await uow.SaveChangesAsync(ct);
|
||||
@@ -1,6 +1,6 @@
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public class CreateProjectRequest
|
||||
{
|
||||
@@ -47,7 +47,11 @@ public class ProjectDto
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public ProjectStatus Status { get; set; }
|
||||
public Guid CreatedBy { get; set; }
|
||||
public string CreatedByName { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid? UpdatedBy { get; set; }
|
||||
public string? UpdatedByName { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
internal static class ProjectAccess
|
||||
{
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record GetProjectQuery(Guid UserId, Guid ProjectId) : IRequest<ProjectDto>;
|
||||
|
||||
+16
-8
@@ -1,14 +1,14 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Projects;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record GetProjectMembersQuery(Guid UserId, Guid ProjectId) : IRequest<List<ProjectMemberDto>>;
|
||||
public record GetProjectMembersQuery(Guid UserId, Guid ProjectId, int Page, int PageSize) : IRequest<PagedResult<ProjectMemberDto>>;
|
||||
|
||||
public class GetProjectMembersHandler(IUnitOfWork uow) : IRequestHandler<GetProjectMembersQuery, List<ProjectMemberDto>>
|
||||
public class GetProjectMembersHandler(IUnitOfWork uow) : IRequestHandler<GetProjectMembersQuery, PagedResult<ProjectMemberDto>>
|
||||
{
|
||||
public async Task<List<ProjectMemberDto>> Handle(GetProjectMembersQuery query, CancellationToken ct)
|
||||
public async Task<PagedResult<ProjectMemberDto>> Handle(GetProjectMembersQuery query, CancellationToken ct)
|
||||
{
|
||||
var isMember = await uow.Projects.IsMemberAsync(query.ProjectId, query.UserId, ct);
|
||||
if (!isMember)
|
||||
@@ -16,10 +16,10 @@ public class GetProjectMembersHandler(IUnitOfWork uow) : IRequestHandler<GetProj
|
||||
throw new NotFoundException("Project not found");
|
||||
}
|
||||
|
||||
var members = await uow.Projects.GetMembersWithUserAsync(query.ProjectId, ct);
|
||||
var members = await uow.Projects.GetMembersWithUserAsync(query.ProjectId, query.Page, query.PageSize, ct);
|
||||
var permissions = await uow.Projects.GetMemberPermissionsAsync(query.ProjectId, ProjectPermissionScreens.Documents, ct);
|
||||
|
||||
return members.Select(m =>
|
||||
var items = members.Items.Select(m =>
|
||||
{
|
||||
permissions.TryGetValue(m.UserId, out var p);
|
||||
return new ProjectMemberDto
|
||||
@@ -34,5 +34,13 @@ public class GetProjectMembersHandler(IUnitOfWork uow) : IRequestHandler<GetProj
|
||||
CanDeleteDocuments = m.Role == MemberRole.Owner || (p?.CanDelete ?? false),
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new PagedResult<ProjectMemberDto>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = members.TotalCount,
|
||||
Page = members.Page,
|
||||
PageSize = members.PageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Documents;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Documents;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record GetProjectOverviewQuery(Guid UserId, Guid ProjectId) : IRequest<ProjectOverviewDto>;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record GetProjectsQuery(Guid UserId, int Page, int PageSize) : IRequest<PagedResult<ProjectDto>>;
|
||||
|
||||
public class GetProjectsHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<GetProjectsQuery, PagedResult<ProjectDto>>
|
||||
{
|
||||
public async Task<PagedResult<ProjectDto>> Handle(GetProjectsQuery query, CancellationToken ct)
|
||||
{
|
||||
var projects = await uow.Projects.GetForUserAsync(query.UserId, query.Page, query.PageSize, ct);
|
||||
return new PagedResult<ProjectDto>
|
||||
{
|
||||
Items = mapper.Map<List<ProjectDto>>(projects.Items),
|
||||
TotalCount = projects.TotalCount,
|
||||
Page = projects.Page,
|
||||
PageSize = projects.PageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
namespace mws.backend.dotnet.application.Projects;
|
||||
|
||||
public record SearchProjectsQuery(Guid UserId, string Term) : IRequest<List<ProjectDto>>;
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Tasks;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Tasks;
|
||||
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
||||
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
namespace mws.backend.dotnet.application.Tasks;
|
||||
|
||||
public record CreateTaskCommand(Guid UserId, Guid ProjectId, CreateTaskRequest Request) : IRequest<TaskDto>;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
namespace mws.backend.dotnet.application.Tasks;
|
||||
|
||||
public record DeleteTaskCommand(Guid UserId, Guid TaskId) : IRequest;
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
namespace mws.backend.dotnet.application.Tasks;
|
||||
|
||||
public record UpdateTaskCommand(Guid UserId, Guid TaskId, UpdateTaskRequest Request) : IRequest<TaskDto>;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Mws.Domain.Tasks;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
using mws.backend.dotnet.domain.Tasks;
|
||||
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
||||
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
namespace mws.backend.dotnet.application.Tasks;
|
||||
|
||||
public class CreateTaskRequest
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
namespace mws.backend.dotnet.application.Tasks;
|
||||
|
||||
public record GetTaskQuery(Guid UserId, Guid TaskId) : IRequest<TaskDto>;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
||||
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
||||
|
||||
namespace mws.backend.dotnet.application.Tasks;
|
||||
|
||||
public record GetTasksQuery(Guid UserId, Guid ProjectId, TaskStatus? Status, TaskPriority? Priority, Guid? AssigneeId, int Page, int PageSize)
|
||||
: IRequest<PagedResult<TaskDto>>;
|
||||
|
||||
public class GetTasksHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<GetTasksQuery, PagedResult<TaskDto>>
|
||||
{
|
||||
public async Task<PagedResult<TaskDto>> Handle(GetTasksQuery query, CancellationToken ct)
|
||||
{
|
||||
await TaskAccess.EnsureMemberAccessAsync(uow, query.UserId, query.ProjectId, ct);
|
||||
|
||||
var tasks = await uow.Tasks.GetForProjectAsync(query.ProjectId, query.Status, query.Priority, query.AssigneeId, query.Page, query.PageSize, ct);
|
||||
return new PagedResult<TaskDto>
|
||||
{
|
||||
Items = mapper.Map<List<TaskDto>>(tasks.Items),
|
||||
TotalCount = tasks.TotalCount,
|
||||
Page = tasks.Page,
|
||||
PageSize = tasks.PageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Mws.Application.Common;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
namespace mws.backend.dotnet.application.Tasks;
|
||||
|
||||
public record SearchTasksQuery(Guid UserId, string Term) : IRequest<List<TaskDto>>;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using AutoMapper;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Tasks;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.domain.Tasks;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
namespace mws.backend.dotnet.application.Tasks;
|
||||
|
||||
internal static class TaskAccess
|
||||
{
|
||||
@@ -0,0 +1,58 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Auth;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
using mws.backend.dotnet.domain.Users;
|
||||
|
||||
namespace mws.backend.dotnet.application.Users;
|
||||
|
||||
public record CreateUserCommand(Guid ActorUserId, CreateUserRequest Request) : IRequest<UserDto>;
|
||||
|
||||
public class CreateUserHandler(IUnitOfWork uow, IPasswordHasher passwordHasher, IPermissionService permissions, IMapper mapper)
|
||||
: IRequestHandler<CreateUserCommand, UserDto>
|
||||
{
|
||||
private const string Screen = "users";
|
||||
|
||||
public async Task<UserDto> Handle(CreateUserCommand command, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(command.ActorUserId, Screen, PermissionAction.Create, ct);
|
||||
|
||||
var request = command.Request;
|
||||
var username = request.Username.Trim();
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password))
|
||||
{
|
||||
throw new BadRequestException("Username and password are required");
|
||||
}
|
||||
|
||||
if (await uow.Users.ExistsByUsernameAsync(username, ct))
|
||||
{
|
||||
throw new BadRequestException("Username already exists");
|
||||
}
|
||||
|
||||
Role? role = null;
|
||||
if (request.RoleId.HasValue)
|
||||
{
|
||||
role = await uow.Roles.GetByIdAsync(request.RoleId.Value, ct)
|
||||
?? throw new BadRequestException("Role not found");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = username,
|
||||
PasswordHash = passwordHasher.Hash(request.Password),
|
||||
DisplayName = request.DisplayName.Trim(),
|
||||
IsActive = true,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
UserRoles = role != null ? new List<UserRole> { new UserRole { RoleId = role.Id } } : new List<UserRole>()
|
||||
};
|
||||
|
||||
uow.Users.Add(user);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<UserDto>(user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
|
||||
namespace mws.backend.dotnet.application.Users;
|
||||
|
||||
public record DeleteUserCommand(Guid ActorUserId, Guid Id) : IRequest;
|
||||
|
||||
public class DeleteUserHandler(IUnitOfWork uow, IPermissionService permissions) : IRequestHandler<DeleteUserCommand>
|
||||
{
|
||||
private const string Screen = "users";
|
||||
|
||||
public async Task Handle(DeleteUserCommand command, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(command.ActorUserId, Screen, PermissionAction.Delete, ct);
|
||||
|
||||
var user = await uow.Users.GetByIdAsync(command.Id, ct)
|
||||
?? throw new NotFoundException("User not found");
|
||||
|
||||
var soleOwnerProjectIds = await uow.Projects.GetOwnedProjectIdsAsync(command.Id, ct);
|
||||
|
||||
foreach (var projectId in soleOwnerProjectIds)
|
||||
{
|
||||
var ownerCount = await uow.Projects.CountOwnersAsync(projectId, ct);
|
||||
if (ownerCount <= 1)
|
||||
{
|
||||
throw new BadRequestException("Cannot delete a user that is the sole owner of a project");
|
||||
}
|
||||
}
|
||||
|
||||
uow.Users.Remove(user);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,16 +1,16 @@
|
||||
using MediatR;
|
||||
using Mws.Application.Auth;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using mws.backend.dotnet.application.Auth;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
|
||||
namespace Mws.Application.Accounts;
|
||||
namespace mws.backend.dotnet.application.Users;
|
||||
|
||||
public record ResetPasswordCommand(Guid ActorUserId, Guid Id, ResetPasswordRequest Request) : IRequest;
|
||||
|
||||
public class ResetPasswordHandler(IUnitOfWork uow, IPasswordHasher passwordHasher, IPermissionService permissions)
|
||||
: IRequestHandler<ResetPasswordCommand>
|
||||
{
|
||||
private const string Screen = "accounts";
|
||||
private const string Screen = "users";
|
||||
|
||||
public async Task Handle(ResetPasswordCommand command, CancellationToken ct)
|
||||
{
|
||||
@@ -22,7 +22,7 @@ public class ResetPasswordHandler(IUnitOfWork uow, IPasswordHasher passwordHashe
|
||||
}
|
||||
|
||||
var user = await uow.Users.GetByIdAsync(command.Id, ct)
|
||||
?? throw new NotFoundException("Account not found");
|
||||
?? throw new NotFoundException("User not found");
|
||||
|
||||
user.PasswordHash = passwordHasher.Hash(command.Request.NewPassword);
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
@@ -0,0 +1,36 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
|
||||
namespace mws.backend.dotnet.application.Users;
|
||||
|
||||
public record UpdateUserCommand(Guid ActorUserId, Guid Id, UpdateUserRequest Request) : IRequest<UserDto>;
|
||||
|
||||
public class UpdateUserHandler(IUnitOfWork uow, IPermissionService permissions, IMapper mapper)
|
||||
: IRequestHandler<UpdateUserCommand, UserDto>
|
||||
{
|
||||
private const string Screen = "users";
|
||||
|
||||
public async Task<UserDto> Handle(UpdateUserCommand command, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(command.ActorUserId, Screen, PermissionAction.Edit, ct);
|
||||
|
||||
var user = await uow.Users.GetByIdWithRoleAsync(command.Id, ct)
|
||||
?? throw new NotFoundException("User not found");
|
||||
|
||||
var request = command.Request;
|
||||
if (request.RoleId.HasValue)
|
||||
{
|
||||
var role = await uow.Roles.GetByIdAsync(request.RoleId.Value, ct)
|
||||
?? throw new BadRequestException("Role not found");
|
||||
}
|
||||
|
||||
user.DisplayName = request.DisplayName.Trim();
|
||||
user.IsActive = request.IsActive;
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<UserDto>(user);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
namespace Mws.Application.Accounts;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
|
||||
public class CreateAccountRequest
|
||||
namespace mws.backend.dotnet.application.Users;
|
||||
|
||||
public class CreateUserRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public Guid RoleId { get; set; }
|
||||
public Guid? RoleId { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateAccountRequest
|
||||
public class UpdateUserRequest
|
||||
{
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public Guid RoleId { get; set; }
|
||||
public Guid? RoleId { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -20,7 +22,7 @@ public class ResetPasswordRequest
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AccountDto
|
||||
public class UserDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
@@ -30,3 +32,17 @@ public class AccountDto
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class UserRoleDetailDto
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public List<RoleDto> AssignedRoles { get; set; } = [];
|
||||
public List<RoleDto> UnassignedRoles { get; set; } = [];
|
||||
}
|
||||
|
||||
public class AssignRoleRequest
|
||||
{
|
||||
public Guid RoleId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
|
||||
namespace mws.backend.dotnet.application.Users;
|
||||
|
||||
public record GetUsersQuery(Guid ActorUserId, string? Term, int Page, int PageSize) : IRequest<PagedResult<UserDto>>;
|
||||
|
||||
public class GetUsersHandler(IUnitOfWork uow, IPermissionService permissions, IMapper mapper)
|
||||
: IRequestHandler<GetUsersQuery, PagedResult<UserDto>>
|
||||
{
|
||||
private const string Screen = "users";
|
||||
|
||||
public async Task<PagedResult<UserDto>> Handle(GetUsersQuery query, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(query.ActorUserId, Screen, PermissionAction.View, ct);
|
||||
|
||||
var users = await uow.Users.SearchWithRolePagedAsync(query.Term, query.Page, query.PageSize, ct);
|
||||
return new PagedResult<UserDto>
|
||||
{
|
||||
Items = mapper.Map<List<UserDto>>(users.Items),
|
||||
TotalCount = users.TotalCount,
|
||||
Page = users.Page,
|
||||
PageSize = users.PageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,11 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>mws.backend.dotnet.application</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\mws.domain\mws.domain.csproj" />
|
||||
<ProjectReference Include="..\domain\domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Domain.Documents;
|
||||
namespace mws.backend.dotnet.domain.Documents;
|
||||
|
||||
public enum DocumentType
|
||||
{
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace mws.backend.dotnet.domain.MasterData;
|
||||
|
||||
public class MasterDataEntry
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Group { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Domain.Projects;
|
||||
namespace mws.backend.dotnet.domain.Projects;
|
||||
|
||||
public enum ProjectStatus
|
||||
{
|
||||
@@ -12,8 +12,13 @@ public class Project
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public ProjectStatus Status { get; set; } = ProjectStatus.Active;
|
||||
public Guid CreatedBy { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid? UpdatedBy { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public List<ProjectMember> Members { get; set; } = [];
|
||||
|
||||
public mws.backend.dotnet.domain.Users.User? CreatedByUser { get; set; }
|
||||
public mws.backend.dotnet.domain.Users.User? UpdatedByUser { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Domain.Projects;
|
||||
namespace mws.backend.dotnet.domain.Projects;
|
||||
|
||||
public enum MemberRole
|
||||
{
|
||||
@@ -13,5 +13,5 @@ public class ProjectMember
|
||||
public MemberRole Role { get; set; } = MemberRole.Member;
|
||||
|
||||
public Project Project { get; set; } = null!;
|
||||
public Mws.Domain.Users.User User { get; set; } = null!;
|
||||
public mws.backend.dotnet.domain.Users.User User { get; set; } = null!;
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Domain.Projects;
|
||||
namespace mws.backend.dotnet.domain.Projects;
|
||||
|
||||
public static class ProjectPermissionScreens
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Domain.Roles;
|
||||
namespace mws.backend.dotnet.domain.Roles;
|
||||
|
||||
public class Role
|
||||
{
|
||||
@@ -9,4 +9,5 @@ public class Role
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public List<RolePermission> Permissions { get; set; } = [];
|
||||
public List<mws.backend.dotnet.domain.Users.UserRole> UserRoles { get; set; } = [];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Domain.Roles;
|
||||
namespace mws.backend.dotnet.domain.Roles;
|
||||
|
||||
public class RolePermission
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Domain.Tasks;
|
||||
namespace mws.backend.dotnet.domain.Tasks;
|
||||
|
||||
public enum TaskStatus
|
||||
{
|
||||
@@ -29,5 +29,5 @@ public class TaskItem
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public Mws.Domain.Users.User? Assignee { get; set; }
|
||||
public mws.backend.dotnet.domain.Users.User? Assignee { get; set; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Mws.Domain.Roles;
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace Mws.Domain.Users;
|
||||
namespace mws.backend.dotnet.domain.Users;
|
||||
|
||||
public class User
|
||||
{
|
||||
@@ -8,10 +8,8 @@ public class User
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public Guid RoleId { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public Role Role { get; set; } = null!;
|
||||
public List<UserRole> UserRoles { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using mws.backend.dotnet.domain.Roles;
|
||||
|
||||
namespace mws.backend.dotnet.domain.Users;
|
||||
|
||||
public class UserRole
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public Guid RoleId { get; set; }
|
||||
|
||||
public User User { get; set; } = null!;
|
||||
public Role Role { get; set; } = null!;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>mws.backend.dotnet.domain</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
using Mws.Application.Auth;
|
||||
using mws.backend.dotnet.application.Auth;
|
||||
|
||||
namespace Mws.Infrastructure.Authentication;
|
||||
namespace mws.backend.dotnet.infrastructure.Authentication;
|
||||
|
||||
public class BcryptPasswordHasher : IPasswordHasher
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Mws.Infrastructure.Authentication;
|
||||
namespace mws.backend.dotnet.infrastructure.Authentication;
|
||||
|
||||
public class JwtOptions
|
||||
{
|
||||
+2
-2
@@ -2,9 +2,9 @@ using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Mws.Application.Auth;
|
||||
using mws.backend.dotnet.application.Auth;
|
||||
|
||||
namespace Mws.Infrastructure.Authentication;
|
||||
namespace mws.backend.dotnet.infrastructure.Authentication;
|
||||
|
||||
public class JwtTokenService(JwtOptions options) : ITokenService
|
||||
{
|
||||
@@ -1,13 +1,13 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Mws.Application.Auth;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Infrastructure.Authentication;
|
||||
using Mws.Infrastructure.Persistence;
|
||||
using mws.backend.dotnet.application.Auth;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Permissions;
|
||||
using mws.backend.dotnet.infrastructure.Authentication;
|
||||
using mws.backend.dotnet.infrastructure.Persistence;
|
||||
|
||||
namespace Mws.Infrastructure;
|
||||
namespace mws.backend.dotnet.infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user