Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:react/recommended'
'plugin:react/recommended',
'plugin:react-hooks/recommended'
],
settings: {
react: { version: 'detect' },
Expand All @@ -35,7 +36,7 @@ module.exports = {
overrides: [
{
files: ['packages/client/src/**/*.{ts,tsx}'],
env: { browser: true, node: true, es2021: true },
env: { browser: true, es2021: true },
parserOptions: { project: ['./packages/client/tsconfig.json'] },
rules: { 'import/no-named-as-default-member': 'off' }
},
Expand All @@ -44,7 +45,6 @@ module.exports = {
env: { node: true, es2021: true },
parserOptions: { project: ['./packages/server/tsconfig.json'] },
rules: {
'@typescript-eslint/no-var-requires': 'off',
'import/no-named-as-default-member': 'off',
'import/no-named-as-default': 'off',
'no-console': 'off'
Expand Down
91 changes: 26 additions & 65 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,36 @@
# Project Guidelines

## Project Overview

TypeScript monorepo (npm workspaces) with a **React 18 + Vite** single-page app (`packages/client`) and an **Express** REST API (`packages/server`). Firebase Authentication (Google Sign-In) handles identity; the server validates Firebase ID tokens on every protected route via firebase-admin.
TypeScript monorepo (`packages/client` — React 18 + Vite + Tailwind, `packages/server` — Express). Firebase Auth (Google Sign-In); server validates ID tokens via firebase-admin.

## Architecture

```
packages/
client/ React SPA — Firebase client SDK, React Query, axios, Tailwind
server/ Express API — firebase-admin token verification, layered architecture
```

**Server layers** (strictly top-down, each layer imports only the one below):
- `routes/` → `controllers/` → `services/` → `repositories/`
- `repositories/userRepository.ts` wraps all firebase-admin calls — mock this in tests, never firebase-admin directly.
- Middleware order in `app.ts`: `security (helmet)` → `logging (morgan)` → `rateLimiting` → `cors` → `express.json()` → `routes` → `errorHandler` (always last).

**Client layers:**
- `features/<feature>/` — UI components scoped to a feature (auth, login, dashboard, common).
- `api/services/` — raw axios calls; `api/hooks.ts` — React Query wrappers that resolve the Bearer token before calling a service.
- `features/auth/AuthContext.tsx` — single source of auth state (`user`, `loading`, `getIdToken`).

## Build and Test
- **Server** (strict layering): `routes/` → `controllers/` → `services/` → `repositories/`
- **Middleware order**: `helmet` → `morgan` → `rateLimiter` → `cors` → `express.json()` → `routes` → `errorHandler`
- **Client**: `features/<feature>/` for UI; `api/services/` for axios calls; `api/hooks.ts` for React Query wrappers; `features/auth/AuthContext.tsx` for auth state.

## Key Conventions

- TypeScript strict mode; no `any` — use `unknown` + type-guard.
- `async/await` only; wrap handlers in `try/catch`, forward with `next(err)`.
- Named exports everywhere; default export only for page-level components.
- `zod` for all runtime validation; config via `packages/server/src/config.ts` — never read `process.env` directly in feature code.
- Error responses: `res.status(xxx).json({ error: 'message' })` — consistent shape, no per-route variations.
- All routes mount under `/api` in `routes/index.ts`.
- `CORS_ORIGIN` env var drives allowed origins — never hard-code.
- Get auth token via `useAuth().getIdToken()` inside React Query `queryFn`, not at component level.
- In server tests, mock `../../src/firebase` and `../../src/repositories/userRepository` before importing `createApp`.
- All protected routes require `authMiddleware`; `req.user` is a `DecodedIdToken`.
- `helmet()` is global — do not remove or override without justification.
- Never log `Authorization` headers or Firebase ID tokens.
- Do not expose service account JSON in client bundles or version control.
- Conventional Commits: `feat:`, `fix:`, `chore:`, `test:`, `docs:`, `refactor:`.

## Commands

```bash
# Install (run from repo root)
npm install

# Dev (starts client :5173 and server :3001 concurrently)
npm run dev

# Build both packages
npm run build

# Lint all packages
npm run lint

# Test client
npm install # from repo root
npm run dev # client :5173 + server :3001
npm run build && npm run lint # build & lint all
npm run test --workspace=packages/client

# Test server
npm run test --workspace=packages/server
```

## Code Style

- **TypeScript strict mode** throughout; no `any` unless unavoidable — use `unknown` + type-guard instead.
- `async/await` everywhere; never mix `.then()` chains. Always wrap async route handlers / service calls in `try/catch` and forward errors with `next(err)` on the server side.
- Prefer named exports for components and functions; default export only for page-level React components.
- Use `zod` for all runtime input/environment validation (see `packages/server/src/config.ts` for the pattern).
- **Conventional Commits**: `feat:`, `fix:`, `chore:`, `test:`, `docs:`, `refactor:`.

## Project Conventions

- **Auth in API calls**: obtain the token via `useAuth().getIdToken()` inside a React Query `queryFn`, not at component level. See `packages/client/src/api/hooks.ts → useMe`.
- **Firebase Admin mocking**: in server tests always mock `../../src/firebase` and `../../src/repositories/userRepository` before importing `createApp`. See `packages/server/tests/e2e/userRoutes.test.ts`.
- **Config** is validated once at startup via `packages/server/src/config.ts`; access runtime config only through the exported `config` object, never `process.env` directly in feature code.
- **CORS origin** is driven by `CORS_ORIGIN` env var (default `http://localhost:5173`); never hard-code origins.
- **Rate limiting** is global on the Express app; if adding stricter per-route limits, apply them at the router level before the global one.
- **Error responses**: always `res.status(xxx).json({ error: 'message' })` — no custom error shape per route.
- **Route mounting**: all routes live under `/api` in `routes/index.ts`; add a new feature router there.

## Integration Points

- `packages/client/vite.config.ts` proxies `/api/*` → `http://localhost:3001` in dev; the same path hits the real Express server in production (nginx reverse-proxy or equivalent).
- Firebase credentials: client reads `VITE_FIREBASE_*` env vars; server reads `FIREBASE_SERVICE_ACCOUNT_JSON` (preferred) or individual `FIREBASE_PROJECT_ID / FIREBASE_CLIENT_EMAIL / FIREBASE_PRIVATE_KEY` vars.

## Security

- Never log the raw `Authorization` header or Firebase ID tokens.
- Server-side — all protected routes must use `authMiddleware` before any controller; `req.user` is a `DecodedIdToken` set by that middleware.
- Do not expose the firebase-admin service account JSON in client bundles or version control.
- `helmet()` is already applied globally; do not remove or override its defaults without justification.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Documentation files are located in the `docs/` directory and cover various aspec
- [client-testing.md](docs/client-testing.md) – guidance for writing and running client‑side tests
- [docker-dev.md](docs/docker-dev.md) – running the full stack locally with Docker Compose and hot reload (see `.env.example`)
- [docker-prod.md](docs/docker-prod.md) – building and running production images with multi-stage Dockerfiles (includes healthcheck details)
- [postgres.md](docs/postgres.md) – PostgreSQL configuration and usage

---

Expand Down Expand Up @@ -120,6 +121,29 @@ FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
"
```

If you plan to run the API against a PostgreSQL database (the default for
Docker Compose setup), also set a connection string. The compose files start
a `postgres:15-alpine` container with the following default credentials:

```yaml
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=app
```

Those values are wired into the server service via the
`DATABASE_URL` environment variable, which you can override locally like so:

```env
DATABASE_URL=postgres://postgres:postgres@localhost:5432/app
```

In production the compose configuration builds the URL from
`${POSTGRES_USER}`, `${POSTGRES_PASSWORD}`, and `${POSTGRES_DB}`. If the
variable is omitted entirely the server still starts, but it skips database
initialization and repo methods will log a notice; user persistence will be
in-memory only.

### 4. Run in development

```bash
Expand Down Expand Up @@ -218,5 +242,8 @@ Notes:
|---|---|---|---|
| GET | `/api/health` | Public | Server health check |
| GET | `/api/me` | Bearer token | Returns the authenticated user's profile |
| PUT | `/api/me` | Bearer token | Update name/picture in database |
| DELETE | `/api/me` | Bearer token | Delete account and revoke tokens |
| GET | `/api/users` | Bearer token | (Authenticated) list all users |

See [docs/auth.md](docs/auth.md) for how token‑based auth works and [docs/architecture.md](docs/architecture.md) for a full system overview.
18 changes: 17 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ services:
depends_on:
- server

postgres:
image: postgres:15-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
- POSTGRES_DB=${POSTGRES_DB:-app}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"

server:
build:
context: .
Expand All @@ -30,10 +42,14 @@ services:
environment:
- NODE_ENV=production
- PORT=${SERVER_PORT:-3001}
- DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-app}
depends_on:
- postgres
healthcheck:
test: ["CMD-SHELL","curl -f http://localhost:${SERVER_PORT:-3001}/api/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3

volumes: {}
volumes:
postgres_data:
19 changes: 17 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ services:
- .env
environment:
- VITE_API_URL=http://server:${SERVER_PORT:-3001}
command: npm run dev
command: sh -c "npm install && npm run dev"
postgres:
image: postgres:15-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"

server:
build:
Expand All @@ -32,7 +43,10 @@ services:
- .env
environment:
- PORT=${SERVER_PORT}
command: npm run dev
- DATABASE_URL=postgres://postgres:postgres@postgres:5432/app
depends_on:
- postgres
command: sh -c "npm install && npm run dev"
healthcheck:
test: ["CMD-SHELL","curl -f http://localhost:${SERVER_PORT:-3001}/api/health || exit 1"]
interval: 30s
Expand All @@ -42,3 +56,4 @@ services:
volumes:
client_node_modules:
server_node_modules:
postgres_data:
1 change: 1 addition & 0 deletions docs/backend-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ packages/server/
- **`config.test.ts`**: validates environment variable parsing and defaults.
- **`middleware/authMiddleware.test.ts`**: exercises auth middleware with mocked `verifyIdToken` responses.
- **`controllers/userController.test.ts`**: ensures the controller calls the service and returns JSON.
- **`repositories/userRepository.test.ts`**: verifies SQL queries and parameter binding; the `db` helper is mocked so no real database is required.

All unit tests use `vi.mock()` to stub dependencies and `vi.mocked()` for type-checked mock access. Mocks are reset between examples.

Expand Down
13 changes: 13 additions & 0 deletions docs/client-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ The Firebase module is stubbed globally to prevent real initialization.
- **Objective**: exercise all three rendering branches of `ProtectedRoute`.
- **Technique**: stub `useAuth()` return value with loading, unauthenticated, and authenticated states; wrap in `MemoryRouter`.

### `features/dashboard/Dashboard.test.tsx`

- **Objective**: validate interactive dashboard behaviour now that users can edit
their name/picture and view a list of all users.
- **Additions**:
- form inputs pre‑populate from `useMe()` and send updates via
`useUpdateProfile()`.
- a secondary query hook `useUsers()` fetches `/api/users`; tests mock it and
assert that the list renders when data is returned.
- **Technique**: in addition to existing mocks for auth and API hooks,
`useUsers` is also stubbed; `userEvent` types into inputs and clicks the save
button, and the mutation is inspected for correct arguments.

### `api/services/userService.test.ts`

- **Objective**: unit‑test HTTP helper functions without network.
Expand Down
51 changes: 51 additions & 0 deletions docs/postgres.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# PostgreSQL Setup

The template includes optional PostgreSQL support for persisting user data
(in addition to Firebase auth). The server reads its connection string from
`DATABASE_URL`.

## Docker Compose

Both development and production compose files define a `postgres` service:

```yaml
services:
postgres:
image: postgres:15-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
```

When `DATABASE_URL` is omitted, the server still boots but skips database
initialization and repository operations. The same credentials are used for
the `server` container via
`postgres://postgres:postgres@postgres:5432/app`.

### Environment variable

Set in `packages/server/.env`:

```env
DATABASE_URL=postgres://postgres:postgres@localhost:5432/app
```

Use a different URL in production; see `docker-compose.prod.yml` for examples
with `${POSTGRES_*}` variables.

## Code hooks

* `config.ts` uses zod to validate the URL.
* `db.ts` creates a `pg.Pool` from the URL, runs `initDb()` on startup to
create a `users` table, and exports a `query` helper.

## Testing

Server unit tests mock repositories and do not require a running database. The
URL is optional.
Loading