From e95d52ccdd46692892a521699f0177cdc8a241cb Mon Sep 17 00:00:00 2001 From: joshua Date: Thu, 29 Oct 2026 17:02:26 +0200 Subject: [PATCH] WIP 1.51.14: people and messaging on v2 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ls1vMdg2GYqbnKZmPaX1ot --- backend/src/v2/deprecation.ts | 43 + backend/src/v2/index.ts | 1 + backend/src/v2/resources/money.ts | 2 +- backend/src/v2/resources/peopleMessaging.ts | 138 + backend/src/v2/route.ts | 4 + backend/tests/v2.test.ts | 1 + e2e/api-v2.spec.ts | 27 + .../src/app/(auth)/forgot-password/page.tsx | 4 +- frontend/src/app/activate-account/page.tsx | 4 +- frontend/src/app/campus/[slug]/page.tsx | 2 +- frontend/src/app/dashboard/admin/page.tsx | 6 +- .../src/app/dashboard/admin/settings/page.tsx | 24 +- .../src/app/dashboard/admin/users/page.tsx | 35 +- frontend/src/app/dashboard/staff/page.tsx | 4 +- .../app/dashboard/supervisor/coupons/page.tsx | 4 +- .../supervisor/email-attendees/page.tsx | 22 +- .../app/dashboard/supervisor/events/page.tsx | 4 +- .../src/app/dashboard/supervisor/page.tsx | 6 +- .../supervisor/whatsapp-attendees/page.tsx | 25 +- .../src/app/dashboard/user/donate/page.tsx | 3 +- .../src/app/dashboard/user/profile/page.tsx | 29 +- frontend/src/app/events/page.tsx | 2 +- frontend/src/app/page.tsx | 2 +- .../src/app/registration/success/page.tsx | 2 +- frontend/src/app/reset-password/page.tsx | 4 +- frontend/src/app/self-service/page.tsx | 31 +- frontend/src/app/series/[slug]/page.tsx | 2 +- frontend/src/app/set-banner/page.tsx | 10 +- frontend/src/app/sitemap.ts | 3 +- frontend/src/components/admin/CampusesTab.tsx | 3 +- .../src/components/dashboard/TodayPanel.tsx | 4 +- frontend/src/components/reports/ReportsV2.tsx | 4 +- frontend/src/components/shared/BannerBar.tsx | 4 +- .../src/components/shared/CommandPalette.tsx | 4 +- .../src/components/shared/SavedFilters.tsx | 10 +- .../src/components/volunteers/RosterSheet.tsx | 3 +- frontend/src/contexts/AuthContext.tsx | 14 +- frontend/src/lib/analytics.ts | 2 +- frontend/src/lib/api-v2/openapi.json | 4597 +++++++++++++++++ frontend/src/lib/api-v2/schema.d.ts | 4272 +++++++++++++++ frontend/src/lib/api.ts | 43 +- frontend/src/lib/export.ts | 29 +- 42 files changed, 9250 insertions(+), 183 deletions(-) create mode 100644 backend/src/v2/resources/peopleMessaging.ts 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);