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.
6.4 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.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. Dependencies point inward only: Api → Application ← Infrastructure, both Application and Infrastructure → 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 (rolestable), not a hardcoded enum —User.RoleIdFK's toRole. - Mws.Application — use cases. One folder per aggregate (
Auth/,Projects/,Tasks/,Documents/,Accounts/,Roles/,Permissions/) plus sharedCommon/(DTO interfaces + exception types). Each service depends only onIApplicationDbContext, 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 → Application service
→ IApplicationDbContext (→ 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 services 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 service (GetProjectForUserAsync,EnsureMemberAccessAsync,GetMemberRoleAsync). Mutating project settings requiresMemberRole.Owner. Add a new check the same way. - Screen-level role permissions (admin screens only: Accounts, Roles).
Roleis a DB entity (rolestable), not an enum — a user'sUser.RoleIdpoints to one.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 ofAccountService/RoleServicemethods and inline inRolesController. 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 role takes effect immediately without re-login.ScreenCatalog.csis 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/menureturns 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.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 inDocumentService.DeleteAsyncvia a manual descendant walk; 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 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 viaRoleService.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.
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.