1138 lines
26 KiB
Markdown
1138 lines
26 KiB
Markdown
# API Endpoint Changes
|
|
|
|
This document provides a comprehensive mapping of V1 API endpoints to their V2 equivalents, including request/response format changes, authentication differences, and code migration examples.
|
|
|
|
## Overview
|
|
|
|
V2 API represents a complete redesign with:
|
|
|
|
- **RESTful conventions** (proper HTTP methods)
|
|
- **Unified namespace** (single API at `/api/*`)
|
|
- **JWT authentication** (Bearer tokens instead of sessions)
|
|
- **Zod validation** (type-safe request validation)
|
|
- **Standardized responses** (`{ success, data, pagination }` structure)
|
|
|
|
!!! tip "Migration Strategy"
|
|
Update frontend API calls incrementally, starting with authentication (foundational), then module by module (campaigns, locations, shifts, etc.).
|
|
|
|
## Authentication Changes
|
|
|
|
### V1 Authentication (Session Cookies)
|
|
|
|
**V1 Login**:
|
|
```javascript
|
|
// POST /auth/login
|
|
fetch('http://localhost:3333/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include', // Send/receive cookies
|
|
body: JSON.stringify({
|
|
email: 'admin@example.com',
|
|
password: 'password123'
|
|
})
|
|
});
|
|
|
|
// Response: 302 Redirect to /dashboard
|
|
// Session cookie set automatically
|
|
|
|
// Subsequent requests
|
|
fetch('http://localhost:3333/campaigns', {
|
|
credentials: 'include' // Sends session cookie
|
|
});
|
|
```
|
|
|
|
### V2 Authentication (JWT Bearer Tokens)
|
|
|
|
**V2 Login**:
|
|
```typescript
|
|
// POST /api/auth/login
|
|
const response = await fetch('http://localhost:4000/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
email: 'admin@example.com',
|
|
password: 'Admin123!'
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Response:
|
|
// {
|
|
// "success": true,
|
|
// "data": {
|
|
// "user": {
|
|
// "id": "clx1a2b3c4d5e6f7g8h9i",
|
|
// "email": "admin@example.com",
|
|
// "name": "Admin User",
|
|
// "role": "SUPER_ADMIN"
|
|
// },
|
|
// "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
// "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
// }
|
|
// }
|
|
|
|
// Store tokens (localStorage, sessionStorage, or memory)
|
|
localStorage.setItem('accessToken', data.data.accessToken);
|
|
localStorage.setItem('refreshToken', data.data.refreshToken);
|
|
|
|
// Subsequent requests
|
|
fetch('http://localhost:4000/api/influence/campaigns', {
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
|
|
}
|
|
});
|
|
```
|
|
|
|
**V2 Token Refresh**:
|
|
```typescript
|
|
// POST /api/auth/refresh
|
|
const response = await fetch('http://localhost:4000/api/auth/refresh', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
refreshToken: localStorage.getItem('refreshToken')
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Response:
|
|
// {
|
|
// "success": true,
|
|
// "data": {
|
|
// "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
// "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // New token (rotation)
|
|
// }
|
|
// }
|
|
|
|
// Update stored tokens
|
|
localStorage.setItem('accessToken', data.data.accessToken);
|
|
localStorage.setItem('refreshToken', data.data.refreshToken);
|
|
```
|
|
|
|
### Authentication Endpoint Mapping
|
|
|
|
| V1 Endpoint | V2 Endpoint | Method | Changes |
|
|
|-------------|-------------|--------|---------|
|
|
| `/auth/login` | `/api/auth/login` | POST | Returns JWT tokens instead of setting cookie |
|
|
| `/auth/logout` | `/api/auth/logout` | POST | Requires `refreshToken` in body |
|
|
| `/auth/register` | `/api/auth/register` | POST | Always creates USER role (no role in request) |
|
|
| `/auth/me` | `/api/auth/me` | GET | Returns 401 if invalid (not 404) |
|
|
| - | `/api/auth/refresh` | POST | New: refresh token rotation |
|
|
|
|
## Influence Module API
|
|
|
|
### Campaigns
|
|
|
|
#### V1 Campaign Endpoints
|
|
|
|
```javascript
|
|
// List campaigns
|
|
GET /campaigns
|
|
Query: ?page=1
|
|
|
|
// View campaign
|
|
GET /campaigns/:id
|
|
|
|
// Create campaign (admin)
|
|
POST /campaigns/create
|
|
Body: { Title, Description, Slug, IsActive }
|
|
|
|
// Update campaign (admin)
|
|
POST /campaigns/:id/edit
|
|
Body: { Title, Description, Slug, IsActive }
|
|
|
|
// Delete campaign (admin)
|
|
POST /campaigns/:id/delete
|
|
```
|
|
|
|
#### V2 Campaign Endpoints
|
|
|
|
```typescript
|
|
// List campaigns
|
|
GET /api/influence/campaigns
|
|
Query: ?page=1&limit=20&search=query&active=true&highlighted=false
|
|
Auth: Optional (public returns only active campaigns)
|
|
|
|
// Get campaign by ID
|
|
GET /api/influence/campaigns/:id
|
|
Auth: Required (admin)
|
|
|
|
// Get campaign by slug (public)
|
|
GET /api/influence/campaigns/public/:slug
|
|
Auth: None
|
|
|
|
// Create campaign
|
|
POST /api/influence/campaigns
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Body: {
|
|
"title": "Save the Trees",
|
|
"description": "Campaign description",
|
|
"slug": "save-the-trees",
|
|
"active": true,
|
|
"highlighted": false,
|
|
"targetLevel": "federal",
|
|
"targetPosition": "MP",
|
|
"responseWallEnabled": true
|
|
}
|
|
|
|
// Update campaign
|
|
PUT /api/influence/campaigns/:id
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Body: { title, description, ... } // Partial update
|
|
|
|
// Delete campaign
|
|
DELETE /api/influence/campaigns/:id
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Toggle active status
|
|
PATCH /api/influence/campaigns/:id/toggle-active
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Toggle highlighted status
|
|
PATCH /api/influence/campaigns/:id/toggle-highlighted
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
```
|
|
|
|
#### Campaign Response Format Changes
|
|
|
|
**V1 Response**:
|
|
```json
|
|
{
|
|
"list": [
|
|
{
|
|
"Id": 1,
|
|
"Title": "Save the Trees",
|
|
"Description": "Campaign description",
|
|
"Slug": "save-the-trees",
|
|
"IsActive": true,
|
|
"Created": "2024-01-15T10:30:00Z"
|
|
}
|
|
],
|
|
"pageInfo": {
|
|
"totalRows": 100,
|
|
"page": 1,
|
|
"pageSize": 20
|
|
}
|
|
}
|
|
```
|
|
|
|
**V2 Response**:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"id": "clx1a2b3c4d5e6f7g8h9i",
|
|
"title": "Save the Trees",
|
|
"description": "Campaign description",
|
|
"slug": "save-the-trees",
|
|
"active": true,
|
|
"highlighted": false,
|
|
"targetLevel": "federal",
|
|
"targetPosition": "MP",
|
|
"responseWallEnabled": true,
|
|
"createdAt": "2024-01-15T10:30:00.000Z",
|
|
"updatedAt": "2024-01-15T10:30:00.000Z",
|
|
"createdBy": {
|
|
"id": "clx1a2b3c4d5e6f7g8h9i",
|
|
"name": "Admin User",
|
|
"email": "admin@example.com"
|
|
}
|
|
}
|
|
],
|
|
"pagination": {
|
|
"page": 1,
|
|
"limit": 20,
|
|
"total": 100,
|
|
"totalPages": 5
|
|
}
|
|
}
|
|
```
|
|
|
|
### Representatives
|
|
|
|
#### V1 Representative Endpoints
|
|
|
|
```javascript
|
|
// Lookup representatives by postal code
|
|
POST /representatives/lookup
|
|
Body: { postalCode: "M5V 1A1" }
|
|
|
|
// List cached representatives (admin)
|
|
GET /admin/representatives
|
|
```
|
|
|
|
#### V2 Representative Endpoints
|
|
|
|
```typescript
|
|
// Lookup representatives (public)
|
|
POST /api/influence/representatives/lookup
|
|
Auth: None
|
|
Body: { "postalCode": "M5V1A1" }
|
|
Response: {
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"name": "John Doe",
|
|
"email": "john.doe@parl.gc.ca",
|
|
"district": "Toronto Centre",
|
|
"party": "Liberal",
|
|
"level": "federal",
|
|
"photoUrl": "https://..."
|
|
}
|
|
]
|
|
}
|
|
|
|
// List cached representatives (admin)
|
|
GET /api/influence/representatives
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Query: ?page=1&limit=20&level=federal&party=Liberal&search=John
|
|
|
|
// Get representative stats (admin)
|
|
GET /api/influence/representatives/stats
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Response: {
|
|
"success": true,
|
|
"data": {
|
|
"total": 338,
|
|
"byLevel": { "federal": 338, "provincial": 124 },
|
|
"byParty": { "Liberal": 159, "Conservative": 119, "NDP": 25 }
|
|
}
|
|
}
|
|
|
|
// Get representative by ID (admin)
|
|
GET /api/influence/representatives/:id
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Delete representative (admin)
|
|
DELETE /api/influence/representatives/:id
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Health check
|
|
GET /api/influence/representatives/health
|
|
Auth: None
|
|
```
|
|
|
|
### Campaign Emails
|
|
|
|
#### V1 Email Endpoints
|
|
|
|
```javascript
|
|
// Send campaign email
|
|
POST /campaigns/:id/send-email
|
|
Body: { senderName, senderEmail, postalCode }
|
|
```
|
|
|
|
#### V2 Email Endpoints
|
|
|
|
```typescript
|
|
// Send campaign email (public)
|
|
POST /api/influence/campaign-emails/send-email
|
|
Auth: None
|
|
Rate Limit: 30 requests/hour per IP
|
|
Body: {
|
|
"campaignId": "clx1a2b3c4d5e6f7g8h9i",
|
|
"postalCode": "M5V1A1",
|
|
"senderName": "Jane Doe",
|
|
"senderEmail": "jane@example.com",
|
|
"customMessage": "Optional custom message"
|
|
}
|
|
|
|
// Track mailto clicks (public)
|
|
GET /api/influence/campaign-emails/track-mailto/:emailId
|
|
Auth: None
|
|
|
|
// List campaign emails (admin)
|
|
GET /api/influence/campaign-emails
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Query: ?campaignId=xxx&page=1&limit=20&sortBy=createdAt&sortOrder=desc
|
|
|
|
// Get campaign email stats (admin)
|
|
GET /api/influence/campaign-emails/stats/:campaignId
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Response: {
|
|
"success": true,
|
|
"data": {
|
|
"totalEmails": 1234,
|
|
"queuedEmails": 5,
|
|
"sentEmails": 1200,
|
|
"failedEmails": 29,
|
|
"mailtoClicks": 340
|
|
}
|
|
}
|
|
```
|
|
|
|
### Email Queue
|
|
|
|
#### V2 Email Queue Endpoints (New)
|
|
|
|
```typescript
|
|
// Get queue stats (admin)
|
|
GET /api/influence/email-queue/stats
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Response: {
|
|
"success": true,
|
|
"data": {
|
|
"waiting": 10,
|
|
"active": 2,
|
|
"completed": 5000,
|
|
"failed": 15,
|
|
"delayed": 0,
|
|
"paused": false
|
|
}
|
|
}
|
|
|
|
// Pause queue (admin)
|
|
POST /api/influence/email-queue/pause
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Resume queue (admin)
|
|
POST /api/influence/email-queue/resume
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Clean completed jobs (admin)
|
|
POST /api/influence/email-queue/clean
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Query: ?grace=3600 (seconds)
|
|
|
|
// Retry failed jobs (admin)
|
|
POST /api/influence/email-queue/retry-failed
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
```
|
|
|
|
### Response Wall
|
|
|
|
#### V1 Response Endpoints
|
|
|
|
```javascript
|
|
// Submit response
|
|
POST /responses/submit
|
|
Body: { campaignId, name, email, message }
|
|
|
|
// List responses
|
|
GET /responses/:campaignId
|
|
```
|
|
|
|
#### V2 Response Endpoints
|
|
|
|
```typescript
|
|
// Submit response (public)
|
|
POST /api/influence/responses/submit
|
|
Auth: None
|
|
Body: {
|
|
"campaignId": "clx1a2b3c4d5e6f7g8h9i",
|
|
"name": "Jane Doe",
|
|
"email": "jane@example.com",
|
|
"message": "I support this campaign!",
|
|
"ipAddress": "192.168.1.1" // Auto-captured by server
|
|
}
|
|
// Sends verification email
|
|
|
|
// Verify response email
|
|
GET /api/influence/responses/verify/:token
|
|
Auth: None
|
|
|
|
// List responses (public)
|
|
GET /api/influence/responses/campaign/:campaignId
|
|
Auth: None
|
|
Query: ?page=1&limit=20&sortBy=upvotes&sortOrder=desc
|
|
Response: Only returns APPROVED responses
|
|
|
|
// Upvote response (public)
|
|
POST /api/influence/responses/:id/upvote
|
|
Auth: Optional (tracks by IP + userId if logged in)
|
|
Body: { "ipAddress": "192.168.1.1" }
|
|
|
|
// List responses (admin)
|
|
GET /api/influence/responses
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Query: ?page=1&limit=20&campaignId=xxx&status=PENDING&sortBy=createdAt&sortOrder=desc
|
|
|
|
// Get response detail (admin)
|
|
GET /api/influence/responses/:id
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Approve response (admin)
|
|
PATCH /api/influence/responses/:id/approve
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Reject response (admin)
|
|
PATCH /api/influence/responses/:id/reject
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Delete response (admin)
|
|
DELETE /api/influence/responses/:id
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
```
|
|
|
|
## Map Module API
|
|
|
|
### Locations
|
|
|
|
#### V1 Location Endpoints
|
|
|
|
```javascript
|
|
// List locations
|
|
GET /locations
|
|
Query: ?page=1
|
|
|
|
// Create location (admin)
|
|
POST /locations/create
|
|
Body: { Address, Latitude, Longitude, SupportLevel, Notes }
|
|
|
|
// Update location (admin)
|
|
POST /locations/:id/edit
|
|
Body: { Address, Latitude, Longitude, SupportLevel, Notes }
|
|
|
|
// Delete location (admin)
|
|
POST /locations/:id/delete
|
|
```
|
|
|
|
#### V2 Location Endpoints
|
|
|
|
```typescript
|
|
// List locations (admin)
|
|
GET /api/map/locations
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Query: ?page=1&limit=20&search=query&supportLevel=SUPPORT&cutId=xxx&geocoded=true
|
|
|
|
// List locations (public map)
|
|
GET /api/map/locations/public
|
|
Auth: None
|
|
Query: ?bounds=minLat,minLng,maxLat,maxLng (returns only geocoded locations)
|
|
|
|
// Get location by ID (admin)
|
|
GET /api/map/locations/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
|
|
// Create location (admin)
|
|
POST /api/map/locations
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: {
|
|
"address": "123 Main St",
|
|
"city": "Toronto",
|
|
"province": "ON",
|
|
"postalCode": "M5V1A1",
|
|
"country": "Canada",
|
|
"latitude": 43.6532,
|
|
"longitude": -79.3832,
|
|
"supportLevel": "SUPPORT",
|
|
"notes": "Spoke with resident",
|
|
"contactName": "John Doe",
|
|
"contactPhone": "416-555-1234",
|
|
"contactEmail": "john@example.com",
|
|
"cutId": "clx1a2b3c4d5e6f7g8h9i"
|
|
}
|
|
|
|
// Update location (admin)
|
|
PUT /api/map/locations/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: { address, city, ... } // Partial update
|
|
|
|
// Delete location (admin)
|
|
DELETE /api/map/locations/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
|
|
// Bulk delete locations (admin)
|
|
POST /api/map/locations/bulk-delete
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: { "ids": ["id1", "id2", "id3"] }
|
|
|
|
// Export locations CSV (admin)
|
|
GET /api/map/locations/export
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Query: ?supportLevel=SUPPORT&cutId=xxx
|
|
|
|
// Import locations CSV (admin)
|
|
POST /api/map/locations/import
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Content-Type: multipart/form-data
|
|
Body: FormData with CSV file
|
|
|
|
// Geocode location (admin)
|
|
POST /api/map/locations/:id/geocode
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Query: ?provider=nominatim (optional)
|
|
|
|
// Bulk geocode (admin)
|
|
POST /api/map/locations/bulk-geocode
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Query: ?limit=100&provider=nominatim
|
|
|
|
// Reverse geocode (admin)
|
|
POST /api/map/locations/reverse-geocode
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: { "latitude": 43.6532, "longitude": -79.3832 }
|
|
|
|
// Get location stats (admin)
|
|
GET /api/map/locations/stats
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Response: {
|
|
"success": true,
|
|
"data": {
|
|
"total": 10000,
|
|
"geocoded": 9500,
|
|
"notGeocoded": 500,
|
|
"bySupportLevel": {
|
|
"STRONG_SUPPORT": 1200,
|
|
"SUPPORT": 3400,
|
|
"UNDECIDED": 2100,
|
|
"OPPOSED": 1800,
|
|
"STRONG_OPPOSED": 800,
|
|
"UNKNOWN": 700
|
|
}
|
|
}
|
|
}
|
|
|
|
// NAR Import (admin, new in V2)
|
|
GET /api/map/locations/nar/datasets
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Response: List of available NAR datasets (provinces)
|
|
|
|
POST /api/map/locations/nar/import
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: {
|
|
"province": "24",
|
|
"cityFilter": "Toronto",
|
|
"postalCodeFilter": "M5V",
|
|
"cutId": "clx1a2b3c4d5e6f7g8h9i",
|
|
"residentialOnly": true
|
|
}
|
|
```
|
|
|
|
### Cuts (Territories)
|
|
|
|
#### V1 Cut Endpoints
|
|
|
|
```javascript
|
|
// List cuts (admin)
|
|
GET /admin/cuts
|
|
|
|
// Create cut (admin)
|
|
POST /admin/cuts/create
|
|
Body: { Name, GeoJSON }
|
|
```
|
|
|
|
#### V2 Cut Endpoints
|
|
|
|
```typescript
|
|
// List cuts (admin)
|
|
GET /api/map/cuts
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Query: ?page=1&limit=20&search=query
|
|
|
|
// List cuts (public map)
|
|
GET /api/map/cuts/public
|
|
Auth: None
|
|
Response: Only returns active cuts with GeoJSON
|
|
|
|
// Get cut by ID (admin)
|
|
GET /api/map/cuts/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
|
|
// Create cut (admin)
|
|
POST /api/map/cuts
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: {
|
|
"name": "Downtown Toronto",
|
|
"description": "Downtown canvassing area",
|
|
"color": "#FF5733",
|
|
"coordinates": [[[-79.4, 43.6], [-79.3, 43.6], ...]]
|
|
}
|
|
|
|
// Update cut (admin)
|
|
PUT /api/map/cuts/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: { name, description, color, coordinates }
|
|
|
|
// Delete cut (admin)
|
|
DELETE /api/map/cuts/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
|
|
// Get locations in cut (admin)
|
|
GET /api/map/cuts/:id/locations
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Query: ?page=1&limit=20
|
|
```
|
|
|
|
### Shifts
|
|
|
|
#### V1 Shift Endpoints
|
|
|
|
```javascript
|
|
// List shifts
|
|
GET /shifts
|
|
|
|
// Create shift (admin)
|
|
POST /shifts/create
|
|
Body: { Name, StartTime, EndTime, Location, Capacity }
|
|
|
|
// Signup for shift
|
|
POST /shifts/:id/signup
|
|
Body: { name, email, phone }
|
|
```
|
|
|
|
#### V2 Shift Endpoints
|
|
|
|
```typescript
|
|
// List shifts (public)
|
|
GET /api/map/shifts/public
|
|
Auth: None
|
|
Query: ?upcoming=true&startDate=2024-01-01&endDate=2024-12-31
|
|
Response: Only returns future shifts with available capacity
|
|
|
|
// List shifts (admin)
|
|
GET /api/map/shifts
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Query: ?page=1&limit=20&startDate=2024-01-01&endDate=2024-12-31&cutId=xxx
|
|
|
|
// Get shift by ID (admin)
|
|
GET /api/map/shifts/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
|
|
// Create shift (admin)
|
|
POST /api/map/shifts
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: {
|
|
"name": "Downtown Canvassing",
|
|
"description": "Canvassing shift for downtown area",
|
|
"startTime": "2024-02-15T09:00:00Z",
|
|
"endTime": "2024-02-15T12:00:00Z",
|
|
"location": "Community Center, 123 Main St",
|
|
"capacity": 20,
|
|
"requirements": "Comfortable shoes, water bottle",
|
|
"cutId": "clx1a2b3c4d5e6f7g8h9i"
|
|
}
|
|
|
|
// Update shift (admin)
|
|
PUT /api/map/shifts/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: { name, startTime, ... }
|
|
|
|
// Delete shift (admin)
|
|
DELETE /api/map/shifts/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
|
|
// Signup for shift (public)
|
|
POST /api/map/shifts/:id/signup
|
|
Auth: None
|
|
Body: {
|
|
"name": "Jane Doe",
|
|
"email": "jane@example.com",
|
|
"phone": "416-555-1234",
|
|
"notes": "First time volunteering"
|
|
}
|
|
// Creates TEMP user if email doesn't exist, sends confirmation email
|
|
|
|
// Cancel signup (public)
|
|
DELETE /api/map/shifts/:shiftId/signups/:userId
|
|
Auth: Optional (user can cancel own signup)
|
|
|
|
// List signups for shift (admin)
|
|
GET /api/map/shifts/:id/signups
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
|
|
// Update signup status (admin)
|
|
PATCH /api/map/shifts/:shiftId/signups/:userId
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: { "status": "COMPLETED" }
|
|
|
|
// Email all shift signups (admin)
|
|
POST /api/map/shifts/:id/email-signups
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Body: {
|
|
"subject": "Shift Reminder",
|
|
"message": "Don't forget about tomorrow's shift!"
|
|
}
|
|
|
|
// Get shift stats (admin)
|
|
GET /api/map/shifts/stats
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Response: {
|
|
"success": true,
|
|
"data": {
|
|
"totalShifts": 50,
|
|
"upcomingShifts": 12,
|
|
"totalSignups": 234,
|
|
"signupsByStatus": {
|
|
"CONFIRMED": 200,
|
|
"COMPLETED": 30,
|
|
"CANCELLED": 4
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Canvassing (New in V2)
|
|
|
|
```typescript
|
|
// Start canvass session (volunteer)
|
|
POST /api/map/canvass/sessions/start
|
|
Auth: Required (any authenticated user)
|
|
Body: {
|
|
"shiftId": "clx1a2b3c4d5e6f7g8h9i",
|
|
"cutId": "clx1a2b3c4d5e6f7g8h9i"
|
|
}
|
|
|
|
// End canvass session (volunteer)
|
|
POST /api/map/canvass/sessions/end
|
|
Auth: Required (any authenticated user)
|
|
Body: { "sessionId": "clx1a2b3c4d5e6f7g8h9i" }
|
|
|
|
// Get walking route (volunteer)
|
|
GET /api/map/canvass/routes/:cutId
|
|
Auth: Required (any authenticated user)
|
|
Response: Optimized walking route (nearest-neighbor algorithm)
|
|
|
|
// Record visit (volunteer)
|
|
POST /api/map/canvass/visits
|
|
Auth: Required (any authenticated user)
|
|
Rate Limit: 30 requests/minute
|
|
Body: {
|
|
"sessionId": "clx1a2b3c4d5e6f7g8h9i",
|
|
"locationId": "clx1a2b3c4d5e6f7g8h9i",
|
|
"outcome": "CONTACT_MADE",
|
|
"supportLevel": "SUPPORT",
|
|
"notes": "Very interested in campaign"
|
|
}
|
|
|
|
// Get canvass dashboard stats (admin)
|
|
GET /api/map/canvass/dashboard/stats
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Response: {
|
|
"success": true,
|
|
"data": {
|
|
"activeSessions": 5,
|
|
"totalVisitsToday": 234,
|
|
"totalVisitsWeek": 1420,
|
|
"avgVisitsPerSession": 47
|
|
}
|
|
}
|
|
|
|
// Get activity feed (admin)
|
|
GET /api/map/canvass/dashboard/activity
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Query: ?limit=50
|
|
|
|
// Get cut progress (admin)
|
|
GET /api/map/canvass/dashboard/cut-progress
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
|
|
// Get leaderboard (admin)
|
|
GET /api/map/canvass/dashboard/leaderboard
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
Query: ?period=week&limit=10
|
|
```
|
|
|
|
### GPS Tracking (New in V2)
|
|
|
|
```typescript
|
|
// Start tracking session (volunteer)
|
|
POST /api/map/tracking/sessions/start
|
|
Auth: Required (any authenticated user)
|
|
Body: { "sessionId": "clx1a2b3c4d5e6f7g8h9i" }
|
|
|
|
// Record GPS point (volunteer)
|
|
POST /api/map/tracking/points
|
|
Auth: Required (any authenticated user)
|
|
Body: {
|
|
"sessionId": "clx1a2b3c4d5e6f7g8h9i",
|
|
"latitude": 43.6532,
|
|
"longitude": -79.3832,
|
|
"accuracy": 10.5,
|
|
"altitude": 120.3,
|
|
"speed": 1.2
|
|
}
|
|
|
|
// End tracking session (volunteer)
|
|
POST /api/map/tracking/sessions/end
|
|
Auth: Required (any authenticated user)
|
|
Body: { "sessionId": "clx1a2b3c4d5e6f7g8h9i" }
|
|
|
|
// Get tracking session (admin)
|
|
GET /api/map/tracking/sessions/:id
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
|
|
// Get tracking points (admin)
|
|
GET /api/map/tracking/sessions/:id/points
|
|
Auth: Required (SUPER_ADMIN, MAP_ADMIN)
|
|
```
|
|
|
|
## Landing Pages & Email Templates (New in V2)
|
|
|
|
### Landing Pages
|
|
|
|
```typescript
|
|
// List landing pages (admin)
|
|
GET /api/pages/admin
|
|
Auth: Required (SUPER_ADMIN)
|
|
Query: ?page=1&limit=20&search=query
|
|
|
|
// Get page by ID (admin)
|
|
GET /api/pages/admin/:id
|
|
Auth: Required (SUPER_ADMIN)
|
|
|
|
// Create page (admin)
|
|
POST /api/pages/admin
|
|
Auth: Required (SUPER_ADMIN)
|
|
Body: {
|
|
"title": "About Us",
|
|
"slug": "about",
|
|
"content": "<html>...</html>",
|
|
"published": true
|
|
}
|
|
|
|
// Update page (admin)
|
|
PUT /api/pages/admin/:id
|
|
Auth: Required (SUPER_ADMIN)
|
|
Body: { title, slug, content, published }
|
|
|
|
// Delete page (admin)
|
|
DELETE /api/pages/admin/:id
|
|
Auth: Required (SUPER_ADMIN)
|
|
|
|
// Export page to MkDocs (admin)
|
|
POST /api/pages/admin/:id/export
|
|
Auth: Required (SUPER_ADMIN)
|
|
Query: ?format=themed&filename=about.html
|
|
|
|
// Get page by slug (public)
|
|
GET /api/pages/public/:slug
|
|
Auth: None
|
|
Response: Rendered HTML page
|
|
```
|
|
|
|
### Email Templates
|
|
|
|
```typescript
|
|
// List templates (admin)
|
|
GET /api/email-templates
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Query: ?page=1&limit=20&category=campaign&published=true
|
|
|
|
// Get template by ID (admin)
|
|
GET /api/email-templates/:id
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Create template (admin)
|
|
POST /api/email-templates
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Body: {
|
|
"name": "Campaign Launch",
|
|
"category": "campaign",
|
|
"subject": "New Campaign: {{campaignTitle}}",
|
|
"htmlBody": "<html>...</html>",
|
|
"textBody": "Plain text version",
|
|
"published": true
|
|
}
|
|
|
|
// Update template (admin)
|
|
PUT /api/email-templates/:id
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Body: { name, subject, htmlBody, ... }
|
|
|
|
// Publish template version (admin)
|
|
POST /api/email-templates/:id/publish
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
|
|
// Send test email (admin)
|
|
POST /api/email-templates/:id/test
|
|
Auth: Required (SUPER_ADMIN, INFLUENCE_ADMIN)
|
|
Body: {
|
|
"toEmail": "test@example.com",
|
|
"variables": {
|
|
"campaignTitle": "Save the Trees",
|
|
"userName": "Test User"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Response Format Standards
|
|
|
|
### Success Response
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": { /* response data */ }
|
|
}
|
|
```
|
|
|
|
### Paginated Response
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [ /* items */ ],
|
|
"pagination": {
|
|
"page": 1,
|
|
"limit": 20,
|
|
"total": 100,
|
|
"totalPages": 5
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error Response
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": {
|
|
"code": "VALIDATION_ERROR",
|
|
"message": "Validation failed",
|
|
"details": [
|
|
{
|
|
"path": ["email"],
|
|
"message": "Invalid email format"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### HTTP Status Codes
|
|
|
|
| Code | V1 Usage | V2 Usage |
|
|
|------|----------|----------|
|
|
| 200 | Success (all responses) | Success (GET, PUT, PATCH) |
|
|
| 201 | - | Created (POST) |
|
|
| 204 | - | No Content (DELETE) |
|
|
| 400 | Validation error | Bad Request (validation error) |
|
|
| 401 | Not logged in | Unauthorized (invalid token) |
|
|
| 403 | - | Forbidden (insufficient permissions) |
|
|
| 404 | Not found | Not Found |
|
|
| 409 | - | Conflict (duplicate resource) |
|
|
| 422 | - | Unprocessable Entity (business logic error) |
|
|
| 429 | - | Too Many Requests (rate limit) |
|
|
| 500 | Server error | Internal Server Error |
|
|
|
|
## Migration Examples
|
|
|
|
### Example 1: Campaign List Page
|
|
|
|
**V1 Code**:
|
|
```javascript
|
|
// Fetch campaigns
|
|
fetch('/campaigns?page=1', {
|
|
credentials: 'include'
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
displayCampaigns(data.list);
|
|
displayPagination(data.pageInfo);
|
|
});
|
|
```
|
|
|
|
**V2 Code**:
|
|
```typescript
|
|
// Fetch campaigns
|
|
const token = localStorage.getItem('accessToken');
|
|
|
|
fetch('/api/influence/campaigns?page=1&limit=20', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
})
|
|
.then(res => res.json())
|
|
.then(response => {
|
|
if (response.success) {
|
|
displayCampaigns(response.data);
|
|
displayPagination(response.pagination);
|
|
} else {
|
|
handleError(response.error);
|
|
}
|
|
});
|
|
```
|
|
|
|
### Example 2: Location Creation
|
|
|
|
**V1 Code**:
|
|
```javascript
|
|
// Create location
|
|
fetch('/locations/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
Address: '123 Main St, Toronto, ON M5V 1A1',
|
|
Latitude: 43.6532,
|
|
Longitude: -79.3832,
|
|
SupportLevel: 'support',
|
|
Notes: 'Spoke with resident'
|
|
})
|
|
});
|
|
```
|
|
|
|
**V2 Code**:
|
|
```typescript
|
|
// Create location
|
|
const token = localStorage.getItem('accessToken');
|
|
|
|
fetch('/api/map/locations', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({
|
|
address: '123 Main St',
|
|
city: 'Toronto',
|
|
province: 'ON',
|
|
postalCode: 'M5V1A1',
|
|
country: 'Canada',
|
|
latitude: 43.6532,
|
|
longitude: -79.3832,
|
|
supportLevel: 'SUPPORT',
|
|
notes: 'Spoke with resident'
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(response => {
|
|
if (response.success) {
|
|
console.log('Created location:', response.data);
|
|
} else {
|
|
handleError(response.error);
|
|
}
|
|
});
|
|
```
|
|
|
|
## Rate Limiting
|
|
|
|
V2 adds rate limiting to prevent abuse:
|
|
|
|
| Endpoint | Limit | Window |
|
|
|----------|-------|--------|
|
|
| `/api/auth/login` | 10 requests | 1 minute |
|
|
| `/api/auth/register` | 10 requests | 1 minute |
|
|
| `/api/influence/campaign-emails/send-email` | 30 requests | 1 hour |
|
|
| `/api/map/canvass/visits` | 30 requests | 1 minute |
|
|
|
|
**Rate Limit Headers** (V2 only):
|
|
```
|
|
X-RateLimit-Limit: 10
|
|
X-RateLimit-Remaining: 8
|
|
X-RateLimit-Reset: 1707835200
|
|
```
|
|
|
|
## Related Documentation
|
|
|
|
- [Migration Overview](index.md) - Migration planning
|
|
- [Breaking Changes](breaking-changes.md) - V1→V2 differences
|
|
- [Data Migration](data-migration.md) - Data transfer guide
|
|
- [Authentication](../architecture/authentication.md) - JWT flow details
|
|
- [API Reference](../api-reference/index.md) - Full API documentation
|
|
|
|
## Next Steps
|
|
|
|
1. **Review endpoint mappings** for your application's usage
|
|
2. **Update API client** to use JWT authentication
|
|
3. **Migrate endpoints incrementally** (auth first, then modules)
|
|
4. **Test error handling** with new response format
|
|
5. **Implement rate limit handling** (exponential backoff)
|
|
|
|
!!! tip "API Testing"
|
|
Use tools like Postman or Thunder Client to test V2 endpoints before frontend migration. Import the V2 API collection from `/docs/postman-collection.json` (if available).
|