89 lines
5.8 KiB
Markdown
89 lines
5.8 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
|
|
.\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 `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.
|
|
- **Drag-and-drop ordering**: `@dnd-kit` is 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-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.
|
|
|
|
### Local development environment
|
|
Required env vars when running the API locally (set in `appsettings.Development.json` or shell):
|
|
- `ConnectionStrings__DefaultConnection` — Postgres connection string
|
|
- `Oidc__Audience` — `budget_api`
|
|
- `OTEL_EXPORTER_OTLP_ENDPOINT` — optional; omit or set to skip telemetry export
|
|
|
|
### Auth / OIDC
|
|
- Authority: `https://auth.stwaddle.com/` (OpenIddict-based, at `src.Auth.Server` in a sibling repo)
|
|
- API audience claim: `budget_api` (underscore — must match `Oidc__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`
|