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
165 changes: 165 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Respondent-Driven Sampling (RDS) App - AI Coding Instructions

## Project Overview

This is a **monolithic React + Node.js application** for homelessness research data collection via Respondent-Driven Sampling. The backend (Express + MongoDB) serves the frontend (React) as static files in production.

**Research Context**: Volunteers conduct surveys with homeless individuals using QR code-based referral chains. Each survey generates 3 child referral codes, creating a social network sampling structure for population estimation.

## Architecture & Key Patterns

### Monorepo Structure

- **`client/`**: React SPA (Vite + TypeScript + Material-UI)
- **`server/`**: Express API (TypeScript + MongoDB via Mongoose)
- **Shared types**: Client imports server permission constants via `@/permissions/*` path alias (see `client/tsconfig.json`)

### Backend Architecture: Domain-Driven Controllers

The backend uses a **layered domain structure** distinct from typical Express apps:

```
server/src/database/{domain}/
├── mongoose/ # Mongoose models + hooks
│ ├── {domain}.model.ts
│ └── {domain}.hooks.ts
├── zod/ # Zod validation schemas
│ ├── {domain}.base.ts # Base schema
│ └── {domain}.validator.ts # Request validators
├── {domain}.controller.ts # Business logic (called from routes)
└── {domain}.utils.ts # Domain utilities
```

**Critical**: Controllers in `database/{domain}/*.controller.ts` contain business logic, NOT routes. Routes in `routes/v2/{domain}.ts` call controller functions. See `server/src/database/survey/survey.controller.ts` (generates unique survey codes) vs `server/src/routes/v2/surveys.ts`.

### Authentication & Authorization Flow

1. **Token Storage**: Client uses Zustand persistent store (`useAuthStore`) → stores JWT in localStorage
2. **Auth Middleware** (`server/src/middleware/auth.ts`):
- Verifies JWT signature
- Checks user approval status (must be `APPROVED`)
- Fetches user's latest survey location for location-based permissions
- Injects CASL `Ability` into `req.authorization`
3. **CASL Permissions** (`server/src/permissions/`):
- Role-based + attribute-based (conditions like `IS_CREATED_BY_SELF`, `HAS_SAME_LOCATION`)
- Permissions are MongoDB query conditions (see `CONDITION_QUERIES` in `constants.ts`)
- Frontend mirrors permission checks via `useAbility` hook

**Example**: Volunteers can only update surveys they created today at their current location:

```typescript
// server/src/permissions/permissions.ts
can(ACTIONS.CASL.UPDATE, SUBJECTS.SURVEY, {
conditions: [CONDITIONS.IS_CREATED_BY_SELF, CONDITIONS.WAS_CREATED_TODAY],
});
```

### Survey Referral Chain System

**Core Concept**: Each survey has `surveyCode` (its own ID) and `parentSurveyCode` (referrer). Upon creation, generates 3 unique `childSurveyCodes` for participants to refer others.

**Key Controller Functions** (`server/src/database/survey/survey.controller.ts`):

- `generateUniqueChildSurveyCodes()`: Creates 3 collision-free 8-char hex codes
- `getParentSurveyCode()`: Finds parent survey by searching `childSurveyCodes` arrays

**QR Code Workflow**:

1. Survey completion → QR page (`client/src/pages/QrPage/`) displays `childSurveyCodes`
2. Participant scans code → `ApplyReferral` page sets `parentSurveyCode` for new survey

## Development Workflows

### Local Development

```bash
# Terminal 1 - Backend (port 1234)
cd server
npm install
npm run dev # tsx with --watch flag

# Terminal 2 - Frontend (port 3000, proxies API to :1234)
cd client
npm install
npm run dev # Vite dev server
```

**Environment Setup**: Copy `server/.env.example` to `server/.env` with MongoDB URI (Azure Cosmos DB), Twilio credentials, JWT secret.

### Testing

```bash
cd server
npm test # Jest with ts-jest
npm run test:watch # Watch mode
npm run test:coverage # Coverage reports
```

**Test Structure**: Tests live alongside source in `__tests__/` folders (e.g., `server/src/database/survey/__tests__/`). Use `@jest/globals` imports for describe/test/expect.

### API Versioning

- **v1 routes** (`/api/auth`, `/api/surveys`): Legacy, being deprecated
- **v2 routes** (`/api/v2/users`, `/api/v2/surveys`): Current, use these for new features
- All v2 routes use Zod validation middleware: `validate(createSurveySchema)` before handler

### Path Aliases (Critical)

Both client and server use `@/*` for imports:

- **Server**: `tsconfig.json` + `tsc-alias` build step resolves to `src/*`
- **Client**: Vite resolves `@/*` to `src/*`, plus special `@/permissions/*` → `../server/src/permissions/*` for shared constants

## Deployment (Azure App Service)

**Build Process**:

```bash
cd client && npm run build # Outputs to client/dist/
cp -r client/dist server/ # Server serves static files
cd server && npm run build # TypeScript compilation to server/build/
node build/index.js # Production server
```

**GitHub Actions**: See `docs/deployment.md`. Workflow builds client, copies to `server/dist/`, deploys `server/` folder to Azure. Uses `AZURE_PUBLISH_PROFILE` secret.

**Manual Deploy**: VSCode Azure extension deploys `server/` folder (configured in `.vscode/settings.json`). Delete `node_modules` before deploy to save bandwidth.

## Security Requirements

**Headers**: Extensive security middleware in `server/src/index.ts`:

- CSP, HSTS, X-Frame-Options, XSS-Protection enforced on ALL responses
- Custom middleware re-applies headers per-route (lines 76-89)
- Do NOT modify security headers without team review

**CORS**: Currently set to `origin: '*'` (line 137) - CHANGE in production to specific domains.

## Common Gotchas

1. **Survey Code Uniqueness**: `childSurveyCodes` must be unique within array AND globally. Controller retries 3 times before throwing `SURVEY_CODE_GENERATION_ERROR`.

2. **Location Context**: Auth middleware derives user's "latest location" from their most recent survey, NOT user profile. This affects permission queries for reading/updating surveys.

3. **SWR Cache**: Frontend uses SWR for data fetching. After mutations (create/update), manually call `mutate()` to invalidate cache (see `client/src/hooks/useApi.tsx` lines 145-148).

4. **Approval Flow**: New users register with `approvalStatus: PENDING`. Admins approve via `/api/v2/users/:id` PATCH. Auth middleware blocks non-approved users at 403.

5. **Mongoose Hooks**: Pre-save validation in `{domain}.hooks.ts` files (e.g., `survey.hooks.ts` validates survey code existence, referral chain validity). These run BEFORE Zod validation.

## Project-Specific Conventions

- **Route handlers**: Return explicit `res.status().json()`, never implicit returns
- **Error responses**: Use domain errors from `server/src/database/utils/errors.ts`, e.g., `throw errors.SURVEY_NOT_FOUND`
- **Zod schemas**: Base schemas in `zod/*.base.ts` (full document), validators in `zod/*.validator.ts` (API requests)
- **Component exports**: Use barrel exports `index.ts` files (e.g., `client/src/pages/index.ts` exports all pages)
- **Styling**: Material-UI theme in `client/src/theme/muiTheme.ts` + legacy CSS files (being migrated)

## Key Files for Context

- **Backend entry**: `server/src/index.ts` - security setup, route registration
- **Auth flow**: `server/src/middleware/auth.ts`, `client/src/contexts/AuthContext.tsx`
- **Permissions**: `server/src/permissions/constants.ts`, `client/src/hooks/useAbility.tsx`
- **Survey logic**: `server/src/database/survey/survey.controller.ts`
- **API client**: `client/src/hooks/useApi.tsx` - all fetch logic with auth
- **Routes**: `server/src/routes/v2/` - RESTful API endpoints
19 changes: 18 additions & 1 deletion client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 18 additions & 5 deletions client/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

import { Action, Condition, Subject } from '@/permissions/constants';
import {
deleteAuthToken,
getAuthToken,
getDecodedAuthToken,
deleteAuthToken
getDecodedAuthToken
} from '@/utils/authTokenHandler';

interface AuthState {
Expand Down Expand Up @@ -62,7 +62,7 @@

export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<AuthState>(() => {
const token = getAuthToken(); // Reads from Zustand
const token = getAuthToken(); // Reads from Zustand
if (!token) return getDefaultAuthState();

const decoded = getDecodedAuthToken();
Expand All @@ -76,18 +76,29 @@
permissions: [],
lastestLocationObjectId: '',
isLoggedIn: true,
// REVIEW: where are we using isLoading?
isLoading: true
};
});

// Fetch additional data on mount
/*
The `useEffect` hook runs once when the component mounts (indicated by the empty dependency array `[]`). This is the authentication context initialization logic that determines whether to load user data or immediately mark the auth state as ready.

**The conditional logic branch works as follows**: If both `state.token` and `state.userObjectId` exist (meaning the user has a valid JWT token and MongoDB user ID stored in localStorage via Zustand), the effect calls `fetchUserContext()` to retrieve the full user profile from the backend. This happens when a user refreshes the page or returns to the app with an existing session. The function will make an API call to `/api/v2/users/:id`, validate the token via the auth middleware, and populate the context with user details like name, role, and permissions.

**If either value is missing**, the effect takes a different path: it immediately sets `isLoading: false` without making any API calls. This signals to the rest of the application that there's no active session to restore, allowing login/registration screens to render immediately instead of showing a loading spinner while waiting for a user fetch that would fail anyway.

**A critical gotcha here**: The empty dependency array means this only runs on mount, so if `state.token` or `state.userObjectId` change later (e.g., after login), this effect won't re-run. The app must handle user data fetching separately in the login flow—typically in the login success handler that sets these values and then manually calls `fetchUserContext()` or triggers a re-render that causes the context to update through other means.

This pattern is common in auth contexts because you want to check for existing sessions exactly once at startup, not repeatedly as state changes during normal app usage. The actual authentication state updates happen through explicit actions (login, logout) rather than reactive effects.
*/
useEffect(() => {
if (state.token && state.userObjectId) {
fetchUserContext(state.userObjectId);
} else {
setState(prev => ({ ...prev, isLoading: false }));
}
}, []);

Check warning on line 101 in client/src/contexts/AuthContext.tsx

View workflow job for this annotation

GitHub Actions / lint-and-type-check (client)

React Hook useEffect has missing dependencies: 'state.token' and 'state.userObjectId'. Either include them or remove the dependency array

const fetchUserContext = async (userObjectId: string) => {
try {
Expand Down Expand Up @@ -125,7 +136,7 @@
isLoading: false
}));
} catch (error) {
console.error('Error fetching user context:', error);

Check warning on line 139 in client/src/contexts/AuthContext.tsx

View workflow job for this annotation

GitHub Actions / lint-and-type-check (client)

Unexpected console statement
setState(prev => ({ ...prev, isLoading: false }));
}
};
Expand Down Expand Up @@ -155,10 +166,11 @@
};

const clearSession = () => {
deleteAuthToken(); // Clears from Zustand
deleteAuthToken(); // Clears from Zustand
setState(getDefaultAuthState());
};

// The value that you want to pass to all the components reading this context inside this provider.
const value: AuthContextValue = {
...state,
setToken,
Expand All @@ -176,7 +188,8 @@
);
};

// REVIEW: Why is this here? Move to a separate file? What does useAuth do?
export const useAuthContext = () => {

Check warning on line 192 in client/src/contexts/AuthContext.tsx

View workflow job for this annotation

GitHub Actions / lint-and-type-check (client)

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuthContext must be used within AuthProvider');
Expand Down
Loading
Loading