Initial commit

Next.js + Express event management app for Hope Family Church.
This commit is contained in:
2026-07-23 15:26:47 +02:00
commit 3d381944d2
246 changed files with 57565 additions and 0 deletions
+525
View File
@@ -0,0 +1,525 @@
# Hope Events — API Documentation
Full interactive docs (with request/response examples) are available at `GET /docs?token=<admin-jwt>` on the running backend.
All authenticated requests require:
```
Authorization: Bearer <jwt>
```
Tokens are issued by `POST /api/users/login`.
---
## Table of Contents
- [Users](#users)
- [Events](#events)
- [Event Options & Variants](#event-options--variants)
- [Event Attachments](#event-attachments)
- [Registrations](#registrations)
- [Payments](#payments)
- [Tickets](#tickets)
- [Broadcasts (Email)](#broadcasts-email)
- [WhatsApp Broadcasts](#whatsapp-broadcasts)
- [Scheduled Messages](#scheduled-messages)
- [Automations](#automations)
- [Reports](#reports)
- [Uploads](#uploads)
- [Sections](#sections)
- [Banner](#banner)
- [Forms](#forms)
- [Yoco Transactions](#yoco-transactions)
- [WhatsApp Admin](#whatsapp-admin)
- [Webhooks](#webhooks)
- [Settings](#settings)
- [Setup](#setup)
- [Static Files](#static-files)
---
## Users
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/users` | public | Register new account |
| POST | `/api/users/login` | public | Login, returns JWT |
| POST | `/api/users/forgot` | public | Request password reset email |
| POST | `/api/users/reset` | public | Reset password with token |
| POST | `/api/users/activate` | public | Activate account from email link |
| GET | `/api/users/profile` | user+ | Own profile |
| PUT | `/api/users/profile` | user+ | Update own profile |
| POST | `/api/users/revoke-sessions` | user+ | Revoke own sessions |
| POST | `/api/users/close-account` | user+ | Deactivate own account |
| GET | `/api/users` | supervisor+ | List users — supports `?search=`, `?role=`, `?isActive=true\|false`, `?page=`, `?limit=` |
| GET | `/api/users/:id` | admin | Get user by ID |
| PUT | `/api/users/:id` | admin | Update user (name, email, role, isActive, phoneNumber) |
| DELETE | `/api/users/:id` | admin | Deactivate user (`isActive: false`) |
| POST | `/api/users/:id/revoke-sessions` | admin | Revoke all sessions for a user |
| POST | `/api/users/:id/anonymize` | admin | Erase personal data — sets name to "Deleted User", clears email/phone, deactivates account |
### POST /api/users
```json
{
"name": "Jane Smith",
"email": "jane@example.com",
"password": "securepass123",
"phoneNumber": "+27821234567"
}
```
Returns: `{ _id, name, email, role, token }`
### POST /api/users/login
```json
{ "email": "jane@example.com", "password": "securepass123" }
```
Returns: `{ _id, name, email, role, token }`
### PUT /api/users/profile
```json
{ "name": "Jane Doe", "phoneNumber": "+27829999999", "notificationPreference": "email" }
```
`notificationPreference`: `"email"` | `"whatsapp"` | `"both"`
---
## Events
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/events` | public | Active, public, upcoming events. Includes `isSoldOut: boolean` per event. |
| GET | `/api/events/all` | staff+ | All events including hidden/past (`?includePast=true`, `?includeInactive=true`) |
| GET | `/api/events/admin/all` | admin | Admin view of all events |
| GET | `/api/events/:id` | public | Full event detail — options, variants, earlyBirdTiers, attachments, form, availableCount per option/variant |
| GET | `/api/events/by-alias/:redirectUrl` | public | Get event by URL alias |
| POST | `/api/events` | supervisor+ | Create event |
| PUT | `/api/events/:id` | supervisor+ | Update event |
| DELETE | `/api/events/:id` | admin | Delete event |
| POST | `/api/events/:id/email-attendees` | supervisor+ | Bulk email attendees (`dryRun: true` to preview) |
| POST | `/api/events/:id/email-attendees/schedule` | supervisor+ | Schedule bulk email |
| POST | `/api/events/:id/whatsapp-attendees` | supervisor+ | Bulk WhatsApp to attendees (`dryRun: true` to preview) |
| POST | `/api/events/:id/whatsapp-attendees/schedule` | supervisor+ | Schedule bulk WhatsApp |
| GET | `/api/events/attachments/status` | admin | Attachment manifest sync status |
| POST | `/api/events/attachments/sync` | admin | Re-sync attachment manifests to DB |
### isSoldOut logic (`GET /api/events`)
`isSoldOut` is `true` when every option on the event that has `stockLimit > 0` has sold out its full allocation (computed from non-cancelled `RegistrationOption` quantities). Events with no limited options are never marked sold out.
### POST /api/events
```json
{
"title": "Family Camp 2025",
"description": "Annual family camp at the farm.",
"startDate": "2025-07-10T08:00:00.000Z",
"endDate": "2025-07-14T17:00:00.000Z",
"registrationDeadline": "2025-07-01T00:00:00.000Z",
"goLiveAt": "2025-05-01T00:00:00.000Z",
"price": 450,
"picture": "/uploads/events/camp.jpg",
"redirectUrl": "camp2025",
"isHidden": false,
"requiresAuth": true,
"form": {
"isRequired": true,
"fields": [
{ "type": "text", "label": "Dietary requirements", "isRequired": false, "order": 0 }
]
}
}
```
---
## Event Options & Variants
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/events/:id/options` | supervisor+ | Add option to event |
| PUT | `/api/events/options/:id` | supervisor+ | Update option (name, price, stockLimit, variants, earlyBirdTiers) |
| DELETE | `/api/events/options/:id` | admin | Delete option (fails if registrations exist) |
| POST | `/api/events/options/:id/variants` | supervisor+ | Add variant to option |
| PUT | `/api/events/variants/:id` | supervisor+ | Update variant |
| DELETE | `/api/events/variants/:id` | admin | Delete variant |
### POST /api/events/:id/options
```json
{
"name": "Adult",
"price": 450,
"isMainTicket": false,
"stockLimit": 100
}
```
### PUT /api/events/options/:id — with variants and early-bird tiers
```json
{
"name": "Adult",
"price": 450,
"stockLimit": 100,
"variants": [
{ "name": "Standard", "price": null, "stockLimit": 60, "order": 0 },
{ "name": "VIP", "price": 600, "stockLimit": 40, "order": 1 }
],
"earlyBirdTiers": [
{ "deadline": "2025-06-01T00:00:00.000Z", "price": 350, "stockLimit": 30, "order": 0, "variantId": null }
]
}
```
`stockLimit: 0` means unlimited. Early-bird tiers can be option-level (`variantId: null`) or variant-level (`variantId: "<uuid>"`).
---
## Event Attachments
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/events/:id/attachments` | public | List attachments for event |
| POST | `/api/events/:id/attachments` | supervisor+ | Upload attachment (`multipart/form-data`, field `file`) |
| DELETE | `/api/events/:eventId/attachments/:id` | supervisor+ | Delete attachment |
Accepted file types: `.pdf`, `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.txt`, `.csv`, `.zip`. Max 10 MB.
---
## Registrations
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/registrations` | optional | Create registration |
| GET | `/api/registrations/myregistrations` | user+ | Own registrations |
| GET | `/api/registrations/:id` | optional | Get registration by ID |
| PUT | `/api/registrations/:id/options` | user+ | Update selected options |
| DELETE | `/api/registrations/:id` | user+ | Cancel registration (blocked if positive payments exist unless admin) |
| GET | `/api/registrations/:id/forms/draft` | optional | Get form draft |
| PUT | `/api/registrations/:id/forms/draft` | optional | Save form draft |
| POST | `/api/registrations/:id/forms/responses` | optional | Submit form responses |
| PUT | `/api/registrations/:id/forms/responses` | supervisor+ | Replace form responses |
| GET | `/api/registrations` | staff+ | All registrations |
| PUT | `/api/registrations/:id` | staff+ | Update registration status |
| GET | `/api/registrations/event/:eventId` | staff+ | Registrations for an event |
| POST | `/api/registrations/manual` | supervisor+ | Manual registration |
### POST /api/registrations
```json
{
"eventId": "ev-uuid-...",
"options": [
{ "eventOptionId": "opt-uuid-...", "quantity": 2 },
{ "eventOptionId": "opt-uuid-...", "quantity": 1, "variantId": "variant-uuid-..." }
]
}
```
For unauthenticated (guest) registration on events with `requiresAuth: false`:
```json
{
"eventId": "ev-uuid-...",
"options": [...],
"guestName": "Jane Smith",
"guestEmail": "jane@example.com",
"guestPhone": "+27821234567"
}
```
### Cancellation rules
| Actor | Condition | Result |
|-------|-----------|--------|
| Owner | No positive payments | 200 — status set to `cancelled` |
| Owner | Any positive payment exists | 403 — must contact organisation |
| Admin | Any state | 200 — always allowed |
| Anyone | Already cancelled | 400 |
---
## Payments
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/payments` | supervisor+ | Record manual payment (cash/EFT) |
| POST | `/api/payments/yoco-checkout` | user+ | Initiate Yoco checkout; re-evaluates early-bird pricing first — returns `{ priceUpdated: true, newTotal }` if prices changed |
| GET | `/api/payments/mypayments` | user+ | Own payments |
| GET | `/api/payments/:id` | user+ | Payment by ID |
| GET | `/api/payments/registration/:registrationId` | user+ | Payments for a registration |
| GET | `/api/payments` | supervisor+ | All payments |
| GET | `/api/payments/event/:eventId` | staff+ | Payments for an event |
| PUT | `/api/payments/assign-donation` | supervisor+ | Assign a donation payment to a registration |
| POST | `/api/payments/refund` | supervisor+ | Create refund record |
| GET | `/api/payments/admin/stats` | admin | Payment statistics |
### POST /api/payments (manual)
```json
{
"registrationId": "reg-uuid-...",
"amount": 450,
"method": "cash",
"note": "Paid at the door"
}
```
### POST /api/payments/yoco-checkout
```json
{
"registrationId": "reg-uuid-...",
"successUrl": "https://events.yourchurch.org/payment/success",
"cancelUrl": "https://events.yourchurch.org/payment/cancel",
"failureUrl": "https://events.yourchurch.org/payment/failure"
}
```
Returns: `{ redirectUrl }` or `{ priceUpdated: true, newTotal, message }` if early-bird prices changed.
---
## Tickets
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/tickets/mytickets` | user+ | Own tickets (excludes cancelled registrations) |
| GET | `/api/tickets/:id` | user+ | Ticket by ID |
| POST | `/api/tickets/email` | user+ | Email own tickets as PDF |
| POST | `/api/tickets/send-to` | staff+ | Send tickets to a specific user |
| GET | `/api/tickets/scan-preview/:qrCode` | staff+ | Preview scan (no mark) |
| GET | `/api/tickets/qr/:qrCode` | staff+ | Ticket by QR code |
| POST | `/api/tickets/scan/:qrCode` | staff+ | Scan/redeem ticket |
| GET | `/api/tickets/event/:eventId` | staff+ | All tickets for an event |
| GET | `/api/tickets/scans/recent` | staff+ | Recent scan activity |
| GET | `/api/tickets/scans/stats` | staff+ | Scan statistics |
| POST | `/api/tickets/generate` | supervisor+ | Generate tickets for a registration |
| GET | `/api/tickets` | supervisor+ | All tickets |
| PUT | `/api/tickets/email-sent` | admin | Bulk mark tickets as emailed |
---
## Broadcasts (Email)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/broadcasts/preview` | supervisor+ | Dry-run — returns recipient count and sample |
| POST | `/api/broadcasts/send` | supervisor+ | Send email broadcast immediately |
| POST | `/api/broadcasts/schedule` | supervisor+ | Schedule email broadcast |
### POST /api/broadcasts/send
```json
{
"subject": "Camp 2025 — Final reminder",
"body": "Hi {{name}}, just a reminder that camp starts tomorrow at 08:00.",
"userIds": ["user-uuid-1", "user-uuid-2"],
"extraEmails": ["extra@example.com"]
}
```
Dynamic placeholders: `{{name}}`, `{{event.title}}`, `{{event.start}}`, `{{event.link}}`, `{{balance}}`, `{{promo.title}}`, `{{promo.link}}`
---
## WhatsApp Broadcasts
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/whatsapp-broadcasts/preview` | supervisor+ | Dry-run |
| POST | `/api/whatsapp-broadcasts/send` | supervisor+ | Send WhatsApp broadcast immediately |
| POST | `/api/whatsapp-broadcasts/schedule` | supervisor+ | Schedule WhatsApp broadcast |
---
## Scheduled Messages
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/scheduled-emails` | supervisor+ | List all queued/scheduled jobs |
| PATCH | `/api/scheduled-emails/:id` | supervisor+ | Edit scheduled job (reschedule, update content) |
| DELETE | `/api/scheduled-emails/:id` | supervisor+ | Cancel a queued job |
---
## Automations
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/automations/schedule` | supervisor+ | Schedule event lifecycle automations (pre-event reminder, thank-you, promo, etc.) |
---
## Reports
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/reports/pdf` | user+ | Generate and download PDF attendance/payment report |
| POST | `/api/reports/email` | user+ | Email PDF report to current user |
---
## Uploads
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/uploads/event-image` | supervisor+ | Upload event cover image (`multipart/form-data`, field `image`). Returns `{ url }`. Max 5 MB. Formats: jpg, jpeg, png, webp, gif. |
| POST | `/api/uploads/logo` | admin (or unauthenticated during setup) | Upload site logo (`multipart/form-data`, field `image`). Returns `{ url }`. Max 2 MB. Formats: jpg, jpeg, png, webp, svg. |
---
## Sections
Groups of event options shown as labelled sections on the registration page (e.g. "Tickets", "Add-ons", "Accommodation").
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/sections` | staff+ | List sections |
| POST | `/api/sections` | supervisor+ | Create section |
| PUT | `/api/sections/:id` | supervisor+ | Update section |
| DELETE | `/api/sections/:id` | admin | Delete section |
### POST /api/sections
```json
{
"eventId": "ev-uuid-...",
"name": "Accommodation",
"allowedOptionIds": ["opt-uuid-1", "opt-uuid-2"]
}
```
---
## Banner
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/banner` | public | Get current site-wide banner (text, colour, active flag) |
| POST | `/api/banner` | supervisor+ | Set site banner |
### POST /api/banner
```json
{
"text": "Camp registrations close Friday!",
"color": "#f59e0b",
"isActive": true
}
```
---
## Forms
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/forms/responses` | staff+ | List all submitted form responses across all events; supports filtering |
---
## Yoco Transactions
Raw webhook events from Yoco. Used for reconciling payments that weren't matched automatically.
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/yoco-transactions` | supervisor+ | All Yoco webhook transactions |
| GET | `/api/yoco-transactions/unreconciled` | supervisor+ | Transactions not yet matched to a payment |
| POST | `/api/yoco-transactions/:id/reconcile` | supervisor+ | Manually link transaction to a registration |
| POST | `/api/yoco-transactions/:id/ignore` | supervisor+ | Mark transaction as intentionally ignored |
---
## WhatsApp Admin
Manages the WAWP WhatsApp API instance. Credentials are stored in `AppSetting` (preferred) or `.env` (fallback).
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/whatsapp/config` | admin | Get current WAWP credentials |
| POST | `/api/whatsapp/config` | admin | Save WAWP credentials to DB |
| GET | `/api/whatsapp/status` | admin | WAWP session status |
| GET | `/api/whatsapp/qr` | admin | QR code for WhatsApp device linking |
| POST | `/api/whatsapp/create-instance` | admin | Create a new WAWP instance |
| POST | `/api/whatsapp/delete-instance` | admin | Delete WAWP instance |
| POST | `/api/whatsapp/request-code` | admin | Request pairing code (alternative to QR) |
| POST | `/api/whatsapp/logout` | admin | Logout current WAWP session |
| POST | `/api/whatsapp/start` | admin | Start WAWP instance |
| POST | `/api/whatsapp/restart` | admin | Restart WAWP instance |
---
## Webhooks
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/webhooks/yoco` | HMAC-verified | Yoco payment webhook — verifies `X-Yoco-Signature`, creates `Payment` + `YocoTransaction`, updates registration status, generates tickets |
| POST | `/api/webhooks/whatsapp` | — | WAWP inbound event webhook |
---
## Settings
Runtime configuration stored in the `AppSetting` table. Sensitive values (SMTP credentials, WAWP token) are stored AES-256-GCM encrypted.
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/settings` | public | Public settings (org name, tagline, colour, logo, legal keys) |
| GET | `/api/settings/all` | admin | All settings — `smtp_pass` masked as `••••••••`, `smtp_user` decrypted |
| GET | `/api/settings/needs-setup` | public | `{ needsSetup: bool }` — true until setup wizard completes |
| PUT | `/api/settings` | admin | Upsert settings; `smtp_user`/`smtp_pass` encrypted before storage; sending `••••••••` for `smtp_pass` keeps existing value |
| POST | `/api/settings/test-smtp` | admin | Test SMTP with provided credentials; sends real test email to admin on success. The admin JWT returned by `POST /api/setup/register` also qualifies during the setup wizard. |
### POST /api/settings/test-smtp
```json
{
"host": "smtp.office365.com",
"port": 587,
"secure": false,
"user": "noreply@yourchurch.org",
"pass": "app-password-here",
"from": "noreply@yourchurch.org"
}
```
**Success (200):**
```json
{ "message": "SMTP connection verified — a test email has been sent to admin@yourchurch.org" }
```
**Failure (400):**
```json
{
"message": "Authentication failed — check your SMTP username and password.",
"raw": "530 5.7.57 Client not authenticated to send mail. [JN3P275CA0121...]"
}
```
The `message` field is always a human-readable sentence. The `raw` field is included on errors only and contains the original SMTP error for debugging. Error code 530 (Microsoft "Client not authenticated") maps to the authentication-failure message.
---
## Setup
One-time first-run wizard. Both endpoints are blocked once any user exists and setup is complete.
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/setup/register` | public (one-time) | Create the first admin account; returns a short-lived setup JWT |
| POST | `/api/setup` | setup token or admin | Save initial site settings (org name, branding, SMTP) |
---
## Static Files
| Path | Description |
|------|-------------|
| `/uploads/events/*` | Event cover images |
| `/uploads/branding/*` | Site logo |
| `/uploads/event-files/*` | Event attachments (PDFs, docs, etc.) |
| `/uploads/tickets-temp/*` | Temporarily hosted ticket PDFs for WhatsApp delivery (auto-deleted after 5 min) |
---
## Auth error responses
| Status | Body | Meaning |
|--------|------|---------|
| 401 | `{ message: "Not authorised, no token" }` | Missing or malformed `Authorization` header |
| 401 | `{ message: "Not authorised, token failed" }` | Invalid or expired JWT |
| 401 | `{ message: "Session has been revoked" }` | Token version mismatch (sessions revoked) |
| 403 | `{ message: "Not authorised as an admin" }` | Insufficient role |
---
*For full interactive docs with expandable examples visit `GET /docs?token=<admin-jwt>` on the running backend.*