diff --git a/Dockerfile b/Dockerfile index 7541ceb..bc68c4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,15 @@ WORKDIR /app/client COPY src/Budget.Client/package*.json ./ RUN npm ci COPY src/Budget.Client/ ./ + +ARG VITE_AUTH_AUTHORITY=https://auth.stwaddle.com/ +ARG VITE_AUTH_CLIENT_ID=budget-client +ARG VITE_AUTH_REDIRECT_URI=https://budget.stwaddle.com/callback + +ENV VITE_AUTH_AUTHORITY=$VITE_AUTH_AUTHORITY +ENV VITE_AUTH_CLIENT_ID=$VITE_AUTH_CLIENT_ID +ENV VITE_AUTH_REDIRECT_URI=$VITE_AUTH_REDIRECT_URI + RUN npm run build # Stage 2: Build and publish ASP.NET app diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b9a3e4 --- /dev/null +++ b/README.md @@ -0,0 +1,213 @@ +# Budget + +A multi-user budget web app built around the 50/30/20 framework. Users manage Income and Outgo entries, view a Summary breakdown by type (Need / Want / Save), and can share budgets with other users in edit or view-only mode. + +**Stack:** ASP.NET Core 10 + Vite/React (TypeScript) — served as a single Docker container. PostgreSQL via EF Core. OIDC authentication against `auth.stwaddle.com`. + +--- + +## Environment Variables + +### Backend (ASP.NET Core) + +| Variable | Required | Description | +|----------|----------|-------------| +| `ConnectionStrings__DefaultConnection` | Yes | Full Npgsql connection string | +| `AUTH__AUTHORITY` | Yes | OIDC authority base URL (trailing slash required) | +| `AUTH__AUDIENCE` | Yes | Expected `aud` claim in access tokens — must match the scope's Resource value in the auth server | +| `ASPNETCORE_ENVIRONMENT` | No | `Production` or `Development` (default: `Production` in Docker) | + +If `ConnectionStrings__DefaultConnection` is absent, the app falls back to assembling a connection string from individual variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `POSTGRES_HOST` | `localhost` | Database host | +| `POSTGRES_PORT` | `5432` | Database port | +| `POSTGRES_DB` | `budget` | Database name | +| `POSTGRES_USER` | `budget` | Database user | +| `POSTGRES_PASSWORD` | `changeme` | Database password | + +### Frontend (Vite — baked in at build time) + +These are compiled into the static assets during the Docker build and cannot be changed at runtime. + +| Variable | Description | +|----------|-------------| +| `VITE_AUTH_AUTHORITY` | OIDC authority base URL — must match `AUTH__AUTHORITY` | +| `VITE_AUTH_CLIENT_ID` | OIDC client ID registered in the auth server | +| `VITE_AUTH_REDIRECT_URI` | Full URL of the `/callback` route (e.g. `https://budget.stwaddle.com/callback`) | + +To change frontend auth config after the image is built, rebuild the image with updated build args. See the Dockerfile. + +--- + +## Production Deployment + +The service is defined in `docker/docker-compose.yaml` and managed alongside the rest of the stwaddle.com infrastructure. + +### docker-compose service + +```yaml +budget: + image: docker.stwaddle.com/budget:latest + environment: + - VIRTUAL_HOST=budget.stwaddle.com + - LETSENCRYPT_HOST=budget.stwaddle.com + - VIRTUAL_PORT=8080 + - ASPNETCORE_ENVIRONMENT=Production + - ConnectionStrings__DefaultConnection=Host=apps-db;Port=5432;Database=budget;Username=postgres;Password=${APPS_DB_PASSWORD} + - AUTH__AUTHORITY=https://auth.stwaddle.com/ + - AUTH__AUDIENCE=budget-api + depends_on: + - apps-db + - auth + networks: + - web + - apps-internal + restart: unless-stopped +``` + +### .env entries + +Add to the `.env` file on the server (alongside the existing entries): + +```env +BUDGET_VIRTUAL_HOST=budget.stwaddle.com +BUDGET_LETSENCRYPT_HOST=budget.stwaddle.com +``` + +`APPS_DB_PASSWORD` is already set in `.env` for the recipes app and is shared. The budget app gets its own `budget` database within the same Postgres instance — EF Core's `MigrateAsync()` creates it automatically on first startup. + +### Build and push + +```powershell +.\build-budget-image.ps1 +``` + +This builds the multi-stage Docker image (Node → .NET SDK → ASP.NET runtime) and pushes it to `docker.stwaddle.com/budget:latest`. + +The `VITE_*` build args default to the production values in the Dockerfile. To override: + +```powershell +docker build ` + --build-arg VITE_AUTH_AUTHORITY=https://auth.stwaddle.com/ ` + --build-arg VITE_AUTH_CLIENT_ID=budget-client ` + --build-arg VITE_AUTH_REDIRECT_URI=https://budget.stwaddle.com/callback ` + -t docker.stwaddle.com/budget:latest . +docker push docker.stwaddle.com/budget:latest +``` + +### DNS + +Add an A record for `budget.stwaddle.com` pointing to the Linode server IP before deploying. The acme-companion will issue the Let's Encrypt certificate automatically. If DNS hasn't propagated when the container first starts: + +```bash +docker compose restart acme-companion +``` + +--- + +## Auth Server Setup + +The budget app requires two registrations in the auth server admin panel at `https://auth.stwaddle.com/Admin/` (requires an account with the Admin role). + +### 1. Create the API Scope + +> **Admin → Scopes → Create** + +| Field | Value | +|-------|-------| +| Name | `budget_api` | +| Display Name | `Budget API` | +| Resources | `budget-api` | + +The **Resources** field is what gets written into the `aud` claim of access tokens. It must exactly match `AUTH__AUDIENCE=budget-api` set on the API container. + +### 2. Register the Client Application + +> **Admin → Sites → Create** + +| Field | Value | +|-------|-------| +| Client ID | `budget-client` | +| Display Name | `Budget` | +| Redirect URIs | `https://budget.stwaddle.com/callback` | +| Post-Logout Redirect URIs | `https://budget.stwaddle.com` | +| API Scopes | ✅ `budget_api` | + +The client is always created as **Public** (no secret) with **Authorization Code + PKCE + Refresh Token** — this is enforced by the auth server for all SPA clients. + +For local development, also add the dev URIs (you can edit the client after creation): +- Redirect URI: `http://localhost:5173/callback` +- Post-Logout URI: `http://localhost:5173` + +### 3. Grant Users Access + +Users must be explicitly granted a role on the client before they can log in. + +> **Admin → Sites → [Budget] → Roles → Grant** + +Assign any user who needs access at least the `user` role. Without a role entry the auth server returns `access_denied` at the authorize endpoint. + +### Token details + +| Property | Value | +|----------|-------| +| Flow | Authorization Code + PKCE | +| Access token lifetime | 15 minutes | +| Refresh token lifetime | 14 days | +| Claims in access token | `sub`, `email`, `name`, `role`, `aud` (`budget-api`) | + +The frontend uses `oidc-client-ts` with automatic silent renewal, so users stay logged in across the 15-minute access token window without re-authenticating. + +--- + +## Local Development + +### Prerequisites + +- .NET 10 SDK +- Node 22 +- PostgreSQL (or Docker) + +### Setup + +1. Copy `.env.example` to `.env` and fill in values for your local Postgres instance. + +2. Start the API: + + ```bash + cd src/Budget.Api + dotnet run + ``` + + The API listens on `http://localhost:5000` by default. EF migrations run automatically on startup. + +3. Start the frontend dev server: + + ```bash + cd src/Budget.Client + npm install + npm run dev + ``` + + Vite proxies `/api` requests to `http://localhost:5000`. The app is available at `http://localhost:5173`. + +4. Set the following in `src/Budget.Client/.env.local` (Vite picks this up automatically, it is gitignored): + + ```env + VITE_AUTH_AUTHORITY=https://auth.stwaddle.com/ + VITE_AUTH_CLIENT_ID=budget-client + VITE_AUTH_REDIRECT_URI=http://localhost:5173/callback + ``` + +5. Set the following in `src/Budget.Api/appsettings.Development.json`: + + ```json + { + "AUTH__AUTHORITY": "https://auth.stwaddle.com/", + "AUTH__AUDIENCE": "budget-api" + } + ``` + +The auth server's dev redirect URI (`http://localhost:5173/callback`) must be registered on the client as described in the Auth Server Setup section above.