Files
budget/CLAUDE.md
T
2026-05-04 17:42:44 -05:00

82 lines
5.3 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Backend (ASP.NET Core)
```bash
# 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)
```bash
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
```powershell
.\build-budget-image.ps1 # builds multi-stage image and pushes to docker.stwaddle.com/budget:latest
```
## 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 `xmin` shadow property as an EF concurrency token
- **BudgetShare** links a budget to another user by email; `IsPending = true` until the target user logs in (resolved by `KnownUserMiddleware`)
- **Frequency** is a C# enum (`Monthly`, `Biweekly`, `Annually`, etc.); `FrequencyCalculator` converts 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 a `role` claim matching either value get a 403
- `RoleClaimType = "role"` and `NameClaimType = "sub"` are set in JWT options; role checks work with `User.IsInRole()`
- `BudgetAuthorizationService` centralises read/write/owner checks against the budget's owner and any accepted shares — always call `CanReadAsync` / `CanWriteAsync` / `IsOwnerAsync` before touching budget data
- The `TryGetUserId` helper pattern (returns an `IActionResult?` error or out-params the user id) is used consistently across all controllers
- **Enum serialisation**: `JsonStringEnumConverter` is registered globally, so all C# enums are sent/received as strings (e.g. `"Monthly"`, not `5`)
- 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` + `zod` schemas defined in `src/schemas/index.ts`. Never use uncontrolled inputs outside of RHF.
- **CSS design system** lives entirely in `src/index.css` as CSS custom properties — no Tailwind, no CSS modules. Use the existing variables (`--color-primary`, `--green-*`, etc.) rather than hardcoded hex values. `App.css` is intentionally near-empty.
- **Icons** come from `lucide-react` (tree-shaken per-import). Use `btn-icon` class for icon-only buttons; always add a `title` attribute 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**: `SortableHeader` component and `SortState` type in `src/components/SortableHeader.tsx`. Sorting is client-side only and derives a view from the drag-ordered `displayItems` array — 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# `FrequencyCalculator` exactly.
- **Auth**: `react-oidc-context` wraps the whole app; `AuthGuard` protects all budget routes; `TokenSync` wires the OIDC access token into the API client on every auth state change. After OIDC callback, navigation uses `window.location.replace('/')` (not `history.replaceState`) so React Router picks up the URL change.
### Auth / OIDC
- Authority: `https://auth.stwaddle.com/` (OpenIddict-based, at `src.Auth.Server` in a sibling repo)
- API audience claim: `budget-api` (must match `AUTH__AUDIENCE` env var)
- Scope: `budget_api` (underscore, not hyphen)
- Roles `admin` and `user` are granted per-user on the client registration in the auth server admin panel. A user with no role gets `access_denied` at the authorize endpoint.
- Frontend OIDC config is baked into the build via `VITE_*` env vars; override locally in `src/Budget.Client/.env.local`