Commit MWS backend source tree
The ASP.NET Core solution (mws.api/mws.application/mws.domain/ mws.infrastructure) existed only as untracked working files. Adding it to version control, plus a .gitignore for build output and local tooling directories, so the CQRS/mediator refactor plan has a real git history to branch and diff against.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
.vs/
|
||||
@@ -0,0 +1,7 @@
|
||||
bin/
|
||||
obj/
|
||||
.vs/
|
||||
.idea/
|
||||
.DS_Store
|
||||
.claude/
|
||||
.serena/
|
||||
@@ -0,0 +1,76 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Backend for My Workspace (MWS) — ASP.NET Core 10 Web API with JWT auth, EF Core, and PostgreSQL.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# build everything
|
||||
dotnet build Mws.slnx
|
||||
|
||||
# run the API (auto-migrates + seeds on startup)
|
||||
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
|
||||
|
||||
# apply migrations explicitly (otherwise the app does it on boot)
|
||||
dotnet ef database update --project Mws.Infrastructure --startup-project Mws.Api
|
||||
|
||||
# run the docker image
|
||||
docker build -t mws.api .
|
||||
docker run --rm -p 8080:8080 mws.api
|
||||
```
|
||||
|
||||
No test project exists. Add one under `Mws.Tests/` (xUnit) when tests are needed — none is expected today.
|
||||
|
||||
## Architecture
|
||||
|
||||
Clean-lite, 4 projects in [Mws.slnx](Mws.slnx). 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.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
|
||||
```
|
||||
|
||||
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>`.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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`.
|
||||
|
||||
### Config
|
||||
|
||||
| Setting | Purpose | Notes |
|
||||
| --- | --- | --- |
|
||||
| `Jwt:Secret` | HMAC signing key | ≥32 chars required, else falls back to dev key. Set `MWS_JWT_SECRET` env var in prod. |
|
||||
| `Jwt:Issuer` / `Jwt:Audience` | Token validation | Defaults `Mws` / `Mws.Clients`. |
|
||||
| `Cors:Origins` | Allowed origins (semicolon-separated) | Default `http://localhost:5173` (Vite frontend). |
|
||||
|
||||
### 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.
|
||||
- All async service methods take `CancellationToken ct = default` and forward it.
|
||||
- Enums serialized as strings globally (`JsonStringEnumConverter` in `Program.cs`).
|
||||
|
||||
## Frontend
|
||||
|
||||
The companion frontend lives at `../project/mws/` (Vite/React). API root in dev: `http://localhost:5xxx` → backend at `http://localhost:5xxx` per CORS config — confirm port mapping if backend URL changes.
|
||||
+19
@@ -0,0 +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/
|
||||
RUN dotnet restore
|
||||
|
||||
COPY app/ app/
|
||||
RUN dotnet publish app/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"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
|
||||
namespace Mws.Api;
|
||||
|
||||
public static class ClaimsExtensions
|
||||
{
|
||||
public static Guid GetUserId(this ClaimsPrincipal principal)
|
||||
{
|
||||
var value = principal.FindFirstValue(JwtRegisteredClaimNames.Sub)
|
||||
?? principal.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? principal.FindFirstValue("sub");
|
||||
if (value is not null && Guid.TryParse(value, out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
throw new UnauthorizedAccessException("Invalid user identity");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Accounts;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/accounts")]
|
||||
[Authorize]
|
||||
public class AccountsController(IAccountService accountService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<AccountDto>>> GetAll([FromQuery] string? q, CancellationToken ct)
|
||||
{
|
||||
return Ok(await accountService.GetAccountsAsync(User.GetUserId(), q, ct));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<AccountDto>> Create([FromBody] CreateAccountRequest request, CancellationToken ct)
|
||||
{
|
||||
return Ok(await accountService.CreateAccountAsync(User.GetUserId(), request, ct));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<AccountDto>> Update(Guid id, [FromBody] UpdateAccountRequest request, CancellationToken ct)
|
||||
{
|
||||
return Ok(await accountService.UpdateAccountAsync(User.GetUserId(), id, request, ct));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await accountService.DeleteAccountAsync(User.GetUserId(), id, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/reset-password")]
|
||||
public async Task<IActionResult> ResetPassword(Guid id, [FromBody] ResetPasswordRequest request, CancellationToken ct)
|
||||
{
|
||||
await accountService.ResetPasswordAsync(User.GetUserId(), id, request, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Auth;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
[AllowAnonymous]
|
||||
public class AuthController(IAuthService authService) : ControllerBase
|
||||
{
|
||||
[HttpPost("login")]
|
||||
public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest request, CancellationToken ct)
|
||||
{
|
||||
var response = await authService.LoginAsync(request, ct);
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Documents;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
[Authorize]
|
||||
public class DocumentsController(IDocumentService documentService) : ControllerBase
|
||||
{
|
||||
[HttpGet("documents/search")]
|
||||
public async Task<ActionResult<List<DocumentNodeDto>>> Search([FromQuery] string? q, CancellationToken ct)
|
||||
{
|
||||
return Ok(await documentService.SearchAsync(User.GetUserId(), q ?? string.Empty, ct));
|
||||
}
|
||||
|
||||
[HttpGet("projects/{projectId:guid}/documents")]
|
||||
public async Task<ActionResult<List<DocumentNodeDto>>> GetTree(Guid projectId, CancellationToken ct)
|
||||
{
|
||||
return Ok(await documentService.GetTreeAsync(User.GetUserId(), projectId, ct));
|
||||
}
|
||||
|
||||
[HttpPost("projects/{projectId:guid}/documents")]
|
||||
public async Task<ActionResult<DocumentDto>> Create(Guid projectId, [FromBody] CreateDocumentRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await documentService.CreateAsync(User.GetUserId(), projectId, request, ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = result.Id }, result);
|
||||
}
|
||||
|
||||
[HttpGet("documents/{id:guid}")]
|
||||
public async Task<ActionResult<DocumentDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
return Ok(await documentService.GetAsync(User.GetUserId(), id, ct));
|
||||
}
|
||||
|
||||
[HttpPut("documents/{id:guid}")]
|
||||
public async Task<ActionResult<DocumentDto>> Update(Guid id, [FromBody] UpdateDocumentRequest request, CancellationToken ct)
|
||||
{
|
||||
return Ok(await documentService.UpdateAsync(User.GetUserId(), id, request, ct));
|
||||
}
|
||||
|
||||
[HttpPut("documents/{id:guid}/move")]
|
||||
public async Task<ActionResult<DocumentDto>> Move(Guid id, [FromBody] MoveDocumentRequest request, CancellationToken ct)
|
||||
{
|
||||
await documentService.MoveAsync(User.GetUserId(), id, request.NewParentId, ct);
|
||||
return Ok(await documentService.GetAsync(User.GetUserId(), id, ct));
|
||||
}
|
||||
|
||||
[HttpDelete("documents/{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await documentService.DeleteAsync(User.GetUserId(), id, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Permissions;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/menu")]
|
||||
[Authorize]
|
||||
public class MenuController(IPermissionService permissions) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<MenuItemDto>>> GetMenu(CancellationToken ct)
|
||||
{
|
||||
return Ok(await permissions.GetMenuAsync(User.GetUserId(), ct));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Projects;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/projects/{projectId:guid}/members")]
|
||||
[Authorize]
|
||||
public class ProjectMembersController(IProjectMemberService memberService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<ProjectMemberDto>>> GetAll(Guid projectId, CancellationToken ct)
|
||||
{
|
||||
return Ok(await memberService.GetMembersAsync(User.GetUserId(), projectId, ct));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ProjectMemberDto>> Add(Guid projectId, [FromBody] AddMemberRequest request, CancellationToken ct)
|
||||
{
|
||||
return Ok(await memberService.AddMemberAsync(User.GetUserId(), projectId, request, ct));
|
||||
}
|
||||
|
||||
[HttpPut("{userId:guid}/document-permissions")]
|
||||
public async Task<ActionResult<ProjectMemberDto>> UpdateDocumentPermissions(
|
||||
Guid projectId, Guid userId, [FromBody] UpdateMemberDocumentPermissionsRequest request, CancellationToken ct)
|
||||
{
|
||||
return Ok(await memberService.UpdateMemberDocumentPermissionsAsync(User.GetUserId(), projectId, userId, request, ct));
|
||||
}
|
||||
|
||||
[HttpDelete("{userId:guid}")]
|
||||
public async Task<IActionResult> Remove(Guid projectId, Guid userId, CancellationToken ct)
|
||||
{
|
||||
await memberService.RemoveMemberAsync(User.GetUserId(), projectId, userId, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Projects;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/projects")]
|
||||
[Authorize]
|
||||
public class ProjectsController(IProjectService projectService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<ProjectDto>>> GetAll(CancellationToken ct)
|
||||
{
|
||||
return Ok(await projectService.GetProjectsAsync(User.GetUserId(), ct));
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<ActionResult<List<ProjectDto>>> Search([FromQuery] string? q, CancellationToken ct)
|
||||
{
|
||||
return Ok(await projectService.SearchAsync(User.GetUserId(), q ?? string.Empty, ct));
|
||||
}
|
||||
|
||||
[HttpGet("{projectId:guid}")]
|
||||
public async Task<ActionResult<ProjectDto>> Get(Guid projectId, CancellationToken ct)
|
||||
{
|
||||
return Ok(await projectService.GetProjectAsync(User.GetUserId(), projectId, ct));
|
||||
}
|
||||
|
||||
[HttpGet("{projectId:guid}/overview")]
|
||||
public async Task<ActionResult<ProjectOverviewDto>> Overview(Guid projectId, CancellationToken ct)
|
||||
{
|
||||
return Ok(await projectService.GetOverviewAsync(User.GetUserId(), projectId, ct));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ProjectDto>> Create([FromBody] CreateProjectRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await projectService.CreateProjectAsync(User.GetUserId(), request, ct);
|
||||
return CreatedAtAction(nameof(Get), new { projectId = result.Id }, result);
|
||||
}
|
||||
|
||||
[HttpPut("{projectId:guid}")]
|
||||
public async Task<ActionResult<ProjectDto>> Update(Guid projectId, [FromBody] UpdateProjectRequest request, CancellationToken ct)
|
||||
{
|
||||
return Ok(await projectService.UpdateProjectAsync(User.GetUserId(), projectId, request, ct));
|
||||
}
|
||||
|
||||
[HttpDelete("{projectId:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid projectId, CancellationToken ct)
|
||||
{
|
||||
await projectService.ArchiveProjectAsync(User.GetUserId(), projectId, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Application.Roles;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/roles")]
|
||||
[Authorize]
|
||||
public class RolesController(IRoleService roleService, IPermissionService permissions) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<RoleDto>>> GetAll(CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "roles", PermissionAction.View, ct);
|
||||
return Ok(await roleService.GetRolesAsync(ct));
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<RoleDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "roles", PermissionAction.View, ct);
|
||||
return Ok(await roleService.GetRoleAsync(id, ct));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<RoleDto>> Create([FromBody] SaveRoleRequest request, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "roles", PermissionAction.Create, ct);
|
||||
return Ok(await roleService.CreateRoleAsync(request, ct));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<RoleDto>> Update(Guid id, [FromBody] SaveRoleRequest request, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "roles", PermissionAction.Edit, ct);
|
||||
return Ok(await roleService.UpdateRoleAsync(id, request, ct));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await permissions.EnsureAsync(User.GetUserId(), "roles", PermissionAction.Delete, ct);
|
||||
await roleService.DeleteRoleAsync(id, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
[Authorize]
|
||||
public class TasksController(ITaskService taskService) : ControllerBase
|
||||
{
|
||||
[HttpGet("tasks/search")]
|
||||
public async Task<ActionResult<List<TaskDto>>> Search([FromQuery] string? q, CancellationToken ct)
|
||||
{
|
||||
return Ok(await taskService.SearchAsync(User.GetUserId(), q ?? string.Empty, ct));
|
||||
}
|
||||
|
||||
[HttpGet("projects/{projectId:guid}/tasks")]
|
||||
public async Task<ActionResult<List<TaskDto>>> GetAll(
|
||||
Guid projectId,
|
||||
[FromQuery] string? status,
|
||||
[FromQuery] string? priority,
|
||||
[FromQuery] Guid? assigneeId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var statusValue = ParseOptional<TaskStatus>(status);
|
||||
var priorityValue = ParseOptional<TaskPriority>(priority);
|
||||
return Ok(await taskService.GetTasksAsync(User.GetUserId(), projectId, statusValue, priorityValue, assigneeId, ct));
|
||||
}
|
||||
|
||||
[HttpPost("projects/{projectId:guid}/tasks")]
|
||||
public async Task<ActionResult<TaskDto>> Create(Guid projectId, [FromBody] CreateTaskRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await taskService.CreateAsync(User.GetUserId(), projectId, request, ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = result.Id }, result);
|
||||
}
|
||||
|
||||
[HttpGet("tasks/{id:guid}")]
|
||||
public async Task<ActionResult<TaskDto>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
return Ok(await taskService.GetAsync(User.GetUserId(), id, ct));
|
||||
}
|
||||
|
||||
[HttpPut("tasks/{id:guid}")]
|
||||
public async Task<ActionResult<TaskDto>> Update(Guid id, [FromBody] UpdateTaskRequest request, CancellationToken ct)
|
||||
{
|
||||
return Ok(await taskService.UpdateAsync(User.GetUserId(), id, request, ct));
|
||||
}
|
||||
|
||||
[HttpDelete("tasks/{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await taskService.DeleteAsync(User.GetUserId(), id, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static TEnum? ParseOptional<TEnum>(string? value) where TEnum : struct, Enum
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Enum.TryParse<TEnum>(value, ignoreCase: true, out var result) ? result : throw new BadRequestException($"Invalid {typeof(TEnum).Name}: {value}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Auth;
|
||||
|
||||
namespace Mws.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/users")]
|
||||
[Authorize]
|
||||
public class UsersController(IUnitOfWork uow, IMapper mapper) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<UserDto>>> GetAll([FromQuery] string? q, CancellationToken ct)
|
||||
{
|
||||
var users = await uow.Users.SearchWithRoleAsync(q, 50, ct);
|
||||
return Ok(mapper.Map<List<UserDto>>(users));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Text.Json;
|
||||
using Mws.Application.Common;
|
||||
|
||||
namespace Mws.Api.Middleware;
|
||||
|
||||
public class ApiExceptionMiddleware(RequestDelegate next, ILogger<ApiExceptionMiddleware> logger)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await HandleAsync(context, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAsync(HttpContext context, Exception ex)
|
||||
{
|
||||
var (statusCode, message) = ex switch
|
||||
{
|
||||
BadRequestException => (StatusCodes.Status400BadRequest, ex.Message),
|
||||
UnauthorizedException => (StatusCodes.Status401Unauthorized, ex.Message),
|
||||
ForbiddenException => (StatusCodes.Status403Forbidden, ex.Message),
|
||||
NotFoundException => (StatusCodes.Status404NotFound, ex.Message),
|
||||
_ => (StatusCodes.Status500InternalServerError, "An unexpected error occurred"),
|
||||
};
|
||||
|
||||
if (statusCode == StatusCodes.Status500InternalServerError)
|
||||
{
|
||||
logger.LogError(ex, "Unhandled exception in {Path}", context.Request.Path);
|
||||
}
|
||||
|
||||
context.Response.StatusCode = statusCode;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(new { message }));
|
||||
}
|
||||
}
|
||||
|
||||
public static class ApiExceptionMiddlewareExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseApiExceptionMiddleware(this IApplicationBuilder app)
|
||||
=> app.UseMiddleware<ApiExceptionMiddleware>();
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.Text;
|
||||
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;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.Converters.Add(
|
||||
new System.Text.Json.Serialization.JsonStringEnumConverter());
|
||||
});
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "MWS API",
|
||||
Version = "v1"
|
||||
});
|
||||
|
||||
options.AddSecurityDefinition("bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
BearerFormat = "JWT",
|
||||
In = ParameterLocation.Header,
|
||||
Description = "JWT Authorization header using Bearer scheme."
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(document =>
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
[new OpenApiSecuritySchemeReference("bearer", document)] = []
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
|
||||
var jwtSection = builder.Configuration.GetSection("Jwt");
|
||||
var jwtSecret = jwtSection["Secret"];
|
||||
var jwtIssuer = jwtSection["Issuer"];
|
||||
var jwtAudience = jwtSection["Audience"];
|
||||
|
||||
builder.Services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
|
||||
ValidIssuer = jwtIssuer,
|
||||
ValidAudience = jwtAudience,
|
||||
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret))
|
||||
};
|
||||
|
||||
options.MapInboundClaims = false;
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
var origins = builder.Configuration["Cors:Origins"];
|
||||
options.AddPolicy("Frontend", policy =>
|
||||
{
|
||||
policy
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.WithOrigins(origins.Split(';', StringSplitOptions.RemoveEmptyEntries));
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseApiExceptionMiddleware();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.MigrateAsync();
|
||||
await DbSeeder.SeedAsync(db, scope.ServiceProvider.GetRequiredService<IPasswordHasher>());
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.SwaggerEndpoint(
|
||||
"/swagger/v1/swagger.json",
|
||||
"MWS API v1");
|
||||
|
||||
options.EnablePersistAuthorization();
|
||||
});
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseCors("Frontend");
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:2000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:3000;http://localhost:2000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=mws;Username=postgres;Password=Pa55w0rd"
|
||||
},
|
||||
"Jwt": {
|
||||
"Secret": "mws-development-secret-key-change-me-in-production-123456",
|
||||
"Issuer": "Mws",
|
||||
"Audience": "Mws.Clients",
|
||||
"Expiration": "12:00:00"
|
||||
},
|
||||
"Cors": {
|
||||
"Origins": "*"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>mws.backend.dotnet</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\mws.application\mws.application.csproj" />
|
||||
<ProjectReference Include="..\mws.domain\mws.domain.csproj" />
|
||||
<ProjectReference Include="..\mws.infrastructure\mws.infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,116 @@
|
||||
using AutoMapper;
|
||||
using Mws.Application.Auth;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Domain.Users;
|
||||
|
||||
namespace Mws.Application.Accounts;
|
||||
|
||||
public class AccountService(IUnitOfWork uow, IPasswordHasher passwordHasher, IPermissionService permissions, IMapper mapper) : IAccountService
|
||||
{
|
||||
private const string Screen = "accounts";
|
||||
|
||||
public async Task<List<AccountDto>> GetAccountsAsync(Guid actorUserId, string? term, CancellationToken ct = default)
|
||||
{
|
||||
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.View, ct);
|
||||
|
||||
var users = await uow.Users.SearchWithRoleAsync(term, null, ct);
|
||||
return mapper.Map<List<AccountDto>>(users);
|
||||
}
|
||||
|
||||
public async Task<AccountDto> CreateAccountAsync(Guid actorUserId, CreateAccountRequest request, CancellationToken ct = default)
|
||||
{
|
||||
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.Create, ct);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
var role = await uow.Roles.GetByIdAsync(request.RoleId, 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(),
|
||||
RoleId = role.Id,
|
||||
IsActive = true,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
uow.Users.Add(user);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
user.Role = role;
|
||||
return mapper.Map<AccountDto>(user);
|
||||
}
|
||||
|
||||
public async Task<AccountDto> UpdateAccountAsync(Guid actorUserId, Guid id, UpdateAccountRequest request, CancellationToken ct = default)
|
||||
{
|
||||
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.Edit, ct);
|
||||
|
||||
var user = await uow.Users.GetByIdWithRoleAsync(id, ct)
|
||||
?? throw new NotFoundException("Account not found");
|
||||
|
||||
var role = await uow.Roles.GetByIdAsync(request.RoleId, ct)
|
||||
?? throw new BadRequestException("Role not found");
|
||||
|
||||
user.DisplayName = request.DisplayName.Trim();
|
||||
user.RoleId = role.Id;
|
||||
user.Role = role;
|
||||
user.IsActive = request.IsActive;
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<AccountDto>(user);
|
||||
}
|
||||
|
||||
public async Task DeleteAccountAsync(Guid actorUserId, Guid id, CancellationToken ct = default)
|
||||
{
|
||||
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.Delete, ct);
|
||||
|
||||
var user = await uow.Users.GetByIdAsync(id, ct)
|
||||
?? throw new NotFoundException("Account not found");
|
||||
|
||||
var soleOwnerProjectIds = await uow.Projects.GetOwnedProjectIdsAsync(id, ct);
|
||||
|
||||
foreach (var projectId in soleOwnerProjectIds)
|
||||
{
|
||||
var ownerCount = await uow.Projects.CountOwnersAsync(projectId, ct);
|
||||
if (ownerCount <= 1)
|
||||
{
|
||||
throw new BadRequestException("Cannot delete an account that is the sole owner of a project");
|
||||
}
|
||||
}
|
||||
|
||||
uow.Users.Remove(user);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task ResetPasswordAsync(Guid actorUserId, Guid id, ResetPasswordRequest request, CancellationToken ct = default)
|
||||
{
|
||||
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.Edit, ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.NewPassword))
|
||||
{
|
||||
throw new BadRequestException("New password is required");
|
||||
}
|
||||
|
||||
var user = await uow.Users.GetByIdAsync(id, ct)
|
||||
?? throw new NotFoundException("Account not found");
|
||||
|
||||
user.PasswordHash = passwordHasher.Hash(request.NewPassword);
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Mws.Application.Accounts;
|
||||
|
||||
public class CreateAccountRequest
|
||||
{
|
||||
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 class UpdateAccountRequest
|
||||
{
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public Guid RoleId { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
public class ResetPasswordRequest
|
||||
{
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AccountDto
|
||||
{
|
||||
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 bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Mws.Application.Accounts;
|
||||
|
||||
public interface IAccountService
|
||||
{
|
||||
Task<List<AccountDto>> GetAccountsAsync(Guid actorUserId, string? term, CancellationToken ct = default);
|
||||
Task<AccountDto> CreateAccountAsync(Guid actorUserId, CreateAccountRequest request, CancellationToken ct = default);
|
||||
Task<AccountDto> UpdateAccountAsync(Guid actorUserId, Guid id, UpdateAccountRequest request, CancellationToken ct = default);
|
||||
Task DeleteAccountAsync(Guid actorUserId, Guid id, CancellationToken ct = default);
|
||||
Task ResetPasswordAsync(Guid actorUserId, Guid id, ResetPasswordRequest request, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using AutoMapper;
|
||||
using Mws.Application.Common;
|
||||
|
||||
namespace Mws.Application.Auth;
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<LoginResponse> LoginAsync(LoginRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class AuthService(IUnitOfWork uow, IPasswordHasher passwordHasher, ITokenService tokenService, IMapper mapper)
|
||||
: IAuthService
|
||||
{
|
||||
public async Task<LoginResponse> LoginAsync(LoginRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var username = request.Username.Trim();
|
||||
var user = await uow.Users.GetByUsernameWithRoleAsync(username, cancellationToken);
|
||||
|
||||
if (user is null || !passwordHasher.Verify(request.Password, user.PasswordHash))
|
||||
{
|
||||
throw new UnauthorizedException("Invalid username or password");
|
||||
}
|
||||
|
||||
if (!user.IsActive)
|
||||
{
|
||||
throw new ForbiddenException("Account disabled");
|
||||
}
|
||||
|
||||
return new LoginResponse
|
||||
{
|
||||
Token = tokenService.CreateToken(user.Id, user.Username),
|
||||
User = mapper.Map<UserDto>(user),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Mws.Application.Auth;
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
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;
|
||||
public UserDto User { get; set; } = null!;
|
||||
}
|
||||
|
||||
public interface IPasswordHasher
|
||||
{
|
||||
string Hash(string password);
|
||||
bool Verify(string password, string hash);
|
||||
}
|
||||
|
||||
public interface ITokenService
|
||||
{
|
||||
string CreateToken(Guid userId, string username);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Mws.Application.Common;
|
||||
|
||||
public class NotFoundException(string message) : Exception(message);
|
||||
|
||||
public class ForbiddenException(string message) : Exception(message);
|
||||
|
||||
public class UnauthorizedException(string message) : Exception(message);
|
||||
|
||||
public class BadRequestException(string message) : Exception(message);
|
||||
@@ -0,0 +1,14 @@
|
||||
using Mws.Application.Common.Repositories;
|
||||
|
||||
namespace Mws.Application.Common;
|
||||
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
IUserRepository Users { get; }
|
||||
IProjectRepository Projects { get; }
|
||||
ITaskRepository Tasks { get; }
|
||||
IDocumentRepository Documents { get; }
|
||||
IRoleRepository Roles { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using AutoMapper;
|
||||
using Mws.Application.Accounts;
|
||||
using Mws.Application.Auth;
|
||||
using Mws.Application.Documents;
|
||||
using Mws.Application.Projects;
|
||||
using Mws.Application.Roles;
|
||||
using Mws.Application.Tasks;
|
||||
using Mws.Domain.Documents;
|
||||
using Mws.Domain.Roles;
|
||||
using Mws.Domain.Tasks;
|
||||
using Mws.Domain.Users;
|
||||
|
||||
namespace Mws.Application.Common;
|
||||
|
||||
public class MappingProfile : Profile
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<Domain.Projects.Project, ProjectDto>();
|
||||
CreateMap<Role, RoleDto>();
|
||||
CreateMap<RolePermission, PermissionEntryDto>();
|
||||
CreateMap<User, AccountDto>();
|
||||
CreateMap<User, UserDto>();
|
||||
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,15 @@
|
||||
using Mws.Domain.Documents;
|
||||
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
|
||||
public interface IDocumentRepository : IRepository<Document>
|
||||
{
|
||||
Task<Document?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
Task<List<Document>> GetTreeForProjectAsync(Guid projectId, CancellationToken ct = default);
|
||||
Task<Document?> GetInProjectAsync(Guid parentId, Guid projectId, CancellationToken ct = default);
|
||||
Task<Guid?> GetParentIdAsync(Guid documentId, CancellationToken ct = default);
|
||||
Task<List<Document>> GetChildrenAsync(Guid parentId, CancellationToken ct = default);
|
||||
Task<List<Document>> SearchAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default);
|
||||
Task<int> CountByTypeForProjectAsync(Guid projectId, DocumentType type, CancellationToken ct = default);
|
||||
Task<List<Document>> GetRecentForProjectAsync(Guid projectId, DocumentType type, int take, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Mws.Domain.Projects;
|
||||
|
||||
namespace Mws.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<List<Project>> SearchForUserAsync(Guid userId, string? term, CancellationToken ct = default);
|
||||
Task<int> CountMembersAsync(Guid projectId, CancellationToken ct = default);
|
||||
|
||||
Task<bool> IsMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default);
|
||||
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<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);
|
||||
Task<List<Guid>> GetDocumentViewableProjectIdsAsync(Guid userId, CancellationToken ct = default);
|
||||
void AddMember(ProjectMember member);
|
||||
void RemoveMember(ProjectMember member);
|
||||
|
||||
Task<ProjectMemberPermission?> GetMemberPermissionAsync(Guid projectId, Guid userId, string screen, CancellationToken ct = default);
|
||||
Task<Dictionary<Guid, ProjectMemberPermission>> GetMemberPermissionsAsync(Guid projectId, string screen, CancellationToken ct = default);
|
||||
void AddMemberPermission(ProjectMemberPermission permission);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
|
||||
public interface IRepository<in T> where T : class
|
||||
{
|
||||
void Add(T entity);
|
||||
void Remove(T entity);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Mws.Domain.Roles;
|
||||
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
|
||||
public interface IRoleRepository : IRepository<Role>
|
||||
{
|
||||
Task<List<Role>> GetAllWithPermissionsAsync(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);
|
||||
void RemovePermissions(IEnumerable<RolePermission> permissions);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Mws.Domain.Tasks;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.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<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);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Mws.Domain.Users;
|
||||
|
||||
namespace Mws.Application.Common.Repositories;
|
||||
|
||||
public interface IUserRepository : IRepository<User>
|
||||
{
|
||||
Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
Task<User?> GetByIdWithRoleAsync(Guid id, CancellationToken ct = default);
|
||||
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<User>> SearchWithRoleAsync(string? term, int? take, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Mws.Domain.Documents;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
|
||||
public class CreateDocumentRequest
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public DocumentType Type { get; set; } = DocumentType.Document;
|
||||
public Guid? ParentId { get; set; }
|
||||
public string? Content { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateDocumentRequest
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Content { get; set; }
|
||||
}
|
||||
|
||||
public class MoveDocumentRequest
|
||||
{
|
||||
public Guid? NewParentId { get; set; }
|
||||
}
|
||||
|
||||
public class DocumentDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
public Guid? ParentId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Content { get; set; }
|
||||
public DocumentType Type { get; set; }
|
||||
public Guid CreatedBy { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid? UpdatedBy { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class DocumentNodeDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid? ParentId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public DocumentType Type { get; set; }
|
||||
public Guid CreatedBy { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid? UpdatedBy { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public List<DocumentNodeDto> Children { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
using AutoMapper;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Domain.Documents;
|
||||
using Mws.Domain.Projects;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
|
||||
public class DocumentService(IUnitOfWork uow, IMapper mapper) : IDocumentService
|
||||
{
|
||||
public async Task<List<DocumentNodeDto>> GetTreeAsync(Guid userId, Guid projectId, CancellationToken ct = default)
|
||||
{
|
||||
await EnsureDocumentPermissionAsync(userId, projectId, PermissionAction.View, ct);
|
||||
|
||||
var docs = await uow.Documents.GetTreeForProjectAsync(projectId, ct);
|
||||
|
||||
var nodes = docs.ToDictionary(d => d.Id, d => mapper.Map<DocumentNodeDto>(d));
|
||||
|
||||
var roots = new List<DocumentNodeDto>();
|
||||
foreach (var node in nodes.Values)
|
||||
{
|
||||
if (node.ParentId is { } parentId && nodes.TryGetValue(parentId, out var parent))
|
||||
{
|
||||
parent.Children.Add(node);
|
||||
}
|
||||
else
|
||||
{
|
||||
roots.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
public async Task<DocumentDto> GetAsync(Guid userId, Guid documentId, CancellationToken ct = default)
|
||||
{
|
||||
var doc = await GetDocumentForUserAsync(userId, documentId, ct);
|
||||
return mapper.Map<DocumentDto>(doc);
|
||||
}
|
||||
|
||||
public async Task<DocumentDto> CreateAsync(Guid userId, Guid projectId, CreateDocumentRequest request, CancellationToken ct = default)
|
||||
{
|
||||
await EnsureDocumentPermissionAsync(userId, projectId, PermissionAction.Create, ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
throw new BadRequestException("Title is required");
|
||||
}
|
||||
|
||||
if (request.ParentId is { } parentId)
|
||||
{
|
||||
var parent = await uow.Documents.GetInProjectAsync(parentId, projectId, ct)
|
||||
?? throw new BadRequestException("Parent document not found");
|
||||
if (parent.Type != DocumentType.Folder)
|
||||
{
|
||||
throw new BadRequestException("Parent must be a folder");
|
||||
}
|
||||
}
|
||||
|
||||
if (request.Type == DocumentType.Folder && !string.IsNullOrWhiteSpace(request.Content))
|
||||
{
|
||||
throw new BadRequestException("Folders cannot have content");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var doc = new Document
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ProjectId = projectId,
|
||||
ParentId = request.ParentId,
|
||||
Title = request.Title.Trim(),
|
||||
Type = request.Type,
|
||||
Content = request.Type == DocumentType.Document ? request.Content : null,
|
||||
CreatedBy = userId,
|
||||
CreatedAt = now,
|
||||
UpdatedBy = userId,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
uow.Documents.Add(doc);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<DocumentDto>(doc);
|
||||
}
|
||||
|
||||
public async Task<DocumentDto> UpdateAsync(Guid userId, Guid documentId, UpdateDocumentRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var doc = await GetDocumentForUserAsync(userId, documentId, ct);
|
||||
await EnsureDocumentPermissionAsync(userId, doc.ProjectId, PermissionAction.Edit, ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
throw new BadRequestException("Title is required");
|
||||
}
|
||||
|
||||
doc.Title = request.Title.Trim();
|
||||
if (doc.Type == DocumentType.Document)
|
||||
{
|
||||
doc.Content = request.Content;
|
||||
doc.UpdatedAt = DateTime.UtcNow;
|
||||
doc.UpdatedBy = userId;
|
||||
}
|
||||
else
|
||||
{
|
||||
doc.UpdatedAt = DateTime.UtcNow;
|
||||
doc.UpdatedBy = userId;
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<DocumentDto>(doc);
|
||||
}
|
||||
|
||||
public async Task MoveAsync(Guid userId, Guid documentId, Guid? newParentId, CancellationToken ct = default)
|
||||
{
|
||||
var doc = await GetDocumentForUserAsync(userId, documentId, ct);
|
||||
await EnsureDocumentPermissionAsync(userId, doc.ProjectId, PermissionAction.Edit, ct);
|
||||
|
||||
if (newParentId == documentId)
|
||||
{
|
||||
throw new BadRequestException("A document cannot be moved into itself");
|
||||
}
|
||||
|
||||
if (newParentId is { } parentId)
|
||||
{
|
||||
var parent = await uow.Documents.GetByIdAsync(parentId, ct)
|
||||
?? throw new NotFoundException("Parent folder not found");
|
||||
if (parent.ProjectId != doc.ProjectId)
|
||||
{
|
||||
throw new BadRequestException("Parent must belong to the same project");
|
||||
}
|
||||
if (parent.Type != DocumentType.Folder)
|
||||
{
|
||||
throw new BadRequestException("Parent must be a folder");
|
||||
}
|
||||
|
||||
var cursor = parent.ParentId;
|
||||
while (cursor is not null)
|
||||
{
|
||||
if (cursor == documentId)
|
||||
{
|
||||
throw new BadRequestException("A folder cannot be moved into its own descendant");
|
||||
}
|
||||
cursor = await uow.Documents.GetParentIdAsync(cursor.Value, ct);
|
||||
}
|
||||
}
|
||||
|
||||
doc.ParentId = newParentId;
|
||||
doc.UpdatedAt = DateTime.UtcNow;
|
||||
doc.UpdatedBy = userId;
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid userId, Guid documentId, CancellationToken ct = default)
|
||||
{
|
||||
var doc = await GetDocumentForUserAsync(userId, documentId, ct);
|
||||
await EnsureDocumentPermissionAsync(userId, doc.ProjectId, PermissionAction.Delete, ct);
|
||||
|
||||
await DeleteDescendantsAsync(documentId, ct);
|
||||
uow.Documents.Remove(doc);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<List<DocumentNodeDto>> SearchAsync(Guid userId, string term, CancellationToken ct = default)
|
||||
{
|
||||
var projectIds = await uow.Projects.GetDocumentViewableProjectIdsAsync(userId, ct);
|
||||
var results = await uow.Documents.SearchAsync(projectIds, term, 20, ct);
|
||||
return mapper.Map<List<DocumentNodeDto>>(results);
|
||||
}
|
||||
|
||||
private async Task DeleteDescendantsAsync(Guid parentId, CancellationToken ct)
|
||||
{
|
||||
var children = await uow.Documents.GetChildrenAsync(parentId, ct);
|
||||
foreach (var child in children)
|
||||
{
|
||||
await DeleteDescendantsAsync(child.Id, ct);
|
||||
uow.Documents.Remove(child);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureDocumentPermissionAsync(
|
||||
Guid userId, Guid projectId, PermissionAction action, CancellationToken ct = default)
|
||||
{
|
||||
var member = await uow.Projects.GetMemberAsync(projectId, userId, ct)
|
||||
?? throw new NotFoundException("Project not found");
|
||||
|
||||
if (!await HasDocumentPermissionAsync(member, action, ct))
|
||||
{
|
||||
throw new ForbiddenException("You do not have permission to access documents in this project");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> HasDocumentPermissionAsync(ProjectMember member, PermissionAction action, CancellationToken ct)
|
||||
{
|
||||
if (member.Role == MemberRole.Owner)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var permission = await uow.Projects.GetMemberPermissionAsync(member.ProjectId, member.UserId, ProjectPermissionScreens.Documents, ct);
|
||||
|
||||
return action switch
|
||||
{
|
||||
PermissionAction.View => permission?.CanView ?? false,
|
||||
PermissionAction.Create => permission?.CanCreate ?? false,
|
||||
PermissionAction.Edit => permission?.CanEdit ?? false,
|
||||
PermissionAction.Delete => permission?.CanDelete ?? false,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Document> GetDocumentForUserAsync(Guid userId, Guid documentId, CancellationToken ct = default)
|
||||
{
|
||||
var doc = await uow.Documents.GetByIdAsync(documentId, ct)
|
||||
?? throw new NotFoundException("Document not found");
|
||||
|
||||
var member = await uow.Projects.GetMemberAsync(doc.ProjectId, userId, ct);
|
||||
|
||||
if (member is null)
|
||||
{
|
||||
throw new NotFoundException("Document not found");
|
||||
}
|
||||
|
||||
if (!await HasDocumentPermissionAsync(member, PermissionAction.View, ct))
|
||||
{
|
||||
throw new ForbiddenException("You do not have permission to view this document");
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Mws.Application.Common;
|
||||
|
||||
namespace Mws.Application.Documents;
|
||||
|
||||
public interface IDocumentService
|
||||
{
|
||||
Task<List<DocumentNodeDto>> GetTreeAsync(Guid userId, Guid projectId, CancellationToken ct = default);
|
||||
Task<DocumentDto> GetAsync(Guid userId, Guid documentId, CancellationToken ct = default);
|
||||
Task<DocumentDto> CreateAsync(Guid userId, Guid projectId, CreateDocumentRequest request, CancellationToken ct = default);
|
||||
Task<DocumentDto> UpdateAsync(Guid userId, Guid documentId, UpdateDocumentRequest request, CancellationToken ct = default);
|
||||
Task MoveAsync(Guid userId, Guid documentId, Guid? newParentId, CancellationToken ct = default);
|
||||
Task DeleteAsync(Guid userId, Guid documentId, CancellationToken ct = default);
|
||||
Task<List<DocumentNodeDto>> SearchAsync(Guid userId, string term, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Mws.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; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Mws.Application.Permissions;
|
||||
|
||||
public interface IPermissionService
|
||||
{
|
||||
Task<List<MenuItemDto>> GetMenuAsync(Guid userId, CancellationToken ct = default);
|
||||
Task EnsureAsync(Guid userId, string screen, PermissionAction action, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Mws.Application.Common;
|
||||
|
||||
namespace Mws.Application.Permissions;
|
||||
|
||||
public class PermissionService(IUnitOfWork uow) : IPermissionService
|
||||
{
|
||||
public async Task<List<MenuItemDto>> GetMenuAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var roleId = await uow.Users.GetRoleIdAsync(userId, ct);
|
||||
var permissions = await uow.Roles.GetPermissionsAsync(roleId, ct);
|
||||
|
||||
return ScreenCatalog.Screens.Select(s =>
|
||||
{
|
||||
permissions.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 roleId = await uow.Users.GetRoleIdAsync(userId, ct);
|
||||
var permission = await uow.Roles.GetPermissionAsync(roleId, screen, ct);
|
||||
|
||||
var allowed = action switch
|
||||
{
|
||||
PermissionAction.View => permission?.CanView ?? false,
|
||||
PermissionAction.Create => permission?.CanCreate ?? false,
|
||||
PermissionAction.Edit => permission?.CanEdit ?? false,
|
||||
PermissionAction.Delete => permission?.CanDelete ?? false,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (!allowed)
|
||||
{
|
||||
throw new ForbiddenException($"Not permitted to {action} on {screen}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Mws.Application.Permissions;
|
||||
|
||||
public record ScreenDefinition(string Key, string Label, string Path);
|
||||
|
||||
public static class ScreenCatalog
|
||||
{
|
||||
public static readonly IReadOnlyList<ScreenDefinition> Screens =
|
||||
[
|
||||
new("dashboard", "Dashboard", "/"),
|
||||
new("projects", "Projects", "/projects"),
|
||||
new("tasks", "Tasks", "/tasks"),
|
||||
new("documents", "Documents", "/documents"),
|
||||
new("accounts", "Accounts", "/accounts"),
|
||||
new("roles", "Roles", "/roles"),
|
||||
];
|
||||
|
||||
public static readonly IReadOnlySet<string> Keys = Screens.Select(s => s.Key).ToHashSet();
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Mws.Domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
|
||||
public class CreateProjectRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateProjectRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public ProjectStatus Status { get; set; } = ProjectStatus.Active;
|
||||
}
|
||||
|
||||
public class AddMemberRequest
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public MemberRole Role { get; set; } = MemberRole.Member;
|
||||
}
|
||||
|
||||
public class ProjectMemberDto
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public MemberRole Role { get; set; }
|
||||
public bool CanViewDocuments { get; set; }
|
||||
public bool CanCreateDocuments { get; set; }
|
||||
public bool CanEditDocuments { get; set; }
|
||||
public bool CanDeleteDocuments { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateMemberDocumentPermissionsRequest
|
||||
{
|
||||
public bool CanViewDocuments { get; set; }
|
||||
public bool CanCreateDocuments { get; set; }
|
||||
public bool CanEditDocuments { get; set; }
|
||||
public bool CanDeleteDocuments { get; set; }
|
||||
}
|
||||
|
||||
public class ProjectDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public ProjectStatus Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class ProjectOverviewDto
|
||||
{
|
||||
public ProjectDto Project { get; set; } = null!;
|
||||
public int MemberCount { get; set; }
|
||||
public int DocumentCount { get; set; }
|
||||
public Dictionary<string, int> TaskCountsByStatus { get; set; } = [];
|
||||
public List<RecentTaskDto> RecentTasks { get; set; } = [];
|
||||
public List<RecentDocumentDto> RecentDocuments { get; set; } = [];
|
||||
}
|
||||
|
||||
public class RecentTaskDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class RecentDocumentDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Mws.Application.Common;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
|
||||
public interface IProjectService
|
||||
{
|
||||
Task<List<ProjectDto>> GetProjectsAsync(Guid userId, CancellationToken ct = default);
|
||||
Task<ProjectDto> GetProjectAsync(Guid userId, Guid projectId, CancellationToken ct = default);
|
||||
Task<ProjectDto> CreateProjectAsync(Guid userId, CreateProjectRequest request, CancellationToken ct = default);
|
||||
Task<ProjectDto> UpdateProjectAsync(Guid userId, Guid projectId, UpdateProjectRequest request, CancellationToken ct = default);
|
||||
Task ArchiveProjectAsync(Guid userId, Guid projectId, CancellationToken ct = default);
|
||||
Task<ProjectOverviewDto> GetOverviewAsync(Guid userId, Guid projectId, CancellationToken ct = default);
|
||||
Task<List<ProjectDto>> SearchAsync(Guid userId, string term, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IProjectMemberService
|
||||
{
|
||||
Task<List<ProjectMemberDto>> GetMembersAsync(Guid userId, Guid projectId, CancellationToken ct = default);
|
||||
Task<ProjectMemberDto> AddMemberAsync(Guid userId, Guid projectId, AddMemberRequest request, CancellationToken ct = default);
|
||||
Task<ProjectMemberDto> UpdateMemberDocumentPermissionsAsync(Guid userId, Guid projectId, Guid memberUserId, UpdateMemberDocumentPermissionsRequest request, CancellationToken ct = default);
|
||||
Task RemoveMemberAsync(Guid userId, Guid projectId, Guid memberUserId, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
|
||||
public class ProjectMemberService(IUnitOfWork uow) : IProjectMemberService
|
||||
{
|
||||
public async Task<List<ProjectMemberDto>> GetMembersAsync(Guid userId, Guid projectId, CancellationToken ct = default)
|
||||
{
|
||||
var isMember = await uow.Projects.IsMemberAsync(projectId, userId, ct);
|
||||
if (!isMember)
|
||||
{
|
||||
throw new NotFoundException("Project not found");
|
||||
}
|
||||
|
||||
var members = await uow.Projects.GetMembersWithUserAsync(projectId, ct);
|
||||
var permissions = await uow.Projects.GetMemberPermissionsAsync(projectId, ProjectPermissionScreens.Documents, ct);
|
||||
|
||||
return members.Select(m =>
|
||||
{
|
||||
permissions.TryGetValue(m.UserId, out var p);
|
||||
return new ProjectMemberDto
|
||||
{
|
||||
UserId = m.UserId,
|
||||
Username = m.User.Username,
|
||||
DisplayName = m.User.DisplayName,
|
||||
Role = m.Role,
|
||||
CanViewDocuments = m.Role == MemberRole.Owner || (p?.CanView ?? false),
|
||||
CanCreateDocuments = m.Role == MemberRole.Owner || (p?.CanCreate ?? false),
|
||||
CanEditDocuments = m.Role == MemberRole.Owner || (p?.CanEdit ?? false),
|
||||
CanDeleteDocuments = m.Role == MemberRole.Owner || (p?.CanDelete ?? false),
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<ProjectMemberDto> AddMemberAsync(Guid userId, Guid projectId, AddMemberRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (await GetMemberRoleAsync(userId, projectId, ct) != MemberRole.Owner)
|
||||
{
|
||||
throw new ForbiddenException("Only the project owner can add members");
|
||||
}
|
||||
|
||||
var user = await uow.Users.GetByIdAsync(request.UserId, ct)
|
||||
?? throw new NotFoundException("User not found");
|
||||
|
||||
var already = await uow.Projects.IsMemberAsync(projectId, request.UserId, ct);
|
||||
if (already)
|
||||
{
|
||||
throw new BadRequestException("User is already a member of this project");
|
||||
}
|
||||
|
||||
var role = request.Role is MemberRole.Owner or MemberRole.Member ? request.Role : MemberRole.Member;
|
||||
var member = new ProjectMember
|
||||
{
|
||||
ProjectId = projectId,
|
||||
UserId = request.UserId,
|
||||
Role = role,
|
||||
};
|
||||
|
||||
uow.Projects.AddMember(member);
|
||||
uow.Projects.AddMemberPermission(new ProjectMemberPermission
|
||||
{
|
||||
ProjectId = projectId,
|
||||
UserId = request.UserId,
|
||||
Screen = ProjectPermissionScreens.Documents,
|
||||
CanView = true,
|
||||
CanCreate = role == MemberRole.Owner,
|
||||
CanEdit = role == MemberRole.Owner,
|
||||
CanDelete = role == MemberRole.Owner,
|
||||
});
|
||||
await uow.SaveChangesAsync(ct);
|
||||
|
||||
return new ProjectMemberDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
Username = user.Username,
|
||||
DisplayName = user.DisplayName,
|
||||
Role = member.Role,
|
||||
CanViewDocuments = true,
|
||||
CanCreateDocuments = role == MemberRole.Owner,
|
||||
CanEditDocuments = role == MemberRole.Owner,
|
||||
CanDeleteDocuments = role == MemberRole.Owner,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ProjectMemberDto> UpdateMemberDocumentPermissionsAsync(
|
||||
Guid userId, Guid projectId, Guid memberUserId, UpdateMemberDocumentPermissionsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (await GetMemberRoleAsync(userId, projectId, ct) != MemberRole.Owner)
|
||||
{
|
||||
throw new ForbiddenException("Only the project owner can change member permissions");
|
||||
}
|
||||
|
||||
var member = await uow.Projects.GetMemberWithUserAsync(projectId, memberUserId, ct)
|
||||
?? throw new NotFoundException("Member not found in project");
|
||||
|
||||
if (member.Role == MemberRole.Owner)
|
||||
{
|
||||
throw new BadRequestException("Owner permissions cannot be changed");
|
||||
}
|
||||
|
||||
var permission = await uow.Projects.GetMemberPermissionAsync(projectId, memberUserId, ProjectPermissionScreens.Documents, ct);
|
||||
if (permission is null)
|
||||
{
|
||||
permission = new ProjectMemberPermission { ProjectId = projectId, UserId = memberUserId, Screen = ProjectPermissionScreens.Documents };
|
||||
uow.Projects.AddMemberPermission(permission);
|
||||
}
|
||||
|
||||
permission.CanView = request.CanViewDocuments;
|
||||
permission.CanCreate = request.CanCreateDocuments;
|
||||
permission.CanEdit = request.CanEditDocuments;
|
||||
permission.CanDelete = request.CanDeleteDocuments;
|
||||
await uow.SaveChangesAsync(ct);
|
||||
|
||||
return new ProjectMemberDto
|
||||
{
|
||||
UserId = member.UserId,
|
||||
Username = member.User.Username,
|
||||
DisplayName = member.User.DisplayName,
|
||||
Role = member.Role,
|
||||
CanViewDocuments = permission.CanView,
|
||||
CanCreateDocuments = permission.CanCreate,
|
||||
CanEditDocuments = permission.CanEdit,
|
||||
CanDeleteDocuments = permission.CanDelete,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task RemoveMemberAsync(Guid userId, Guid projectId, Guid memberUserId, CancellationToken ct = default)
|
||||
{
|
||||
if (await GetMemberRoleAsync(userId, projectId, ct) != MemberRole.Owner)
|
||||
{
|
||||
throw new ForbiddenException("Only the project owner can remove members");
|
||||
}
|
||||
|
||||
var member = await uow.Projects.GetMemberAsync(projectId, memberUserId, ct)
|
||||
?? throw new NotFoundException("Member not found in project");
|
||||
|
||||
var owners = await uow.Projects.CountOwnersAsync(projectId, ct);
|
||||
|
||||
if (member.Role == MemberRole.Owner && owners <= 1)
|
||||
{
|
||||
throw new BadRequestException("Cannot remove the last owner of the project");
|
||||
}
|
||||
|
||||
uow.Projects.RemoveMember(member);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
private Task<MemberRole?> GetMemberRoleAsync(Guid userId, Guid projectId, CancellationToken ct = default) =>
|
||||
uow.Projects.GetMemberRoleAsync(projectId, userId, ct);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using AutoMapper;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Documents;
|
||||
using Mws.Domain.Projects;
|
||||
|
||||
namespace Mws.Application.Projects;
|
||||
|
||||
public class ProjectService(IUnitOfWork uow, IMapper mapper) : IProjectService
|
||||
{
|
||||
public async Task<List<ProjectDto>> GetProjectsAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var projects = await uow.Projects.GetForUserAsync(userId, ct);
|
||||
return mapper.Map<List<ProjectDto>>(projects);
|
||||
}
|
||||
|
||||
public async Task<ProjectDto> GetProjectAsync(Guid userId, Guid projectId, CancellationToken ct = default)
|
||||
{
|
||||
var project = await GetProjectForUserAsync(userId, projectId, ct);
|
||||
return mapper.Map<ProjectDto>(project);
|
||||
}
|
||||
|
||||
public async Task<ProjectDto> CreateProjectAsync(Guid userId, CreateProjectRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new BadRequestException("Project name is required");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var project = new Project
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = request.Name.Trim(),
|
||||
Description = request.Description,
|
||||
Status = ProjectStatus.Active,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
project.Members.Add(new ProjectMember
|
||||
{
|
||||
ProjectId = project.Id,
|
||||
UserId = userId,
|
||||
Role = MemberRole.Owner,
|
||||
});
|
||||
|
||||
uow.Projects.Add(project);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<ProjectDto>(project);
|
||||
}
|
||||
|
||||
public async Task<ProjectDto> UpdateProjectAsync(Guid userId, Guid projectId, UpdateProjectRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var project = await GetProjectForUserAsync(userId, projectId, ct);
|
||||
|
||||
if (MemberRole.Owner != await GetMemberRoleAsync(userId, projectId, ct))
|
||||
{
|
||||
throw new ForbiddenException("Only the project owner can update the project");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new BadRequestException("Project name is required");
|
||||
}
|
||||
|
||||
project.Name = request.Name.Trim();
|
||||
project.Description = request.Description;
|
||||
project.Status = request.Status;
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<ProjectDto>(project);
|
||||
}
|
||||
|
||||
public async Task ArchiveProjectAsync(Guid userId, Guid projectId, CancellationToken ct = default)
|
||||
{
|
||||
var project = await GetProjectForUserAsync(userId, projectId, ct);
|
||||
|
||||
if (await GetMemberRoleAsync(userId, projectId, ct) != MemberRole.Owner)
|
||||
{
|
||||
throw new ForbiddenException("Only the project owner can archive the project");
|
||||
}
|
||||
|
||||
project.Status = ProjectStatus.Archived;
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<ProjectOverviewDto> GetOverviewAsync(Guid userId, Guid projectId, CancellationToken ct = default)
|
||||
{
|
||||
var project = await GetProjectForUserAsync(userId, projectId, ct);
|
||||
|
||||
var memberCount = await uow.Projects.CountMembersAsync(projectId, ct);
|
||||
var documentCount = await uow.Documents.CountByTypeForProjectAsync(projectId, DocumentType.Document, ct);
|
||||
|
||||
var taskCounts = await uow.Tasks.CountByStatusForProjectAsync(projectId, ct);
|
||||
var taskCountsByStatus = taskCounts.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value);
|
||||
|
||||
var recentTasks = await uow.Tasks.GetRecentForProjectAsync(projectId, 5, ct);
|
||||
var recentDocuments = await uow.Documents.GetRecentForProjectAsync(projectId, DocumentType.Document, 5, ct);
|
||||
|
||||
return new ProjectOverviewDto
|
||||
{
|
||||
Project = mapper.Map<ProjectDto>(project),
|
||||
MemberCount = memberCount,
|
||||
DocumentCount = documentCount,
|
||||
TaskCountsByStatus = taskCountsByStatus,
|
||||
RecentTasks = mapper.Map<List<RecentTaskDto>>(recentTasks),
|
||||
RecentDocuments = mapper.Map<List<RecentDocumentDto>>(recentDocuments),
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<ProjectDto>> SearchAsync(Guid userId, string term, CancellationToken ct = default)
|
||||
{
|
||||
var projects = await uow.Projects.SearchForUserAsync(userId, term, ct);
|
||||
return mapper.Map<List<ProjectDto>>(projects);
|
||||
}
|
||||
|
||||
protected async Task<Project> GetProjectForUserAsync(Guid userId, Guid projectId, CancellationToken ct = default)
|
||||
{
|
||||
return await uow.Projects.GetForUserAsync(userId, projectId, ct)
|
||||
?? throw new NotFoundException("Project not found");
|
||||
}
|
||||
|
||||
protected Task<MemberRole?> GetMemberRoleAsync(Guid userId, Guid projectId, CancellationToken ct = default) =>
|
||||
uow.Projects.GetMemberRoleAsync(projectId, userId, ct);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Mws.Application.Roles;
|
||||
|
||||
public class PermissionEntryDto
|
||||
{
|
||||
public string Screen { get; set; } = string.Empty;
|
||||
public bool CanView { get; set; }
|
||||
public bool CanCreate { get; set; }
|
||||
public bool CanEdit { get; set; }
|
||||
public bool CanDelete { get; set; }
|
||||
}
|
||||
|
||||
public class RoleDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsSystem { get; set; }
|
||||
public List<PermissionEntryDto> Permissions { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SaveRoleRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<PermissionEntryDto> Permissions { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Mws.Application.Roles;
|
||||
|
||||
public interface IRoleService
|
||||
{
|
||||
Task<List<RoleDto>> GetRolesAsync(CancellationToken ct = default);
|
||||
Task<RoleDto> GetRoleAsync(Guid id, CancellationToken ct = default);
|
||||
Task<RoleDto> CreateRoleAsync(SaveRoleRequest request, CancellationToken ct = default);
|
||||
Task<RoleDto> UpdateRoleAsync(Guid id, SaveRoleRequest request, CancellationToken ct = default);
|
||||
Task DeleteRoleAsync(Guid id, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using AutoMapper;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Domain.Roles;
|
||||
|
||||
namespace Mws.Application.Roles;
|
||||
|
||||
public class RoleService(IUnitOfWork uow, IMapper mapper) : IRoleService
|
||||
{
|
||||
public async Task<List<RoleDto>> GetRolesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var roles = await uow.Roles.GetAllWithPermissionsAsync(ct);
|
||||
return mapper.Map<List<RoleDto>>(roles);
|
||||
}
|
||||
|
||||
public async Task<RoleDto> GetRoleAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var role = await uow.Roles.GetByIdWithPermissionsAsync(id, ct)
|
||||
?? throw new NotFoundException("Role not found");
|
||||
return mapper.Map<RoleDto>(role);
|
||||
}
|
||||
|
||||
public async Task<RoleDto> CreateRoleAsync(SaveRoleRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var name = request.Name.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new BadRequestException("Role name is required");
|
||||
}
|
||||
|
||||
if (await uow.Roles.ExistsByNameAsync(name, null, ct))
|
||||
{
|
||||
throw new BadRequestException("Role name already exists");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var role = new Role
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
IsSystem = false,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
Permissions = BuildPermissions(request.Permissions),
|
||||
};
|
||||
|
||||
uow.Roles.Add(role);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<RoleDto>(role);
|
||||
}
|
||||
|
||||
public async Task<RoleDto> UpdateRoleAsync(Guid id, SaveRoleRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var role = await uow.Roles.GetByIdWithPermissionsAsync(id, ct)
|
||||
?? throw new NotFoundException("Role not found");
|
||||
|
||||
var name = request.Name.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new BadRequestException("Role name is required");
|
||||
}
|
||||
|
||||
if (await uow.Roles.ExistsByNameAsync(name, id, ct))
|
||||
{
|
||||
throw new BadRequestException("Role name already exists");
|
||||
}
|
||||
|
||||
role.Name = name;
|
||||
role.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
uow.Roles.RemovePermissions(role.Permissions.ToList());
|
||||
role.Permissions = BuildPermissions(request.Permissions);
|
||||
foreach (var p in role.Permissions)
|
||||
{
|
||||
p.RoleId = role.Id;
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return mapper.Map<RoleDto>(role);
|
||||
}
|
||||
|
||||
public async Task DeleteRoleAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var role = await uow.Roles.GetByIdAsync(id, ct)
|
||||
?? throw new NotFoundException("Role not found");
|
||||
|
||||
if (role.IsSystem)
|
||||
{
|
||||
throw new BadRequestException("Cannot delete a system role");
|
||||
}
|
||||
|
||||
if (await uow.Users.ExistsByRoleIdAsync(id, ct))
|
||||
{
|
||||
throw new BadRequestException("Cannot delete a role that is assigned to accounts");
|
||||
}
|
||||
|
||||
uow.Roles.Remove(role);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
private static List<RolePermission> BuildPermissions(List<PermissionEntryDto> entries)
|
||||
{
|
||||
var byScreen = entries.Where(e => ScreenCatalog.Keys.Contains(e.Screen)).ToDictionary(e => e.Screen);
|
||||
|
||||
return ScreenCatalog.Keys.Select(key =>
|
||||
{
|
||||
byScreen.TryGetValue(key, out var entry);
|
||||
return new RolePermission
|
||||
{
|
||||
Screen = key,
|
||||
CanView = entry?.CanView ?? false,
|
||||
CanCreate = entry?.CanCreate ?? false,
|
||||
CanEdit = entry?.CanEdit ?? false,
|
||||
CanDelete = entry?.CanDelete ?? false,
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Mws.Domain.Tasks;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
|
||||
public class CreateTaskRequest
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public TaskStatus? Status { get; set; }
|
||||
public TaskPriority? Priority { get; set; }
|
||||
public Guid? AssigneeId { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateTaskRequest
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public TaskStatus Status { get; set; } = TaskStatus.Todo;
|
||||
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
||||
public Guid? AssigneeId { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
}
|
||||
|
||||
public class TaskDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public TaskStatus Status { get; set; }
|
||||
public TaskPriority Priority { get; set; }
|
||||
public Guid? AssigneeId { get; set; }
|
||||
public string? AssigneeName { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Mws.Application.Common;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
|
||||
public interface ITaskService
|
||||
{
|
||||
Task<List<TaskDto>> GetTasksAsync(Guid userId, Guid projectId, TaskStatus? status, TaskPriority? priority, Guid? assigneeId, CancellationToken ct = default);
|
||||
Task<TaskDto> GetAsync(Guid userId, Guid taskId, CancellationToken ct = default);
|
||||
Task<TaskDto> CreateAsync(Guid userId, Guid projectId, CreateTaskRequest request, CancellationToken ct = default);
|
||||
Task<TaskDto> UpdateAsync(Guid userId, Guid taskId, UpdateTaskRequest request, CancellationToken ct = default);
|
||||
Task DeleteAsync(Guid userId, Guid taskId, CancellationToken ct = default);
|
||||
Task<List<TaskDto>> SearchAsync(Guid userId, string term, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using AutoMapper;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Domain.Tasks;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.Application.Tasks;
|
||||
|
||||
public class TaskService(IUnitOfWork uow, IMapper mapper) : ITaskService
|
||||
{
|
||||
public async Task<List<TaskDto>> GetTasksAsync(Guid userId, Guid projectId, TaskStatus? status, TaskPriority? priority, Guid? assigneeId, CancellationToken ct = default)
|
||||
{
|
||||
await EnsureMemberAccessAsync(userId, projectId, ct);
|
||||
|
||||
var tasks = await uow.Tasks.GetForProjectAsync(projectId, status, priority, assigneeId, ct);
|
||||
return mapper.Map<List<TaskDto>>(tasks);
|
||||
}
|
||||
|
||||
public async Task<TaskDto> GetAsync(Guid userId, Guid taskId, CancellationToken ct = default)
|
||||
{
|
||||
var task = await GetTaskForUserAsync(userId, taskId, ct);
|
||||
return await GetDtoAsync(task.Id, ct);
|
||||
}
|
||||
|
||||
public async Task<TaskDto> CreateAsync(Guid userId, Guid projectId, CreateTaskRequest request, CancellationToken ct = default)
|
||||
{
|
||||
await EnsureMemberAccessAsync(userId, projectId, ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
throw new BadRequestException("Task title is required");
|
||||
}
|
||||
|
||||
if (request.AssigneeId.HasValue && !await IsMemberAsync(request.AssigneeId.Value, projectId, ct))
|
||||
{
|
||||
throw new BadRequestException("Assignee must be a member of the project");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var task = new TaskItem
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ProjectId = projectId,
|
||||
Title = request.Title.Trim(),
|
||||
Description = request.Description,
|
||||
Status = request.Status ?? TaskStatus.Todo,
|
||||
Priority = request.Priority ?? TaskPriority.Medium,
|
||||
AssigneeId = request.AssigneeId,
|
||||
DueDate = request.DueDate,
|
||||
CreatedBy = userId,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
uow.Tasks.Add(task);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return await GetDtoAsync(task.Id, ct);
|
||||
}
|
||||
|
||||
public async Task<TaskDto> UpdateAsync(Guid userId, Guid taskId, UpdateTaskRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var task = await GetTaskForUserAsync(userId, taskId, ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
throw new BadRequestException("Task title is required");
|
||||
}
|
||||
|
||||
if (request.AssigneeId.HasValue && !await IsMemberAsync(request.AssigneeId.Value, task.ProjectId, ct))
|
||||
{
|
||||
throw new BadRequestException("Assignee must be a member of the project");
|
||||
}
|
||||
|
||||
task.Title = request.Title.Trim();
|
||||
task.Description = request.Description;
|
||||
task.Status = request.Status;
|
||||
task.Priority = request.Priority;
|
||||
task.AssigneeId = request.AssigneeId;
|
||||
task.DueDate = request.DueDate;
|
||||
task.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await uow.SaveChangesAsync(ct);
|
||||
return await GetDtoAsync(task.Id, ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid userId, Guid taskId, CancellationToken ct = default)
|
||||
{
|
||||
var task = await GetTaskForUserAsync(userId, taskId, ct);
|
||||
uow.Tasks.Remove(task);
|
||||
await uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<List<TaskDto>> SearchAsync(Guid userId, string term, CancellationToken ct = default)
|
||||
{
|
||||
var projectIds = await uow.Projects.GetProjectIdsForUserAsync(userId, ct);
|
||||
var tasks = await uow.Tasks.SearchWithAssigneeAsync(projectIds, term, 20, ct);
|
||||
return mapper.Map<List<TaskDto>>(tasks);
|
||||
}
|
||||
|
||||
private async Task<TaskDto> GetDtoAsync(Guid id, CancellationToken ct)
|
||||
{
|
||||
var task = await uow.Tasks.GetWithAssigneeAsync(id, ct)
|
||||
?? throw new NotFoundException("Task not found");
|
||||
return mapper.Map<TaskDto>(task);
|
||||
}
|
||||
|
||||
private async Task<TaskItem> GetTaskForUserAsync(Guid userId, Guid taskId, CancellationToken ct = default)
|
||||
{
|
||||
var task = await uow.Tasks.GetByIdAsync(taskId, ct)
|
||||
?? throw new NotFoundException("Task not found");
|
||||
|
||||
if (!await IsMemberAsync(userId, task.ProjectId, ct))
|
||||
{
|
||||
throw new NotFoundException("Task not found");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private async Task EnsureMemberAccessAsync(Guid userId, Guid projectId, CancellationToken ct = default)
|
||||
{
|
||||
if (!await IsMemberAsync(userId, projectId, ct))
|
||||
{
|
||||
throw new NotFoundException("Project not found");
|
||||
}
|
||||
}
|
||||
|
||||
private Task<bool> IsMemberAsync(Guid userId, Guid projectId, CancellationToken ct = default) =>
|
||||
uow.Projects.IsMemberAsync(projectId, userId, ct);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\mws.domain\mws.domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mws.api", "mws.api\mws.api.csproj", "{9099030B-4019-410C-AEBE-83099B58ABA6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mws.application", "mws.application\mws.application.csproj", "{D876692E-C85A-49CB-8897-18D6C884CB0F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mws.domain", "mws.domain\mws.domain.csproj", "{338B6456-F229-4738-85EE-3AD2D84052C6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mws.infrastructure", "mws.infrastructure\mws.infrastructure.csproj", "{FEFFBC10-B99E-4A56-9F32-6D925D192606}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{9099030B-4019-410C-AEBE-83099B58ABA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9099030B-4019-410C-AEBE-83099B58ABA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9099030B-4019-410C-AEBE-83099B58ABA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9099030B-4019-410C-AEBE-83099B58ABA6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D876692E-C85A-49CB-8897-18D6C884CB0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D876692E-C85A-49CB-8897-18D6C884CB0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D876692E-C85A-49CB-8897-18D6C884CB0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D876692E-C85A-49CB-8897-18D6C884CB0F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{338B6456-F229-4738-85EE-3AD2D84052C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{338B6456-F229-4738-85EE-3AD2D84052C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{338B6456-F229-4738-85EE-3AD2D84052C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{338B6456-F229-4738-85EE-3AD2D84052C6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FEFFBC10-B99E-4A56-9F32-6D925D192606}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FEFFBC10-B99E-4A56-9F32-6D925D192606}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FEFFBC10-B99E-4A56-9F32-6D925D192606}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FEFFBC10-B99E-4A56-9F32-6D925D192606}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Mws.Domain.Documents;
|
||||
|
||||
public enum DocumentType
|
||||
{
|
||||
Folder = 1,
|
||||
Document = 2,
|
||||
}
|
||||
|
||||
public class Document
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
public Guid? ParentId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Content { get; set; }
|
||||
public DocumentType Type { get; set; } = DocumentType.Document;
|
||||
public Guid CreatedBy { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid? UpdatedBy { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public Document? Parent { get; set; }
|
||||
public List<Document> Children { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Mws.Domain.Projects;
|
||||
|
||||
public enum ProjectStatus
|
||||
{
|
||||
Active = 1,
|
||||
Archived = 2,
|
||||
}
|
||||
|
||||
public class Project
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public ProjectStatus Status { get; set; } = ProjectStatus.Active;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public List<ProjectMember> Members { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Mws.Domain.Projects;
|
||||
|
||||
public enum MemberRole
|
||||
{
|
||||
Owner = 1,
|
||||
Member = 2,
|
||||
}
|
||||
|
||||
public class ProjectMember
|
||||
{
|
||||
public Guid ProjectId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public MemberRole Role { get; set; } = MemberRole.Member;
|
||||
|
||||
public Project Project { get; set; } = null!;
|
||||
public Mws.Domain.Users.User User { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Mws.Domain.Projects;
|
||||
|
||||
public static class ProjectPermissionScreens
|
||||
{
|
||||
public const string Documents = "documents";
|
||||
}
|
||||
|
||||
public class ProjectMemberPermission
|
||||
{
|
||||
public Guid ProjectId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string Screen { get; set; } = string.Empty;
|
||||
public bool CanView { get; set; }
|
||||
public bool CanCreate { get; set; }
|
||||
public bool CanEdit { get; set; }
|
||||
public bool CanDelete { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Mws.Domain.Roles;
|
||||
|
||||
public class Role
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsSystem { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public List<RolePermission> Permissions { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Mws.Domain.Roles;
|
||||
|
||||
public class RolePermission
|
||||
{
|
||||
public Guid RoleId { get; set; }
|
||||
public string Screen { get; set; } = string.Empty;
|
||||
public bool CanView { get; set; }
|
||||
public bool CanCreate { get; set; }
|
||||
public bool CanEdit { get; set; }
|
||||
public bool CanDelete { get; set; }
|
||||
|
||||
public Role Role { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Mws.Domain.Tasks;
|
||||
|
||||
public enum TaskStatus
|
||||
{
|
||||
Todo = 1,
|
||||
InProgress = 2,
|
||||
Done = 3,
|
||||
Cancelled = 4,
|
||||
}
|
||||
|
||||
public enum TaskPriority
|
||||
{
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3,
|
||||
}
|
||||
|
||||
public class TaskItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public TaskStatus Status { get; set; } = TaskStatus.Todo;
|
||||
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
||||
public Guid? AssigneeId { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
public Guid CreatedBy { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public Mws.Domain.Users.User? Assignee { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Mws.Domain.Roles;
|
||||
|
||||
namespace Mws.Domain.Users;
|
||||
|
||||
public class User
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
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!;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
using Mws.Application.Auth;
|
||||
|
||||
namespace Mws.Infrastructure.Authentication;
|
||||
|
||||
public class BcryptPasswordHasher : IPasswordHasher
|
||||
{
|
||||
public string Hash(string password) => BCrypt.Net.BCrypt.HashPassword(password, BCrypt.Net.BCrypt.GenerateSalt());
|
||||
|
||||
public bool Verify(string password, string hash) => BCrypt.Net.BCrypt.Verify(password, hash);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Mws.Infrastructure.Authentication;
|
||||
|
||||
public class JwtOptions
|
||||
{
|
||||
public string Secret { get; set; } = string.Empty;
|
||||
public string Issuer { get; set; } = "Mws";
|
||||
public string Audience { get; set; } = "Mws.Clients";
|
||||
public TimeSpan Expiration { get; set; } = TimeSpan.FromHours(12);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Mws.Application.Auth;
|
||||
|
||||
namespace Mws.Infrastructure.Authentication;
|
||||
|
||||
public class JwtTokenService(JwtOptions options) : ITokenService
|
||||
{
|
||||
public string CreateToken(Guid userId, string username)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new(ClaimTypes.Name, username),
|
||||
};
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.Secret));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: options.Issuer,
|
||||
audience: options.Audience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.Add(options.Expiration),
|
||||
signingCredentials: creds);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Mws.Application.Accounts;
|
||||
using Mws.Application.Auth;
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Documents;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Application.Projects;
|
||||
using Mws.Application.Roles;
|
||||
using Mws.Application.Tasks;
|
||||
using Mws.Infrastructure.Authentication;
|
||||
using Mws.Infrastructure.Persistence;
|
||||
|
||||
namespace Mws.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString("Default")
|
||||
?? "Host=localhost;Port=5432;Database=mws;Username=mws;Password=mws";
|
||||
|
||||
services.AddDbContext<AppDbContext>(options => options.UseNpgsql(connectionString));
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
|
||||
services.AddAutoMapper(cfg => { }, typeof(MappingProfile).Assembly);
|
||||
|
||||
services.AddScoped<IPasswordHasher, BcryptPasswordHasher>();
|
||||
|
||||
var jwtSection = configuration.GetSection("Jwt");
|
||||
var jwtOptions = jwtSection.Get<JwtOptions>() ?? new JwtOptions();
|
||||
if (string.IsNullOrWhiteSpace(jwtOptions.Secret) || jwtOptions.Secret.Length < 32)
|
||||
{
|
||||
jwtOptions.Secret = Environment.GetEnvironmentVariable("MWS_JWT_SECRET")
|
||||
?? "mws-development-secret-key-change-me-in-production-123456";
|
||||
}
|
||||
services.Configure<JwtOptions>(jwtSection.Bind);
|
||||
services.AddSingleton(jwtOptions);
|
||||
services.AddScoped<ITokenService, JwtTokenService>();
|
||||
|
||||
services.AddScoped<IAuthService, AuthService>();
|
||||
services.AddScoped<IAccountService, AccountService>();
|
||||
services.AddScoped<IPermissionService, PermissionService>();
|
||||
services.AddScoped<IRoleService, RoleService>();
|
||||
services.AddScoped<IProjectService, ProjectService>();
|
||||
services.AddScoped<IProjectMemberService, ProjectMemberService>();
|
||||
services.AddScoped<IDocumentService, DocumentService>();
|
||||
services.AddScoped<ITaskService, TaskService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Mws.Infrastructure.Persistence;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Mws.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260809054503_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("ProjectId", "ParentId");
|
||||
|
||||
b.ToTable("documents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("ProjectId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("project_members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Users.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Documents.Document", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Projects.Project", "Project")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Mws.Domain.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Users.User", "Assignee")
|
||||
.WithMany()
|
||||
.HasForeignKey("AssigneeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Assignee");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Mws.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "documents",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ParentId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
|
||||
Content = table.Column<string>(type: "text", nullable: true),
|
||||
Type = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_documents", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_documents_documents_ParentId",
|
||||
column: x => x.ParentId,
|
||||
principalTable: "documents",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "projects",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_projects", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Username = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "project_members",
|
||||
columns: table => new
|
||||
{
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Role = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_project_members", x => new { x.ProjectId, x.UserId });
|
||||
table.ForeignKey(
|
||||
name: "FK_project_members_projects_ProjectId",
|
||||
column: x => x.ProjectId,
|
||||
principalTable: "projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_project_members_users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tasks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
|
||||
Description = table.Column<string>(type: "text", nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
Priority = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
AssigneeId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
DueDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_tasks", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_tasks_users_AssigneeId",
|
||||
column: x => x.AssigneeId,
|
||||
principalTable: "users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_documents_ParentId",
|
||||
table: "documents",
|
||||
column: "ParentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_documents_ProjectId_ParentId",
|
||||
table: "documents",
|
||||
columns: new[] { "ProjectId", "ParentId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_project_members_UserId",
|
||||
table: "project_members",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_tasks_AssigneeId",
|
||||
table: "tasks",
|
||||
column: "AssigneeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_tasks_ProjectId",
|
||||
table: "tasks",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_tasks_Status",
|
||||
table: "tasks",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_users_Username",
|
||||
table: "users",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "documents");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "project_members");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tasks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "projects");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Mws.Infrastructure.Persistence;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Mws.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260811133605_AddRolesAndPermissions")]
|
||||
partial class AddRolesAndPermissions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("ProjectId", "ParentId");
|
||||
|
||||
b.ToTable("documents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("ProjectId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("project_members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Screen")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("CanCreate")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanDelete")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanEdit")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanView")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("RoleId", "Screen");
|
||||
|
||||
b.ToTable("role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Users.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Documents.Document", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Projects.Project", "Project")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Mws.Domain.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Roles.Role", "Role")
|
||||
.WithMany("Permissions")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Users.User", "Assignee")
|
||||
.WithMany()
|
||||
.HasForeignKey("AssigneeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Assignee");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Users.User", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Roles.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
|
||||
{
|
||||
b.Navigation("Permissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Mws.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRolesAndPermissions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsActive",
|
||||
table: "users",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "RoleId",
|
||||
table: "users",
|
||||
type: "uuid",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "roles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
IsSystem = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_roles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "role_permissions",
|
||||
columns: table => new
|
||||
{
|
||||
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Screen = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
CanView = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CanCreate = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CanEdit = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CanDelete = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_role_permissions", x => new { x.RoleId, x.Screen });
|
||||
table.ForeignKey(
|
||||
name: "FK_role_permissions_roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_users_RoleId",
|
||||
table: "users",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_roles_Name",
|
||||
table: "roles",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_users_roles_RoleId",
|
||||
table: "users",
|
||||
column: "RoleId",
|
||||
principalTable: "roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_users_roles_RoleId",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "role_permissions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "roles");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_users_RoleId",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsActive",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RoleId",
|
||||
table: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Mws.Infrastructure.Persistence;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Mws.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260812124332_AddMemberDocumentPermissions")]
|
||||
partial class AddMemberDocumentPermissions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("ProjectId", "ParentId");
|
||||
|
||||
b.ToTable("documents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("CanCreateDocuments")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanDeleteDocuments")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanEditDocuments")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanViewDocuments")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("ProjectId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("project_members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Screen")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("CanCreate")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanDelete")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanEdit")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanView")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("RoleId", "Screen");
|
||||
|
||||
b.ToTable("role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Users.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Documents.Document", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Projects.Project", "Project")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Mws.Domain.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Roles.Role", "Role")
|
||||
.WithMany("Permissions")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Users.User", "Assignee")
|
||||
.WithMany()
|
||||
.HasForeignKey("AssigneeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Assignee");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Users.User", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Roles.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
|
||||
{
|
||||
b.Navigation("Permissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Mws.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMemberDocumentPermissions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CanCreateDocuments",
|
||||
table: "project_members",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CanDeleteDocuments",
|
||||
table: "project_members",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CanEditDocuments",
|
||||
table: "project_members",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CanViewDocuments",
|
||||
table: "project_members",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.Sql("UPDATE \"project_members\" SET \"CanViewDocuments\" = true, \"CanCreateDocuments\" = true, \"CanEditDocuments\" = true, \"CanDeleteDocuments\" = true WHERE \"Role\" = 'Owner';");
|
||||
migrationBuilder.Sql("UPDATE \"project_members\" SET \"CanViewDocuments\" = true WHERE \"Role\" = 'Member';");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CanCreateDocuments",
|
||||
table: "project_members");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CanDeleteDocuments",
|
||||
table: "project_members");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CanEditDocuments",
|
||||
table: "project_members");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CanViewDocuments",
|
||||
table: "project_members");
|
||||
}
|
||||
}
|
||||
}
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Mws.Infrastructure.Persistence;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Mws.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260812130925_SplitProjectMemberPermissions")]
|
||||
partial class SplitProjectMemberPermissions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("ProjectId", "ParentId");
|
||||
|
||||
b.ToTable("documents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("ProjectId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("project_members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMemberPermission", b =>
|
||||
{
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Screen")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("CanCreate")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanDelete")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanEdit")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanView")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("ProjectId", "UserId", "Screen");
|
||||
|
||||
b.ToTable("project_member_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Screen")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("CanCreate")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanDelete")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanEdit")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanView")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("RoleId", "Screen");
|
||||
|
||||
b.ToTable("role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Users.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Documents.Document", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Projects.Project", "Project")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Mws.Domain.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMemberPermission", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Projects.ProjectMember", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId", "UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Roles.Role", "Role")
|
||||
.WithMany("Permissions")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Users.User", "Assignee")
|
||||
.WithMany()
|
||||
.HasForeignKey("AssigneeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Assignee");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Users.User", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Roles.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
|
||||
{
|
||||
b.Navigation("Permissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Mws.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SplitProjectMemberPermissions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "project_member_permissions",
|
||||
columns: table => new
|
||||
{
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Screen = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
CanView = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CanCreate = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CanEdit = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CanDelete = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_project_member_permissions", x => new { x.ProjectId, x.UserId, x.Screen });
|
||||
table.ForeignKey(
|
||||
name: "FK_project_member_permissions_project_members_ProjectId_UserId",
|
||||
columns: x => new { x.ProjectId, x.UserId },
|
||||
principalTable: "project_members",
|
||||
principalColumns: new[] { "ProjectId", "UserId" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
INSERT INTO "project_member_permissions" ("ProjectId", "UserId", "Screen", "CanView", "CanCreate", "CanEdit", "CanDelete")
|
||||
SELECT "ProjectId", "UserId", 'documents', "CanViewDocuments", "CanCreateDocuments", "CanEditDocuments", "CanDeleteDocuments"
|
||||
FROM "project_members";
|
||||
""");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CanCreateDocuments",
|
||||
table: "project_members");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CanDeleteDocuments",
|
||||
table: "project_members");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CanEditDocuments",
|
||||
table: "project_members");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CanViewDocuments",
|
||||
table: "project_members");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CanCreateDocuments",
|
||||
table: "project_members",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CanDeleteDocuments",
|
||||
table: "project_members",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CanEditDocuments",
|
||||
table: "project_members",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CanViewDocuments",
|
||||
table: "project_members",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "project_members" m
|
||||
SET "CanViewDocuments" = p."CanView",
|
||||
"CanCreateDocuments" = p."CanCreate",
|
||||
"CanEditDocuments" = p."CanEdit",
|
||||
"CanDeleteDocuments" = p."CanDelete"
|
||||
FROM "project_member_permissions" p
|
||||
WHERE p."ProjectId" = m."ProjectId" AND p."UserId" = m."UserId" AND p."Screen" = 'documents';
|
||||
""");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "project_member_permissions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Mws.Infrastructure.Persistence;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Mws.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.HasIndex("ProjectId", "ParentId");
|
||||
|
||||
b.ToTable("documents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("ProjectId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("project_members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMemberPermission", b =>
|
||||
{
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Screen")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("CanCreate")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanDelete")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanEdit")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanView")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("ProjectId", "UserId", "Screen");
|
||||
|
||||
b.ToTable("project_member_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Screen")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("CanCreate")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanDelete")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanEdit")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("CanView")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("RoleId", "Screen");
|
||||
|
||||
b.ToTable("role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssigneeId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Users.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Documents.Document", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMember", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Projects.Project", "Project")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Mws.Domain.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.ProjectMemberPermission", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Projects.ProjectMember", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId", "UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.RolePermission", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Roles.Role", "Role")
|
||||
.WithMany("Permissions")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Tasks.TaskItem", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Users.User", "Assignee")
|
||||
.WithMany()
|
||||
.HasForeignKey("AssigneeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Assignee");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Users.User", b =>
|
||||
{
|
||||
b.HasOne("Mws.Domain.Roles.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Documents.Document", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Projects.Project", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Mws.Domain.Roles.Role", b =>
|
||||
{
|
||||
b.Navigation("Permissions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Mws.Domain.Documents;
|
||||
using Mws.Domain.Projects;
|
||||
using Mws.Domain.Roles;
|
||||
using Mws.Domain.Tasks;
|
||||
using Mws.Domain.Users;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Project> Projects => Set<Project>();
|
||||
public DbSet<ProjectMember> ProjectMembers => Set<ProjectMember>();
|
||||
public DbSet<ProjectMemberPermission> ProjectMemberPermissions => Set<ProjectMemberPermission>();
|
||||
public DbSet<Document> Documents => Set<Document>();
|
||||
public DbSet<TaskItem> Tasks => Set<TaskItem>();
|
||||
public DbSet<Role> Roles => Set<Role>();
|
||||
public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Mws.Domain.Documents;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Configuration;
|
||||
|
||||
public class DocumentConfiguration : IEntityTypeConfiguration<Document>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Document> e)
|
||||
{
|
||||
e.ToTable("documents");
|
||||
e.HasKey(d => d.Id);
|
||||
e.Property(d => d.Title).HasMaxLength(300).IsRequired();
|
||||
e.Property(d => d.Type).HasConversion<string>().HasMaxLength(20);
|
||||
e.Property(d => d.Content).HasColumnType("text");
|
||||
|
||||
e.HasIndex(d => new { d.ProjectId, d.ParentId });
|
||||
e.HasIndex(d => d.ParentId);
|
||||
|
||||
e.HasOne(d => d.Parent)
|
||||
.WithMany(d => d.Children)
|
||||
.HasForeignKey(d => d.ParentId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Mws.Domain.Projects;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Configuration;
|
||||
|
||||
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Project> e)
|
||||
{
|
||||
e.ToTable("projects");
|
||||
e.HasKey(p => p.Id);
|
||||
e.Property(p => p.Name).HasMaxLength(200).IsRequired();
|
||||
e.Property(p => p.Description).HasMaxLength(2000);
|
||||
e.Property(p => p.Status).HasConversion<string>().HasMaxLength(20);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Mws.Domain.Projects;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Configuration;
|
||||
|
||||
public class ProjectMemberConfiguration : IEntityTypeConfiguration<ProjectMember>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProjectMember> e)
|
||||
{
|
||||
e.ToTable("project_members");
|
||||
e.HasKey(m => new { m.ProjectId, m.UserId });
|
||||
e.Property(m => m.Role).HasConversion<string>().HasMaxLength(20);
|
||||
|
||||
e.HasOne(m => m.Project)
|
||||
.WithMany(p => p.Members)
|
||||
.HasForeignKey(m => m.ProjectId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
e.HasOne(m => m.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Mws.Domain.Projects;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Configuration;
|
||||
|
||||
public class ProjectMemberPermissionConfiguration : IEntityTypeConfiguration<ProjectMemberPermission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProjectMemberPermission> e)
|
||||
{
|
||||
e.ToTable("project_member_permissions");
|
||||
e.HasKey(p => new { p.ProjectId, p.UserId, p.Screen });
|
||||
e.Property(p => p.Screen).HasMaxLength(50).IsRequired();
|
||||
|
||||
e.HasOne<ProjectMember>()
|
||||
.WithMany()
|
||||
.HasForeignKey(p => new { p.ProjectId, p.UserId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Mws.Domain.Roles;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Configuration;
|
||||
|
||||
public class RoleConfiguration : IEntityTypeConfiguration<Role>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Role> e)
|
||||
{
|
||||
e.ToTable("roles");
|
||||
e.HasKey(r => r.Id);
|
||||
e.Property(r => r.Name).HasMaxLength(100).IsRequired();
|
||||
e.HasIndex(r => r.Name).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Mws.Domain.Roles;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Configuration;
|
||||
|
||||
public class RolePermissionConfiguration : IEntityTypeConfiguration<RolePermission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RolePermission> e)
|
||||
{
|
||||
e.ToTable("role_permissions");
|
||||
e.HasKey(p => new { p.RoleId, p.Screen });
|
||||
e.Property(p => p.Screen).HasMaxLength(50).IsRequired();
|
||||
|
||||
e.HasOne(p => p.Role)
|
||||
.WithMany(r => r.Permissions)
|
||||
.HasForeignKey(p => p.RoleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Mws.Domain.Tasks;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Configuration;
|
||||
|
||||
public class TaskConfiguration : IEntityTypeConfiguration<TaskItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TaskItem> e)
|
||||
{
|
||||
e.ToTable("tasks");
|
||||
e.HasKey(t => t.Id);
|
||||
e.Property(t => t.Title).HasMaxLength(300).IsRequired();
|
||||
e.Property(t => t.Description).HasColumnType("text");
|
||||
e.Property(t => t.Status).HasConversion<string>().HasMaxLength(20);
|
||||
e.Property(t => t.Priority).HasConversion<string>().HasMaxLength(20);
|
||||
|
||||
e.HasIndex(t => t.ProjectId);
|
||||
e.HasIndex(t => t.AssigneeId);
|
||||
e.HasIndex(t => t.Status);
|
||||
|
||||
e.HasOne(t => t.Assignee)
|
||||
.WithMany()
|
||||
.HasForeignKey(t => t.AssigneeId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Mws.Domain.Users;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Configuration;
|
||||
|
||||
public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> e)
|
||||
{
|
||||
e.ToTable("users");
|
||||
e.HasKey(u => u.Id);
|
||||
e.Property(u => u.Username).HasMaxLength(100).IsRequired();
|
||||
e.HasIndex(u => u.Username).IsUnique();
|
||||
e.Property(u => u.PasswordHash).HasMaxLength(200).IsRequired();
|
||||
e.Property(u => u.DisplayName).HasMaxLength(150).IsRequired();
|
||||
|
||||
e.HasOne(u => u.Role)
|
||||
.WithMany()
|
||||
.HasForeignKey(u => u.RoleId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Mws.Application.Auth;
|
||||
using Mws.Application.Permissions;
|
||||
using Mws.Domain.Documents;
|
||||
using Mws.Domain.Projects;
|
||||
using Mws.Domain.Roles;
|
||||
using Mws.Domain.Tasks;
|
||||
using Mws.Domain.Users;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence;
|
||||
|
||||
public static class DbSeeder
|
||||
{
|
||||
public static async Task SeedAsync(AppDbContext db, IPasswordHasher passwordHasher)
|
||||
{
|
||||
if (await db.Users.AnyAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var adminRole = new Role { Id = Guid.NewGuid(), Name = "Admin", IsSystem = true, CreatedAt = now, UpdatedAt = now };
|
||||
var memberRole = new Role { Id = Guid.NewGuid(), Name = "Member", IsSystem = true, CreatedAt = now, UpdatedAt = now };
|
||||
|
||||
adminRole.Permissions = ScreenCatalog.Screens.Select(s => new RolePermission
|
||||
{
|
||||
RoleId = adminRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = true,
|
||||
}).ToList();
|
||||
|
||||
memberRole.Permissions = ScreenCatalog.Screens
|
||||
.Where(s => s.Key is not ("accounts" or "roles"))
|
||||
.Select(s => new RolePermission
|
||||
{
|
||||
RoleId = memberRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = false,
|
||||
}).ToList();
|
||||
|
||||
db.Roles.AddRange(adminRole, memberRole);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var admin = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = "admin",
|
||||
PasswordHash = passwordHasher.Hash("password"),
|
||||
DisplayName = "Admin",
|
||||
RoleId = adminRole.Id,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
var alice = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = "alice",
|
||||
PasswordHash = passwordHasher.Hash("password"),
|
||||
DisplayName = "Alice",
|
||||
RoleId = memberRole.Id,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
var bob = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = "bob",
|
||||
PasswordHash = passwordHasher.Hash("password"),
|
||||
DisplayName = "Bob",
|
||||
RoleId = memberRole.Id,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
db.Users.AddRange(admin, alice, bob);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var project = new Project
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "MWS",
|
||||
Description = "My Workspace - internal tool for the team.",
|
||||
Status = ProjectStatus.Active,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
db.Projects.Add(project);
|
||||
|
||||
db.ProjectMembers.AddRange(
|
||||
new ProjectMember { ProjectId = project.Id, UserId = admin.Id, Role = MemberRole.Owner },
|
||||
new ProjectMember { ProjectId = project.Id, UserId = alice.Id, Role = MemberRole.Member },
|
||||
new ProjectMember { ProjectId = project.Id, UserId = bob.Id, Role = MemberRole.Member });
|
||||
|
||||
db.ProjectMemberPermissions.AddRange(
|
||||
new ProjectMemberPermission { ProjectId = project.Id, UserId = alice.Id, Screen = ProjectPermissionScreens.Documents, CanView = true },
|
||||
new ProjectMemberPermission { ProjectId = project.Id, UserId = bob.Id, Screen = ProjectPermissionScreens.Documents, CanView = true });
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var requirements = new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
|
||||
Title = "Requirements", Type = DocumentType.Folder,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
};
|
||||
|
||||
var design = new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
|
||||
Title = "Technical Design", Type = DocumentType.Folder,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
};
|
||||
|
||||
var notes = new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
|
||||
Title = "Notes", Type = DocumentType.Folder,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
};
|
||||
|
||||
db.Documents.AddRange(requirements, design, notes);
|
||||
db.Documents.Add(new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
|
||||
Title = "Authentication", Type = DocumentType.Document,
|
||||
Content = "<h2>Authentication</h2><p>Phase 1 uses simple username/password with JWT.</p>",
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
});
|
||||
db.Documents.Add(new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
|
||||
Title = "User Management", Type = DocumentType.Document,
|
||||
Content = "<h2>User Management</h2><p>Users are created via seeding in Phase 1.</p>",
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
});
|
||||
db.Documents.Add(new Document
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = design.Id,
|
||||
Title = "API Design", Type = DocumentType.Document,
|
||||
Content = "<h2>API Design</h2><p>REST API with ASP.NET Core Web API.</p>",
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.Tasks.AddRange(
|
||||
new TaskItem
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Set up solution",
|
||||
Status = TaskStatus.Done, Priority = TaskPriority.High, AssigneeId = admin.Id,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
|
||||
},
|
||||
new TaskItem
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Implement authentication",
|
||||
Description = "Login endpoint with BCrypt and JWT.",
|
||||
Status = TaskStatus.InProgress, Priority = TaskPriority.High, AssigneeId = alice.Id,
|
||||
DueDate = now.AddDays(3),
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
|
||||
},
|
||||
new TaskItem
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Task board drag & drop",
|
||||
Status = TaskStatus.Todo, Priority = TaskPriority.Medium, AssigneeId = bob.Id,
|
||||
DueDate = now.AddDays(7),
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
|
||||
},
|
||||
new TaskItem
|
||||
{
|
||||
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Polish documents UI",
|
||||
Status = TaskStatus.Todo, Priority = TaskPriority.Low,
|
||||
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Mws.Application.Common.Repositories;
|
||||
using Mws.Domain.Documents;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class DocumentRepository(AppDbContext db) : RepositoryBase<Document>(db), IDocumentRepository
|
||||
{
|
||||
public Task<Document?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(d => d.Id == id, ct);
|
||||
|
||||
public Task<List<Document>> GetTreeForProjectAsync(Guid projectId, CancellationToken ct = default) =>
|
||||
Set.Where(d => d.ProjectId == projectId).OrderBy(d => d.Title).ToListAsync(ct);
|
||||
|
||||
public Task<Document?> GetInProjectAsync(Guid parentId, Guid projectId, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(d => d.Id == parentId && d.ProjectId == projectId, ct);
|
||||
|
||||
public Task<Guid?> GetParentIdAsync(Guid documentId, CancellationToken ct = default) =>
|
||||
Set.Where(d => d.Id == documentId).Select(d => d.ParentId).SingleAsync(ct);
|
||||
|
||||
public Task<List<Document>> GetChildrenAsync(Guid parentId, CancellationToken ct = default) =>
|
||||
Set.Where(d => d.ParentId == parentId).ToListAsync(ct);
|
||||
|
||||
public Task<List<Document>> SearchAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Where(d => projectIds.Contains(d.ProjectId));
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var lower = term.Trim().ToLower();
|
||||
query = query.Where(d => d.Title.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
return query.OrderByDescending(d => d.UpdatedAt).Take(take).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public Task<int> CountByTypeForProjectAsync(Guid projectId, DocumentType type, CancellationToken ct = default) =>
|
||||
Set.CountAsync(d => d.ProjectId == projectId && d.Type == type, ct);
|
||||
|
||||
public Task<List<Document>> GetRecentForProjectAsync(Guid projectId, DocumentType type, int take, CancellationToken ct = default) =>
|
||||
Set.Where(d => d.ProjectId == projectId && d.Type == type).OrderByDescending(d => d.UpdatedAt).Take(take).ToListAsync(ct);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Mws.Application.Common.Repositories;
|
||||
using Mws.Domain.Projects;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class ProjectRepository(AppDbContext db) : RepositoryBase<Project>(db), IProjectRepository
|
||||
{
|
||||
public Task<Project?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(p => p.Id == id, ct);
|
||||
|
||||
public Task<Project?> GetForUserAsync(Guid userId, Guid projectId, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(p => p.Id == projectId && p.Members.Any(m => m.UserId == userId), ct);
|
||||
|
||||
public Task<List<Project>> GetForUserAsync(Guid userId, CancellationToken ct = default) =>
|
||||
Set.Where(p => p.Members.Any(m => m.UserId == userId))
|
||||
.OrderByDescending(p => p.UpdatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public Task<List<Project>> SearchForUserAsync(Guid userId, string? term, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Where(p => p.Members.Any(m => m.UserId == userId));
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var lower = term.Trim().ToLower();
|
||||
query = query.Where(p => p.Name.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
return query.OrderByDescending(p => p.UpdatedAt).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public Task<int> CountMembersAsync(Guid projectId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.CountAsync(m => m.ProjectId == projectId, ct);
|
||||
|
||||
public Task<bool> IsMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.AnyAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
|
||||
|
||||
public Task<MemberRole?> GetMemberRoleAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers
|
||||
.Where(m => m.ProjectId == projectId && m.UserId == userId)
|
||||
.Select(m => (MemberRole?)m.Role)
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
public Task<ProjectMember?> GetMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.FirstOrDefaultAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
|
||||
|
||||
public Task<ProjectMember?> GetMemberWithUserAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.Include(m => m.User).FirstOrDefaultAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
|
||||
|
||||
public Task<List<ProjectMember>> GetMembersWithUserAsync(Guid projectId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers
|
||||
.Where(m => m.ProjectId == projectId)
|
||||
.Include(m => m.User)
|
||||
.OrderByDescending(m => m.Role)
|
||||
.ThenBy(m => m.User.DisplayName)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public Task<int> CountOwnersAsync(Guid projectId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.CountAsync(m => m.ProjectId == projectId && m.Role == MemberRole.Owner, ct);
|
||||
|
||||
public Task<List<Guid>> GetProjectIdsForUserAsync(Guid userId, CancellationToken ct = default) =>
|
||||
Set.Where(p => p.Members.Any(m => m.UserId == userId)).Select(p => p.Id).ToListAsync(ct);
|
||||
|
||||
public Task<List<Guid>> GetOwnedProjectIdsAsync(Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers.Where(m => m.UserId == userId && m.Role == MemberRole.Owner).Select(m => m.ProjectId).ToListAsync(ct);
|
||||
|
||||
public Task<List<Guid>> GetDocumentViewableProjectIdsAsync(Guid userId, CancellationToken ct = default) =>
|
||||
Db.ProjectMembers
|
||||
.Where(m => m.UserId == userId &&
|
||||
(m.Role == MemberRole.Owner ||
|
||||
Db.ProjectMemberPermissions.Any(p => p.ProjectId == m.ProjectId && p.UserId == userId && p.Screen == ProjectPermissionScreens.Documents && p.CanView)))
|
||||
.Select(m => m.ProjectId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public void AddMember(ProjectMember member) => Db.ProjectMembers.Add(member);
|
||||
|
||||
public void RemoveMember(ProjectMember member) => Db.ProjectMembers.Remove(member);
|
||||
|
||||
public Task<ProjectMemberPermission?> GetMemberPermissionAsync(Guid projectId, Guid userId, string screen, CancellationToken ct = default) =>
|
||||
Db.ProjectMemberPermissions.FirstOrDefaultAsync(p => p.ProjectId == projectId && p.UserId == userId && p.Screen == screen, ct);
|
||||
|
||||
public Task<Dictionary<Guid, ProjectMemberPermission>> GetMemberPermissionsAsync(Guid projectId, string screen, CancellationToken ct = default) =>
|
||||
Db.ProjectMemberPermissions
|
||||
.Where(p => p.ProjectId == projectId && p.Screen == screen)
|
||||
.ToDictionaryAsync(p => p.UserId, ct);
|
||||
|
||||
public void AddMemberPermission(ProjectMemberPermission permission) => Db.ProjectMemberPermissions.Add(permission);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Mws.Application.Common.Repositories;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public abstract class RepositoryBase<T>(AppDbContext db) : IRepository<T> where T : class
|
||||
{
|
||||
protected readonly AppDbContext Db = db;
|
||||
protected DbSet<T> Set => Db.Set<T>();
|
||||
|
||||
public void Add(T entity) => Set.Add(entity);
|
||||
public void Remove(T entity) => Set.Remove(entity);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Mws.Application.Common.Repositories;
|
||||
using Mws.Domain.Roles;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class RoleRepository(AppDbContext db) : RepositoryBase<Role>(db), IRoleRepository
|
||||
{
|
||||
public Task<List<Role>> GetAllWithPermissionsAsync(CancellationToken ct = default) =>
|
||||
Set.Include(r => r.Permissions).OrderBy(r => r.Name).ToListAsync(ct);
|
||||
|
||||
public Task<Role?> GetByIdWithPermissionsAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.Include(r => r.Permissions).FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
|
||||
public Task<Role?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
|
||||
public Task<bool> ExistsByNameAsync(string name, Guid? excludeId, CancellationToken ct = default) =>
|
||||
Set.AnyAsync(r => r.Name == name && (excludeId == null || r.Id != excludeId), ct);
|
||||
|
||||
public Task<RolePermission?> GetPermissionAsync(Guid roleId, string screen, CancellationToken ct = default) =>
|
||||
Db.RolePermissions.FirstOrDefaultAsync(p => p.RoleId == roleId && p.Screen == screen, ct);
|
||||
|
||||
public Task<Dictionary<string, RolePermission>> GetPermissionsAsync(Guid roleId, CancellationToken ct = default) =>
|
||||
Db.RolePermissions.Where(p => p.RoleId == roleId).ToDictionaryAsync(p => p.Screen, ct);
|
||||
|
||||
public void RemovePermissions(IEnumerable<RolePermission> permissions) => Db.RolePermissions.RemoveRange(permissions);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Mws.Application.Common.Repositories;
|
||||
using Mws.Domain.Tasks;
|
||||
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
|
||||
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class TaskRepository(AppDbContext db) : RepositoryBase<TaskItem>(db), ITaskRepository
|
||||
{
|
||||
public Task<TaskItem?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(t => t.Id == id, ct);
|
||||
|
||||
public Task<TaskItem?> GetWithAssigneeAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.Include(t => t.Assignee).FirstOrDefaultAsync(t => t.Id == id, ct);
|
||||
|
||||
public Task<List<TaskItem>> GetForProjectAsync(
|
||||
Guid projectId, TaskStatus? status, TaskPriority? priority, Guid? assigneeId, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Where(t => t.ProjectId == projectId);
|
||||
|
||||
if (status.HasValue)
|
||||
{
|
||||
query = query.Where(t => t.Status == status);
|
||||
}
|
||||
if (priority.HasValue)
|
||||
{
|
||||
query = query.Where(t => t.Priority == priority);
|
||||
}
|
||||
if (assigneeId.HasValue)
|
||||
{
|
||||
query = query.Where(t => t.AssigneeId == assigneeId);
|
||||
}
|
||||
|
||||
return query.Include(t => t.Assignee).OrderByDescending(t => t.UpdatedAt).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public Task<List<TaskItem>> SearchWithAssigneeAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Where(t => projectIds.Contains(t.ProjectId));
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var lower = term.Trim().ToLower();
|
||||
query = query.Where(t => t.Title.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
return query.Include(t => t.Assignee).OrderByDescending(t => t.UpdatedAt).Take(take).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<TaskStatus, int>> CountByStatusForProjectAsync(Guid projectId, CancellationToken ct = default)
|
||||
{
|
||||
return await Set.Where(t => t.ProjectId == projectId)
|
||||
.GroupBy(t => t.Status)
|
||||
.Select(g => new { Status = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(a => a.Status, a => a.Count, ct);
|
||||
}
|
||||
|
||||
public Task<List<TaskItem>> GetRecentForProjectAsync(Guid projectId, int take, CancellationToken ct = default) =>
|
||||
Set.Where(t => t.ProjectId == projectId).OrderByDescending(t => t.UpdatedAt).Take(take).ToListAsync(ct);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Mws.Application.Common.Repositories;
|
||||
using Mws.Domain.Users;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class UserRepository(AppDbContext db) : RepositoryBase<User>(db), IUserRepository
|
||||
{
|
||||
public Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
|
||||
public Task<User?> GetByIdWithRoleAsync(Guid id, CancellationToken ct = default) =>
|
||||
Set.Include(u => u.Role).FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
|
||||
public Task<User?> GetByUsernameWithRoleAsync(string username, CancellationToken ct = default) =>
|
||||
Set.Include(u => u.Role).SingleOrDefaultAsync(u => u.Username == username, ct);
|
||||
|
||||
public Task<bool> ExistsByUsernameAsync(string username, CancellationToken ct = default) =>
|
||||
Set.AnyAsync(u => u.Username == username, ct);
|
||||
|
||||
public Task<bool> ExistsByRoleIdAsync(Guid roleId, CancellationToken ct = default) =>
|
||||
Set.AnyAsync(u => u.RoleId == roleId, ct);
|
||||
|
||||
public Task<Guid> GetRoleIdAsync(Guid userId, CancellationToken ct = default) =>
|
||||
Set.Where(u => u.Id == userId).Select(u => u.RoleId).SingleOrDefaultAsync(ct);
|
||||
|
||||
public Task<List<User>> SearchWithRoleAsync(string? term, int? take, CancellationToken ct = default)
|
||||
{
|
||||
var query = Set.Include(u => u.Role).AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
var lower = term.Trim().ToLower();
|
||||
query = query.Where(u => u.Username.ToLower().Contains(lower) || u.DisplayName.ToLower().Contains(lower));
|
||||
}
|
||||
|
||||
query = query.OrderBy(u => u.DisplayName);
|
||||
if (take is { } n)
|
||||
{
|
||||
query = query.Take(n);
|
||||
}
|
||||
|
||||
return query.ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Mws.Application.Common;
|
||||
using Mws.Application.Common.Repositories;
|
||||
using Mws.Infrastructure.Persistence.Repositories;
|
||||
|
||||
namespace Mws.Infrastructure.Persistence;
|
||||
|
||||
public class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public UnitOfWork(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
Users = new UserRepository(db);
|
||||
Projects = new ProjectRepository(db);
|
||||
Tasks = new TaskRepository(db);
|
||||
Documents = new DocumentRepository(db);
|
||||
Roles = new RoleRepository(db);
|
||||
}
|
||||
|
||||
public IUserRepository Users { get; }
|
||||
public IProjectRepository Projects { get; }
|
||||
public ITaskRepository Tasks { get; }
|
||||
public IDocumentRepository Documents { get; }
|
||||
public IRoleRepository Roles { get; }
|
||||
|
||||
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\mws.application\mws.application.csproj" />
|
||||
<ProjectReference Include="..\mws.domain\mws.domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.11" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user