# 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.backend.dotnet.sln # 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 --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.backend.dotnet.sln](mws.backend.dotnet.sln). Dependencies point inward only: `Api → Application ← Infrastructure`, both `Application` and `Infrastructure → Domain`. - **[Mws.Domain](Mws.Domain/)** — POCOs only. Entities (`User`, `Project`, `ProjectMember`, `TaskItem`, `Document`, `Role`, `RolePermission`, `UserRole`, `MasterDataEntry`) and enums (`ProjectStatus`, `MemberRole`, `TaskStatus`, `TaskPriority`, `DocumentType`). No EF attributes. Roles are data (`roles` table), not a hardcoded enum — users link to roles through the `UserRole` join table (`user_roles`, many-to-many; `User.UserRoles`). - **[Mws.Application](Mws.Application/)** — use cases. One folder per module (`Auth/`, `Users/`, `Projects/`, `Tasks/`, `Documents/`, `MasterData/`, `Permissions/`) plus shared `Common/` (DTOs, repositories, exceptions). Module names match controllers/routes (`api/users`, `api/permissions`, `api/masterdata`). Use cases are MediatR command/query handlers; each depends on `IUnitOfWork` (via `Common/Repositories/`) and `IPermissionService`, never on EF Core directly. - **[Mws.Infrastructure](Mws.Infrastructure/)** — EF Core + auth wiring. `AppDbContext` (in [Mws.Infrastructure/Persistence/AppDbContext.cs](Mws.Infrastructure/Persistence/AppDbContext.cs)) configures all entities via fluent API and stores enums as strings. `Authentication/` contains `BcryptPasswordHasher` and `JwtTokenService`. Migrations live under `Migrations/`. `DependencyInjection.AddInfrastructure` is the single composition root. - **[Mws.Api](Mws.Api/)** — controllers, middleware, `Program.cs`. Thin: controllers resolve a service via DI, get the user id from `User.GetUserId()` ([Mws.Api/ClaimsExtensions.cs](Mws.Api/ClaimsExtensions.cs)), and delegate. ### Request flow ``` HTTP → ApiExceptionMiddleware → JwtBearer auth → Controller → MediatR handler → IUnitOfWork (repos) → AppDbContext → PostgreSQL ``` Exceptions in [Mws.Application/Common/Exceptions.cs](Mws.Application/Common/Exceptions.cs) (`BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`) are caught by [Mws.Api/Middleware/ApiExceptionMiddleware.cs](Mws.Api/Middleware/ApiExceptionMiddleware.cs) and mapped to 400/401/403/404. Anything else → 500 with a generic message (original logged). Use these exceptions in handlers instead of returning `Result`. ### 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 handlers via `ProjectAccess`/`TaskAccess` helpers and `IProjectRepository` (`GetMemberRoleAsync`). Mutating project settings requires `MemberRole.Owner`. Add a new check the same way. 2. **Screen-level role permissions** (admin screens only: Users, Permissions, Master Data). `Role` is a DB entity (`roles` table), not an enum — a user links to roles via the `UserRole` join table. `RolePermission` is a per-(role, screen) CRUD matrix (`CanView`/`CanCreate`/`CanEdit`/`CanDelete`). `IPermissionService.EnsureAsync(userId, screen, action)` (in `Mws.Application/Permissions/PermissionService.cs`) is the gate — called at the top of the users/roles/masterdata handlers. There is **no** `[Authorize(Roles=...)]` JWT-claim-based gating — the JWT only carries `sub`/`name`; role/permission is always resolved fresh from the DB per request, so changing a role's permissions or a user's roles takes effect immediately without re-login. `ScreenCatalog.cs` is the single source of truth for valid screen keys (`dashboard`, `projects`, `tasks`, `documents`, `users`, `permissions`, `masterdata`) — adding a new admin-gated screen means adding it there. `GET /api/menu` returns the CRUD matrix per screen for the caller's roles so the frontend can render nav + gate buttons; it does not itself enforce anything server-side. ### Database - PostgreSQL via `Npgsql.EntityFrameworkCore.PostgreSQL` 10. Connection string key is `ConnectionStrings:Default` (see [Mws.Api/appsettings.json](Mws.Api/appsettings.json)); fallback to `localhost:5432` dev creds `mws/mws`. Override via env if you must. - Enum columns are stored as `varchar(20)` (not ints) — preserve that for any new enum. - Self-referencing `Document.ParentId` uses `Restrict` to block accidental cycles. Cascade delete is implemented in `DeleteDocumentHandler` via a manual descendant walk (`DocumentAccess.DeleteDescendantsAsync`); do not switch the FK to cascade. - `DbSeeder` ([Mws.Infrastructure/Persistence/DbSeeder.cs](Mws.Infrastructure/Persistence/DbSeeder.cs)) seeds two system roles (`Admin` — full CRUD on every screen; `Member` — view-only on Projects, CRUD on Tasks/Documents (no delete), and no access to Users/Permissions/Master Data) plus admin/namdh (password: `password`, admin → Admin role, namdh → Member) and one demo project on first boot. System roles are re-synced to this canonical permission matrix on every startup (`SyncSystemRoleAsync`), so edits made to them through the Roles UI do not persist across restarts. Skips user/project seeding if any users exist. System roles (`Role.IsSystem = true`) can't be deleted via `DeleteRoleCommand`. ### Config | 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 - No comments in code. Code must be self-documenting through clear naming and structure. - `net10.0`, `enable`, `enable` on every project. - DTOs live next to the module that returns them (e.g. `ProjectDto` in `Projects/Contracts.cs`). Use `Contracts.cs` per folder; don't sprawl. - All async service methods take `CancellationToken ct = default` and forward it. - Enums serialized as strings globally (`JsonStringEnumConverter` in `Program.cs`). ## 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.