diff --git a/backend/src/v2/deprecation.ts b/backend/src/v2/deprecation.ts
index 83a55d9..bee905b 100644
--- a/backend/src/v2/deprecation.ts
+++ b/backend/src/v2/deprecation.ts
@@ -46,6 +46,49 @@ const SUCCESSORS: Successor[] = [
{ method: 'POST', v1: /^\/api\/events\/([^/]+)\/copy-from$/, v2: m => `/api/v2/events/${m[1]}/copy-from` },
{ method: 'GET', v1: /^\/api\/events\/([^/]+)\/notify-recipients$/, v2: m => `/api/v2/events/${m[1]}/notify-recipients` },
{ method: 'PUT', v1: /^\/api\/events\/([^/]+)\/notify-recipients$/, v2: m => `/api/v2/events/${m[1]}/notify-recipients` },
+ // 1.51.14: people and messaging
+ { method: 'POST', v1: /^\/api\/users\/login\/2fa\/resend$/, v2: () => '/api/v2/auth/login/code/resend' },
+ { method: 'POST', v1: /^\/api\/users\/forgot$/, v2: () => '/api/v2/auth/password/forgot' },
+ { method: 'POST', v1: /^\/api\/users\/reset$/, v2: () => '/api/v2/auth/password/reset' },
+ { method: 'POST', v1: /^\/api\/users\/activate$/, v2: () => '/api/v2/auth/activate' },
+ { method: 'POST', v1: /^\/api\/users\/revoke-sessions$/, v2: () => '/api/v2/me/sessions/revoke' },
+ { method: 'POST', v1: /^\/api\/users\/close-account$/, v2: () => '/api/v2/me/close' },
+ { method: 'GET', v1: /^\/api\/users\/activity$/, v2: () => '/api/v2/me/activity' },
+ { method: 'POST', v1: /^\/api\/users\/verify-password$/, v2: () => '/api/v2/me/verify-password' },
+ { method: 'POST', v1: /^\/api\/users\/2fa\/(enable|confirm|disable)$/, v2: m => `/api/v2/me/two-factor/${m[1]}` },
+ { method: 'GET', v1: /^\/api\/users\/check-exists$/, v2: () => '/api/v2/users/exists' },
+ { method: 'POST', v1: /^\/api\/users\/([^/]+)\/revoke-sessions$/, v2: m => `/api/v2/users/${m[1]}/sessions/revoke` },
+ { method: 'POST', v1: /^\/api\/users\/([^/]+)\/anonymize$/, v2: m => `/api/v2/users/${m[1]}/anonymize` },
+ { method: 'GET', v1: /^\/api\/users\/?$/, v2: () => '/api/v2/users' },
+ // (/api/users/profile is /api/v2/me — see 1.51.2 below.)
+ { method: 'GET', v1: /^\/api\/users\/(?!profile$)([^/]+)$/, v2: m => `/api/v2/users/${m[1]}` },
+ { method: 'PUT', v1: /^\/api\/users\/(?!profile$)([^/]+)$/, v2: m => `/api/v2/users/${m[1]}` },
+ { method: 'DELETE', v1: /^\/api\/users\/([^/]+)$/, v2: m => `/api/v2/users/${m[1]}` },
+ { method: 'GET', v1: /^\/api\/events\/admin\/all$/, v2: () => '/api/v2/admin/events' },
+ { method: 'POST', v1: /^\/api\/events\/([^/]+)\/(email|whatsapp)-attendees$/, v2: m => `/api/v2/events/${m[1]}/${m[2]}-attendees` },
+ { method: 'POST', v1: /^\/api\/events\/([^/]+)\/(email|whatsapp)-attendees\/schedule$/, v2: m => `/api/v2/events/${m[1]}/${m[2]}-attendees/schedule` },
+ { method: 'POST', v1: /^\/api\/broadcasts\/(preview|send|schedule)$/, v2: m => `/api/v2/broadcasts/email/${m[1]}` },
+ { method: 'POST', v1: /^\/api\/whatsapp-broadcasts\/(preview|send|schedule)$/, v2: m => `/api/v2/broadcasts/whatsapp/${m[1]}` },
+ { method: 'GET', v1: /^\/api\/scheduled-emails\/?$/, v2: () => '/api/v2/scheduled-messages' },
+ { method: 'PATCH', v1: /^\/api\/scheduled-emails\/([^/]+)$/, v2: m => `/api/v2/scheduled-messages/${m[1]}` },
+ { method: 'DELETE', v1: /^\/api\/scheduled-emails\/([^/]+)$/, v2: m => `/api/v2/scheduled-messages/${m[1]}` },
+ { method: 'POST', v1: /^\/api\/automations\/schedule$/, v2: () => '/api/v2/automations/schedule' },
+ { method: 'GET', v1: /^\/api\/whatsapp\/(config|status|qr)$/, v2: m => `/api/v2/whatsapp/${m[1]}` },
+ { method: 'POST', v1: /^\/api\/whatsapp\/config$/, v2: () => '/api/v2/whatsapp/config' },
+ { method: 'POST', v1: /^\/api\/whatsapp\/create-instance$/, v2: () => '/api/v2/whatsapp/instance' },
+ { method: 'POST', v1: /^\/api\/whatsapp\/delete-instance$/, v2: () => '/api/v2/whatsapp/instance' },
+ { method: 'POST', v1: /^\/api\/whatsapp\/(request-code|logout|start|restart)$/, v2: m => `/api/v2/whatsapp/${m[1]}` },
+ { method: 'GET', v1: /^\/api\/banner\/?$/, v2: () => '/api/v2/banner' },
+ { method: 'POST', v1: /^\/api\/banner\/?$/, v2: () => '/api/v2/banner' },
+ { method: 'GET', v1: /^\/api\/saved-filters\/?$/, v2: () => '/api/v2/saved-filters' },
+ { method: 'POST', v1: /^\/api\/saved-filters\/?$/, v2: () => '/api/v2/saved-filters' },
+ { method: 'PUT', v1: /^\/api\/saved-filters\/([^/]+)$/, v2: m => `/api/v2/saved-filters/${m[1]}` },
+ { method: 'DELETE', v1: /^\/api\/saved-filters\/([^/]+)$/, v2: m => `/api/v2/saved-filters/${m[1]}` },
+ { method: 'GET', v1: /^\/api\/dashboard\/(today|search)$/, v2: m => `/api/v2/dashboard/${m[1]}` },
+ { method: 'GET', v1: /^\/api\/stats\/(staff|supervisor|admin|overview)$/, v2: m => `/api/v2/stats/${m[1]}` },
+ { method: 'POST', v1: /^\/api\/analytics\/track$/, v2: () => '/api/v2/analytics/track' },
+ { method: 'GET', v1: /^\/api\/analytics\/funnel$/, v2: () => '/api/v2/analytics/funnel' },
+ { method: 'POST', v1: /^\/api\/reports\/(pdf|excel|email|whatsapp)$/, v2: m => `/api/v2/reports/${m[1]}` },
// 1.51.13: kids check-in and volunteer rosters
{ method: 'GET', v1: /^\/api\/kids\/me$/, v2: () => '/api/v2/me/family' },
{ method: 'POST', v1: /^\/api\/kids\/me$/, v2: () => '/api/v2/me/family' },
diff --git a/backend/src/v2/index.ts b/backend/src/v2/index.ts
index 4c71503..bcc6db6 100644
--- a/backend/src/v2/index.ts
+++ b/backend/src/v2/index.ts
@@ -17,6 +17,7 @@ import './resources/registrationsManage';
import './resources/money';
import './resources/attendance';
import './resources/kidsVolunteers';
+import './resources/peopleMessaging';
import './resources/checkin';
import './resources/auth';
diff --git a/backend/src/v2/resources/money.ts b/backend/src/v2/resources/money.ts
index 2c5db45..4901dd1 100644
--- a/backend/src/v2/resources/money.ts
+++ b/backend/src/v2/resources/money.ts
@@ -115,7 +115,7 @@ v1Route({
});
v1Route({
method: 'post', path: '/registrations/:id/payment-link', tag: 'Payments', summary: 'Make a link to pay',
- description: 'Supervisors: a checkout for `amount` (part of what\x27s owed) for someone else to pay. Answers `redirectUrl` (the link) — or `priceUpdated` when an early-bird price ran out.',
+ description: 'Supervisors: a checkout for `amount` (part of what\'s owed) for someone else to pay. Answers `redirectUrl` (the link) — or `priceUpdated` when an early-bird price ran out.',
access: 'supervisor', campusScope: 'scopeEvent(registration)', middleware: [cs.scopeEvent({ resolve: 'registration' })], params: idParams,
body: z.looseObject({ amount: z.union([z.number(), z.string()]).optional() }), map: ctx => ({ body: { ...(ctx.req.body || {}), registrationId: ctx.params.id } }), v1: pay.createCheckout,
});
diff --git a/backend/src/v2/resources/peopleMessaging.ts b/backend/src/v2/resources/peopleMessaging.ts
new file mode 100644
index 0000000..1ac1793
--- /dev/null
+++ b/backend/src/v2/resources/peopleMessaging.ts
@@ -0,0 +1,138 @@
+/**
+ * People and messaging on v2 (1.51.14): the rest of accounts (password reset, activation,
+ * two-step sign-in, sessions, closing an account), messaging attendees, email and WhatsApp
+ * broadcasts, scheduled messages, automations, the site banner, saved filters, the dashboards,
+ * stats, analytics and report files. Each route runs the v1 handler (viaV1.ts) behind the same
+ * access and campus checks as v1.
+ */
+import { z } from 'zod';
+import type { Request } from 'express';
+import { id, idParams } from '../schemas';
+import { v1Route } from '../viaV1';
+
+/* eslint-disable @typescript-eslint/no-explicit-any -- v1 handlers and middleware are JavaScript */
+const rateLimit = require('express-rate-limit') as (opts: object) => any;
+const { loginLimiter } = require('../../middleware/authMiddleware') as { loginLimiter: any };
+const cs = require('../../utils/campusScope') as {
+ scopeEvent(opts?: { need?: 'view' | 'manage'; from?: (req: any) => unknown }): any;
+ scopeBroadcast(opts: { freeformField: string }): any;
+ scopeList: any;
+ unscopedOnly: any;
+ campusIndependent(reason: string): any;
+};
+const users = require('../../controllers/userController');
+const events = require('../../controllers/eventController');
+const wa = require('../../controllers/whatsappController');
+const bc = require('../../controllers/broadcastController');
+const wbc = require('../../controllers/whatsappBroadcastController');
+const sched = require('../../controllers/scheduledEmailsController');
+const auto = require('../../controllers/automationController');
+const banner = require('../../controllers/bannerController');
+const stats = require('../../controllers/statsController');
+const analytics = require('../../controllers/analyticsController');
+const reports = require('../../controllers/reportController');
+// These two keep their handlers in the route file; v2 runs the v1 router with the v1 path.
+const savedFilterRouter = require('../../routes/savedFilterRoutes');
+const dashboardRouter = require('../../routes/dashboardRoutes');
+
+const loose = z.looseObject({});
+const filters = (...names: string[]) => z.looseObject(Object.fromEntries(names.map(n => [n, z.string().max(4000).optional()])));
+const SCOPE_LIST = 'scopeList (the handler filters by listWhere)';
+/** The query string of the v2 request, for a v1 router's URL. */
+const qs = (req: Request) => (req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '');
+
+// ─── Accounts ────────────────────────────────────────────────────────────────
+
+v1Route({ method: 'post', path: '/auth/login/code/resend', tag: 'Accounts', summary: 'Send the sign-in code again', access: 'public', middleware: [loginLimiter], body: loose, v1: users.resendLoginTwoFactor });
+v1Route({ method: 'post', path: '/auth/password/forgot', tag: 'Accounts', summary: 'Email me a link to reset my password', description: 'NOT_FOUND when no account has that email (as v1 does).', access: 'public', body: z.looseObject({ email: z.string().max(200) }), v1: users.requestPasswordReset });
+v1Route({ method: 'post', path: '/auth/password/reset', tag: 'Accounts', summary: 'Set a new password with a reset link', access: 'public', body: loose, v1: users.resetPassword });
+v1Route({ method: 'post', path: '/auth/activate', tag: 'Accounts', summary: 'Activate an account with its link', access: 'public', body: loose, v1: users.activateAccount });
+v1Route({ method: 'post', path: '/me/sessions/revoke', tag: 'Me', summary: 'Sign out everywhere else', access: 'user', body: loose, v1: users.revokeMySession });
+v1Route({ method: 'post', path: '/me/close', tag: 'Me', summary: 'Close my account', description: 'Needs the password. Personal details are removed; payment records stay, anonymised.', access: 'user', body: loose, v1: users.closeAccount });
+v1Route({ method: 'get', path: '/me/activity', tag: 'Me', summary: 'Recent sign-ins and changes to my account', access: 'user', v1: users.getMyActivity });
+v1Route({ method: 'post', path: '/me/verify-password', tag: 'Me', summary: 'Check my password (before a sensitive change)', access: 'user', middleware: [loginLimiter], body: loose, v1: users.verifyPassword });
+v1Route({ method: 'post', path: '/me/two-factor/enable', tag: 'Me', summary: 'Start turning on two-step sign-in', description: 'Sends a code to confirm with.', access: 'user', middleware: [loginLimiter], body: loose, v1: users.startEnableTwoFactor });
+v1Route({ method: 'post', path: '/me/two-factor/confirm', tag: 'Me', summary: 'Confirm two-step sign-in with the code', access: 'user', middleware: [loginLimiter], body: loose, v1: users.confirmEnableTwoFactor });
+v1Route({ method: 'post', path: '/me/two-factor/disable', tag: 'Me', summary: 'Turn off two-step sign-in', access: 'user', middleware: [loginLimiter], body: loose, v1: users.disableTwoFactor });
+v1Route({
+ method: 'get', path: '/users/exists', tag: 'Accounts', summary: 'Is there an account with this email or phone?',
+ access: 'supervisor', campusScope: 'campusIndependent (accounts are shared across campuses)', middleware: [cs.campusIndependent('accounts are shared across campuses')],
+ query: filters('email', 'phone', 'phoneNumber'), v1: users.checkUserExists,
+});
+v1Route({
+ method: 'get', path: '/users', tag: 'Accounts', summary: 'Every account',
+ description: 'Paged (`page`, `limit`) and searchable (`q`, `role`, `campus`, …) — see v1 for every filter.',
+ access: 'supervisor', campusScope: 'campusIndependent (accounts are shared across campuses)', middleware: [cs.campusIndependent('accounts are shared across campuses')],
+ query: filters('page', 'limit', 'search', 'role', 'isActive', 'campus'), v1: users.getUsers,
+});
+v1Route({ method: 'get', path: '/users/:id', tag: 'Accounts', summary: 'One account', access: 'admin', params: idParams, v1: users.getUserById });
+v1Route({ method: 'patch', path: '/users/:id', tag: 'Accounts', summary: 'Change an account', description: 'Name, contact details, role, campus limits, active.', access: 'admin', params: idParams, body: loose, v1: users.updateUser });
+v1Route({ method: 'delete', path: '/users/:id', tag: 'Accounts', summary: 'Delete an account', access: 'admin', params: idParams, v1: users.deleteUser });
+v1Route({ method: 'post', path: '/users/:id/sessions/revoke', tag: 'Accounts', summary: 'Sign someone out everywhere', access: 'admin', params: idParams, body: loose, v1: users.adminRevokeUserSessions });
+v1Route({ method: 'post', path: '/users/:id/anonymize', tag: 'Accounts', summary: 'Anonymise an account', description: 'Admins. Personal details are removed for good; payment records stay.', access: 'admin', params: idParams, body: loose, v1: users.anonymizeUser });
+
+// ─── Messaging an event's people ─────────────────────────────────────────────
+
+const toEvent = { campusScope: 'scopeEvent(view)', middleware: [cs.scopeEvent()] };
+v1Route({
+ method: 'get', path: '/event-listing', tag: 'Events', summary: 'Upcoming public events in full',
+ description: 'What the site\'s event lists show (ticket options, prices, early-bird tiers), not paged. `campus` (id or slug) and `series` (slug) narrow it. For a paged, slimmer list use GET /events.',
+ access: 'public', query: filters('campus', 'series'), v1: events.getEvents,
+});
+v1Route({ method: 'get', path: '/admin/events', tag: 'Events (staff)', summary: 'Every event, including deactivated ones', access: 'admin', v1: events.getAllEvents });
+v1Route({ method: 'post', path: '/events/:id/email-attendees', tag: 'Messaging', summary: 'Email an event\'s people', description: 'Filtered by status or chosen people; news consent is checked.', access: 'supervisor', ...toEvent, params: idParams, body: loose, v1: events.emailEventAttendees });
+v1Route({ method: 'post', path: '/events/:id/email-attendees/schedule', tag: 'Messaging', summary: 'Schedule an email to an event\'s people', access: 'supervisor', ...toEvent, params: idParams, body: loose, v1: events.scheduleEmailEventAttendees });
+v1Route({ method: 'post', path: '/events/:id/whatsapp-attendees', tag: 'Messaging', summary: 'WhatsApp an event\'s people', access: 'supervisor', ...toEvent, params: idParams, body: loose, v1: events.whatsappEventAttendees });
+v1Route({ method: 'post', path: '/events/:id/whatsapp-attendees/schedule', tag: 'Messaging', summary: 'Schedule a WhatsApp to an event\'s people', access: 'supervisor', ...toEvent, params: idParams, body: loose, v1: events.scheduleWhatsappEventAttendees });
+
+for (const [channel, field, ctrl] of [['email', 'emails', bc], ['whatsapp', 'phones', wbc]] as const) {
+ const scope = { campusScope: `scopeBroadcast(freeform ${field})`, middleware: [cs.scopeBroadcast({ freeformField: field })] };
+ const [preview, send, schedule] = channel === 'email' ? [ctrl.previewBroadcast, ctrl.sendBroadcast, ctrl.scheduleBroadcast] : [ctrl.previewWhatsAppBroadcast, ctrl.sendWhatsAppBroadcast, ctrl.scheduleWhatsAppBroadcast];
+ const name = channel === 'email' ? 'an email' : 'a WhatsApp';
+ v1Route({ method: 'post', path: `/broadcasts/${channel}/preview`, tag: 'Messaging', summary: `Who ${name} broadcast would reach`, access: 'supervisor', ...scope, body: loose, v1: preview });
+ v1Route({ method: 'post', path: `/broadcasts/${channel}/send`, tag: 'Messaging', summary: `Send ${name} broadcast`, description: 'To campuses, events, roles or listed people; news consent is checked.', access: 'supervisor', ...scope, body: loose, v1: send });
+ v1Route({ method: 'post', path: `/broadcasts/${channel}/schedule`, tag: 'Messaging', summary: `Schedule ${name} broadcast`, access: 'supervisor', ...scope, body: loose, v1: schedule });
+}
+v1Route({ method: 'get', path: '/scheduled-messages', tag: 'Messaging', summary: 'Scheduled emails and WhatsApps', access: 'supervisor', campusScope: SCOPE_LIST, middleware: [cs.scopeList], query: filters('status', 'eventId', 'channel'), v1: sched.listScheduledEmails });
+v1Route({ method: 'patch', path: '/scheduled-messages/:id', tag: 'Messaging', summary: 'Change a scheduled message', access: 'supervisor', campusScope: SCOPE_LIST, middleware: [cs.scopeList], params: idParams, body: loose, v1: sched.updateScheduledEmail });
+v1Route({ method: 'delete', path: '/scheduled-messages/:id', tag: 'Messaging', summary: 'Cancel a scheduled message', access: 'supervisor', campusScope: SCOPE_LIST, middleware: [cs.scopeList], params: idParams, v1: sched.deleteScheduledEmail });
+v1Route({
+ method: 'post', path: '/automations/schedule', tag: 'Messaging', summary: 'Schedule an event\'s reminder messages',
+ access: 'supervisor', campusScope: 'scopeEvent(body.eventId)', middleware: [cs.scopeEvent({ from: (req: any) => req.body?.eventId })], body: z.looseObject({ eventId: id }), v1: auto.scheduleAutomations,
+});
+
+// ─── WhatsApp connection (admins) ────────────────────────────────────────────
+
+v1Route({ method: 'get', path: '/whatsapp/config', tag: 'WhatsApp', summary: 'The WhatsApp connection settings', access: 'admin', v1: wa.getConfigHandler });
+v1Route({ method: 'put', path: '/whatsapp/config', tag: 'WhatsApp', summary: 'Change the WhatsApp connection settings', access: 'admin', body: loose, v1: wa.saveConfigHandler });
+v1Route({ method: 'post', path: '/whatsapp/instance', tag: 'WhatsApp', summary: 'Create the WhatsApp instance', access: 'admin', body: loose, v1: wa.createInstanceHandler });
+v1Route({ method: 'delete', path: '/whatsapp/instance', tag: 'WhatsApp', summary: 'Delete the WhatsApp instance', access: 'admin', v1: wa.deleteInstanceHandler });
+v1Route({ method: 'get', path: '/whatsapp/status', tag: 'WhatsApp', summary: 'Is WhatsApp connected?', access: 'admin', v1: wa.getStatusHandler });
+v1Route({ method: 'get', path: '/whatsapp/qr', tag: 'WhatsApp', summary: 'The QR code to link a phone', access: 'admin', v1: wa.getQrHandler });
+for (const [action, summary, handler] of [['request-code', 'Link a phone with a code instead', wa.requestCodeHandler], ['logout', 'Unlink the phone', wa.logoutHandler], ['start', 'Start the WhatsApp session', wa.startHandler], ['restart', 'Restart the WhatsApp session', wa.restartHandler]] as const) {
+ v1Route({ method: 'post', path: `/whatsapp/${action}`, tag: 'WhatsApp', summary, access: 'admin', body: loose, v1: handler });
+}
+
+// ─── Banner, saved filters, dashboards, stats, analytics, reports ────────────
+
+v1Route({ method: 'get', path: '/banner', tag: 'Site', summary: 'The site-wide banner', access: 'public', v1: banner.getBanner });
+v1Route({ method: 'put', path: '/banner', tag: 'Site', summary: 'Set the site-wide banner', access: 'supervisor', campusScope: 'unscopedOnly (admins and unscoped supervisors)', middleware: [cs.unscopedOnly], body: loose, v1: banner.setBanner });
+
+const OWN = 'campusIndependent (a person\'s own saved filters)';
+v1Route({ method: 'get', path: '/saved-filters', tag: 'Me', summary: 'My saved filters', description: 'For one list page (`page`).', access: 'staff', campusScope: OWN, query: filters('page'), v1: savedFilterRouter, map: ctx => ({ url: `/${qs(ctx.req)}` }) });
+v1Route({ method: 'post', path: '/saved-filters', tag: 'Me', summary: 'Save a filter', description: 'By name (replaces one with the same name); up to 30 per page.', access: 'staff', campusScope: OWN, body: loose, v1: savedFilterRouter, map: () => ({ url: '/' }) });
+v1Route({ method: 'put', path: '/saved-filters/:id', tag: 'Me', summary: 'Change a saved filter', access: 'staff', campusScope: OWN, params: idParams, body: loose, v1: savedFilterRouter, map: ctx => ({ url: `/${encodeURIComponent(ctx.params.id)}` }) });
+v1Route({ method: 'delete', path: '/saved-filters/:id', tag: 'Me', summary: 'Delete a saved filter', access: 'staff', campusScope: OWN, params: idParams, v1: savedFilterRouter, map: ctx => ({ url: `/${encodeURIComponent(ctx.params.id)}` }) });
+v1Route({ method: 'get', path: '/dashboard/today', tag: 'Dashboard', summary: 'Today at a glance', access: 'staff', campusScope: SCOPE_LIST, middleware: [cs.scopeList], v1: dashboardRouter, map: ctx => ({ url: `/today${qs(ctx.req)}` }) });
+v1Route({ method: 'get', path: '/dashboard/search', tag: 'Dashboard', summary: 'Search people, registrations and events', access: 'staff', campusScope: SCOPE_LIST, middleware: [cs.scopeList], query: filters('q'), v1: dashboardRouter, map: ctx => ({ url: `/search${qs(ctx.req)}` }) });
+v1Route({ method: 'get', path: '/stats/staff', tag: 'Dashboard', summary: 'Staff dashboard numbers', access: 'staff', campusScope: SCOPE_LIST, middleware: [cs.scopeList], v1: stats.getStaffDashboardStats });
+v1Route({ method: 'get', path: '/stats/supervisor', tag: 'Dashboard', summary: 'Supervisor dashboard numbers', access: 'supervisor', campusScope: SCOPE_LIST, middleware: [cs.scopeList], v1: stats.getSupervisorDashboardStats });
+v1Route({ method: 'get', path: '/stats/admin', tag: 'Dashboard', summary: 'Admin dashboard numbers', access: 'admin', v1: stats.getAdminDashboardStats });
+v1Route({ method: 'get', path: '/stats/overview', tag: 'Dashboard', summary: 'Overview numbers', access: 'supervisor', campusScope: SCOPE_LIST, middleware: [cs.scopeList], query: filters('from', 'to', 'campus'), v1: stats.getOverviewStats });
+const trackLimiter = rateLimit({ windowMs: 60 * 1000, max: 60, standardHeaders: true, legacyHeaders: false });
+v1Route({ method: 'post', path: '/analytics/track', tag: 'Dashboard', summary: 'Record a step on the way to registering', access: 'public', middleware: [trackLimiter], body: loose, v1: analytics.trackStage });
+v1Route({ method: 'get', path: '/analytics/funnel', tag: 'Dashboard', summary: 'How many get from viewing to paying', access: 'supervisor', campusScope: SCOPE_LIST, middleware: [cs.scopeList], query: filters('eventId', 'from', 'to'), v1: analytics.getFunnel });
+v1Route({ method: 'post', path: '/reports/pdf', tag: 'Reports', summary: 'A report as a PDF', description: 'The report\'s rows and columns in the body.', access: 'user', body: loose, file: { contentType: 'application/pdf' }, v1: reports.generatePdf });
+v1Route({ method: 'post', path: '/reports/excel', tag: 'Reports', summary: 'A report as an Excel file', access: 'user', body: loose, file: { contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, v1: reports.generateExcel });
+v1Route({ method: 'post', path: '/reports/email', tag: 'Reports', summary: 'Email a report as a PDF', access: 'user', body: loose, v1: reports.emailPdf });
+v1Route({ method: 'post', path: '/reports/whatsapp', tag: 'Reports', summary: 'WhatsApp a report as a PDF', access: 'user', body: loose, v1: reports.whatsappPdf });
diff --git a/backend/src/v2/route.ts b/backend/src/v2/route.ts
index d607037..568a6a3 100644
--- a/backend/src/v2/route.ts
+++ b/backend/src/v2/route.ts
@@ -78,6 +78,10 @@ export function defineRoute
r.method === def.method && r.path === def.path)) {
+ throw new Error(`${def.method.toUpperCase()} /api/v2${def.path} is defined twice`);
+ }
routes.push(def as AnyRoute);
return def;
}
diff --git a/backend/tests/v2.test.ts b/backend/tests/v2.test.ts
index c410ba7..58c3a62 100644
--- a/backend/tests/v2.test.ts
+++ b/backend/tests/v2.test.ts
@@ -51,6 +51,7 @@ jest.mock('../src/utils/campusScope', () => ({
scopeBody: () => (_req: unknown, _res: unknown, next: () => void) => next(),
listWhere: async () => ({}),
scopeList: (_req: unknown, _res: unknown, next: () => void) => next(),
+ scopeBroadcast: () => (_req: unknown, _res: unknown, next: () => void) => next(),
unscopedOnly: (_req: unknown, _res: unknown, next: () => void) => next(),
campusIndependent: () => (_req: unknown, _res: unknown, next: () => void) => next(),
}));
diff --git a/e2e/api-v2.spec.ts b/e2e/api-v2.spec.ts
index ffbf78b..c558c4a 100644
--- a/e2e/api-v2.spec.ts
+++ b/e2e/api-v2.spec.ts
@@ -275,6 +275,33 @@ test.describe("API v2", () => {
expect((await request.get(`${API}/api/v2/kids/events`, { headers: user })).status()).toBe(403);
});
+ test("people and messaging: event listing, dashboards, saved filters, accounts", async ({ request }) => {
+ const staff = { Authorization: `Bearer ${seed().users.staff.token}` };
+ const sup = { Authorization: `Bearer ${seed().users.supervisor.token}` };
+ const listing = (await (await request.get(`${API}/api/v2/event-listing`)).json()).data;
+ expect(Array.isArray(listing)).toBe(true);
+ expect(listing[0]).toHaveProperty("eventOptions");
+
+ for (const path of ["/api/v2/stats/staff", "/api/v2/dashboard/today", "/api/v2/banner"]) {
+ const res = await request.get(`${API}${path}`, { headers: staff });
+ expect(res.status(), path).toBe(200);
+ }
+
+ // Saved filters run the v1 router: save one, list it, delete it.
+ const saved = await request.post(`${API}/api/v2/saved-filters`, { headers: sup, data: { page: "events", name: "E2E v2 filter", params: { q: "x" } } });
+ expect(saved.status()).toBe(200);
+ const list = (await (await request.get(`${API}/api/v2/saved-filters?page=events`, { headers: sup })).json()).data;
+ const mine = (Array.isArray(list) ? list : list.filters || []).find((x: { name: string }) => x.name === "E2E v2 filter");
+ expect(mine).toBeTruthy();
+ expect((await request.delete(`${API}/api/v2/saved-filters/${mine.id}`, { headers: sup })).status()).toBe(200);
+
+ const users = (await (await request.get(`${API}/api/v2/users?page=1&limit=2`, { headers: sup })).json()).data;
+ expect(users).toMatchObject({ page: 1, limit: 2 });
+ // As v1: an unknown email is NOT_FOUND.
+ const forgot = await request.post(`${API}/api/v2/auth/password/forgot`, { data: { email: "nobody-e2e@example.com" } });
+ expect((await forgot.json()).error.code).toBe("NOT_FOUND");
+ });
+
test("an admin can read the OpenAPI reference at /docs/v2", async ({ page, request }) => {
const headers = { Authorization: `Bearer ${seed().users.user.token}` };
expect((await request.get(`${API}/api/v2/openapi.json`, { headers })).status()).toBe(403);
diff --git a/frontend/src/app/(auth)/forgot-password/page.tsx b/frontend/src/app/(auth)/forgot-password/page.tsx
index 715b97f..c357428 100644
--- a/frontend/src/app/(auth)/forgot-password/page.tsx
+++ b/frontend/src/app/(auth)/forgot-password/page.tsx
@@ -2,9 +2,9 @@
import { useState } from "react";
import { Navbar } from "@/components/layout/Navbar";
import { Footer } from "@/components/layout/Footer";
-import { apiFetch } from "@/lib/api";
import { KeyRound } from "lucide-react";
import { errorMessage } from "@/lib/utils";
+import { apiV2Data } from "@/lib/apiV2";
export default function ForgotPasswordPage() {
const [email, setEmail] = useState("");
@@ -17,7 +17,7 @@ export default function ForgotPasswordPage() {
try {
setLoading(true);
setStatus(null);
- const res = await apiFetch<{ message?: string }>("/api/users/forgot", { method: "POST", body: { email } });
+ const res = await (apiV2Data("post", "/auth/password/forgot", { body: { email } }) as Promise<{ message?: string }>);
setStatus(res?.message || "If that email exists, a password reset link has been sent.");
} catch (err) {
try {
diff --git a/frontend/src/app/activate-account/page.tsx b/frontend/src/app/activate-account/page.tsx
index fb61f25..e9d5e9f 100644
--- a/frontend/src/app/activate-account/page.tsx
+++ b/frontend/src/app/activate-account/page.tsx
@@ -3,12 +3,12 @@ import React, { Suspense, useMemo, useState } from "react";
import Link from "next/link";
import { Navbar } from "@/components/layout/Navbar";
import { Footer } from "@/components/layout/Footer";
-import { apiFetch } from "@/lib/api";
import { saveToken } from "@/lib/auth";
import { useSearchParams } from "next/navigation";
import { appName } from "@/lib/siteConfig";
import { ShieldCheck } from "lucide-react";
import { errorMessage } from "@/lib/utils";
+import { apiV2Data } from "@/lib/apiV2";
function ActivateAccountContent() {
const search = useSearchParams();
@@ -29,7 +29,7 @@ function ActivateAccountContent() {
try {
setLoading(true);
setStatus(null);
- const data = await apiFetch<{ token?: string }>("/api/users/activate", { method: "POST", body: { token, password, acceptTerms: accepted } });
+ const data = await (apiV2Data("post", "/auth/activate", { body: { token, password, acceptTerms: accepted } }) as Promise<{ token?: string }>);
// Auto-login: save the session token (under the key AuthContext reads) and load the
// dashboard fresh so the new session is picked up.
if (data?.token) saveToken(data.token);
diff --git a/frontend/src/app/campus/[slug]/page.tsx b/frontend/src/app/campus/[slug]/page.tsx
index 6ae8acb..b7db1b8 100644
--- a/frontend/src/app/campus/[slug]/page.tsx
+++ b/frontend/src/app/campus/[slug]/page.tsx
@@ -42,7 +42,7 @@ export default async function CampusPage({ params }: { params: Promise<{ slug: s
const campus = await loadCampus(slug);
if (!campus) notFound();
- const events = await apiFetchListForPrerender(`/api/events?campus=${encodeURIComponent(campus.slug)}`, { nextOptions: { next: { revalidate } } });
+ const events = await apiFetchListForPrerender(`/api/v2/event-listing?campus=${encodeURIComponent(campus.slug)}`, { nextOptions: { next: { revalidate } } });
const now = Date.now();
const upcoming = (events || [])
.filter(e => e.cashupStatus !== "closed" && new Date(e.endDate || e.startDate).getTime() > now)
diff --git a/frontend/src/app/dashboard/admin/page.tsx b/frontend/src/app/dashboard/admin/page.tsx
index e7c8e94..ea8c0b9 100644
--- a/frontend/src/app/dashboard/admin/page.tsx
+++ b/frontend/src/app/dashboard/admin/page.tsx
@@ -3,7 +3,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
-import { apiFetch } from "@/lib/api";
import { useStableState } from "@/hooks/useStableState";
import { useVisiblePolling } from "@/hooks/useVisiblePolling";
import {
@@ -19,6 +18,7 @@ import type { DashboardStats, PaymentStats } from "@/types";
import { CampusFilter } from "@/components/shared/CampusFilter";
import { TodayPanel } from "@/components/dashboard/TodayPanel";
import { CommandPaletteButton } from "@/components/shared/CommandPalette";
+import { apiV2Data } from "@/lib/apiV2";
const formatRand = (n: number) => `R ${(n || 0).toFixed(2)}`;
const formatRandAxis = (n: number) => `R${new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format(n)}`;
@@ -93,9 +93,9 @@ export default function AdminDashboardPage() {
try {
if (isFirstLoad) setLoadingStats(true);
const [dash, overviewData] = await Promise.all([
- apiFetch("/api/stats/admin", { authToken: token }),
+ (apiV2Data("get", "/stats/admin", { authToken: token }) as Promise),
// The month-over-month tiles follow the campus filter; today's totals stay church-wide.
- apiFetch(`/api/stats/overview${campus ? `?campus=${encodeURIComponent(campus)}` : ""}`, { authToken: token }),
+ (apiV2Data("get", "/stats/overview", { query: { campus: campus || undefined }, authToken: token }) as Promise),
]);
setPaymentStats(dash.paymentStats);
setOverview(overviewData);
diff --git a/frontend/src/app/dashboard/admin/settings/page.tsx b/frontend/src/app/dashboard/admin/settings/page.tsx
index ffccfda..3ae0344 100644
--- a/frontend/src/app/dashboard/admin/settings/page.tsx
+++ b/frontend/src/app/dashboard/admin/settings/page.tsx
@@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import { apiFetch, ApiError, resolveToApiOrigin } from "@/lib/api";
+import { apiV2Raw, apiV2Data } from "@/lib/apiV2";
import { useSiteSettings } from "@/contexts/SiteSettingsContext";
import { useDismissingState } from "@/hooks/useDismissingState";
import { Building2, Palette, Bell, Mail, Scale, MessageCircle, CreditCard, DatabaseBackup, Timer, KeyRound, MapPinned, type LucideIcon, Webhook } from "lucide-react";
@@ -884,7 +885,7 @@ function WhatsAppTab({ active }: { active: boolean }) {
const fetchConfig = useCallback(async () => {
if (!token) return;
try {
- const res = await apiFetch("/api/whatsapp/config", { authToken: token });
+ const res = await (apiV2Data("get", "/whatsapp/config", { authToken: token }) as Promise);
setCfg(res);
} catch {
// network error — leave cfg null, tab shows loading state
@@ -898,7 +899,7 @@ function WhatsAppTab({ active }: { active: boolean }) {
const fetchStatus = useCallback(async () => {
if (!token || step !== 3) return;
try {
- const res = await apiFetch("/api/whatsapp/status", { authToken: token });
+ const res = await (apiV2Data("get", "/whatsapp/status", { authToken: token }) as Promise);
setStatus(res.status ?? null);
setStatusMsg(res.message ?? null);
} catch {
@@ -921,7 +922,7 @@ function WhatsAppTab({ active }: { active: boolean }) {
const fetchQr = useCallback(async () => {
if (!token) return;
try {
- const res = await apiFetch<{ qr?: string }>("/api/whatsapp/qr", { authToken: token });
+ const res = await (apiV2Data("get", "/whatsapp/qr", { authToken: token }) as Promise<{ qr?: string }>);
if (res.qr) setQrSrc(`data:image/png;base64,${res.qr}`);
} catch {
setQrSrc(null);
@@ -945,7 +946,9 @@ function WhatsAppTab({ active }: { active: boolean }) {
setBusy(action);
setActionMsg(null);
try {
- const res = await apiFetch<{ message?: string }>(`/api/whatsapp/${action}`, { method: "POST", authToken: token, body });
+ const res = action === "delete-instance"
+ ? await apiV2Raw<{ message?: string }>("delete", "/whatsapp/instance", { authToken: token })
+ : await apiV2Raw<{ message?: string }>("post", `/whatsapp/${action}`, { authToken: token, body: body || {} });
setActionMsg({ type: "ok", text: res?.message || `${action} successful.` });
await fetchStatus();
await fetchConfig();
@@ -970,7 +973,7 @@ function WhatsAppTab({ active }: { active: boolean }) {
setSavingToken(true);
setActionMsg(null);
try {
- await apiFetch("/api/whatsapp/config", { method: "POST", authToken: token!, body: { token: inputToken.trim(), instanceId: "" } });
+ await apiV2Data("put", "/whatsapp/config", { authToken: token, body: { token: inputToken.trim(), instanceId: "" } });
setInputToken("");
await fetchConfig();
} catch (e) {
@@ -988,7 +991,7 @@ function WhatsAppTab({ active }: { active: boolean }) {
setSavingInstance(true);
setActionMsg(null);
try {
- await apiFetch("/api/whatsapp/config", { method: "POST", authToken: token!, body: { token: "", instanceId: inputInstanceId.trim() } });
+ await apiV2Data("put", "/whatsapp/config", { authToken: token, body: { token: "", instanceId: inputInstanceId.trim() } });
setInputInstanceId("");
await fetchConfig();
} catch (e) {
@@ -1002,7 +1005,7 @@ function WhatsAppTab({ active }: { active: boolean }) {
setSavingInstance(true);
setActionMsg(null);
try {
- const res = await apiFetch<{ message?: string }>("/api/whatsapp/create-instance", { method: "POST", authToken: token! });
+ const res = await (apiV2Data("post", "/whatsapp/instance", { authToken: token, body: {} }) as Promise<{ message?: string }>);
setActionMsg({ type: "ok", text: res?.message || "Instance created." });
await fetchConfig();
} catch (e) {
@@ -1023,7 +1026,7 @@ function WhatsAppTab({ active }: { active: boolean }) {
const resetToken = async () => {
if (!confirm("This will clear your saved access token. You will need to re-enter it. Continue?")) return;
try {
- await apiFetch("/api/whatsapp/config", { method: "POST", authToken: token!, body: { token: "_clear_", instanceId: "" } });
+ await apiV2Data("put", "/whatsapp/config", { authToken: token, body: { token: "_clear_", instanceId: "" } });
} catch {}
setCfg(prev => prev ? { ...prev, hasToken: false, hasInstance: false, configured: false, tokenMasked: "", instanceId: "" } : null);
};
@@ -1257,9 +1260,8 @@ function WhatsAppTab({ active }: { active: boolean }) {
setSavingToken(true);
setActionMsg(null);
try {
- await apiFetch("/api/whatsapp/config", {
- method: "POST",
- authToken: token!,
+ await apiV2Data("put", "/whatsapp/config", {
+ authToken: token,
body: { token: inputToken.trim() || undefined, instanceId: inputInstanceId.trim() || undefined },
});
setActionMsg({ type: "ok", text: "Credentials updated." });
diff --git a/frontend/src/app/dashboard/admin/users/page.tsx b/frontend/src/app/dashboard/admin/users/page.tsx
index 37b64fc..62038fb 100644
--- a/frontend/src/app/dashboard/admin/users/page.tsx
+++ b/frontend/src/app/dashboard/admin/users/page.tsx
@@ -13,6 +13,7 @@ import type { Paginated } from "@/types";
import { BackLink } from "@/components/shared/BackLink";
import { useCampuses } from "@/hooks/useCampuses";
import { SavedFilters } from "@/components/shared/SavedFilters";
+import { apiV2Data, apiV2 } from "@/lib/apiV2";
interface UserItem {
id: string;
@@ -91,7 +92,7 @@ export default function AdminUsersPage() {
if (debouncedQuery.trim()) qs.set("search", debouncedQuery.trim());
if (roleFilter) qs.set("role", roleFilter);
if (activeFilter !== "") qs.set("isActive", activeFilter);
- return `/api/users?${qs.toString()}`;
+ return Object.fromEntries(qs);
}, [debouncedQuery, roleFilter, activeFilter, pageSize]);
const loadUsers = useCallback(async (p = 1, ps = pageSize) => {
@@ -99,8 +100,7 @@ export default function AdminUsersPage() {
setError(null);
setFetching(true);
try {
- const url = buildQuery(p, ps);
- const res = await apiFetch>(url, { authToken: token });
+ const res = await (apiV2Data("get", "/users", { query: buildQuery(p, ps), authToken: token }) as Promise>);
setUsers(Array.isArray(res?.data) ? res.data : []);
setTotal(res?.total ?? 0);
setTotalPages(res?.pages ?? 1);
@@ -131,13 +131,16 @@ export default function AdminUsersPage() {
if (!cEmail || !cPassword) { setError("Email and password are required"); return; }
try {
setCreating(true);
- const created = await apiFetch<{ id?: string }>("/api/users", {
- method: "POST",
- body: { name: cName || cEmail.split("@")[0], email: cEmail, password: cPassword, phoneNumber: cPhone || undefined },
+ // An admin adding someone is a sign-up on their behalf (they agreed to the terms for them).
+ const { data: made } = await apiV2("post", "/auth/signup", {
+ body: { name: cName || cEmail.split("@")[0], email: cEmail, password: cPassword, acceptTerms: true, ...(cPhone ? { phone: cPhone } : {}) },
});
+ const created = { id: made.user.id };
if (cRole && cRole !== "user" && created?.id) {
- await apiFetch(`/api/users/${encodeURIComponent(created.id)}`, {
- method: "PUT", authToken: token!, body: { role: cRole },
+ await apiV2Data("patch", "/users/{id}", {
+ params: { id: created.id },
+ authToken: token,
+ body: { role: cRole },
});
}
resetCreateForm();
@@ -156,7 +159,7 @@ export default function AdminUsersPage() {
const [scope, setScope] = useState<{ id: string; name: string }[] | null>(null);
const startEdit = (u: UserItem) => {
setEditingId(u.id); setEditData({ ...u, password: "" }); setScope(null);
- apiFetch<{ scopedCampuses?: { id: string; name: string }[] }>(`/api/users/${encodeURIComponent(u.id)}`, { authToken: token! })
+ (apiV2Data("get", "/users/{id}", { params: { id: u.id }, authToken: token }) as Promise<{ scopedCampuses?: { id: string; name: string }[] }>)
.then(full => setScope(full.scopedCampuses || []))
.catch(() => setScope(null));
};
@@ -181,8 +184,10 @@ export default function AdminUsersPage() {
payload.password = editData.password.trim();
}
if (campusInfo.enabled && canBeScoped && scope) payload.scopedCampusIds = scope.map(c => c.id);
- await apiFetch(`/api/users/${encodeURIComponent(editingId)}`, {
- method: "PUT", authToken: token!, body: payload,
+ await apiV2Data("patch", "/users/{id}", {
+ params: { id: editingId },
+ authToken: token,
+ body: payload,
});
setEditingId(null);
setEditData({});
@@ -197,7 +202,7 @@ export default function AdminUsersPage() {
const revokeUserSessions = async (id: string, name: string) => {
if (!confirm(`Revoke all active sessions for ${name}? They will be signed out on all devices.`)) return;
try {
- await apiFetch(`/api/users/${encodeURIComponent(id)}/revoke-sessions`, { method: "POST", authToken: token! });
+ await apiV2Data("post", "/users/{id}/sessions/revoke", { params: { id: id }, authToken: token, body: {} });
} catch (e) {
setError(errorMessage(e) || "Failed to revoke sessions");
}
@@ -228,7 +233,7 @@ export default function AdminUsersPage() {
: `Turn off two-factor authentication for ${name}? They will be able to log in with just their password until they turn it back on.`;
if (!confirm(message)) return;
try {
- await apiFetch(`/api/users/${encodeURIComponent(id)}`, { method: "PUT", authToken: token!, body: { twoFactorEnabled: false } });
+ await apiV2Data("patch", "/users/{id}", { params: { id: id }, authToken: token, body: { twoFactorEnabled: false } });
await loadUsers(page);
} catch (e) {
setError(errorMessage(e) || "Failed to reset two-factor authentication");
@@ -238,7 +243,7 @@ export default function AdminUsersPage() {
const deactivate = async (id: string) => {
if (!confirm("Deactivate this user? Their account will be disabled.")) return;
try {
- await apiFetch(`/api/users/${encodeURIComponent(id)}`, { method: "DELETE", authToken: token! });
+ await apiV2Data("delete", "/users/{id}", { params: { id: id }, authToken: token });
await loadUsers(page);
} catch (e) {
setError(errorMessage(e) || "Failed to deactivate user");
@@ -248,7 +253,7 @@ export default function AdminUsersPage() {
const deleteUserData = async (id: string, name: string) => {
if (!confirm(`Delete personal data for "${name}"?\n\nThis will set their name to "Deleted User", clear their email and phone number, and deactivate the account. This action cannot be undone.`)) return;
try {
- await apiFetch(`/api/users/${encodeURIComponent(id)}/anonymize`, { method: "POST", authToken: token! });
+ await apiV2Data("post", "/users/{id}/anonymize", { params: { id: id }, authToken: token, body: {} });
await loadUsers(page);
} catch (e) {
setError(errorMessage(e) || "Failed to delete user data");
diff --git a/frontend/src/app/dashboard/staff/page.tsx b/frontend/src/app/dashboard/staff/page.tsx
index 8409c0d..d987b01 100644
--- a/frontend/src/app/dashboard/staff/page.tsx
+++ b/frontend/src/app/dashboard/staff/page.tsx
@@ -3,7 +3,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
-import { apiFetch } from "@/lib/api";
import { useStableState } from "@/hooks/useStableState";
import { useVisiblePolling } from "@/hooks/useVisiblePolling";
import { QrCode, Ticket, Activity, UserCheck, Clock, Users, ClipboardCheck, Baby, HandHeart } from "lucide-react";
@@ -13,6 +12,7 @@ import type { DashboardStats, RecentScan, ScanStats } from "@/types";
import { CampusScopeNote } from "@/components/shared/CampusFilter";
import { TodayPanel } from "@/components/dashboard/TodayPanel";
import { CommandPaletteButton } from "@/components/shared/CommandPalette";
+import { apiV2Data } from "@/lib/apiV2";
const QUICK_ACTIONS = [
{ href: "/dashboard/staff/ticket-scanning", label: "Scan tickets", description: "Use your device camera to validate tickets", icon: QrCode },
@@ -52,7 +52,7 @@ export default function StaffDashboardPage() {
const isFirstLoad = !hasLoadedOnce.current;
try {
if (isFirstLoad) setLoadingStats(true);
- const data = await apiFetch("/api/stats/staff", { authToken: token });
+ const data = await (apiV2Data("get", "/stats/staff", { authToken: token }) as Promise);
setStats(data.scanStats);
setRecentScans(Array.isArray(data.recentScans) ? data.recentScans : []);
} catch {
diff --git a/frontend/src/app/dashboard/supervisor/coupons/page.tsx b/frontend/src/app/dashboard/supervisor/coupons/page.tsx
index d24637f..84babee 100644
--- a/frontend/src/app/dashboard/supervisor/coupons/page.tsx
+++ b/frontend/src/app/dashboard/supervisor/coupons/page.tsx
@@ -3,7 +3,7 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
-import { apiFetch, fetchManagedEvents } from "@/lib/api";
+import { fetchManagedEvents } from "@/lib/api";
import { useDismissingState } from "@/hooks/useDismissingState";
import { TicketPercent, Plus, X, Search, Pencil, Power, Users, CalendarClock, ListChecks } from "lucide-react";
import { errorMessage } from "@/lib/utils";
@@ -446,7 +446,7 @@ function CouponModal({ token, events, couponId, initial, onClose, onSaved }: {
if (d.restrictTo !== "users" || userQuery.trim().length < 2) { setUserResults([]); return; }
const t = setTimeout(async () => {
try {
- const res = await apiFetch<{ data: { id: string; name: string; email: string }[] }>(`/api/users?search=${encodeURIComponent(userQuery.trim())}&limit=8`, { authToken: token });
+ const res = await (apiV2Data("get", "/users", { query: { search: userQuery.trim(), limit: "8" }, authToken: token }) as Promise<{ data: { id: string; name: string; email: string }[] }>);
setUserResults(res?.data || []);
} catch { setUserResults([]); }
}, 300);
diff --git a/frontend/src/app/dashboard/supervisor/email-attendees/page.tsx b/frontend/src/app/dashboard/supervisor/email-attendees/page.tsx
index 2d28727..75a62bf 100644
--- a/frontend/src/app/dashboard/supervisor/email-attendees/page.tsx
+++ b/frontend/src/app/dashboard/supervisor/email-attendees/page.tsx
@@ -3,7 +3,7 @@
import React, { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useRouter, useSearchParams } from "next/navigation";
-import { apiFetch, fetchAllUsers, fetchManagedEvents } from "@/lib/api";
+import { fetchAllUsers, fetchManagedEvents } from "@/lib/api";
import { useDismissingState } from "@/hooks/useDismissingState";
import { Mail, Users, Zap, Clock } from "lucide-react";
import { AllocationGroupFilter } from "@/components/shared/AllocationGroupFilter";
@@ -280,7 +280,7 @@ function EmailAttendeesPageInner() {
try {
if (!token) return;
setLoadingScheduled(true);
- const res = await apiFetch<{jobs: ScheduledJob[]}>(`/api/scheduled-emails?channel=email`, { authToken: token });
+ const res = await (apiV2Data("get", "/scheduled-messages", { query: { channel: "email" }, authToken: token }) as Promise<{jobs: ScheduledJob[]}>);
setScheduled(Array.isArray(res?.jobs) ? res.jobs : []);
} catch {
// ignore here; surfaces via UI when tab open
@@ -315,7 +315,7 @@ function EmailAttendeesPageInner() {
body.html = '';
}
}
- await apiFetch(`/api/scheduled-emails/${encodeURIComponent(editing.id)}`, { method: 'PATCH', authToken: token, body });
+ await apiV2Data("patch", "/scheduled-messages/{id}", { params: { id: editing.id }, authToken: token, body });
setInfo('Scheduled email updated.');
setEditing(null);
loadScheduled();
@@ -329,7 +329,7 @@ function EmailAttendeesPageInner() {
const removeJob = async (job: ScheduledJob) => {
try {
if (!token) { setError('Not authenticated'); return; }
- await apiFetch(`/api/scheduled-emails/${encodeURIComponent(job.id)}`, { method: 'DELETE', authToken: token });
+ await apiV2Data("delete", "/scheduled-messages/{id}", { params: { id: job.id }, authToken: token });
setInfo('Scheduled email removed.');
loadScheduled();
} catch (e) {
@@ -432,7 +432,7 @@ function EmailAttendeesPageInner() {
if (templateKey !== 'tickets') {
if (body.trim().startsWith('<')) payload.html = body; else payload.text = body;
}
- const res = await apiFetch(`/api/events/${encodeURIComponent(eventId)}/email-attendees`, { method: 'POST', authToken: token, body: payload });
+ const res = await (apiV2Data("post", "/events/{id}/email-attendees", { params: { id: eventId }, authToken: token, body: payload }) as Promise);
setPreviewCount(res?.matched ?? 0);
setPreviewSample(Array.isArray(res?.recipients) ? res.recipients : null);
setInfo(matchedText(res));
@@ -473,7 +473,7 @@ function EmailAttendeesPageInner() {
if (templateKey !== 'tickets') {
if (body.trim().startsWith('<')) payload.html = body; else payload.text = body.replace(/\n/g, '\n');
}
- const res = await apiFetch(`/api/events/${encodeURIComponent(eventId)}/email-attendees`, { method: 'POST', authToken: token, body: payload });
+ const res = await (apiV2Data("post", "/events/{id}/email-attendees", { params: { id: eventId }, authToken: token, body: payload }) as Promise);
const queued = res?.queued ?? res?.matched ?? 0;
setInfo(`Queued ${queued} recipient(s) for sending.`);
// Reset form to default state
@@ -670,7 +670,7 @@ function EmailAttendeesPageInner() {
if (templateKey !== 'tickets') {
if (body.trim().startsWith('<')) payload.html = body; else payload.text = body.replace(/\n/g, '\n');
}
- const res = await apiFetch(`/api/events/${encodeURIComponent(eventId)}/email-attendees/schedule`, { method: 'POST', authToken: token, body: payload });
+ const res = await (apiV2Data("post", "/events/{id}/email-attendees/schedule", { params: { id: eventId }, authToken: token, body: payload }) as Promise);
if (res?.job?.id) {
setInfo('Email scheduled. It will be sent around the specified time.');
} else {
@@ -779,7 +779,7 @@ Jane Doe
if (!broadcastSubject.trim()) { setError('Subject is required'); return; }
if (!broadcastBody.trim()) { setError('Message is required'); return; }
const body = { userIds: selectedUserIds, emails: broadcastEmails, eventId: broadcastEventId || undefined, kind: broadcastKind };
- const res = await apiFetch(`/api/broadcasts/preview`, { method: 'POST', authToken: token, body });
+ const res = await (apiV2Data("post", "/broadcasts/email/preview", { authToken: token, body }) as Promise);
setBroadcastPreviewCount(res?.matched ?? 0);
setBroadcastPreviewSample(Array.isArray(res?.recipients) ? res.recipients : null);
setInfo(matchedText(res));
@@ -796,7 +796,7 @@ Jane Doe
if (!broadcastBody.trim()) { setError('Message is required'); return; }
const payload: { subject: string; eventId?: string; userIds: string[]; emails: string; kind: MessageKind } & EmailContent = { subject: broadcastSubject, eventId: broadcastEventId || undefined, userIds: selectedUserIds, emails: broadcastEmails, kind: broadcastKind };
if (broadcastBody.trim().startsWith('<')) payload.html = broadcastBody; else payload.text = broadcastBody;
- const res = await apiFetch(`/api/broadcasts/send`, { method: 'POST', authToken: token, body: payload });
+ const res = await (apiV2Data("post", "/broadcasts/email/send", { authToken: token, body: payload }) as Promise);
setInfo(`Sent ${res?.sent ?? 0} out of ${res?.matched ?? 0} recipient(s).${res?.skipped ? ` ${res.skipped} skipped — not opted in to news or unsubscribed.` : ''}`);
// Reset broadcast form
resetBroadcastForm();
@@ -814,7 +814,7 @@ Jane Doe
const whenIso = new Date(broadcastScheduledAtLocal).toISOString();
const payload: { scheduledAt: string; subject: string; eventId?: string; userIds: string[]; emails: string; kind: MessageKind } & EmailContent = { scheduledAt: whenIso, subject: broadcastSubject, eventId: broadcastEventId || undefined, userIds: selectedUserIds, emails: broadcastEmails, kind: broadcastKind };
if (broadcastBody.trim().startsWith('<')) payload.html = broadcastBody; else payload.text = broadcastBody;
- const res = await apiFetch(`/api/broadcasts/schedule`, { method: 'POST', authToken: token, body: payload });
+ const res = await (apiV2Data("post", "/broadcasts/email/schedule", { authToken: token, body: payload }) as Promise);
if (res?.job?.id) setInfo('Broadcast scheduled. It will be sent around the specified time.'); else setInfo('Scheduled.');
// Reset broadcast form
resetBroadcastForm();
@@ -1085,7 +1085,7 @@ With gratitude`);
jobs.push(payload);
}
if (jobs.length === 0) { onError('Please enable at least one automation and fill in details'); return; }
- const res = await apiFetch<{ message?: string }>(`/api/automations/schedule`, { method: 'POST', authToken: token, body: { eventId: autoEventId, jobs } });
+ const res = await (apiV2Data("post", "/automations/schedule", { authToken: token, body: { eventId: autoEventId, jobs } }) as Promise<{ message?: string }>);
onInfo(res?.message || `Scheduled ${jobs.length} automation(s).`);
// Reset to defaults (keep event selection)
setEnablePre(true); setEnableFinal(true); setEnableThanks(true); setEnablePromo(false);
diff --git a/frontend/src/app/dashboard/supervisor/events/page.tsx b/frontend/src/app/dashboard/supervisor/events/page.tsx
index ab5e8d4..292949b 100644
--- a/frontend/src/app/dashboard/supervisor/events/page.tsx
+++ b/frontend/src/app/dashboard/supervisor/events/page.tsx
@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta
import { createPortal } from "react-dom";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
-import { apiFetch, API_BASE, fetchManagedEvents, fetchEvent } from "@/lib/api";
+import { API_BASE, fetchManagedEvents, fetchEvent } from "@/lib/api";
import { ApiImage } from "@/components/shared/ApiImage";
import { useDismissingState } from "@/hooks/useDismissingState";
import { useSiteSettings } from "@/contexts/SiteSettingsContext";
@@ -689,7 +689,7 @@ function NotifyRecipientsPicker({ selected, onChange, disabled }: { selected: No
setSearching(true);
const t = setTimeout(async () => {
try {
- const res = await apiFetch>(`/api/users?search=${encodeURIComponent(search.trim())}&limit=10`, { authToken: token || undefined });
+ const res = await (apiV2Data("get", "/users", { query: { search: search.trim(), limit: "10" }, authToken: token }) as Promise>);
if (!cancelled) setResults(Array.isArray(res?.data) ? res.data : []);
} catch {
if (!cancelled) setResults([]);
diff --git a/frontend/src/app/dashboard/supervisor/page.tsx b/frontend/src/app/dashboard/supervisor/page.tsx
index 01f4f9b..e587d77 100644
--- a/frontend/src/app/dashboard/supervisor/page.tsx
+++ b/frontend/src/app/dashboard/supervisor/page.tsx
@@ -3,7 +3,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
-import { apiFetch } from "@/lib/api";
import { useStableState } from "@/hooks/useStableState";
import { useVisiblePolling } from "@/hooks/useVisiblePolling";
import {
@@ -19,6 +18,7 @@ import type { DashboardStats, PaymentStats, RecentScan, ScanStats } from "@/type
import { CampusFilter, CampusScopeNote } from "@/components/shared/CampusFilter";
import { TodayPanel } from "@/components/dashboard/TodayPanel";
import { CommandPaletteButton } from "@/components/shared/CommandPalette";
+import { apiV2Data } from "@/lib/apiV2";
const formatRand = (n: number) => `R ${(n || 0).toFixed(2)}`;
const formatRandAxis = (n: number) => `R${new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format(n)}`;
@@ -91,9 +91,9 @@ export default function SupervisorDashboardPage() {
try {
if (isFirstLoad) setLoadingStats(true);
const [data, overviewData] = await Promise.all([
- apiFetch("/api/stats/supervisor", { authToken: token }),
+ (apiV2Data("get", "/stats/supervisor", { authToken: token }) as Promise),
// The month-over-month tiles follow the campus filter; today's totals stay church-wide.
- apiFetch(`/api/stats/overview${campus ? `?campus=${encodeURIComponent(campus)}` : ""}`, { authToken: token }),
+ (apiV2Data("get", "/stats/overview", { query: { campus: campus || undefined }, authToken: token }) as Promise),
]);
setScanStats(data.scanStats);
setPaymentStats(data.paymentStats);
diff --git a/frontend/src/app/dashboard/supervisor/whatsapp-attendees/page.tsx b/frontend/src/app/dashboard/supervisor/whatsapp-attendees/page.tsx
index b96198c..1c886af 100644
--- a/frontend/src/app/dashboard/supervisor/whatsapp-attendees/page.tsx
+++ b/frontend/src/app/dashboard/supervisor/whatsapp-attendees/page.tsx
@@ -3,7 +3,7 @@
import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useRouter, useSearchParams } from "next/navigation";
-import { apiFetch, fetchAllUsers, fetchManagedEvents } from "@/lib/api";
+import { fetchAllUsers, fetchManagedEvents } from "@/lib/api";
import { useDismissingState } from "@/hooks/useDismissingState";
import { MessageCircle, Users, Zap, Send, Clock, type LucideIcon } from "lucide-react";
import { AllocationGroupFilter } from "@/components/shared/AllocationGroupFilter";
@@ -249,8 +249,9 @@ function WAAutomationsPanel({ events, token, onInfo, onError }: { events: Event[
if (jobs.length === 0) { onError("Please enable at least one automation and fill in details"); return; }
let scheduled = 0;
for (const job of jobs) {
- await apiFetch(`/api/events/${encodeURIComponent(autoEventId)}/whatsapp-attendees/schedule`, {
- method: "POST", authToken: token,
+ await apiV2Data("post", "/events/{id}/whatsapp-attendees/schedule", {
+ params: { id: autoEventId },
+ authToken: token,
body: { message: job.message, scheduledAt: job.scheduledAt, filter: { status: "paid" } },
});
scheduled++;
@@ -462,7 +463,7 @@ function WhatsAppAttendeesPageInner() {
if (!token) { setError("Not authenticated"); return; }
if (!eventId) { setError("Please select an event"); return; }
if (templateKey !== "tickets" && !message.trim()) { setError("Message is required"); return; }
- const res = await apiFetch(`/api/events/${encodeURIComponent(eventId)}/whatsapp-attendees`, { method: "POST", authToken: token, body: buildPayload(true) });
+ const res = await (apiV2Data("post", "/events/{id}/whatsapp-attendees", { params: { id: eventId }, authToken: token, body: buildPayload(true) }) as Promise);
setPreviewCount(res?.matched ?? 0);
setPreviewSample(Array.isArray(res?.recipients) ? res.recipients : null);
setInfo(matchedText(res));
@@ -482,7 +483,7 @@ function WhatsAppAttendeesPageInner() {
if (!eventId) { setError("Please select an event"); return; }
if (templateKey !== "tickets" && !message.trim()) { setError("Message is required"); return; }
setSending(true);
- const res = await apiFetch(`/api/events/${encodeURIComponent(eventId)}/whatsapp-attendees`, { method: "POST", authToken: token, body: buildPayload() });
+ const res = await (apiV2Data("post", "/events/{id}/whatsapp-attendees", { params: { id: eventId }, authToken: token, body: buildPayload() }) as Promise);
setInfo(`Queued ${res?.queued ?? res?.matched ?? 0} recipient(s) for sending.`);
resetAttendeesForm();
} catch (e) { setError(errorMessage(e) || "Failed to send"); } finally { setSending(false); }
@@ -501,7 +502,7 @@ function WhatsAppAttendeesPageInner() {
filter: { status: status !== "any" ? status : undefined, attendeeIds: selectedAttendeeIds.length ? selectedAttendeeIds : undefined, allocationGroupId: groupId || undefined },
template: templateKey, kind: messageKind, scheduledAt: whenIso,
};
- const res = await apiFetch(`/api/events/${encodeURIComponent(eventId)}/whatsapp-attendees/schedule`, { method: "POST", authToken: token, body: payload });
+ const res = await (apiV2Data("post", "/events/{id}/whatsapp-attendees/schedule", { params: { id: eventId }, authToken: token, body: payload }) as Promise);
setInfo(res?.job?.id ? "WhatsApp scheduled. It will be sent around the specified time." : "Scheduled.");
resetAttendeesForm();
} catch (e) { setError(errorMessage(e) || "Failed to schedule"); }
@@ -554,7 +555,7 @@ function WhatsAppAttendeesPageInner() {
try {
if (!token) return;
setLoadingScheduled(true);
- const res = await apiFetch<{ jobs: ScheduledJob[] }>(`/api/scheduled-emails?channel=whatsapp`, { authToken: token });
+ const res = await (apiV2Data("get", "/scheduled-messages", { query: { channel: "whatsapp" }, authToken: token }) as Promise<{ jobs: ScheduledJob[] }>);
setScheduled(Array.isArray(res?.jobs) ? res.jobs : []);
} catch { } finally { setLoadingScheduled(false); }
}, [token]);
@@ -569,7 +570,7 @@ function WhatsAppAttendeesPageInner() {
const body: { scheduledAt?: string; message?: string } = {};
if (editWhen) body.scheduledAt = new Date(editWhen).toISOString();
if (editMessage.trim()) body.message = editMessage;
- await apiFetch(`/api/scheduled-emails/${encodeURIComponent(editing.id)}`, { method: "PATCH", authToken: token, body });
+ await apiV2Data("patch", "/scheduled-messages/{id}", { params: { id: editing.id }, authToken: token, body });
setInfo("Scheduled message updated.");
setEditing(null);
loadScheduled();
@@ -579,7 +580,7 @@ function WhatsAppAttendeesPageInner() {
const removeJob = async (job: ScheduledJob) => {
try {
if (!token) { setError("Not authenticated"); return; }
- await apiFetch(`/api/scheduled-emails/${encodeURIComponent(job.id)}`, { method: "DELETE", authToken: token });
+ await apiV2Data("delete", "/scheduled-messages/{id}", { params: { id: job.id }, authToken: token });
setInfo("Scheduled message removed.");
loadScheduled();
} catch (e) { setError(errorMessage(e) || "Failed to remove"); }
@@ -828,7 +829,7 @@ function WhatsAppAttendeesPageInner() {
try {
setError(null); setInfo(null); setBroadcastPreviewCount(null); setBroadcastPreviewSample(null);
if (!token) { setError("Not authenticated"); return; }
- const res = await apiFetch(`/api/whatsapp-broadcasts/preview`, { method: "POST", authToken: token, body: { userIds: selectedUserIds, phones: broadcastPhones, eventId: broadcastEventId || undefined, kind: broadcastKind } });
+ const res = await (apiV2Data("post", "/broadcasts/whatsapp/preview", { authToken: token, body: { userIds: selectedUserIds, phones: broadcastPhones, eventId: broadcastEventId || undefined, kind: broadcastKind } }) as Promise);
setBroadcastPreviewCount(res?.matched ?? 0);
setBroadcastPreviewSample(Array.isArray(res?.recipients) ? res.recipients : null);
setInfo(matchedText(res));
@@ -840,7 +841,7 @@ function WhatsAppAttendeesPageInner() {
setError(null); setInfo(null);
if (!token) { setError("Not authenticated"); return; }
if (!broadcastMessage.trim()) { setError("Message is required"); return; }
- const res = await apiFetch(`/api/whatsapp-broadcasts/send`, { method: "POST", authToken: token, body: { message: broadcastMessage, userIds: selectedUserIds, phones: broadcastPhones, eventId: broadcastEventId || undefined, kind: broadcastKind } });
+ const res = await (apiV2Data("post", "/broadcasts/whatsapp/send", { authToken: token, body: { message: broadcastMessage, userIds: selectedUserIds, phones: broadcastPhones, eventId: broadcastEventId || undefined, kind: broadcastKind } }) as Promise);
setInfo(`Sent ${res?.sent ?? 0} out of ${res?.matched ?? 0} recipient(s).${res?.skipped ? ` ${res.skipped} skipped — not opted in to news or unsubscribed.` : ""}`);
resetBroadcastForm();
} catch (e) { setError(errorMessage(e) || "Failed to send"); }
@@ -852,7 +853,7 @@ function WhatsAppAttendeesPageInner() {
if (!token) { setError("Not authenticated"); return; }
if (!broadcastMessage.trim()) { setError("Message is required"); return; }
const whenIso = new Date(broadcastScheduledAtLocal).toISOString();
- const res = await apiFetch(`/api/whatsapp-broadcasts/schedule`, { method: "POST", authToken: token, body: { scheduledAt: whenIso, message: broadcastMessage, userIds: selectedUserIds, phones: broadcastPhones, eventId: broadcastEventId || undefined, kind: broadcastKind } });
+ const res = await (apiV2Data("post", "/broadcasts/whatsapp/schedule", { authToken: token, body: { scheduledAt: whenIso, message: broadcastMessage, userIds: selectedUserIds, phones: broadcastPhones, eventId: broadcastEventId || undefined, kind: broadcastKind } }) as Promise);
if (res?.job?.id) setInfo("WhatsApp broadcast scheduled."); else setInfo("Scheduled.");
resetBroadcastForm();
} catch (e) { setError(errorMessage(e) || "Failed to schedule broadcast"); }
diff --git a/frontend/src/app/dashboard/user/donate/page.tsx b/frontend/src/app/dashboard/user/donate/page.tsx
index 0fa1e7b..d3133b2 100644
--- a/frontend/src/app/dashboard/user/donate/page.tsx
+++ b/frontend/src/app/dashboard/user/donate/page.tsx
@@ -1,7 +1,6 @@
"use client";
import React, { useEffect, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
-import { apiFetch } from "@/lib/api";
import { apiV2Data } from "@/lib/apiV2";
import { useDismissingState } from "@/hooks/useDismissingState";
import { HandHeart } from "lucide-react";
@@ -23,7 +22,7 @@ export default function DonatePage() {
useEffect(() => {
(async () => {
try {
- const evs = await apiFetch("/api/events");
+ const evs = await (apiV2Data("get", "/events") as Promise);
setEvents(evs);
if (evs.length > 0) setEventId(evs[0].id);
} catch (e) {
diff --git a/frontend/src/app/dashboard/user/profile/page.tsx b/frontend/src/app/dashboard/user/profile/page.tsx
index db23a4a..26759cf 100644
--- a/frontend/src/app/dashboard/user/profile/page.tsx
+++ b/frontend/src/app/dashboard/user/profile/page.tsx
@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
import Link from "next/link";
-import { apiFetch } from "@/lib/api";
import { isValidZAPhone } from "@/lib/phone";
import { useDismissingState } from "@/hooks/useDismissingState";
import { TwoFactorChallenge, TwoFactorMethod } from "@/lib/auth";
@@ -12,7 +11,7 @@ import { User, Lock, ShieldAlert, ShieldCheck, ShieldOff, Trash2, Clock, LogIn,
import { errorMessage } from "@/lib/utils";
import { BackLink } from "@/components/shared/BackLink";
import { useCampuses } from "@/hooks/useCampuses";
-import { apiV2 } from "@/lib/apiV2";
+import { apiV2, apiV2Data } from "@/lib/apiV2";
type ActivityEvent = { id: string; type: "login" | "password_changed" | "password_reset" | "two_factor_enabled" | "two_factor_disabled"; device: string | null; createdAt: string };
@@ -127,10 +126,10 @@ export default function UserProfilePage() {
setRevoking(true);
setRevokeMsg(null);
try {
- const res = await apiFetch<{ token?: string; message?: string }>("/api/users/revoke-sessions", {
- method: "POST",
+ const res = await (apiV2Data("post", "/me/sessions/revoke", {
authToken: token,
- });
+ body: {},
+ }) as Promise<{ token?: string; message?: string }>);
// Revoking sessions invalidates the token this device was using too —
// save the freshly issued one so this device stays signed in.
if (res?.token) updateToken(res.token);
@@ -150,7 +149,7 @@ export default function UserProfilePage() {
if (!token) return;
setLoadingActivity(true);
try {
- const res = await apiFetch("/api/users/activity", { authToken: token });
+ const res = await (apiV2Data("get", "/me/activity", { authToken: token }) as Promise);
setActivity(Array.isArray(res) ? res : []);
} catch {
// Non-critical — just leave the list empty rather than showing an error banner.
@@ -198,11 +197,10 @@ export default function UserProfilePage() {
setTfBusy(true);
setTfMsg(null);
try {
- const res = await apiFetch("/api/users/2fa/enable", {
- method: "POST",
+ const res = await (apiV2Data("post", "/me/two-factor/enable", {
authToken: token,
body: { method: tfMethod, password: tfPassword },
- });
+ }) as Promise);
setTfChallenge(res);
setTfCode("");
setTfPassword("");
@@ -220,11 +218,10 @@ export default function UserProfilePage() {
setTfBusy(true);
setTfMsg(null);
try {
- const res = await apiFetch<{ twoFactorMethod: TwoFactorMethod }>("/api/users/2fa/confirm", {
- method: "POST",
+ const res = await (apiV2Data("post", "/me/two-factor/confirm", {
authToken: token,
body: { challengeId: tfChallenge.challengeId, code: tfCode },
- });
+ }) as Promise<{ twoFactorMethod: TwoFactorMethod }>);
const wasEnabled = tfEnabled;
setTfEnabled(true);
setTfMethod(res.twoFactorMethod);
@@ -244,8 +241,7 @@ export default function UserProfilePage() {
setTfBusy(true);
setTfMsg(null);
try {
- await apiFetch("/api/users/2fa/disable", {
- method: "POST",
+ await apiV2Data("post", "/me/two-factor/disable", {
authToken: token,
body: { password: tfPassword },
});
@@ -273,11 +269,10 @@ export default function UserProfilePage() {
setClosing(true);
setCloseMsg(null);
try {
- const res = await apiFetch<{ message?: string }>("/api/users/close-account", {
- method: "POST",
+ const res = await (apiV2Data("post", "/me/close", {
authToken: token,
body: { password: closePassword, deleteData },
- });
+ }) as Promise<{ message?: string }>);
setCloseMsg({ type: "ok", text: res?.message || "Account closed." });
// Slight delay so the user can read the message, then log out
setTimeout(() => logout(), 2500);
diff --git a/frontend/src/app/events/page.tsx b/frontend/src/app/events/page.tsx
index 256d3f1..3237c08 100644
--- a/frontend/src/app/events/page.tsx
+++ b/frontend/src/app/events/page.tsx
@@ -32,7 +32,7 @@ type Event = {
};
export default async function EventsPage() {
- const events = await apiFetchListForPrerender("/api/events", { nextOptions: { next: { revalidate: 60 } } });
+ const events = await apiFetchListForPrerender("/api/v2/event-listing", { nextOptions: { next: { revalidate: 60 } } });
const now = Date.now();
const upcoming = (events || []).filter(e => {
if (e.cashupStatus === "closed") return false;
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index 46d9679..08ee64f 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -49,7 +49,7 @@ async function getOrgName(): Promise {
export default async function HomePage() {
const [events, displayName] = await Promise.all([
- apiFetchListForPrerender("/api/events", { nextOptions: { next: { revalidate: 60 } } }),
+ apiFetchListForPrerender("/api/v2/event-listing", { nextOptions: { next: { revalidate: 60 } } }),
getOrgName(),
]);
const now = Date.now();
diff --git a/frontend/src/app/registration/success/page.tsx b/frontend/src/app/registration/success/page.tsx
index d00cf9e..a6754e6 100644
--- a/frontend/src/app/registration/success/page.tsx
+++ b/frontend/src/app/registration/success/page.tsx
@@ -75,7 +75,7 @@ function RegistrationSuccessContent() {
(async () => {
try {
if (token && reg?.id) {
- await (await import('@/lib/api')).apiFetch('/api/tickets/generate', { method: 'POST', authToken: token, body: { registrationId: reg.id } });
+ await apiV2Data("post", "/tickets/generate", { authToken: token, body: { registrationId: reg.id } });
}
} catch {}
setTimeout(() => router.replace('/dashboard'), 600);
diff --git a/frontend/src/app/reset-password/page.tsx b/frontend/src/app/reset-password/page.tsx
index 62ad4e6..35b74b3 100644
--- a/frontend/src/app/reset-password/page.tsx
+++ b/frontend/src/app/reset-password/page.tsx
@@ -2,10 +2,10 @@
import React, { Suspense, useMemo, useState } from "react";
import { Navbar } from "@/components/layout/Navbar";
import { Footer } from "@/components/layout/Footer";
-import { apiFetch } from "@/lib/api";
import { useSearchParams, useRouter } from "next/navigation";
import { KeyRound } from "lucide-react";
import { errorMessage } from "@/lib/utils";
+import { apiV2Data } from "@/lib/apiV2";
function ResetPasswordContent() {
const search = useSearchParams();
@@ -24,7 +24,7 @@ function ResetPasswordContent() {
try {
setLoading(true);
setStatus(null);
- await apiFetch("/api/users/reset", { method: "POST", body: { token, password } });
+ await apiV2Data("post", "/auth/password/reset", { body: { token, password } });
setStatus("Your password has been reset. You can now log in.");
setTimeout(() => router.push("/login"), 1500);
} catch (err) {
diff --git a/frontend/src/app/self-service/page.tsx b/frontend/src/app/self-service/page.tsx
index c8054df..3061d4c 100644
--- a/frontend/src/app/self-service/page.tsx
+++ b/frontend/src/app/self-service/page.tsx
@@ -2,7 +2,7 @@
import { ExtraFieldInput, isExtraFieldType } from "@/components/shared/FormFieldExtras";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { apiFetch, fetchManagedEvents, fetchEvent } from "@/lib/api";
+import { fetchManagedEvents, fetchEvent } from "@/lib/api";
import { AuthResponse, TwoFactorChallenge, isTwoFactorChallenge } from "@/lib/auth";
import { CouponCodeField, AppliedCoupon } from "@/components/shared/CouponCodeField";
import { BundleBuilder, BundleOffers, BundleSavings, useBundleQuote } from "@/components/shared/BundleDeals";
@@ -13,7 +13,8 @@ import { choosesSessions, isPickOption } from "@/lib/sessions";
import { SessionPicker } from "@/components/events/SessionPicker";
import { ModalOverlay } from "@/components/shared/ModalOverlay";
import { EventPicker } from "@/components/shared/EventPicker";
-import { apiV2Data } from "@/lib/apiV2";
+import { apiV2Data, apiV2 } from "@/lib/apiV2";
+import { toAuthResponse } from "@/contexts/AuthContext";
// ─── Types ────────────────────────────────────────────────────────────────────
type EarlyBirdTier = { deadline: string; price: number; order?: number; variantId?: string | null };
@@ -337,15 +338,16 @@ export default function SelfServicePage() {
try {
// Second pass for accounts with two-factor on: exchange the code that was
// sent after the password step. The kiosk never asks to be remembered.
- const data = setupChallenge
- ? await apiFetch("/api/users/login/2fa", {
- method: "POST",
- body: { challengeId: setupChallenge.challengeId, code: setupCode, rememberDevice: false },
- })
- : await apiFetch("/api/users/login", {
- method: "POST",
- body: { email: setupEmail, password: setupPassword },
- });
+ let data: AuthResponse | TwoFactorChallenge;
+ if (setupChallenge) {
+ const { data: session } = await apiV2("post", "/auth/login/code", { body: { challengeId: setupChallenge.challengeId, code: setupCode, rememberDevice: false } });
+ data = toAuthResponse(session);
+ } else {
+ const { data: res } = await apiV2("post", "/auth/login", { body: { identifier: setupEmail, password: setupPassword } });
+ data = res.status === "code_required"
+ ? { twoFactorRequired: true, challengeId: res.challengeId, method: res.method, destination: res.destination || "" }
+ : toAuthResponse(res);
+ }
if (isTwoFactorChallenge(data)) {
setSetupChallenge(data);
setSetupCode("");
@@ -394,8 +396,7 @@ export default function SelfServicePage() {
// holds, rather than logging in again — a fresh login would send a new
// two-factor code every time the event is changed.
if (!supervisorToken) throw new Error("Your session has expired. Reload the kiosk and sign in again.");
- await apiFetch("/api/users/verify-password", {
- method: "POST",
+ await apiV2Data("post", "/me/verify-password", {
authToken: supervisorToken,
body: { password: changePassword },
});
@@ -635,10 +636,10 @@ export default function SelfServicePage() {
const params = new URLSearchParams();
if (kind === "email") params.set("email", value);
else params.set("phone", value);
- const data = await apiFetch<{
+ const data = await (apiV2Data("get", "/users/exists", { query: Object.fromEntries(params), authToken: supervisorToken }) as Promise<{
exists: boolean;
user?: MatchedAccount | null;
- }>(`/api/users/check-exists?${params.toString()}`, { authToken: supervisorToken });
+ }>);
if (data?.exists && data.user) {
setMatch(data.user);
setLookupMessage("Account found — details filled in below.");
diff --git a/frontend/src/app/series/[slug]/page.tsx b/frontend/src/app/series/[slug]/page.tsx
index 822f856..bece240 100644
--- a/frontend/src/app/series/[slug]/page.tsx
+++ b/frontend/src/app/series/[slug]/page.tsx
@@ -47,7 +47,7 @@ export default async function SeriesPage({ params }: { params: Promise<{ slug: s
const series = await loadSeries(slug);
if (!series) notFound();
- const events = await apiFetchListForPrerender(`/api/events?series=${encodeURIComponent(series.slug)}`, { nextOptions: { next: { revalidate } } });
+ const events = await apiFetchListForPrerender(`/api/v2/event-listing?series=${encodeURIComponent(series.slug)}`, { nextOptions: { next: { revalidate } } });
const now = Date.now();
const upcoming = (events || [])
.filter(e => e.cashupStatus !== "closed" && new Date(e.endDate || e.startDate).getTime() > now)
diff --git a/frontend/src/app/set-banner/page.tsx b/frontend/src/app/set-banner/page.tsx
index a959867..adefba6 100644
--- a/frontend/src/app/set-banner/page.tsx
+++ b/frontend/src/app/set-banner/page.tsx
@@ -3,9 +3,9 @@
import React, { useEffect, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
-import { apiFetch } from "@/lib/api";
import { Megaphone } from "lucide-react";
import { errorMessage } from "@/lib/utils";
+import { apiV2Data } from "@/lib/apiV2";
type BannerType = "info" | "warning" | "success" | "danger";
@@ -38,7 +38,7 @@ export default function SetBannerPage() {
// Load current banner
useEffect(() => {
- apiFetch<{ message?: string; type?: BannerType; liveFrom?: string | null; liveTill?: string | null } | null>("/api/banner")
+ (apiV2Data("get", "/banner") as Promise<{ message?: string; type?: BannerType; liveFrom?: string | null; liveTill?: string | null } | null>)
.then(b => {
if (!b) return;
setMessage(b.message || "");
@@ -65,8 +65,7 @@ export default function SetBannerPage() {
setStatus(null);
setBusy(true);
try {
- await apiFetch("/api/banner", {
- method: "POST",
+ await apiV2Data("put", "/banner", {
authToken: token,
body: {
message,
@@ -89,8 +88,7 @@ export default function SetBannerPage() {
setStatus(null);
setBusy(true);
try {
- await apiFetch("/api/banner", {
- method: "POST",
+ await apiV2Data("put", "/banner", {
authToken: token,
body: { message: "", type: "info", liveFrom: null, liveTill: null },
});
diff --git a/frontend/src/app/sitemap.ts b/frontend/src/app/sitemap.ts
index 0c069b7..e173c4d 100644
--- a/frontend/src/app/sitemap.ts
+++ b/frontend/src/app/sitemap.ts
@@ -1,5 +1,4 @@
import type { MetadataRoute } from "next";
-import { apiFetch } from "@/lib/api";
import { appUrl } from "@/lib/siteConfig";
import { apiV2Data } from "@/lib/apiV2";
@@ -18,7 +17,7 @@ export default async function sitemap(): Promise {
let events: Event[] = [];
try {
- events = await apiFetch("/api/events", { nextOptions: { next: { revalidate } } });
+ events = await (apiV2Data("get", "/events", { nextOptions: { next: { revalidate } } }) as Promise);
} catch {
// Backend unreachable at build/revalidate time — ship the static entries only,
// same degrade-gracefully posture as layout.tsx's getServerSettings.
diff --git a/frontend/src/components/admin/CampusesTab.tsx b/frontend/src/components/admin/CampusesTab.tsx
index b508d38..040a149 100644
--- a/frontend/src/components/admin/CampusesTab.tsx
+++ b/frontend/src/components/admin/CampusesTab.tsx
@@ -3,7 +3,6 @@
import React, { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { MapPin, Plus, ArrowUp, ArrowDown, Archive, ArchiveRestore, Trash2, Pencil } from "lucide-react";
-import { apiFetch } from "@/lib/api";
import { useAuth } from "@/hooks/useAuth";
import { useDismissingState } from "@/hooks/useDismissingState";
import { reloadCampuses, type Campus } from "@/hooks/useCampuses";
@@ -41,7 +40,7 @@ function RecipientsField({ value, onChange }: { value: Person[]; onChange: (v: P
let cancelled = false;
const t = setTimeout(async () => {
try {
- const res = await apiFetch<{ data?: (Person & { role?: string })[] }>(`/api/users?search=${encodeURIComponent(search.trim())}&limit=10`, { authToken: token });
+ const res = await (apiV2Data("get", "/users", { query: { search: search.trim(), limit: "10" }, authToken: token }) as Promise<{ data?: (Person & { role?: string })[] }>);
if (!cancelled) setResults((res?.data || []).filter(u => u.role !== "user"));
} catch { if (!cancelled) setResults([]); }
}, 300);
diff --git a/frontend/src/components/dashboard/TodayPanel.tsx b/frontend/src/components/dashboard/TodayPanel.tsx
index 919b78d..d12857d 100644
--- a/frontend/src/components/dashboard/TodayPanel.tsx
+++ b/frontend/src/components/dashboard/TodayPanel.tsx
@@ -4,10 +4,10 @@ import React, { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { CalendarClock, ArrowRight, Baby, HandHeart, UserCheck, Banknote, Inbox, CheckCircle2 } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
-import { apiFetch } from "@/lib/api";
import { useVisiblePolling } from "@/hooks/useVisiblePolling";
import { CampusBadge } from "@/components/shared/CampusFilter";
import type { Event } from "@/types";
+import { apiV2Data } from "@/lib/apiV2";
type TodayEvent = {
id: string; title: string; location: string | null; campus: Event["campus"]; isHidden: boolean; kidsCheckIn: boolean;
@@ -35,7 +35,7 @@ export function TodayPanel({ supervisor }: { supervisor: boolean }) {
const [data, setData] = useState(null);
const load = useCallback(async () => {
if (!token) return;
- try { setData(await apiFetch("/api/dashboard/today", { authToken: token })); } catch { /* keep what's shown */ }
+ try { setData(await (apiV2Data("get", "/dashboard/today", { authToken: token }) as Promise)); } catch { /* keep what's shown */ }
}, [token]);
useEffect(() => { load(); }, [load]);
useVisiblePolling(load, 30000, !!token);
diff --git a/frontend/src/components/reports/ReportsV2.tsx b/frontend/src/components/reports/ReportsV2.tsx
index 4e13803..e7447cc 100644
--- a/frontend/src/components/reports/ReportsV2.tsx
+++ b/frontend/src/components/reports/ReportsV2.tsx
@@ -3,7 +3,7 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
-import { apiFetch, API_BASE, fetchManagedEvents } from "@/lib/api";
+import { API_BASE, fetchManagedEvents } from "@/lib/api";
import { downloadReportExcel, emailReportPdf, whatsappReportPdf, viewReportPdf, type ReportPdfPayload } from "@/lib/export";
import { Printer, Mail, FileSpreadsheet, MessageCircle, ShoppingCart, CreditCard, Gift, Clock, HandHeart, Search, TrendingUp, TicketPercent, ShieldCheck, Star, MessageSquareHeart, Send, Eye, MousePointerClick, BadgeCheck, Users, Ticket as TicketIcon } from "lucide-react";
import { REPORTS, type ReportKey } from "./ReportCatalog";
@@ -374,7 +374,7 @@ export default function ReportsV2({ backHref }: { backHref?: string } = {}) {
if (dateFrom) query.set("from", dateFrom);
if (dateTo) query.set("to", dateTo);
try {
- return await apiFetch(`/api/analytics/funnel?${query.toString()}`, { authToken: token });
+ return await (apiV2Data("get", "/analytics/funnel", { query: Object.fromEntries(query), authToken: token }) as Promise);
} catch {
return null;
}
diff --git a/frontend/src/components/shared/BannerBar.tsx b/frontend/src/components/shared/BannerBar.tsx
index a73199f..12b63ec 100644
--- a/frontend/src/components/shared/BannerBar.tsx
+++ b/frontend/src/components/shared/BannerBar.tsx
@@ -1,7 +1,7 @@
"use client";
import React, { useEffect, useState } from "react";
-import { apiFetch } from "@/lib/api";
+import { apiV2Data } from "@/lib/apiV2";
type BannerType = "info" | "warning" | "success" | "danger";
@@ -28,7 +28,7 @@ export function BannerBar() {
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
- apiFetch("/api/banner")
+ (apiV2Data("get", "/banner") as Promise)
.then(b => {
if (!b?.message) return;
setBanner(b);
diff --git a/frontend/src/components/shared/CommandPalette.tsx b/frontend/src/components/shared/CommandPalette.tsx
index 9f9722a..5f28214 100644
--- a/frontend/src/components/shared/CommandPalette.tsx
+++ b/frontend/src/components/shared/CommandPalette.tsx
@@ -4,8 +4,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useRouter } from "next/navigation";
import { Search, Calendar, User, Banknote, ClipboardList, CornerDownLeft, Zap, FileText, Settings, BarChart2 } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
-import { apiFetch } from "@/lib/api";
import { matchPages, type CommandPage } from "@/lib/commandPages";
+import { apiV2Data } from "@/lib/apiV2";
type Result = { type: "event" | "payment" | "person" | "registration"; id: string; title: string; subtitle: string; href: string };
type Item = { key: string; group: string; title: string; subtitle?: string; href: string; icon: React.ElementType };
@@ -59,7 +59,7 @@ export function CommandPalette() {
const q = query.trim();
if (q.length < 2) { setResults([]); return; }
const timer = setTimeout(() => {
- apiFetch<{ results: Result[] }>(`/api/dashboard/search?q=${encodeURIComponent(q)}`, { authToken: token })
+ (apiV2Data("get", "/dashboard/search", { query: { q: q }, authToken: token }) as Promise<{ results: Result[] }>)
.then(r => setResults(r.results || []))
.catch(() => setResults([]));
}, 200);
diff --git a/frontend/src/components/shared/SavedFilters.tsx b/frontend/src/components/shared/SavedFilters.tsx
index 1d3d49f..7d430ab 100644
--- a/frontend/src/components/shared/SavedFilters.tsx
+++ b/frontend/src/components/shared/SavedFilters.tsx
@@ -3,8 +3,8 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Bookmark, Star, Trash2, Check } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
-import { apiFetch } from "@/lib/api";
import { errorMessage } from "@/lib/utils";
+import { apiV2Data } from "@/lib/apiV2";
export type FilterValues = Record;
type Saved = { id: string; name: string; params: FilterValues; isDefault: boolean };
@@ -35,7 +35,7 @@ export function SavedFilters({ page, value, onApply, skipDefault = false, classN
const load = useCallback(async () => {
if (!token) return;
try {
- const rows = await apiFetch(`/api/saved-filters?page=${page}`, { authToken: token });
+ const rows = await (apiV2Data("get", "/saved-filters", { query: { page: page }, authToken: token }) as Promise);
setList(rows);
// The default, once, when the page opens.
if (!defaultDone.current) {
@@ -64,14 +64,14 @@ export function SavedFilters({ page, value, onApply, skipDefault = false, classN
const save = () => act(async () => {
setSaving(true);
try {
- const row = await apiFetch("/api/saved-filters", { method: "POST", authToken: token || undefined, body: { page, name, params: value, isDefault: asDefault } });
+ const row = await (apiV2Data("post", "/saved-filters", { authToken: token, body: { page, name, params: value, isDefault: asDefault } }) as Promise);
setApplied(row.id); setName(""); setAsDefault(false);
} finally { setSaving(false); }
});
- const toggleDefault = (f: Saved) => act(() => apiFetch(`/api/saved-filters/${f.id}`, { method: "PUT", authToken: token || undefined, body: { isDefault: !f.isDefault } }));
+ const toggleDefault = (f: Saved) => act(() => apiV2Data("put", "/saved-filters/{id}", { params: { id: f.id }, authToken: token, body: { isDefault: !f.isDefault } }));
const remove = (f: Saved) => act(async () => {
if (!confirm(`Delete the saved filter "${f.name}"?`)) return;
- await apiFetch(`/api/saved-filters/${f.id}`, { method: "DELETE", authToken: token || undefined });
+ await apiV2Data("delete", "/saved-filters/{id}", { params: { id: f.id }, authToken: token });
});
const current = list.find(f => f.id === applied);
diff --git a/frontend/src/components/volunteers/RosterSheet.tsx b/frontend/src/components/volunteers/RosterSheet.tsx
index 9c619aa..d9c1c44 100644
--- a/frontend/src/components/volunteers/RosterSheet.tsx
+++ b/frontend/src/components/volunteers/RosterSheet.tsx
@@ -1,7 +1,6 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
-import { apiFetch } from "@/lib/api";
import { errorMessage } from "@/lib/utils";
import { useVisiblePolling } from "@/hooks/useVisiblePolling";
import { CheckCircle2, Pencil, Plus, Printer, Trash2, UserPlus, X } from "lucide-react";
@@ -82,7 +81,7 @@ export function RosterSheet({ token, eventId, canEdit, kidsRooms = [], onMessage
const search = async () => {
if (!token || query.trim().length < 2) return;
try {
- const res = await apiFetch<{ data: { id: string; name: string; email: string }[] }>(`/api/users?search=${encodeURIComponent(query.trim())}&limit=8&isActive=true`, { authToken: token });
+ const res = await (apiV2Data("get", "/users", { query: { search: query.trim(), limit: "8", isActive: "true" }, authToken: token }) as Promise<{ data: { id: string; name: string; email: string }[] }>);
setFound(res.data);
} catch (e) {
onMessage({ type: "err", text: errorMessage(e) || "Search failed" });
diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx
index 2092258..194e248 100644
--- a/frontend/src/contexts/AuthContext.tsx
+++ b/frontend/src/contexts/AuthContext.tsx
@@ -1,13 +1,14 @@
"use client";
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
-import { apiFetch, ApiError } from "@/lib/api";
+import { ApiError } from "@/lib/api";
import { AuthResponse, AuthUser, TwoFactorChallenge, clearToken, getDeviceToken, getToken, saveDeviceToken, saveToken } from "@/lib/auth";
-import { apiV2, type V2Result } from "@/lib/apiV2";
+import { apiV2, type V2Result, apiV2Data } from "@/lib/apiV2";
// A v2 "signed_in" answer as the session the app keeps (the user's fields plus the token).
type V2Session = Extract["data"], { status: "signed_in" }>;
-const toAuthResponse = (s: V2Session): AuthResponse => ({
+/** A v2 sign-in answer as the app's AuthResponse (also used by the self-service kiosk). */
+export const toAuthResponse = (s: V2Session): AuthResponse => ({
...s.user, token: s.token, newsPromptPending: s.newsPromptPending, noticePromptPending: s.noticePromptPending,
...(s.deviceToken ? { deviceToken: s.deviceToken } : {}),
});
@@ -58,7 +59,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// Validate the stored token by fetching the user profile.
// Only update React state after a successful response so we never
// hold a half-initialised (token set, user null) state.
- const me = await apiFetch("/api/users/profile", { authToken: t });
+ const me = await (apiV2Data("get", "/me", { authToken: t }) as Promise);
setToken(t);
setUser(me);
cacheUser(me);
@@ -117,10 +118,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}, [startSession]);
const resendTwoFactor = useCallback((challengeId: string) => {
- return apiFetch("/api/users/login/2fa/resend", {
- method: "POST",
+ return (apiV2Data("post", "/auth/login/code/resend", {
body: { challengeId },
- });
+ }) as Promise);
}, []);
const register = useCallback(async (data: { name: string; email: string; password: string; phoneNumber?: string; notificationPreference?: "email" | "whatsapp" | "both"; newsOptIn?: boolean }) => {
diff --git a/frontend/src/lib/analytics.ts b/frontend/src/lib/analytics.ts
index 112dc4e..0a6a6aa 100644
--- a/frontend/src/lib/analytics.ts
+++ b/frontend/src/lib/analytics.ts
@@ -106,6 +106,6 @@ export function trackFunnel(eventId: string, stage: "view" | "start") {
if (!sessionId || !eventId) return;
const body = JSON.stringify({ sessionId, eventId, stage, source: sourceFromUrl() });
try {
- fetch(`${API_BASE}/api/analytics/track`, { method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true }).catch(() => {});
+ fetch(`${API_BASE}/api/v2/analytics/track`, { method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true }).catch(() => {});
} catch { /* ignore */ }
}
diff --git a/frontend/src/lib/api-v2/openapi.json b/frontend/src/lib/api-v2/openapi.json
index b47aa65..e962018 100644
--- a/frontend/src/lib/api-v2/openapi.json
+++ b/frontend/src/lib/api-v2/openapi.json
@@ -76,6 +76,21 @@
},
{
"name": "Accounts"
+ },
+ {
+ "name": "Messaging"
+ },
+ {
+ "name": "WhatsApp"
+ },
+ {
+ "name": "Site"
+ },
+ {
+ "name": "Dashboard"
+ },
+ {
+ "name": "Reports"
}
],
"paths": {
@@ -29119,6 +29134,4588 @@
}
}
},
+ "/auth/login/code/resend": {
+ "post": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Send the sign-in code again",
+ "description": "**Who:** Anyone.",
+ "operationId": "postAuthLoginCodeResend",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/password/forgot": {
+ "post": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Email me a link to reset my password",
+ "description": "NOT_FOUND when no account has that email (as v1 does).\n\n**Who:** Anyone.",
+ "operationId": "postAuthPasswordForgot",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "maxLength": 200
+ }
+ },
+ "required": [
+ "email"
+ ],
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/password/reset": {
+ "post": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Set a new password with a reset link",
+ "description": "**Who:** Anyone.",
+ "operationId": "postAuthPasswordReset",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/activate": {
+ "post": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Activate an account with its link",
+ "description": "**Who:** Anyone.",
+ "operationId": "postAuthActivate",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/me/sessions/revoke": {
+ "post": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Sign out everywhere else",
+ "description": "**Who:** Any signed-in person.",
+ "operationId": "postMeSessionsRevoke",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/me/close": {
+ "post": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Close my account",
+ "description": "Needs the password. Personal details are removed; payment records stay, anonymised.\n\n**Who:** Any signed-in person.",
+ "operationId": "postMeClose",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/me/activity": {
+ "get": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Recent sign-ins and changes to my account",
+ "description": "**Who:** Any signed-in person.",
+ "operationId": "getMeActivity",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/me/verify-password": {
+ "post": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Check my password (before a sensitive change)",
+ "description": "**Who:** Any signed-in person.",
+ "operationId": "postMeVerify-password",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/me/two-factor/enable": {
+ "post": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Start turning on two-step sign-in",
+ "description": "Sends a code to confirm with.\n\n**Who:** Any signed-in person.",
+ "operationId": "postMeTwo-factorEnable",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/me/two-factor/confirm": {
+ "post": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Confirm two-step sign-in with the code",
+ "description": "**Who:** Any signed-in person.",
+ "operationId": "postMeTwo-factorConfirm",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/me/two-factor/disable": {
+ "post": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Turn off two-step sign-in",
+ "description": "**Who:** Any signed-in person.",
+ "operationId": "postMeTwo-factorDisable",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users/exists": {
+ "get": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Is there an account with this email or phone?",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "getUsersExists",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "email",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "phone",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "phoneNumber",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users": {
+ "get": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Every account",
+ "description": "Paged (`page`, `limit`) and searchable (`q`, `role`, `campus`, …) — see v1 for every filter.\n\n**Who:** Supervisors and admins.",
+ "operationId": "getUsers",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "search",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "role",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "isActive",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "campus",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users/{id}": {
+ "get": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "One account",
+ "description": "**Who:** Admins only.",
+ "operationId": "getUsersId",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "patch": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Change an account",
+ "description": "Name, contact details, role, campus limits, active.\n\n**Who:** Admins only.",
+ "operationId": "patchUsersId",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Delete an account",
+ "description": "**Who:** Admins only.",
+ "operationId": "deleteUsersId",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users/{id}/sessions/revoke": {
+ "post": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Sign someone out everywhere",
+ "description": "**Who:** Admins only.",
+ "operationId": "postUsersIdSessionsRevoke",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users/{id}/anonymize": {
+ "post": {
+ "tags": [
+ "Accounts"
+ ],
+ "summary": "Anonymise an account",
+ "description": "Admins. Personal details are removed for good; payment records stay.\n\n**Who:** Admins only.",
+ "operationId": "postUsersIdAnonymize",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/event-listing": {
+ "get": {
+ "tags": [
+ "Events"
+ ],
+ "summary": "Upcoming public events in full",
+ "description": "What the site's event lists show (ticket options, prices, early-bird tiers), not paged. `campus` (id or slug) and `series` (slug) narrow it. For a paged, slimmer list use GET /events.\n\n**Who:** Anyone.",
+ "operationId": "getEvent-listing",
+ "parameters": [
+ {
+ "name": "campus",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "series",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/admin/events": {
+ "get": {
+ "tags": [
+ "Events (staff)"
+ ],
+ "summary": "Every event, including deactivated ones",
+ "description": "**Who:** Admins only.",
+ "operationId": "getAdminEvents",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/events/{id}/email-attendees": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Email an event's people",
+ "description": "Filtered by status or chosen people; news consent is checked.\n\n**Who:** Supervisors and admins.",
+ "operationId": "postEventsIdEmail-attendees",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/events/{id}/email-attendees/schedule": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Schedule an email to an event's people",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "postEventsIdEmail-attendeesSchedule",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/events/{id}/whatsapp-attendees": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "WhatsApp an event's people",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "postEventsIdWhatsapp-attendees",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/events/{id}/whatsapp-attendees/schedule": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Schedule a WhatsApp to an event's people",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "postEventsIdWhatsapp-attendeesSchedule",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/broadcasts/email/preview": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Who an email broadcast would reach",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "postBroadcastsEmailPreview",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/broadcasts/email/send": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Send an email broadcast",
+ "description": "To campuses, events, roles or listed people; news consent is checked.\n\n**Who:** Supervisors and admins.",
+ "operationId": "postBroadcastsEmailSend",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/broadcasts/email/schedule": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Schedule an email broadcast",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "postBroadcastsEmailSchedule",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/broadcasts/whatsapp/preview": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Who a WhatsApp broadcast would reach",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "postBroadcastsWhatsappPreview",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/broadcasts/whatsapp/send": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Send a WhatsApp broadcast",
+ "description": "To campuses, events, roles or listed people; news consent is checked.\n\n**Who:** Supervisors and admins.",
+ "operationId": "postBroadcastsWhatsappSend",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/broadcasts/whatsapp/schedule": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Schedule a WhatsApp broadcast",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "postBroadcastsWhatsappSchedule",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/scheduled-messages": {
+ "get": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Scheduled emails and WhatsApps",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "getScheduled-messages",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "status",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "eventId",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "channel",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/scheduled-messages/{id}": {
+ "patch": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Change a scheduled message",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "patchScheduled-messagesId",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Cancel a scheduled message",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "deleteScheduled-messagesId",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/automations/schedule": {
+ "post": {
+ "tags": [
+ "Messaging"
+ ],
+ "summary": "Schedule an event's reminder messages",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "postAutomationsSchedule",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "eventId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ },
+ "required": [
+ "eventId"
+ ],
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/whatsapp/config": {
+ "get": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "The WhatsApp connection settings",
+ "description": "**Who:** Admins only.",
+ "operationId": "getWhatsappConfig",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "put": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "Change the WhatsApp connection settings",
+ "description": "**Who:** Admins only.",
+ "operationId": "putWhatsappConfig",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/whatsapp/instance": {
+ "post": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "Create the WhatsApp instance",
+ "description": "**Who:** Admins only.",
+ "operationId": "postWhatsappInstance",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "Delete the WhatsApp instance",
+ "description": "**Who:** Admins only.",
+ "operationId": "deleteWhatsappInstance",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/whatsapp/status": {
+ "get": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "Is WhatsApp connected?",
+ "description": "**Who:** Admins only.",
+ "operationId": "getWhatsappStatus",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/whatsapp/qr": {
+ "get": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "The QR code to link a phone",
+ "description": "**Who:** Admins only.",
+ "operationId": "getWhatsappQr",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/whatsapp/request-code": {
+ "post": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "Link a phone with a code instead",
+ "description": "**Who:** Admins only.",
+ "operationId": "postWhatsappRequest-code",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/whatsapp/logout": {
+ "post": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "Unlink the phone",
+ "description": "**Who:** Admins only.",
+ "operationId": "postWhatsappLogout",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/whatsapp/start": {
+ "post": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "Start the WhatsApp session",
+ "description": "**Who:** Admins only.",
+ "operationId": "postWhatsappStart",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/whatsapp/restart": {
+ "post": {
+ "tags": [
+ "WhatsApp"
+ ],
+ "summary": "Restart the WhatsApp session",
+ "description": "**Who:** Admins only.",
+ "operationId": "postWhatsappRestart",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/banner": {
+ "get": {
+ "tags": [
+ "Site"
+ ],
+ "summary": "The site-wide banner",
+ "description": "**Who:** Anyone.",
+ "operationId": "getBanner",
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ }
+ }
+ },
+ "put": {
+ "tags": [
+ "Site"
+ ],
+ "summary": "Set the site-wide banner",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "putBanner",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/saved-filters": {
+ "get": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "My saved filters",
+ "description": "For one list page (`page`).\n\n**Who:** Staff, supervisors and admins.",
+ "operationId": "getSaved-filters",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Save a filter",
+ "description": "By name (replaces one with the same name); up to 30 per page.\n\n**Who:** Staff, supervisors and admins.",
+ "operationId": "postSaved-filters",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/saved-filters/{id}": {
+ "put": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Change a saved filter",
+ "description": "**Who:** Staff, supervisors and admins.",
+ "operationId": "putSaved-filtersId",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "Me"
+ ],
+ "summary": "Delete a saved filter",
+ "description": "**Who:** Staff, supervisors and admins.",
+ "operationId": "deleteSaved-filtersId",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The id",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (NOT_FOUND)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/dashboard/today": {
+ "get": {
+ "tags": [
+ "Dashboard"
+ ],
+ "summary": "Today at a glance",
+ "description": "**Who:** Staff, supervisors and admins.",
+ "operationId": "getDashboardToday",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/dashboard/search": {
+ "get": {
+ "tags": [
+ "Dashboard"
+ ],
+ "summary": "Search people, registrations and events",
+ "description": "**Who:** Staff, supervisors and admins.",
+ "operationId": "getDashboardSearch",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stats/staff": {
+ "get": {
+ "tags": [
+ "Dashboard"
+ ],
+ "summary": "Staff dashboard numbers",
+ "description": "**Who:** Staff, supervisors and admins.",
+ "operationId": "getStatsStaff",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stats/supervisor": {
+ "get": {
+ "tags": [
+ "Dashboard"
+ ],
+ "summary": "Supervisor dashboard numbers",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "getStatsSupervisor",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stats/admin": {
+ "get": {
+ "tags": [
+ "Dashboard"
+ ],
+ "summary": "Admin dashboard numbers",
+ "description": "**Who:** Admins only.",
+ "operationId": "getStatsAdmin",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stats/overview": {
+ "get": {
+ "tags": [
+ "Dashboard"
+ ],
+ "summary": "Overview numbers",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "getStatsOverview",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "from",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "to",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "campus",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/analytics/track": {
+ "post": {
+ "tags": [
+ "Dashboard"
+ ],
+ "summary": "Record a step on the way to registering",
+ "description": "**Who:** Anyone.",
+ "operationId": "postAnalyticsTrack",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/analytics/funnel": {
+ "get": {
+ "tags": [
+ "Dashboard"
+ ],
+ "summary": "How many get from viewing to paying",
+ "description": "**Who:** Supervisors and admins.",
+ "operationId": "getAnalyticsFunnel",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "eventId",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "from",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ },
+ {
+ "name": "to",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "maxLength": 4000
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Not allowed for this role (FORBIDDEN)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/reports/pdf": {
+ "post": {
+ "tags": [
+ "Reports"
+ ],
+ "summary": "A report as a PDF",
+ "description": "The report's rows and columns in the body.\n\n**Who:** Any signed-in person.",
+ "operationId": "postReportsPdf",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "The file",
+ "content": {
+ "application/pdf": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/reports/excel": {
+ "post": {
+ "tags": [
+ "Reports"
+ ],
+ "summary": "A report as an Excel file",
+ "description": "**Who:** Any signed-in person.",
+ "operationId": "postReportsExcel",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "The file",
+ "content": {
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/reports/email": {
+ "post": {
+ "tags": [
+ "Reports"
+ ],
+ "summary": "Email a report as a PDF",
+ "description": "**Who:** Any signed-in person.",
+ "operationId": "postReportsEmail",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/reports/whatsapp": {
+ "post": {
+ "tags": [
+ "Reports"
+ ],
+ "summary": "WhatsApp a report as a PDF",
+ "description": "**Who:** Any signed-in person.",
+ "operationId": "postReportsWhatsapp",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {}
+ },
+ "required": [
+ "data"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation failed (VALIDATION_FAILED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Not signed in (UNAUTHENTICATED)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/checkin/{qrCode}": {
"get": {
"tags": [
diff --git a/frontend/src/lib/api-v2/schema.d.ts b/frontend/src/lib/api-v2/schema.d.ts
index 82d310b..2f41f87 100644
--- a/frontend/src/lib/api-v2/schema.d.ts
+++ b/frontend/src/lib/api-v2/schema.d.ts
@@ -4476,6 +4476,1144 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/auth/login/code/resend": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Send the sign-in code again
+ * @description **Who:** Anyone.
+ */
+ post: operations["postAuthLoginCodeResend"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/auth/password/forgot": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Email me a link to reset my password
+ * @description NOT_FOUND when no account has that email (as v1 does).
+ *
+ * **Who:** Anyone.
+ */
+ post: operations["postAuthPasswordForgot"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/auth/password/reset": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Set a new password with a reset link
+ * @description **Who:** Anyone.
+ */
+ post: operations["postAuthPasswordReset"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/auth/activate": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Activate an account with its link
+ * @description **Who:** Anyone.
+ */
+ post: operations["postAuthActivate"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/me/sessions/revoke": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Sign out everywhere else
+ * @description **Who:** Any signed-in person.
+ */
+ post: operations["postMeSessionsRevoke"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/me/close": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Close my account
+ * @description Needs the password. Personal details are removed; payment records stay, anonymised.
+ *
+ * **Who:** Any signed-in person.
+ */
+ post: operations["postMeClose"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/me/activity": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Recent sign-ins and changes to my account
+ * @description **Who:** Any signed-in person.
+ */
+ get: operations["getMeActivity"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/me/verify-password": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Check my password (before a sensitive change)
+ * @description **Who:** Any signed-in person.
+ */
+ post: operations["postMeVerify-password"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/me/two-factor/enable": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Start turning on two-step sign-in
+ * @description Sends a code to confirm with.
+ *
+ * **Who:** Any signed-in person.
+ */
+ post: operations["postMeTwo-factorEnable"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/me/two-factor/confirm": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Confirm two-step sign-in with the code
+ * @description **Who:** Any signed-in person.
+ */
+ post: operations["postMeTwo-factorConfirm"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/me/two-factor/disable": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Turn off two-step sign-in
+ * @description **Who:** Any signed-in person.
+ */
+ post: operations["postMeTwo-factorDisable"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/users/exists": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Is there an account with this email or phone?
+ * @description **Who:** Supervisors and admins.
+ */
+ get: operations["getUsersExists"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/users": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Every account
+ * @description Paged (`page`, `limit`) and searchable (`q`, `role`, `campus`, …) — see v1 for every filter.
+ *
+ * **Who:** Supervisors and admins.
+ */
+ get: operations["getUsers"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/users/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * One account
+ * @description **Who:** Admins only.
+ */
+ get: operations["getUsersId"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete an account
+ * @description **Who:** Admins only.
+ */
+ delete: operations["deleteUsersId"];
+ options?: never;
+ head?: never;
+ /**
+ * Change an account
+ * @description Name, contact details, role, campus limits, active.
+ *
+ * **Who:** Admins only.
+ */
+ patch: operations["patchUsersId"];
+ trace?: never;
+ };
+ "/users/{id}/sessions/revoke": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Sign someone out everywhere
+ * @description **Who:** Admins only.
+ */
+ post: operations["postUsersIdSessionsRevoke"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/users/{id}/anonymize": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Anonymise an account
+ * @description Admins. Personal details are removed for good; payment records stay.
+ *
+ * **Who:** Admins only.
+ */
+ post: operations["postUsersIdAnonymize"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/event-listing": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Upcoming public events in full
+ * @description What the site's event lists show (ticket options, prices, early-bird tiers), not paged. `campus` (id or slug) and `series` (slug) narrow it. For a paged, slimmer list use GET /events.
+ *
+ * **Who:** Anyone.
+ */
+ get: operations["getEvent-listing"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/events": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Every event, including deactivated ones
+ * @description **Who:** Admins only.
+ */
+ get: operations["getAdminEvents"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/events/{id}/email-attendees": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Email an event's people
+ * @description Filtered by status or chosen people; news consent is checked.
+ *
+ * **Who:** Supervisors and admins.
+ */
+ post: operations["postEventsIdEmail-attendees"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/events/{id}/email-attendees/schedule": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Schedule an email to an event's people
+ * @description **Who:** Supervisors and admins.
+ */
+ post: operations["postEventsIdEmail-attendeesSchedule"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/events/{id}/whatsapp-attendees": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * WhatsApp an event's people
+ * @description **Who:** Supervisors and admins.
+ */
+ post: operations["postEventsIdWhatsapp-attendees"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/events/{id}/whatsapp-attendees/schedule": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Schedule a WhatsApp to an event's people
+ * @description **Who:** Supervisors and admins.
+ */
+ post: operations["postEventsIdWhatsapp-attendeesSchedule"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/broadcasts/email/preview": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Who an email broadcast would reach
+ * @description **Who:** Supervisors and admins.
+ */
+ post: operations["postBroadcastsEmailPreview"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/broadcasts/email/send": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Send an email broadcast
+ * @description To campuses, events, roles or listed people; news consent is checked.
+ *
+ * **Who:** Supervisors and admins.
+ */
+ post: operations["postBroadcastsEmailSend"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/broadcasts/email/schedule": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Schedule an email broadcast
+ * @description **Who:** Supervisors and admins.
+ */
+ post: operations["postBroadcastsEmailSchedule"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/broadcasts/whatsapp/preview": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Who a WhatsApp broadcast would reach
+ * @description **Who:** Supervisors and admins.
+ */
+ post: operations["postBroadcastsWhatsappPreview"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/broadcasts/whatsapp/send": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Send a WhatsApp broadcast
+ * @description To campuses, events, roles or listed people; news consent is checked.
+ *
+ * **Who:** Supervisors and admins.
+ */
+ post: operations["postBroadcastsWhatsappSend"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/broadcasts/whatsapp/schedule": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Schedule a WhatsApp broadcast
+ * @description **Who:** Supervisors and admins.
+ */
+ post: operations["postBroadcastsWhatsappSchedule"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/scheduled-messages": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Scheduled emails and WhatsApps
+ * @description **Who:** Supervisors and admins.
+ */
+ get: operations["getScheduled-messages"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/scheduled-messages/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /**
+ * Cancel a scheduled message
+ * @description **Who:** Supervisors and admins.
+ */
+ delete: operations["deleteScheduled-messagesId"];
+ options?: never;
+ head?: never;
+ /**
+ * Change a scheduled message
+ * @description **Who:** Supervisors and admins.
+ */
+ patch: operations["patchScheduled-messagesId"];
+ trace?: never;
+ };
+ "/automations/schedule": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Schedule an event's reminder messages
+ * @description **Who:** Supervisors and admins.
+ */
+ post: operations["postAutomationsSchedule"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/whatsapp/config": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * The WhatsApp connection settings
+ * @description **Who:** Admins only.
+ */
+ get: operations["getWhatsappConfig"];
+ /**
+ * Change the WhatsApp connection settings
+ * @description **Who:** Admins only.
+ */
+ put: operations["putWhatsappConfig"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/whatsapp/instance": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Create the WhatsApp instance
+ * @description **Who:** Admins only.
+ */
+ post: operations["postWhatsappInstance"];
+ /**
+ * Delete the WhatsApp instance
+ * @description **Who:** Admins only.
+ */
+ delete: operations["deleteWhatsappInstance"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/whatsapp/status": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Is WhatsApp connected?
+ * @description **Who:** Admins only.
+ */
+ get: operations["getWhatsappStatus"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/whatsapp/qr": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * The QR code to link a phone
+ * @description **Who:** Admins only.
+ */
+ get: operations["getWhatsappQr"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/whatsapp/request-code": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Link a phone with a code instead
+ * @description **Who:** Admins only.
+ */
+ post: operations["postWhatsappRequest-code"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/whatsapp/logout": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Unlink the phone
+ * @description **Who:** Admins only.
+ */
+ post: operations["postWhatsappLogout"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/whatsapp/start": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Start the WhatsApp session
+ * @description **Who:** Admins only.
+ */
+ post: operations["postWhatsappStart"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/whatsapp/restart": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Restart the WhatsApp session
+ * @description **Who:** Admins only.
+ */
+ post: operations["postWhatsappRestart"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/banner": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * The site-wide banner
+ * @description **Who:** Anyone.
+ */
+ get: operations["getBanner"];
+ /**
+ * Set the site-wide banner
+ * @description **Who:** Supervisors and admins.
+ */
+ put: operations["putBanner"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/saved-filters": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * My saved filters
+ * @description For one list page (`page`).
+ *
+ * **Who:** Staff, supervisors and admins.
+ */
+ get: operations["getSaved-filters"];
+ put?: never;
+ /**
+ * Save a filter
+ * @description By name (replaces one with the same name); up to 30 per page.
+ *
+ * **Who:** Staff, supervisors and admins.
+ */
+ post: operations["postSaved-filters"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/saved-filters/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /**
+ * Change a saved filter
+ * @description **Who:** Staff, supervisors and admins.
+ */
+ put: operations["putSaved-filtersId"];
+ post?: never;
+ /**
+ * Delete a saved filter
+ * @description **Who:** Staff, supervisors and admins.
+ */
+ delete: operations["deleteSaved-filtersId"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/dashboard/today": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Today at a glance
+ * @description **Who:** Staff, supervisors and admins.
+ */
+ get: operations["getDashboardToday"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/dashboard/search": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Search people, registrations and events
+ * @description **Who:** Staff, supervisors and admins.
+ */
+ get: operations["getDashboardSearch"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats/staff": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Staff dashboard numbers
+ * @description **Who:** Staff, supervisors and admins.
+ */
+ get: operations["getStatsStaff"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats/supervisor": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Supervisor dashboard numbers
+ * @description **Who:** Supervisors and admins.
+ */
+ get: operations["getStatsSupervisor"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats/admin": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Admin dashboard numbers
+ * @description **Who:** Admins only.
+ */
+ get: operations["getStatsAdmin"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats/overview": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Overview numbers
+ * @description **Who:** Supervisors and admins.
+ */
+ get: operations["getStatsOverview"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/analytics/track": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Record a step on the way to registering
+ * @description **Who:** Anyone.
+ */
+ post: operations["postAnalyticsTrack"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/analytics/funnel": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * How many get from viewing to paying
+ * @description **Who:** Supervisors and admins.
+ */
+ get: operations["getAnalyticsFunnel"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/reports/pdf": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * A report as a PDF
+ * @description The report's rows and columns in the body.
+ *
+ * **Who:** Any signed-in person.
+ */
+ post: operations["postReportsPdf"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/reports/excel": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * A report as an Excel file
+ * @description **Who:** Any signed-in person.
+ */
+ post: operations["postReportsExcel"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/reports/email": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Email a report as a PDF
+ * @description **Who:** Any signed-in person.
+ */
+ post: operations["postReportsEmail"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/reports/whatsapp": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * WhatsApp a report as a PDF
+ * @description **Who:** Any signed-in person.
+ */
+ post: operations["postReportsWhatsapp"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/checkin/{qrCode}": {
parameters: {
query?: never;
@@ -20303,6 +21441,3140 @@ export interface operations {
};
};
};
+ postAuthLoginCodeResend: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postAuthPasswordForgot: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ email: string;
+ } & {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postAuthPasswordReset: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postAuthActivate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postMeSessionsRevoke: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postMeClose: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getMeActivity: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postMeVerify-password": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postMeTwo-factorEnable": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postMeTwo-factorConfirm": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postMeTwo-factorDisable": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getUsersExists: {
+ parameters: {
+ query?: {
+ email?: string;
+ phone?: string;
+ phoneNumber?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getUsers: {
+ parameters: {
+ query?: {
+ page?: string;
+ limit?: string;
+ search?: string;
+ role?: string;
+ isActive?: string;
+ campus?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getUsersId: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteUsersId: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ patchUsersId: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postUsersIdSessionsRevoke: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postUsersIdAnonymize: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "getEvent-listing": {
+ parameters: {
+ query?: {
+ campus?: string;
+ series?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getAdminEvents: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postEventsIdEmail-attendees": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postEventsIdEmail-attendeesSchedule": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postEventsIdWhatsapp-attendees": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postEventsIdWhatsapp-attendeesSchedule": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postBroadcastsEmailPreview: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postBroadcastsEmailSend: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postBroadcastsEmailSchedule: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postBroadcastsWhatsappPreview: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postBroadcastsWhatsappSend: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postBroadcastsWhatsappSchedule: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "getScheduled-messages": {
+ parameters: {
+ query?: {
+ status?: string;
+ eventId?: string;
+ channel?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "deleteScheduled-messagesId": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "patchScheduled-messagesId": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postAutomationsSchedule: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ eventId: string;
+ } & {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getWhatsappConfig: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ putWhatsappConfig: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postWhatsappInstance: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteWhatsappInstance: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getWhatsappStatus: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getWhatsappQr: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postWhatsappRequest-code": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postWhatsappLogout: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postWhatsappStart: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postWhatsappRestart: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getBanner: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ };
+ };
+ putBanner: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "getSaved-filters": {
+ parameters: {
+ query?: {
+ page?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "postSaved-filters": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "putSaved-filtersId": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ "deleteSaved-filtersId": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not found (NOT_FOUND) */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getDashboardToday: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getDashboardSearch: {
+ parameters: {
+ query?: {
+ q?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getStatsStaff: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getStatsSupervisor: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getStatsAdmin: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getStatsOverview: {
+ parameters: {
+ query?: {
+ from?: string;
+ to?: string;
+ campus?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postAnalyticsTrack: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getAnalyticsFunnel: {
+ parameters: {
+ query?: {
+ eventId?: string;
+ from?: string;
+ to?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not allowed for this role (FORBIDDEN) */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postReportsPdf: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description The file */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/pdf": string;
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postReportsExcel: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description The file */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": string;
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postReportsEmail: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ postReportsWhatsapp: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ [key: string]: unknown;
+ };
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: unknown;
+ };
+ };
+ };
+ /** @description Validation failed (VALIDATION_FAILED) */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Not signed in (UNAUTHENTICATED) */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
getCheckinQrCode: {
parameters: {
query?: {
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index de12dc4..63f4b98 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -162,41 +162,15 @@ export async function apiFetch(path: string, options: ApiOptions =
// instead of caching an empty one.
export async function apiFetchListForPrerender(path: string, options: ApiOptions = {}): Promise {
try {
- return (await apiFetch(path, options)) || [];
+ // v2 answers { data: [...] }; v1 answered the bare list.
+ const res = await apiFetch(path, options);
+ return (Array.isArray(res) ? res : res?.data) || [];
} catch (err) {
if (process.env.NEXT_PHASE === "phase-production-build") return [];
throw err;
}
}
-// Several list endpoints (/api/users, /api/payments) are paginated and cap
-// `limit` at 200 server-side, so a single request can silently miss records
-// beyond the first page. This walks every page and returns the full list.
-async function fetchAllPages(path: string, token: string, params: Record = {}): Promise {
- const query = new URLSearchParams({ ...params, limit: "200", page: "1" });
- const first = await apiFetch>(`${path}?${query.toString()}`, { authToken: token });
- const items: T[] = Array.isArray(first?.data) ? first.data : [];
- const pages = first?.pages || 1;
-
- if (pages > 1) {
- // Page 1 told us the total page count — fetch the rest concurrently instead of
- // awaiting them one at a time. This function is called on every dashboard poll
- // (see useVisiblePolling usages), so a serial loop here directly slows those polls.
- const remaining = await Promise.all(
- Array.from({ length: pages - 1 }, (_, i) => {
- const page = i + 2;
- const pageQuery = new URLSearchParams({ ...params, limit: "200", page: String(page) });
- return apiFetch>(`${path}?${pageQuery.toString()}`, { authToken: token });
- })
- );
- for (const res of remaining) {
- if (Array.isArray(res?.data)) items.push(...res.data);
- }
- }
-
- return items;
-}
-
/**
* Every event staff can see (API v2 GET /events/manage): upcoming by default; `past`,
* `inactive` widen it, `closed: false` leaves out cashed-up events. Full records — ticket options,
@@ -244,8 +218,15 @@ export async function fetchEvent(id: string, options: { authToken?: s
return data as unknown as T;
}
-export function fetchAllUsers(token: string, params: Record = {}): Promise {
- return fetchAllPages("/api/users", token, params);
+/** Every account (API v2 GET /users, all pages at once). */
+export async function fetchAllUsers(token: string, params: Record = {}): Promise {
+ const { apiV2Data } = await import("@/lib/apiV2");
+ const page = (n: number) => apiV2Data("get", "/users", { query: { ...params, limit: "200", page: String(n) }, authToken: token }) as Promise>;
+ const first = await page(1);
+ const items: User[] = Array.isArray(first?.data) ? [...first.data] : [];
+ const rest = await Promise.all(Array.from({ length: Math.max(0, (first?.pages || 1) - 1) }, (_, i) => page(i + 2)));
+ for (const res of rest) if (Array.isArray(res?.data)) items.push(...res.data);
+ return items;
}
/** Every payment staff may see (API v2 GET /payments, all pages at once). */
diff --git a/frontend/src/lib/export.ts b/frontend/src/lib/export.ts
index 5d60d73..a01b805 100644
--- a/frontend/src/lib/export.ts
+++ b/frontend/src/lib/export.ts
@@ -53,8 +53,13 @@ export type ReportPdfPayload = {
extraTables?: { title: string; columns: string[]; rows: (string | number)[][] }[];
};
+/** The message in a v2 error body (or the text as it came). */
+function v2ErrorText(text: string): string {
+ try { const j = JSON.parse(text); return j?.error?.message || j?.message || text; } catch { return text; }
+}
+
export async function downloadReportPdf(apiBase: string, authToken: string, payload: ReportPdfPayload) {
- const url = `${apiBase}/api/reports/pdf`;
+ const url = `${apiBase}/api/v2/reports/pdf`;
const res = await fetch(url, {
method: 'POST',
headers: {
@@ -65,7 +70,7 @@ export async function downloadReportPdf(apiBase: string, authToken: string, payl
});
if (!res.ok) {
const text = await res.text().catch(() => '');
- throw new Error(text || `Failed to generate PDF (${res.status})`);
+ throw new Error(v2ErrorText(text) || `Failed to generate PDF (${res.status})`);
}
const blob = await res.blob();
const dl = document.createElement('a');
@@ -80,7 +85,7 @@ export async function downloadReportPdf(apiBase: string, authToken: string, payl
// Downloads a styled .xlsx mirroring the same branded look as the PDF (colored header,
// stat rows, a data-bar chart, banded table with a highlighted total row).
export async function downloadReportExcel(apiBase: string, authToken: string, payload: ReportPdfPayload) {
- const url = `${apiBase}/api/reports/excel`;
+ const url = `${apiBase}/api/v2/reports/excel`;
const res = await fetch(url, {
method: 'POST',
headers: {
@@ -91,7 +96,7 @@ export async function downloadReportExcel(apiBase: string, authToken: string, pa
});
if (!res.ok) {
const text = await res.text().catch(() => '');
- throw new Error(text || `Failed to generate Excel file (${res.status})`);
+ throw new Error(v2ErrorText(text) || `Failed to generate Excel file (${res.status})`);
}
const blob = await res.blob();
const dl = document.createElement('a');
@@ -104,7 +109,7 @@ export async function downloadReportExcel(apiBase: string, authToken: string, pa
}
export async function emailReportPdf(apiBase: string, authToken: string, payload: ReportPdfPayload & { subject?: string; body?: string }) {
- const url = `${apiBase}/api/reports/email`;
+ const url = `${apiBase}/api/v2/reports/email`;
const res = await fetch(url, {
method: 'POST',
headers: {
@@ -115,15 +120,15 @@ export async function emailReportPdf(apiBase: string, authToken: string, payload
});
if (!res.ok) {
const text = await res.text().catch(() => '');
- throw new Error(text || `Failed to email PDF (${res.status})`);
+ throw new Error(v2ErrorText(text) || `Failed to email PDF (${res.status})`);
}
- return res.json();
+ return (await res.json()).data;
}
// Sends the report PDF to the current user's own WhatsApp number (same self-service pattern as
// emailReportPdf — no recipient picker needed).
export async function whatsappReportPdf(apiBase: string, authToken: string, payload: ReportPdfPayload & { caption?: string }) {
- const url = `${apiBase}/api/reports/whatsapp`;
+ const url = `${apiBase}/api/v2/reports/whatsapp`;
const res = await fetch(url, {
method: 'POST',
headers: {
@@ -134,15 +139,15 @@ export async function whatsappReportPdf(apiBase: string, authToken: string, payl
});
if (!res.ok) {
const text = await res.text().catch(() => '');
- throw new Error(text || `Failed to send PDF via WhatsApp (${res.status})`);
+ throw new Error(v2ErrorText(text) || `Failed to send PDF via WhatsApp (${res.status})`);
}
- return res.json();
+ return (await res.json()).data;
}
// "Print": fetches the same PDF as downloadReportPdf but opens it in a new tab instead of
// downloading, so the browser's built-in PDF viewer's print button handles printing.
export async function viewReportPdf(apiBase: string, authToken: string, payload: ReportPdfPayload) {
- const url = `${apiBase}/api/reports/pdf`;
+ const url = `${apiBase}/api/v2/reports/pdf`;
const res = await fetch(url, {
method: 'POST',
headers: {
@@ -153,7 +158,7 @@ export async function viewReportPdf(apiBase: string, authToken: string, payload:
});
if (!res.ok) {
const text = await res.text().catch(() => '');
- throw new Error(text || `Failed to generate PDF (${res.status})`);
+ throw new Error(v2ErrorText(text) || `Failed to generate PDF (${res.status})`);
}
const blob = await res.blob();
const objUrl = URL.createObjectURL(blob);