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
7 changes: 7 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
build
coverage
.venv
.vscode
.DS_Store
67 changes: 67 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module.exports = {
root: true,
env: { es2021: true },
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
sourceType: 'module'
},
plugins: ['@typescript-eslint', 'import', 'react', 'react-hooks'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:react/recommended'
],
settings: {
react: { version: 'detect' },
'import/resolver': {
typescript: {
project: ['./tsconfig.json', './packages/*/tsconfig.json']
}
}
},
rules: {
'import/no-unresolved': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/triple-slash-reference': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'import/default': 'off',
'react/react-in-jsx-scope': 'off'
},
overrides: [
{
files: ['packages/client/src/**/*.{ts,tsx}'],
env: { browser: true, node: true, es2021: true },
parserOptions: { project: ['./packages/client/tsconfig.json'] },
rules: { 'import/no-named-as-default-member': 'off' }
},
{
files: ['packages/server/src/**/*.{ts,tsx}'],
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'
}
},
{
files: ['**/*.test.ts', '**/*.test.tsx', 'tests/**', 'packages/**/tests/**'],
env: { es2021: true },
rules: {
'import/no-extraneous-dependencies': 'off',
'react/display-name': 'off'
}
},
{
files: ['**/*.config.ts', '**/*vite*.ts', '**/*vitest*.ts', '**/*tailwind*.ts'],
parserOptions: { tsconfigRootDir: __dirname },
rules: { '@typescript-eslint/no-explicit-any': 'off' }
}
]
};
47 changes: 47 additions & 0 deletions .github/.copilot-commit-message-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Commit Message Instructions

Follow the **Conventional Commits** specification for every commit.

## Format

```
<type>(<optional scope>): <short summary>

[optional body]

[optional footer(s)]
```

## Types

| Type | When to use |
|---|---|
| `feat` | New feature or capability |
| `fix` | Bug fix |
| `refactor` | Code change that neither fixes a bug nor adds a feature |
| `test` | Adding or updating tests |
| `chore` | Tooling, deps, config changes with no production code change |
| `docs` | Documentation only |
| `perf` | Performance improvement |
| `ci` | CI/CD pipeline changes |

## Scope (optional)

Use the package or layer name: `client`, `server`, `auth`, `middleware`, `routes`, `config`, `docker`.

## Rules

- Summary line: imperative mood, lowercase, no trailing period, max 72 chars.
- Body: wrap at 72 chars; explain *why*, not *what*.
- Reference issues/PRs in the footer: `Closes #123`, `Refs #456`.
- Breaking changes: add `BREAKING CHANGE:` footer or append `!` after the type/scope.

## Examples

```
feat(server): add PUT /api/me endpoint for profile updates
fix(auth): handle token refresh race condition in AuthContext
chore(deps): bump firebase-admin to 13.0.0
test(server): add e2e coverage for DELETE /api/me
refactor(client): extract axios token interceptor into api/axios.ts
```
75 changes: 75 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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.

## 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

```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 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.
43 changes: 43 additions & 0 deletions .github/instructions/client.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
applyTo: "packages/client/**"
---

# Client Conventions

## Feature Structure

Each feature lives in `packages/client/src/features/<feature>/`. Add new UI concerns as a new folder here; do not scatter components in `src/` root.

Current features: `auth/`, `common/`, `dashboard/`, `login/`.

## Auth

- Single source of truth: `features/auth/AuthContext.tsx` — exposes `user`, `loading`, `signInWithGoogle`, `signOut`, `getIdToken`.
- Use `useAuth()` to access auth state; throw/redirect if used outside `<AuthProvider>`.
- Gate every private page with the `<ProtectedRoute>` wrapper (see `features/common/ProtectedRoute.tsx`).
- Obtain Firebase ID tokens inside a React Query `queryFn`, never at component render time.

## API Layer

- Raw axios calls belong in `api/services/<feature>Service.ts`.
- React Query wrappers belong in `api/hooks.ts`; fetch the token there before calling the service.
- The axios instance (`api/axios.ts`) has `baseURL: '/api'`; never hard-code the API base URL elsewhere.
- Response types live in `api/types.ts`; keep them aligned with server response shapes.

## State Management

- Server state: React Query (`@tanstack/react-query`) only.
- Local UI state: `useState` / `useReducer`.
- No global client-side state library needed; avoid adding one unless the need is compelling.

## Styling

- Tailwind CSS utility classes directly in JSX; no CSS modules or styled components.
- Responsive and accessible by default — include `aria-*` attributes for interactive elements.

## Testing

- Test files co-located under `src/` matching the source file name (`*.test.tsx`).
- Mock `useAuth` by spying on `AuthContext`: `vi.spyOn(AuthContext, 'useAuth').mockReturnValue(...)`.
- Use `@testing-library/react` + `@testing-library/user-event`; query by accessible role/text, not by class or test-id unless unavoidable.
- Wrap renders that need routing in `<MemoryRouter>`.
41 changes: 41 additions & 0 deletions .github/instructions/server.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
applyTo: "packages/server/**"
---

# Server Conventions

## Adding a New Feature Route

1. Create `packages/server/src/routes/<feature>.ts` — mount the router; protect every non-public endpoint with `authMiddleware`.
2. Create `packages/server/src/controllers/<feature>Controller.ts` — handle `req`/`res` only; delegate logic to the service.
3. Create `packages/server/src/services/<feature>Service.ts` — pure business logic; no Express imports.
4. If persistence is needed, add `packages/server/src/repositories/<feature>Repository.ts` — only file allowed to import firebase-admin.
5. Register the new router in `packages/server/src/routes/index.ts` under `/api`.

## Middleware

- Middleware order (defined in `app.ts`) must stay: `security` → `logging` → `rateLimiting` → `cors` → `express.json()` → routes → `errorHandler`.
- Never place `errorHandler` before any route or it won't catch errors.
- Per-route rate limits go on the specific router, not the app.

## Error Handling

- All errors forwarded via `next(err)` are caught by `errorHandler` in `middleware/errorHandler.ts`.
- Return `res.status(xxx).json({ error: 'descriptive message' })` — do not invent custom shapes per route.
- Log structured contexts in the service layer; never log the raw `Authorization` header or token strings.

## Testing

- Test files live in `packages/server/tests/` (e2e under `e2e/`, unit under `unit/`).
- **Always** mock `../../src/firebase` and `../../src/repositories/userRepository` at the top of every test file before importing `createApp`. See `tests/e2e/userRoutes.test.ts` for the canonical pattern:
```ts
vi.mock('../../src/firebase', () => ({ default: {} }));
vi.mock('../../src/repositories/userRepository', () => ({ verifyIdToken: vi.fn() }));
```
- Use `supertest` for e2e route tests; use plain `vitest` for unit tests of services and controllers.
- Call `vi.resetAllMocks()` in `beforeEach` to prevent mock bleed between tests.

## Environment / Config

- All env vars are validated at startup in `src/config.ts` using `zod`. Add new vars to the `EnvSchema` there; export them on the `config` object.
- Never access `process.env` outside `config.ts`.
27 changes: 27 additions & 0 deletions .github/instructions/typescript.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
applyTo: "**/*.ts,**/*.tsx"
---

# TypeScript Guidelines

- Enable and respect `strict` mode — no implicit `any`, no non-null assertions (`!`) unless the null/undefined case is provably impossible.
- Use `unknown` + type-guard (`instanceof`, `typeof`, Zod `.safeParse`) instead of casting to `any`.
- Prefer interfaces for object shapes that will be extended; use `type` aliases for unions, intersections, and mapped types.
- All async functions must be fully `await`-ed — never fire-and-forget unless a deliberate side-effect comment explains why.
- Public functions and exported types must have JSDoc `/** */` comments on the `why`, not just `what`.
- Avoid re-exporting everything with barrel `index.ts` files; import directly from the source file to keep tree-shaking effective.
- Use `satisfies` or explicit return types on exported functions so callers get precise inference.

## Client (`packages/client/src`)

- Components: named export for utilities/hooks; **default export only** for page/route-level components.
- Hooks (`useXxx`): one concern per hook; keep side-effects (`useEffect`) inside the hook, not in the component.
- Never call `getIdToken()` at component render time — call it inside a React Query `queryFn` only (see `api/hooks.ts → useMe`).
- Tailwind: use utility classes directly; avoid `@apply` in CSS unless reusing a multi-class pattern 3+ times.

## Server (`packages/server/src`)

- Every async Express handler must end in `try/catch` → `next(err)` or be wrapped in an async error-forwarding helper.
- Import config exclusively from `config.ts`; never read `process.env` directly in feature code.
- Validate all incoming request bodies with `zod` before touching `req.body`.
- Layer boundary: `controllers` import from `services`; `services` import from `repositories`; cross-layer imports are forbidden.
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
ci:
name: Lint · Test · Build
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Test client
run: npm run test --workspace=packages/client

- name: Test server
run: npm run test --workspace=packages/server

- name: Build
run: npm run build
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"file": "./.github/.copilot-commit-message-instructions.md"
}
]
}
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,34 @@ All workspace commands are defined at the root `package.json` and are forwarded

---

## Linting 🔍

The repository includes a shared ESLint configuration at the repo root. The primary lint command is wired at the root `package.json` and forwards to package-level lint scripts.

Run lint across all packages:

```bash
npm run lint
```

Run lint for a single package:

```bash
npm run lint --workspace=packages/client
npm run lint --workspace=packages/server
```

Auto-fix fixable issues across the repository:

```bash
npx eslint . --ext .ts,.tsx --fix
```

Notes:
- The server `lint` script runs `eslint` followed by `tsc --noEmit` so type errors are checked as part of linting.
- See [docs/eslint.md](docs/eslint.md) for full configuration details, rules and overrides.


## API Endpoints

| Method | Path | Auth | Description |
Expand Down
Loading