Files
mws.backend.dotnet/CLAUDE.md
T
namdh f72aaa2329 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.
2026-08-13 23:10:22 +07:00

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 (roles table), not a hardcoded enum — User.RoleId FK's to Role.
  • 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 — 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 → 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:

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