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 (rolestable), not a hardcoded enum — users link to roles through theUserRolejoin table (user_roles, many-to-many;User.UserRoles). - Mws.Application — use cases. One folder per module (
Auth/,Users/,Projects/,Tasks/,Documents/,MasterData/,Permissions/) plus sharedCommon/(DTOs, repositories, exceptions). Module names match controllers/routes (api/users,api/permissions,api/masterdata). Use cases are MediatR command/query handlers; each depends onIUnitOfWork(viaCommon/Repositories/) andIPermissionService, 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/containsBcryptPasswordHasherandJwtTokenService. Migrations live underMigrations/.DependencyInjection.AddInfrastructureis the single composition root. - Mws.Api — controllers, middleware,
Program.cs. Thin: controllers resolve a service via DI, get the user id fromUser.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:
- Project membership (unchanged). JWT carries
sub= userGuid. Every protected project/task/document endpoint looks upProjectMemberto verify the caller belongs to the project — checks live in the application handlers viaProjectAccess/TaskAccesshelpers andIProjectRepository(GetMemberRoleAsync). Mutating project settings requiresMemberRole.Owner. Add a new check the same way. - Screen-level role permissions (admin screens only: Users, Permissions, Master Data).
Roleis a DB entity (rolestable), not an enum — a user links to roles via theUserRolejoin table.RolePermissionis a per-(role, screen) CRUD matrix (CanView/CanCreate/CanEdit/CanDelete).IPermissionService.EnsureAsync(userId, screen, action)(inMws.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 carriessub/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.csis 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/menureturns 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.PostgreSQL10. Connection string key isConnectionStrings:Default(see Mws.Api/appsettings.json); fallback tolocalhost:5432dev credsmws/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.ParentIdusesRestrictto block accidental cycles. Cascade delete is implemented inDeleteDocumentHandlervia 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 viaDeleteRoleCommand.
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.
ProjectDtoinProjects/Contracts.cs). UseContracts.csper folder; don't sprawl. - All async service methods take
CancellationToken ct = defaultand forward it. - Enums serialized as strings globally (
JsonStringEnumConverterinProgram.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.