Files
mws.backend.dotnet/CLAUDE.md
T
2026-08-19 23:25:08 +07:00

6.8 KiB

CLAUDE.md

Backend for My Workspace (MWS) — ASP.NET Core 10 Web API with JWT auth, EF Core, and PostgreSQL.

Commands

# 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 <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.backend.dotnet.sln. Dependencies point inward only: Api → Application ← Infrastructure, both Application and Infrastructure → 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 — 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 — EF Core + auth wiring. AppDbContext (in 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 — controllers, middleware, Program.cs. Thin: controllers resolve a service via DI, get the user id from User.GetUserId() (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 (BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException) are caught by Mws.Api/Middleware/ApiExceptionMiddleware.cs and mapped to 400/401/403/404. Anything else → 500 with a generic message (original logged). Use these exceptions in handlers instead of returning Result<T>.

Authorization model

Two independent mechanisms — don't cross-wire them:

  1. Project membership (unchanged). JWT carries sub = user Guid. Every protected project/task/document endpoint looks up ProjectMember to verify the caller belongs to the project — checks live in the application 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); 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) seeds two system roles (Admin — full CRUD on every screen; Member — CRUD on everything except Users/Permissions/Master Data, no delete) plus admin/alice/bob (password: password, admin → Admin role, alice/bob → Member) and one demo project on first boot. Skips if any users exist. System roles (Role.IsSystem = true) can't be deleted via DeleteRoleCommand.

Config

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 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.