5.8 KiB
5.8 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
Backend (ASP.NET Core)
# Run API (must listen on port 5000 for the Vite proxy to work)
cd src/Budget.Api
ASPNETCORE_URLS=http://localhost:5000 dotnet run
# Build
dotnet build src/Budget.Api/Budget.Api.csproj
# Add an EF Core migration
dotnet ef migrations add <Name> --project src/Budget.Infrastructure --startup-project src/Budget.Api
# Remove last migration (before it has been applied)
dotnet ef migrations remove --project src/Budget.Infrastructure --startup-project src/Budget.Api
Frontend (Vite/React)
cd src/Budget.Client
npm run dev # dev server at http://localhost:5173
npm run build # tsc + vite build (what CI and Docker use)
npm run lint # eslint
Docker
.\deploy.ps1 # builds image, pushes to docker.stwaddle.com/budget:latest, and deploys to prod via SSH
Architecture
The app is a single Docker container: the .NET API serves the compiled React SPA as static files and also handles all /api requests. In development the two processes run separately; Vite proxies /api to http://localhost:5000.
Project layout
src/
Budget.Core/ # Domain models, DTOs, FrequencyCalculator — no EF, no ASP.NET
Budget.Infrastructure/# EF Core DbContext, migrations, BudgetAuthorizationService
Budget.Api/ # ASP.NET Core controllers, middleware, Program.cs
Budget.Client/ # Vite/React SPA
Data model
- Budget (owner, name) → owns Incomes, Outgos, BudgetShares
- All entities use soft delete (
IsDeleted/DeletedAt) with a global EF query filter — hard deletes are not used - Budget uses PostgreSQL's
xminshadow property as an EF concurrency token - BudgetShare links a budget to another user by email;
IsPending = trueuntil the target user logs in (resolved byKnownUserMiddleware) - Frequency is a C# enum (
Monthly,Biweekly,Annually, etc.);FrequencyCalculatorconverts any frequency to monthly/annual equivalents - The 50/30/20 rule is hardcoded in
SummaryController: Need=50%, Want=30%, Save=20%
API conventions
- All controllers require
[Authorize(Roles = "admin,user")]— tokens that lack aroleclaim matching either value get a 403 RoleClaimType = "role"andNameClaimType = "sub"are set in JWT options; role checks work withUser.IsInRole()BudgetAuthorizationServicecentralises read/write/owner checks against the budget's owner and any accepted shares — always callCanReadAsync/CanWriteAsync/IsOwnerAsyncbefore touching budget data- The
TryGetUserIdhelper pattern (returns anIActionResult?error or out-params the user id) is used consistently across all controllers - Enum serialisation:
JsonStringEnumConverteris registered globally, so all C# enums are sent/received as strings (e.g."Monthly", not5) - Write endpoints use the
"writes"rate-limit policy (30 req/min per user); reads use the global limiter (120 req/min) - EF migrations run automatically via
MigrateAsync()at startup
Frontend conventions
- All server state goes through TanStack Query (
src/api/). Query keys follow the pattern['budgets'],['budgets', id],['budgets', id, 'summary'], etc. - All forms use
react-hook-form+zodschemas defined insrc/schemas/index.ts. Never use uncontrolled inputs outside of RHF. - CSS design system lives entirely in
src/index.cssas CSS custom properties — no Tailwind, no CSS modules. Use the existing variables (--color-primary,--green-*, etc.) rather than hardcoded hex values.App.cssis intentionally near-empty. - Icons come from
lucide-react(tree-shaken per-import). Usebtn-iconclass for icon-only buttons; always add atitleattribute for accessibility. - Inline table editing: rows show read-only data; clicking a cell enters edit mode in-place using a
useState(editing)toggle. New rows use a separate input row appended to<tbody>(not a modal). - Sorting:
SortableHeadercomponent andSortStatetype insrc/components/SortableHeader.tsx. Sorting is client-side only and derives a view from the drag-ordereddisplayItemsarray — clearing sort restores drag order. Drag handles are disabled while a sort is active. - Frequency calculations for real-time previews are in
src/utils/frequency.ts, mirroring the C#FrequencyCalculatorexactly. - Drag-and-drop ordering:
@dnd-kitis used on income/outgo rows. Row order is persisted to the server. Sort handles are hidden while a column sort is active. - Auth:
react-oidc-contextwraps the whole app;AuthGuardprotects all budget routes;TokenSyncwires the OIDC access token into the API client on every auth state change. After OIDC callback, navigation useswindow.location.replace('/')(nothistory.replaceState) so React Router picks up the URL change.
Local development environment
Required env vars when running the API locally (set in appsettings.Development.json or shell):
ConnectionStrings__DefaultConnection— Postgres connection stringOidc__Audience—budget_apiOTEL_EXPORTER_OTLP_ENDPOINT— optional; omit or set to skip telemetry export
Auth / OIDC
- Authority:
https://auth.stwaddle.com/(OpenIddict-based, atsrc.Auth.Serverin a sibling repo) - API audience claim:
budget_api(underscore — must matchOidc__Audienceenv var) - Scope:
budget_api(underscore, not hyphen) - Roles
adminanduserare granted per-user on the client registration in the auth server admin panel. A user with no role getsaccess_deniedat the authorize endpoint. - Frontend OIDC config is baked into the build via
VITE_*env vars; override locally insrc/Budget.Client/.env.local