@vicacha-devs/strapi-plugin-events 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +121 -2
- package/dist/admin/CalendarPage-CwCvPDRx.js +1215 -0
- package/dist/admin/CalendarPage-Dz7g19qp.mjs +1215 -0
- package/dist/admin/RRuleInput-BQl-siRj.js +11 -0
- package/dist/admin/RRuleInput-C8bR37X9.mjs +6 -0
- package/dist/admin/SettingsPage-CNG9AEO_.mjs +415 -0
- package/dist/admin/SettingsPage-DhjtKDmY.js +415 -0
- package/dist/admin/ar-mMKUXxch.js +216 -0
- package/dist/admin/ar-tWKEvJLB.mjs +216 -0
- package/dist/admin/de-CGk8UZ3W.js +216 -0
- package/dist/admin/de-OdbJEFs0.mjs +216 -0
- package/dist/admin/en-Dskn9ZPa.mjs +216 -0
- package/dist/admin/en-imdSlLhJ.js +216 -0
- package/dist/admin/es-DcMsMHrI.mjs +216 -0
- package/dist/admin/es-DkqqxUJQ.js +216 -0
- package/dist/admin/fr-BTEIPOFw.mjs +216 -0
- package/dist/admin/fr-D92B_ZTv.js +216 -0
- package/dist/admin/index-BTq_X4uT.js +1005 -0
- package/dist/admin/index-BatOiwBu.mjs +1006 -0
- package/dist/admin/index.js +6 -0
- package/dist/admin/index.mjs +6 -0
- package/dist/admin/it-CAxPpL_9.mjs +216 -0
- package/dist/admin/it-ClKaSCgv.js +216 -0
- package/dist/admin/nl-CS1aUfT7.mjs +216 -0
- package/dist/admin/nl-F6FIi7zw.js +216 -0
- package/dist/admin/pl-BfmBU9FW.mjs +216 -0
- package/dist/admin/pl-p8VLXi02.js +216 -0
- package/dist/admin/pt-BUdH0uSw.js +216 -0
- package/dist/admin/pt-DdulKcbk.mjs +216 -0
- package/dist/admin/src/components/EventSidePanel.d.ts +13 -0
- package/dist/admin/src/components/Initializer.d.ts +5 -0
- package/dist/admin/src/components/PluginIcon.d.ts +2 -0
- package/dist/admin/src/components/RRuleInput.d.ts +3 -0
- package/dist/admin/src/components/events/AttendeesManagerPanel.d.ts +15 -0
- package/dist/admin/src/components/events/EventsDirectoryPanel.d.ts +11 -0
- package/dist/admin/src/components/events/EventsKpiCards.d.ts +16 -0
- package/dist/admin/src/components/events/QuickCreateEventPanel.d.ts +21 -0
- package/dist/admin/src/components/events/calendarPageHelpers.d.ts +8 -0
- package/dist/admin/src/fields/rruleField.d.ts +41 -0
- package/dist/admin/src/index.d.ts +15 -0
- package/dist/admin/src/pages/CalendarPage.d.ts +3 -0
- package/dist/admin/src/pages/SettingsPage.d.ts +3 -0
- package/dist/admin/src/permissions.d.ts +15 -0
- package/dist/admin/src/translations/cmLabels.d.ts +1 -0
- package/dist/admin/src/translations/cmPlaceholders.d.ts +1 -0
- package/dist/admin/tr-Bp50qvL8.mjs +216 -0
- package/dist/admin/tr-C7G9mPtg.js +216 -0
- package/dist/server/index.js +3709 -0
- package/dist/server/index.mjs +3705 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +7 -0
- package/dist/server/src/content-types/event/schema.json.d.ts +279 -0
- package/dist/server/src/content-types/index.d.ts +366 -0
- package/dist/server/src/content-types/registration/schema.json.d.ts +86 -0
- package/dist/server/src/controllers/admin.d.ts +17 -0
- package/dist/server/src/controllers/content-api.d.ts +14 -0
- package/dist/server/src/controllers/index.d.ts +28 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +551 -0
- package/dist/server/src/middlewares/index.d.ts +2 -0
- package/dist/server/src/policies/index.d.ts +2 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin.d.ts +17 -0
- package/dist/server/src/routes/content-api.d.ts +12 -0
- package/dist/server/src/routes/index.d.ts +30 -0
- package/dist/server/src/services/ical.d.ts +18 -0
- package/dist/server/src/services/index.d.ts +110 -0
- package/dist/server/src/services/occurrence.d.ts +44 -0
- package/dist/server/src/services/occurrenceHelpers.d.ts +16 -0
- package/dist/server/src/services/occurrencePayloadHelpers.d.ts +30 -0
- package/dist/server/src/services/occurrenceRsvpBatch.d.ts +25 -0
- package/dist/server/src/services/rsvp.d.ts +51 -0
- package/dist/server/src/services/rsvpHelpers.d.ts +52 -0
- package/dist/server/src/services/rsvpMutations.d.ts +34 -0
- package/dist/server/src/services/rsvpSecurity.d.ts +22 -0
- package/dist/server/src/services/settings.d.ts +10 -0
- package/dist/server/src/types.d.ts +171 -0
- package/dist/shared/calendarSystems.d.ts +6 -0
- package/dist/shared/icalGenerator.d.ts +25 -0
- package/dist/shared/index.d.ts +6 -0
- package/dist/shared/index.js +753 -0
- package/dist/shared/index.mjs +727 -0
- package/dist/shared/pluginId.d.ts +4 -0
- package/dist/shared/rruleEngine.d.ts +20 -0
- package/dist/shared/spotlightHelpers.d.ts +17 -0
- package/dist/shared/types.d.ts +309 -0
- package/dist/shared/urlAndPosterHelpers.d.ts +18 -0
- package/package.json +109 -3
|
@@ -0,0 +1,3709 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
|
+
const shared = require("@vicacha-devs/strapi-plugin-calendar/shared");
|
|
4
|
+
const crypto = require("node:crypto");
|
|
5
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
6
|
+
const crypto__default = /* @__PURE__ */ _interopDefault(crypto);
|
|
7
|
+
const PLUGIN_ID = "events";
|
|
8
|
+
const EVENT_CONTENT_TYPE_UID = `plugin::${PLUGIN_ID}.event`;
|
|
9
|
+
const REGISTRATION_CONTENT_TYPE_UID = `plugin::${PLUGIN_ID}.registration`;
|
|
10
|
+
class PluginValidationError extends Error {
|
|
11
|
+
statusCode = 400;
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "PluginValidationError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class PluginNotFoundError extends Error {
|
|
18
|
+
statusCode = 404;
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "PluginNotFoundError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function getDocumentsClient(strapi, uid) {
|
|
25
|
+
return strapi.documents(
|
|
26
|
+
uid
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
function getSettingsService(strapi) {
|
|
30
|
+
return strapi.plugin(PLUGIN_ID).service("settings");
|
|
31
|
+
}
|
|
32
|
+
function getOccurrenceService(strapi) {
|
|
33
|
+
return strapi.plugin(PLUGIN_ID).service("occurrence");
|
|
34
|
+
}
|
|
35
|
+
function getRsvpService(strapi) {
|
|
36
|
+
return strapi.plugin(PLUGIN_ID).service("rsvp");
|
|
37
|
+
}
|
|
38
|
+
function getIcalService(strapi) {
|
|
39
|
+
return strapi.plugin(PLUGIN_ID).service("ical");
|
|
40
|
+
}
|
|
41
|
+
function clampPaginatedSlice(items, params) {
|
|
42
|
+
const maxLimit = params?.maxLimit ?? 500;
|
|
43
|
+
const defaultLimit = params?.defaultLimit ?? maxLimit;
|
|
44
|
+
if (params?.page !== void 0 || params?.pageSize !== void 0) {
|
|
45
|
+
const rawPage = Number(params.page);
|
|
46
|
+
const page = Number.isFinite(rawPage) && rawPage >= 1 ? Math.min(1e3, Math.floor(rawPage)) : 1;
|
|
47
|
+
const rawSize = Number(params.pageSize ?? params.limit);
|
|
48
|
+
const pageSize = Number.isFinite(rawSize) && rawSize >= 1 ? Math.min(maxLimit, Math.floor(rawSize)) : Math.min(50, maxLimit);
|
|
49
|
+
const offset = (page - 1) * pageSize;
|
|
50
|
+
return items.slice(offset, offset + pageSize);
|
|
51
|
+
}
|
|
52
|
+
if (params?.limit !== void 0) {
|
|
53
|
+
const rawLimit = Number(params.limit);
|
|
54
|
+
const safeLimit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.min(maxLimit, Math.floor(rawLimit)) : Math.min(defaultLimit, maxLimit);
|
|
55
|
+
return items.slice(0, safeLimit);
|
|
56
|
+
}
|
|
57
|
+
return items.slice(0, defaultLimit);
|
|
58
|
+
}
|
|
59
|
+
function handleControllerError(strapi, ctx, err, fallbackMessage) {
|
|
60
|
+
if (err instanceof PluginNotFoundError) {
|
|
61
|
+
if (typeof ctx.notFound === "function") {
|
|
62
|
+
return ctx.notFound(err.message);
|
|
63
|
+
}
|
|
64
|
+
ctx.status = 404;
|
|
65
|
+
ctx.body = { error: { status: 404, message: err.message } };
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
if (err instanceof PluginValidationError) {
|
|
69
|
+
return ctx.badRequest(err.message);
|
|
70
|
+
}
|
|
71
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
72
|
+
strapi.log?.error?.(`[events] ${fallbackMessage}: ${detail}`);
|
|
73
|
+
return ctx.badRequest(fallbackMessage);
|
|
74
|
+
}
|
|
75
|
+
const bootstrap = async ({ strapi }) => {
|
|
76
|
+
const actions = [
|
|
77
|
+
{
|
|
78
|
+
section: "plugins",
|
|
79
|
+
displayName: "Read Calendar & Events",
|
|
80
|
+
uid: "read",
|
|
81
|
+
pluginName: PLUGIN_ID
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
section: "plugins",
|
|
85
|
+
displayName: "Create & Reschedule Events / Manage RSVPs",
|
|
86
|
+
uid: "update",
|
|
87
|
+
pluginName: PLUGIN_ID
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
section: "plugins",
|
|
91
|
+
displayName: "Manage Events Plugin Settings",
|
|
92
|
+
uid: "settings.update",
|
|
93
|
+
pluginName: PLUGIN_ID
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
if (strapi.admin?.services?.permission?.actionProvider?.registerMany) {
|
|
97
|
+
await strapi.admin.services.permission.actionProvider.registerMany(actions);
|
|
98
|
+
}
|
|
99
|
+
const settingsService = getSettingsService(strapi);
|
|
100
|
+
let settings2 = null;
|
|
101
|
+
if (settingsService && typeof settingsService.ensureInitialized === "function") {
|
|
102
|
+
settings2 = await settingsService.ensureInitialized();
|
|
103
|
+
}
|
|
104
|
+
const calendarPlugin = strapi.plugin("calendar");
|
|
105
|
+
const calendarRegistry = calendarPlugin?.service("registry");
|
|
106
|
+
if (calendarRegistry && typeof calendarRegistry.registerSource === "function" && settings2) {
|
|
107
|
+
for (const source of settings2.sources ?? []) {
|
|
108
|
+
try {
|
|
109
|
+
await calendarRegistry.registerSource({
|
|
110
|
+
id: source.id,
|
|
111
|
+
label: source.label,
|
|
112
|
+
uid: source.uid,
|
|
113
|
+
color: source.color,
|
|
114
|
+
enabled: source.enabled,
|
|
115
|
+
fields: {
|
|
116
|
+
title: source.fields.title,
|
|
117
|
+
startDate: source.fields.startDate,
|
|
118
|
+
endDate: source.fields.endDate,
|
|
119
|
+
rrule: source.fields.rrule,
|
|
120
|
+
location: source.fields.location,
|
|
121
|
+
category: source.fields.category,
|
|
122
|
+
status: source.fields.status,
|
|
123
|
+
summary: source.fields.summary,
|
|
124
|
+
slug: source.fields.slug,
|
|
125
|
+
timeSlot: source.fields.timeSlot,
|
|
126
|
+
alternateDateFields: source.fields.alternateDateFields,
|
|
127
|
+
customFields: source.fields.customFields
|
|
128
|
+
},
|
|
129
|
+
timeSlotMap: source.timeSlotMap,
|
|
130
|
+
categoryColors: source.categoryColors,
|
|
131
|
+
defaultAllDay: source.defaultAllDay
|
|
132
|
+
});
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const DEFAULT_PLUGIN_SETTINGS = {
|
|
139
|
+
defaultTimezone: "UTC",
|
|
140
|
+
defaultCapacity: 100,
|
|
141
|
+
waitlistEnabledByDefault: true,
|
|
142
|
+
defaultMaxGuestsPerRegistration: 5,
|
|
143
|
+
sendRsvpEmails: true,
|
|
144
|
+
feedTitle: "Events Calendar",
|
|
145
|
+
sources: [
|
|
146
|
+
{
|
|
147
|
+
id: "builtin-events",
|
|
148
|
+
label: "Events",
|
|
149
|
+
uid: EVENT_CONTENT_TYPE_UID,
|
|
150
|
+
color: "#0D9488",
|
|
151
|
+
enabled: true,
|
|
152
|
+
fields: {
|
|
153
|
+
title: "title",
|
|
154
|
+
startDate: "startDate",
|
|
155
|
+
endDate: "endDate",
|
|
156
|
+
rrule: "recurrence",
|
|
157
|
+
location: "location",
|
|
158
|
+
category: "category",
|
|
159
|
+
status: "eventStatus",
|
|
160
|
+
summary: "summary",
|
|
161
|
+
slug: "slug",
|
|
162
|
+
capacity: "capacity",
|
|
163
|
+
rsvpEnabled: "rsvpEnabled",
|
|
164
|
+
poster: "poster",
|
|
165
|
+
actionLinks: "actionLinks",
|
|
166
|
+
spotlightConfig: "spotlightConfig"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
secondaryCalendars: [],
|
|
171
|
+
recurrencePresets: [
|
|
172
|
+
{ id: "daily", label: "Daily", rrule: "FREQ=DAILY;INTERVAL=1" },
|
|
173
|
+
{
|
|
174
|
+
id: "weekly-mon",
|
|
175
|
+
label: "Weekly on Mondays",
|
|
176
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "weekly-tue",
|
|
180
|
+
label: "Weekly on Tuesdays",
|
|
181
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=TU"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "weekly-wed",
|
|
185
|
+
label: "Weekly on Wednesdays",
|
|
186
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=WE"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "weekly-thu",
|
|
190
|
+
label: "Weekly on Thursdays",
|
|
191
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=TH"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "weekly-fri",
|
|
195
|
+
label: "Weekly on Fridays",
|
|
196
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=FR"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "weekly-sat",
|
|
200
|
+
label: "Weekly on Saturdays",
|
|
201
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=SA"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "weekly-sun",
|
|
205
|
+
label: "Weekly on Sundays",
|
|
206
|
+
rrule: "FREQ=WEEKLY;INTERVAL=1;BYDAY=SU"
|
|
207
|
+
},
|
|
208
|
+
{ id: "monthly", label: "Monthly", rrule: "FREQ=MONTHLY;INTERVAL=1" },
|
|
209
|
+
{ id: "yearly", label: "Yearly", rrule: "FREQ=YEARLY;INTERVAL=1" }
|
|
210
|
+
]
|
|
211
|
+
};
|
|
212
|
+
const config = {
|
|
213
|
+
default: DEFAULT_PLUGIN_SETTINGS,
|
|
214
|
+
validator(config2) {
|
|
215
|
+
if (config2.defaultCapacity !== void 0 && config2.defaultCapacity < 1) {
|
|
216
|
+
throw new Error("events plugin config: defaultCapacity must be >= 1");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const kind$1 = "collectionType";
|
|
221
|
+
const collectionName$1 = "plugin_events_items";
|
|
222
|
+
const info$1 = { "singularName": "event", "pluralName": "events", "displayName": "Event", "description": "Recurring & single events managed by strapi-plugin-events" };
|
|
223
|
+
const options$1 = { "draftAndPublish": true };
|
|
224
|
+
const pluginOptions$1 = { "content-manager": { "visible": true }, "content-type-builder": { "visible": true }, "i18n": { "localized": true } };
|
|
225
|
+
const attributes$1 = { "title": { "type": "string", "required": true, "maxLength": 200, "pluginOptions": { "i18n": { "localized": true } } }, "slug": { "type": "uid", "targetField": "title", "required": true, "pluginOptions": { "i18n": { "localized": true } } }, "summary": { "type": "text", "maxLength": 500, "pluginOptions": { "i18n": { "localized": true } } }, "description": { "type": "richtext", "pluginOptions": { "i18n": { "localized": true } } }, "startDate": { "type": "datetime", "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "endDate": { "type": "datetime", "pluginOptions": { "i18n": { "localized": false } } }, "allDay": { "type": "boolean", "default": false, "pluginOptions": { "i18n": { "localized": false } } }, "timezone": { "type": "string", "default": "UTC", "pluginOptions": { "i18n": { "localized": false } } }, "recurrence": { "type": "customField", "customField": "plugin::events.rrule", "pluginOptions": { "i18n": { "localized": false } } }, "eventStatus": { "type": "enumeration", "enum": ["scheduled", "cancelled", "postponed", "rescheduled"], "default": "scheduled", "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "locationType": { "type": "enumeration", "enum": ["in_person", "online", "hybrid"], "default": "in_person", "pluginOptions": { "i18n": { "localized": false } } }, "location": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } }, "virtualUrl": { "type": "string", "pluginOptions": { "i18n": { "localized": false } } }, "category": { "type": "string", "default": "general", "pluginOptions": { "i18n": { "localized": false } } }, "speaker": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } }, "speakerSlug": { "type": "string", "pluginOptions": { "i18n": { "localized": false } } }, "color": { "type": "string", "default": "#0D9488", "pluginOptions": { "i18n": { "localized": false } } }, "featured": { "type": "boolean", "default": false, "pluginOptions": { "i18n": { "localized": false } } }, "rsvpEnabled": { "type": "boolean", "default": false, "pluginOptions": { "i18n": { "localized": false } } }, "capacity": { "type": "integer", "min": 1, "pluginOptions": { "i18n": { "localized": false } } }, "waitlistEnabled": { "type": "boolean", "default": true, "pluginOptions": { "i18n": { "localized": false } } }, "maxGuestsPerRegistration": { "type": "integer", "min": 1, "max": 50, "default": 5, "pluginOptions": { "i18n": { "localized": false } } }, "registrationDeadline": { "type": "datetime", "pluginOptions": { "i18n": { "localized": false } } }, "alternateDates": { "type": "json", "pluginOptions": { "i18n": { "localized": true } } }, "poster": { "type": "media", "multiple": false, "required": false, "allowedTypes": ["images", "files"], "pluginOptions": { "i18n": { "localized": true } } }, "actionLinks": { "type": "json", "pluginOptions": { "i18n": { "localized": true } } }, "spotlightConfig": { "type": "json", "pluginOptions": { "i18n": { "localized": true } } }, "metadata": { "type": "json", "pluginOptions": { "i18n": { "localized": true } } } };
|
|
226
|
+
const eventSchema = {
|
|
227
|
+
kind: kind$1,
|
|
228
|
+
collectionName: collectionName$1,
|
|
229
|
+
info: info$1,
|
|
230
|
+
options: options$1,
|
|
231
|
+
pluginOptions: pluginOptions$1,
|
|
232
|
+
attributes: attributes$1
|
|
233
|
+
};
|
|
234
|
+
const kind = "collectionType";
|
|
235
|
+
const collectionName = "plugin_events_registrations";
|
|
236
|
+
const info = { "singularName": "registration", "pluralName": "registrations", "displayName": "Event Registration (RSVP)", "description": "Attendee RSVPs, waitlists, and check-ins managed by strapi-plugin-events" };
|
|
237
|
+
const options = { "draftAndPublish": false };
|
|
238
|
+
const pluginOptions = { "content-manager": { "visible": true }, "content-type-builder": { "visible": true } };
|
|
239
|
+
const attributes = { "eventUid": { "type": "string", "required": true, "default": "plugin::events.event" }, "eventDocumentId": { "type": "string", "required": true }, "eventTitle": { "type": "string" }, "occurrenceDate": { "type": "string", "required": true }, "attendeeName": { "type": "string", "required": true, "maxLength": 160 }, "attendeeEmail": { "type": "email", "required": true }, "attendeePhone": { "type": "string", "maxLength": 50 }, "guestsCount": { "type": "integer", "required": true, "min": 1, "max": 50, "default": 1 }, "status": { "type": "enumeration", "enum": ["confirmed", "waitlisted", "cancelled", "checked_in"], "default": "confirmed", "required": true }, "checkInToken": { "type": "string", "private": true }, "checkedInAt": { "type": "datetime" }, "locale": { "type": "string", "default": "es" }, "notes": { "type": "text", "maxLength": 2e3 }, "metadata": { "type": "json" } };
|
|
240
|
+
const registrationSchema = {
|
|
241
|
+
kind,
|
|
242
|
+
collectionName,
|
|
243
|
+
info,
|
|
244
|
+
options,
|
|
245
|
+
pluginOptions,
|
|
246
|
+
attributes
|
|
247
|
+
};
|
|
248
|
+
const contentTypes = {
|
|
249
|
+
event: { schema: eventSchema },
|
|
250
|
+
registration: { schema: registrationSchema }
|
|
251
|
+
};
|
|
252
|
+
function optStr$1(val) {
|
|
253
|
+
return typeof val === "string" && val ? val : void 0;
|
|
254
|
+
}
|
|
255
|
+
function optNum(val) {
|
|
256
|
+
return typeof val === "number" && Number.isFinite(val) ? val : void 0;
|
|
257
|
+
}
|
|
258
|
+
function optQueryInt$1(val) {
|
|
259
|
+
if (typeof val === "number" && Number.isFinite(val)) return val;
|
|
260
|
+
if (typeof val === "string" && val.trim() !== "") {
|
|
261
|
+
const parsed = Number(val);
|
|
262
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
263
|
+
}
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
function buildRescheduleInput(body) {
|
|
267
|
+
return {
|
|
268
|
+
sourceId: optStr$1(body.sourceId),
|
|
269
|
+
uid: optStr$1(body.uid),
|
|
270
|
+
documentId: String(body.documentId ?? ""),
|
|
271
|
+
occurrenceDate: String(body.occurrenceDate ?? ""),
|
|
272
|
+
newStartDate: String(body.newStartDate ?? ""),
|
|
273
|
+
newEndDate: optStr$1(body.newEndDate),
|
|
274
|
+
applyToSeries: Boolean(body.applyToSeries),
|
|
275
|
+
cancelOccurrence: Boolean(body.cancelOccurrence)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function buildQuickCreateInput(body) {
|
|
279
|
+
return {
|
|
280
|
+
sourceId: optStr$1(body.sourceId),
|
|
281
|
+
title: String(body.title ?? ""),
|
|
282
|
+
startDate: String(body.startDate ?? ""),
|
|
283
|
+
endDate: optStr$1(body.endDate),
|
|
284
|
+
location: optStr$1(body.location),
|
|
285
|
+
category: optStr$1(body.category),
|
|
286
|
+
summary: optStr$1(body.summary),
|
|
287
|
+
rrule: optStr$1(body.rrule),
|
|
288
|
+
rsvpEnabled: Boolean(body.rsvpEnabled),
|
|
289
|
+
capacity: optNum(body.capacity)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const admin$1 = ({ strapi }) => {
|
|
293
|
+
return {
|
|
294
|
+
async getSettings(ctx) {
|
|
295
|
+
const data = await getSettingsService(strapi).getSettings();
|
|
296
|
+
ctx.body = { data };
|
|
297
|
+
},
|
|
298
|
+
async updateSettings(ctx) {
|
|
299
|
+
try {
|
|
300
|
+
const payload = ctx.request?.body ?? {};
|
|
301
|
+
const data = await getSettingsService(strapi).updateSettings(payload);
|
|
302
|
+
ctx.body = { data };
|
|
303
|
+
} catch (err) {
|
|
304
|
+
return handleControllerError(
|
|
305
|
+
strapi,
|
|
306
|
+
ctx,
|
|
307
|
+
err,
|
|
308
|
+
"Unable to update settings."
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
async getCalendar(ctx) {
|
|
313
|
+
const q = ctx.query ?? {};
|
|
314
|
+
const settings2 = await getSettingsService(strapi).getSettings();
|
|
315
|
+
const occurrences = await getOccurrenceService(strapi).getOccurrences({
|
|
316
|
+
from: optStr$1(q.from),
|
|
317
|
+
to: optStr$1(q.to),
|
|
318
|
+
category: optStr$1(q.category),
|
|
319
|
+
sourceId: optStr$1(q.sourceId),
|
|
320
|
+
locale: optStr$1(q.locale),
|
|
321
|
+
includeDrafts: true,
|
|
322
|
+
limit: optQueryInt$1(q.limit),
|
|
323
|
+
page: optQueryInt$1(q.page),
|
|
324
|
+
pageSize: optQueryInt$1(q.pageSize)
|
|
325
|
+
});
|
|
326
|
+
ctx.body = {
|
|
327
|
+
data: {
|
|
328
|
+
occurrences,
|
|
329
|
+
settings: settings2
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
async reschedule(ctx) {
|
|
334
|
+
try {
|
|
335
|
+
const body = ctx.request?.body ?? {};
|
|
336
|
+
const updated = await getOccurrenceService(strapi).rescheduleOccurrence(
|
|
337
|
+
buildRescheduleInput(body)
|
|
338
|
+
);
|
|
339
|
+
ctx.body = { data: updated };
|
|
340
|
+
} catch (err) {
|
|
341
|
+
return handleControllerError(
|
|
342
|
+
strapi,
|
|
343
|
+
ctx,
|
|
344
|
+
err,
|
|
345
|
+
"Unable to reschedule event occurrence."
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
async quickCreate(ctx) {
|
|
350
|
+
try {
|
|
351
|
+
const body = ctx.request?.body ?? {};
|
|
352
|
+
const created = await getOccurrenceService(strapi).quickCreateEvent(
|
|
353
|
+
buildQuickCreateInput(body)
|
|
354
|
+
);
|
|
355
|
+
ctx.status = 201;
|
|
356
|
+
ctx.body = { data: created };
|
|
357
|
+
} catch (err) {
|
|
358
|
+
return handleControllerError(
|
|
359
|
+
strapi,
|
|
360
|
+
ctx,
|
|
361
|
+
err,
|
|
362
|
+
"Unable to create event."
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
async getEventStats(ctx) {
|
|
367
|
+
const { documentId } = ctx.params ?? {};
|
|
368
|
+
const q = ctx.query ?? {};
|
|
369
|
+
const data = await getRsvpService(strapi).getEventStats({
|
|
370
|
+
eventUid: optStr$1(q.eventUid),
|
|
371
|
+
eventDocumentId: String(documentId ?? ""),
|
|
372
|
+
occurrenceDate: optStr$1(q.occurrenceDate) ?? null,
|
|
373
|
+
includeDrafts: true
|
|
374
|
+
});
|
|
375
|
+
ctx.body = { data };
|
|
376
|
+
},
|
|
377
|
+
async listRegistrations(ctx) {
|
|
378
|
+
const q = ctx.query ?? {};
|
|
379
|
+
const data = await getRsvpService(strapi).listRegistrations({
|
|
380
|
+
eventDocumentId: optStr$1(q.eventDocumentId),
|
|
381
|
+
occurrenceDate: optStr$1(q.occurrenceDate),
|
|
382
|
+
status: optStr$1(q.status),
|
|
383
|
+
search: optStr$1(q.search),
|
|
384
|
+
limit: optQueryInt$1(q.limit),
|
|
385
|
+
page: optQueryInt$1(q.page),
|
|
386
|
+
pageSize: optQueryInt$1(q.pageSize)
|
|
387
|
+
});
|
|
388
|
+
ctx.body = { data };
|
|
389
|
+
},
|
|
390
|
+
async updateRegistrationStatus(ctx) {
|
|
391
|
+
try {
|
|
392
|
+
const { documentId } = ctx.params ?? {};
|
|
393
|
+
const { status } = ctx.request?.body ?? {};
|
|
394
|
+
const result = await getRsvpService(strapi).updateRegistrationStatus(
|
|
395
|
+
String(documentId ?? ""),
|
|
396
|
+
status
|
|
397
|
+
);
|
|
398
|
+
ctx.body = { data: result };
|
|
399
|
+
} catch (err) {
|
|
400
|
+
return handleControllerError(
|
|
401
|
+
strapi,
|
|
402
|
+
ctx,
|
|
403
|
+
err,
|
|
404
|
+
"Failed to update registration status."
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
async checkIn(ctx) {
|
|
409
|
+
try {
|
|
410
|
+
const { documentId } = ctx.params ?? {};
|
|
411
|
+
const { token } = ctx.request?.body ?? {};
|
|
412
|
+
const data = await getRsvpService(strapi).checkInAttendee({
|
|
413
|
+
documentId: optStr$1(documentId),
|
|
414
|
+
token: optStr$1(token)
|
|
415
|
+
});
|
|
416
|
+
ctx.body = { data };
|
|
417
|
+
} catch (err) {
|
|
418
|
+
return handleControllerError(strapi, ctx, err, "Check-in failed.");
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
async exportRegistrationsCsv(ctx) {
|
|
422
|
+
const q = ctx.query ?? {};
|
|
423
|
+
const csv = await getRsvpService(strapi).exportRegistrationsCsv({
|
|
424
|
+
eventDocumentId: optStr$1(q.eventDocumentId),
|
|
425
|
+
occurrenceDate: optStr$1(q.occurrenceDate),
|
|
426
|
+
status: optStr$1(q.status)
|
|
427
|
+
});
|
|
428
|
+
ctx.set("Content-Type", "text/csv; charset=utf-8");
|
|
429
|
+
ctx.set(
|
|
430
|
+
"Content-Disposition",
|
|
431
|
+
'attachment; filename="event-registrations.csv"'
|
|
432
|
+
);
|
|
433
|
+
ctx.set("X-Content-Type-Options", "nosniff");
|
|
434
|
+
ctx.body = csv;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
function isValidEmail(value) {
|
|
439
|
+
if (/\s/.test(value))
|
|
440
|
+
return false;
|
|
441
|
+
const at = value.indexOf("@");
|
|
442
|
+
if (at < 1 || at !== value.lastIndexOf("@"))
|
|
443
|
+
return false;
|
|
444
|
+
const domain = value.slice(at + 1);
|
|
445
|
+
const dot = domain.indexOf(".");
|
|
446
|
+
return dot > 0 && dot < domain.length - 1;
|
|
447
|
+
}
|
|
448
|
+
function freezeOptions(items) {
|
|
449
|
+
return Object.freeze(items.map((item) => Object.freeze({ ...item })));
|
|
450
|
+
}
|
|
451
|
+
freezeOptions([
|
|
452
|
+
{ value: "es", label: "es — Español (Spanish)" },
|
|
453
|
+
{ value: "en", label: "en — English" },
|
|
454
|
+
{ value: "ar", label: "ar — العربية (Arabic)" },
|
|
455
|
+
{ value: "tr", label: "tr — Türkçe (Turkish)" },
|
|
456
|
+
{ value: "fr", label: "fr — Français (French)" },
|
|
457
|
+
{ value: "de", label: "de — Deutsch (German)" },
|
|
458
|
+
{ value: "pt", label: "pt — Português (Portuguese)" },
|
|
459
|
+
{ value: "it", label: "it — Italiano (Italian)" },
|
|
460
|
+
{ value: "nl", label: "nl — Nederlands (Dutch)" },
|
|
461
|
+
{ value: "pl", label: "pl — Polski (Polish)" },
|
|
462
|
+
{ value: "ru", label: "ru — Русский (Russian)" },
|
|
463
|
+
{ value: "id", label: "id — Bahasa Indonesia" },
|
|
464
|
+
{ value: "ur", label: "ur — اردو (Urdu)" },
|
|
465
|
+
{ value: "zh", label: "zh — 中文 (Chinese)" }
|
|
466
|
+
]);
|
|
467
|
+
freezeOptions([
|
|
468
|
+
{
|
|
469
|
+
value: "islamic-umalqura",
|
|
470
|
+
label: "islamic-umalqura (Hijri Umm al-Qura)"
|
|
471
|
+
},
|
|
472
|
+
{ value: "islamic-civil", label: "islamic-civil (Tabular Islamic Civil)" },
|
|
473
|
+
{ value: "islamic-tbla", label: "islamic-tbla (Tabular Astronomical)" },
|
|
474
|
+
{ value: "hebrew", label: "hebrew (Hebrew)" },
|
|
475
|
+
{ value: "persian", label: "persian (Solar Hijri)" },
|
|
476
|
+
{ value: "none", label: "none (Gregorian Only)" }
|
|
477
|
+
]);
|
|
478
|
+
const DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set([
|
|
479
|
+
"__proto__",
|
|
480
|
+
"constructor",
|
|
481
|
+
"prototype"
|
|
482
|
+
]);
|
|
483
|
+
function isSafeObjectKey(key) {
|
|
484
|
+
return typeof key === "string" && key.length > 0 && !DANGEROUS_OBJECT_KEYS.has(key);
|
|
485
|
+
}
|
|
486
|
+
function sanitizePlainRecord(raw, maxDepth = 4) {
|
|
487
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw) || maxDepth < 0) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
const clean = {};
|
|
491
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
492
|
+
if (!isSafeObjectKey(k)) continue;
|
|
493
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
494
|
+
if (maxDepth === 0) continue;
|
|
495
|
+
clean[k] = sanitizePlainRecord(v, maxDepth - 1);
|
|
496
|
+
} else if (Array.isArray(v)) {
|
|
497
|
+
clean[k] = maxDepth === 0 ? [] : v.map(
|
|
498
|
+
(item) => item && typeof item === "object" && !Array.isArray(item) ? sanitizePlainRecord(item, maxDepth - 1) : item
|
|
499
|
+
);
|
|
500
|
+
} else {
|
|
501
|
+
clean[k] = v;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return clean;
|
|
505
|
+
}
|
|
506
|
+
const ALLOWED_ACTION_VARIANTS = /* @__PURE__ */ new Set([
|
|
507
|
+
"primary",
|
|
508
|
+
"secondary",
|
|
509
|
+
"outline"
|
|
510
|
+
]);
|
|
511
|
+
const HTTP_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:"]);
|
|
512
|
+
const ACTION_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "mailto:"]);
|
|
513
|
+
function isSafeRelativeUrl(trimmed) {
|
|
514
|
+
return trimmed.startsWith("/") && !trimmed.startsWith("//") && !trimmed.includes("\\");
|
|
515
|
+
}
|
|
516
|
+
function isAllowedAbsoluteUrl(trimmed, allowMailto) {
|
|
517
|
+
try {
|
|
518
|
+
const parsed = new URL(trimmed);
|
|
519
|
+
const proto = parsed.protocol.toLowerCase();
|
|
520
|
+
return allowMailto ? ACTION_PROTOCOLS.has(proto) : HTTP_PROTOCOLS.has(proto);
|
|
521
|
+
} catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function sanitizeEventUrl(rawUrl, options2) {
|
|
526
|
+
if (typeof rawUrl !== "string") return null;
|
|
527
|
+
const trimmed = rawUrl.trim();
|
|
528
|
+
if (!trimmed || trimmed.length > 2048 || /[\u0000-\u001F\u007F]/.test(trimmed)) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
const allowMailto = options2?.allowMailto ?? true;
|
|
532
|
+
if (isSafeRelativeUrl(trimmed) || isAllowedAbsoluteUrl(trimmed, allowMailto)) {
|
|
533
|
+
return trimmed;
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
function parseJsonIfNeeded(raw) {
|
|
538
|
+
if (typeof raw !== "string") return raw;
|
|
539
|
+
const trimmed = raw.trim();
|
|
540
|
+
const isJsonLike = trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
541
|
+
if (!isJsonLike) return raw;
|
|
542
|
+
try {
|
|
543
|
+
return JSON.parse(trimmed);
|
|
544
|
+
} catch {
|
|
545
|
+
return raw;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function getTrimmedString(val, maxLen) {
|
|
549
|
+
if (typeof val !== "string") return void 0;
|
|
550
|
+
const trimmed = val.trim();
|
|
551
|
+
if (!trimmed) return void 0;
|
|
552
|
+
return maxLen ? trimmed.slice(0, maxLen) : trimmed;
|
|
553
|
+
}
|
|
554
|
+
function getPositiveInteger(val) {
|
|
555
|
+
if (typeof val !== "number" || !Number.isFinite(val) || val <= 0) {
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
return Math.round(val);
|
|
559
|
+
}
|
|
560
|
+
function unwrapPosterRecord(obj) {
|
|
561
|
+
if (obj.data && typeof obj.data === "object" && !Array.isArray(obj.data)) {
|
|
562
|
+
return unwrapPosterRecord(obj.data);
|
|
563
|
+
}
|
|
564
|
+
if (obj.attributes && typeof obj.attributes === "object" && !Array.isArray(obj.attributes)) {
|
|
565
|
+
return unwrapPosterRecord(obj.attributes);
|
|
566
|
+
}
|
|
567
|
+
return obj;
|
|
568
|
+
}
|
|
569
|
+
function resolvePosterAltText(obj, fallbackAlt) {
|
|
570
|
+
return getTrimmedString(obj.alt) || getTrimmedString(obj.alternativeText) || getTrimmedString(obj.caption) || fallbackAlt || void 0;
|
|
571
|
+
}
|
|
572
|
+
function normalizeEventPoster(rawPoster, fallbackAlt) {
|
|
573
|
+
if (!rawPoster) return void 0;
|
|
574
|
+
if (typeof rawPoster === "string") {
|
|
575
|
+
const safeUrl2 = sanitizeEventUrl(rawPoster, { allowMailto: false });
|
|
576
|
+
return safeUrl2 ? { url: safeUrl2, alt: fallbackAlt || void 0 } : void 0;
|
|
577
|
+
}
|
|
578
|
+
if (Array.isArray(rawPoster)) {
|
|
579
|
+
return rawPoster.length > 0 ? normalizeEventPoster(rawPoster[0], fallbackAlt) : void 0;
|
|
580
|
+
}
|
|
581
|
+
if (typeof rawPoster !== "object") return void 0;
|
|
582
|
+
const obj = unwrapPosterRecord(rawPoster);
|
|
583
|
+
const safeUrl = sanitizeEventUrl(obj.url, { allowMailto: false });
|
|
584
|
+
if (!safeUrl) return void 0;
|
|
585
|
+
return {
|
|
586
|
+
url: safeUrl,
|
|
587
|
+
alt: resolvePosterAltText(obj, fallbackAlt),
|
|
588
|
+
mime: getTrimmedString(obj.mime),
|
|
589
|
+
width: getPositiveInteger(obj.width),
|
|
590
|
+
height: getPositiveInteger(obj.height)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function resolveActionVariant(rawVariant, index2) {
|
|
594
|
+
const candidate = getTrimmedString(rawVariant);
|
|
595
|
+
if (candidate && ALLOWED_ACTION_VARIANTS.has(candidate)) {
|
|
596
|
+
return candidate;
|
|
597
|
+
}
|
|
598
|
+
return index2 === 0 ? "primary" : "outline";
|
|
599
|
+
}
|
|
600
|
+
function normalizeSingleActionLink(item, index2) {
|
|
601
|
+
if (!item || typeof item !== "object") return null;
|
|
602
|
+
const rec = item;
|
|
603
|
+
const label = getTrimmedString(rec.label, 120);
|
|
604
|
+
const safeUrl = sanitizeEventUrl(rec.url);
|
|
605
|
+
if (!label || !safeUrl) return null;
|
|
606
|
+
const external = typeof rec.external === "boolean" ? rec.external : /^https?:\/\//i.test(safeUrl);
|
|
607
|
+
const icon = getTrimmedString(rec.icon, 60);
|
|
608
|
+
return {
|
|
609
|
+
label,
|
|
610
|
+
url: safeUrl,
|
|
611
|
+
variant: resolveActionVariant(rec.variant, index2),
|
|
612
|
+
external,
|
|
613
|
+
...icon ? { icon } : {}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function normalizeEventActionLinks(rawLinks) {
|
|
617
|
+
const parsed = parseJsonIfNeeded(rawLinks);
|
|
618
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
const normalized = parsed.map((item, idx) => normalizeSingleActionLink(item, idx)).filter((link) => link !== null);
|
|
622
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
623
|
+
}
|
|
624
|
+
function isValidCalendarDateKey(dateStr) {
|
|
625
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
const parsed = /* @__PURE__ */ new Date(`${dateStr}T00:00:00.000Z`);
|
|
629
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
return parsed.toISOString().slice(0, 10) === dateStr;
|
|
633
|
+
}
|
|
634
|
+
function toDateKey(dateInput) {
|
|
635
|
+
const d = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
|
|
636
|
+
if (Number.isNaN(d.getTime())) {
|
|
637
|
+
return "";
|
|
638
|
+
}
|
|
639
|
+
return d.toISOString().slice(0, 10);
|
|
640
|
+
}
|
|
641
|
+
function normalizeRRuleString(raw) {
|
|
642
|
+
if (!raw || typeof raw !== "string") {
|
|
643
|
+
return "";
|
|
644
|
+
}
|
|
645
|
+
return raw.replace(/[\u0000-\u001F\u007F]+/g, "").trim().replace(/^RRULE:/i, "").trim();
|
|
646
|
+
}
|
|
647
|
+
function sanitizeOccurrenceOverrides(rawOverrides) {
|
|
648
|
+
const clean = {};
|
|
649
|
+
if (!rawOverrides || typeof rawOverrides !== "object" || Array.isArray(rawOverrides)) {
|
|
650
|
+
return clean;
|
|
651
|
+
}
|
|
652
|
+
for (const [key, ov] of Object.entries(rawOverrides)) {
|
|
653
|
+
if (!isSafeObjectKey(key) || !isValidCalendarDateKey(key)) continue;
|
|
654
|
+
if (!ov || typeof ov !== "object" || Array.isArray(ov)) continue;
|
|
655
|
+
let validStart;
|
|
656
|
+
let startMs = null;
|
|
657
|
+
if (ov.startDate !== void 0 && ov.startDate !== "") {
|
|
658
|
+
if (typeof ov.startDate !== "string") continue;
|
|
659
|
+
const parsedStart = new Date(ov.startDate).getTime();
|
|
660
|
+
if (Number.isNaN(parsedStart)) continue;
|
|
661
|
+
validStart = ov.startDate;
|
|
662
|
+
startMs = parsedStart;
|
|
663
|
+
}
|
|
664
|
+
let validEnd;
|
|
665
|
+
if (ov.endDate !== void 0 && ov.endDate !== "") {
|
|
666
|
+
if (typeof ov.endDate !== "string") continue;
|
|
667
|
+
const parsedEnd = new Date(ov.endDate).getTime();
|
|
668
|
+
if (Number.isNaN(parsedEnd)) continue;
|
|
669
|
+
const minStartMs = startMs ?? (/* @__PURE__ */ new Date(`${key}T00:00:00.000Z`)).getTime();
|
|
670
|
+
if (parsedEnd < minStartMs) continue;
|
|
671
|
+
validEnd = ov.endDate;
|
|
672
|
+
}
|
|
673
|
+
clean[key] = {
|
|
674
|
+
...ov,
|
|
675
|
+
originalDate: typeof ov.originalDate === "string" && isValidCalendarDateKey(ov.originalDate) ? ov.originalDate : key,
|
|
676
|
+
startDate: validStart,
|
|
677
|
+
endDate: validEnd
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return clean;
|
|
681
|
+
}
|
|
682
|
+
function sanitizeRRuleFieldValue(rec, defaultTimezone = "UTC") {
|
|
683
|
+
const rrule = normalizeRRuleString(rec?.rrule);
|
|
684
|
+
const timezone = typeof rec?.timezone === "string" && rec.timezone.trim() ? rec.timezone.trim() : defaultTimezone;
|
|
685
|
+
const exdates = Array.isArray(rec?.exdates) ? rec.exdates.filter(
|
|
686
|
+
(d) => typeof d === "string" && isValidCalendarDateKey(d)
|
|
687
|
+
) : [];
|
|
688
|
+
const overrides = sanitizeOccurrenceOverrides(rec?.overrides);
|
|
689
|
+
return {
|
|
690
|
+
...rec,
|
|
691
|
+
rrule,
|
|
692
|
+
timezone,
|
|
693
|
+
allDay: Boolean(rec?.allDay),
|
|
694
|
+
exdates,
|
|
695
|
+
overrides
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function parseRRuleFieldValue(raw, defaultTimezone = "UTC") {
|
|
699
|
+
const parsed = shared.parseRRuleFieldValue(raw, defaultTimezone);
|
|
700
|
+
return sanitizeRRuleFieldValue(parsed, defaultTimezone);
|
|
701
|
+
}
|
|
702
|
+
function buildRRule(parsed) {
|
|
703
|
+
return shared.buildRRule({
|
|
704
|
+
freq: parsed.freq ?? "NONE",
|
|
705
|
+
interval: parsed.interval ?? 1,
|
|
706
|
+
byDay: parsed.byDay ?? [],
|
|
707
|
+
byMonthDay: parsed.byMonthDay,
|
|
708
|
+
count: parsed.count,
|
|
709
|
+
until: parsed.until
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
const FREQ_UNITS = {
|
|
713
|
+
DAILY: { singular: "day", plural: "days" },
|
|
714
|
+
WEEKLY: { singular: "week", plural: "weeks" },
|
|
715
|
+
MONTHLY: { singular: "month", plural: "months" },
|
|
716
|
+
YEARLY: { singular: "year", plural: "years" }
|
|
717
|
+
};
|
|
718
|
+
function formatIntervalUnit(freq, interval) {
|
|
719
|
+
const unit = Object.hasOwn(FREQ_UNITS, freq) ? FREQ_UNITS[freq] : FREQ_UNITS.YEARLY;
|
|
720
|
+
return interval === 1 ? unit.singular : `${interval} ${unit.plural}`;
|
|
721
|
+
}
|
|
722
|
+
function describeRRule(rruleString) {
|
|
723
|
+
const parsed = shared.parseRRule(normalizeRRuleString(rruleString));
|
|
724
|
+
if (parsed.freq === "NONE") {
|
|
725
|
+
return "Does not repeat";
|
|
726
|
+
}
|
|
727
|
+
let summary = `Every ${formatIntervalUnit(parsed.freq, parsed.interval)}`;
|
|
728
|
+
if (parsed.freq === "WEEKLY" && parsed.byDay.length > 0) {
|
|
729
|
+
summary += ` on ${parsed.byDay.join(", ")}`;
|
|
730
|
+
} else if (parsed.freq === "MONTHLY" && parsed.byMonthDay) {
|
|
731
|
+
summary += ` on day ${parsed.byMonthDay}`;
|
|
732
|
+
}
|
|
733
|
+
if (parsed.until) {
|
|
734
|
+
summary += ` until ${parsed.until}`;
|
|
735
|
+
} else if (parsed.count) {
|
|
736
|
+
summary += ` (${parsed.count} occurrences)`;
|
|
737
|
+
}
|
|
738
|
+
return summary;
|
|
739
|
+
}
|
|
740
|
+
function expandRuleSlots(options2) {
|
|
741
|
+
const baseStartMs = new Date(options2.baseStartDate).getTime();
|
|
742
|
+
if (Number.isNaN(baseStartMs)) {
|
|
743
|
+
return [];
|
|
744
|
+
}
|
|
745
|
+
let safeBaseEnd = null;
|
|
746
|
+
if (typeof options2.baseEndDate === "string" && options2.baseEndDate) {
|
|
747
|
+
const baseEndMs = new Date(options2.baseEndDate).getTime();
|
|
748
|
+
if (!Number.isNaN(baseEndMs) && baseEndMs >= baseStartMs) {
|
|
749
|
+
safeBaseEnd = options2.baseEndDate;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const windowStartIso = typeof options2.windowStart === "string" ? options2.windowStart : options2.windowStart.toISOString();
|
|
753
|
+
const windowEndIso = typeof options2.windowEnd === "string" ? options2.windowEnd : options2.windowEnd.toISOString();
|
|
754
|
+
if (Number.isNaN(new Date(windowStartIso).getTime()) || Number.isNaN(new Date(windowEndIso).getTime())) {
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
757
|
+
return shared.expandRuleSlots({
|
|
758
|
+
baseStartDate: options2.baseStartDate,
|
|
759
|
+
baseEndDate: safeBaseEnd,
|
|
760
|
+
recurrence: sanitizeRRuleFieldValue(
|
|
761
|
+
options2.recurrence,
|
|
762
|
+
options2.recurrence?.timezone || "UTC"
|
|
763
|
+
),
|
|
764
|
+
windowStart: windowStartIso,
|
|
765
|
+
windowEnd: windowEndIso,
|
|
766
|
+
maxOccurrences: options2.maxInstances ?? 366
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
const ALLOWED_SPOTLIGHT_POSITIONS = /* @__PURE__ */ new Set([
|
|
770
|
+
"bottom-end",
|
|
771
|
+
"bottom-start"
|
|
772
|
+
]);
|
|
773
|
+
function resolveSpotlightPosition(rawPos) {
|
|
774
|
+
const candidate = getTrimmedString(rawPos);
|
|
775
|
+
return candidate && ALLOWED_SPOTLIGHT_POSITIONS.has(candidate) ? candidate : void 0;
|
|
776
|
+
}
|
|
777
|
+
function resolveFiniteRoundNumber(val) {
|
|
778
|
+
return typeof val === "number" && Number.isFinite(val) ? Math.round(val) : void 0;
|
|
779
|
+
}
|
|
780
|
+
function normalizeEventSpotlightConfig(rawConfig) {
|
|
781
|
+
const parsed = parseJsonIfNeeded(rawConfig);
|
|
782
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
const rec = parsed;
|
|
786
|
+
const config2 = {
|
|
787
|
+
enabled: Boolean(rec.enabled)
|
|
788
|
+
};
|
|
789
|
+
const badgeText = getTrimmedString(rec.badgeText, 80);
|
|
790
|
+
if (badgeText) config2.badgeText = badgeText;
|
|
791
|
+
const priority = resolveFiniteRoundNumber(rec.priority);
|
|
792
|
+
if (priority !== void 0) config2.priority = priority;
|
|
793
|
+
const position = resolveSpotlightPosition(rec.position);
|
|
794
|
+
if (position) config2.position = position;
|
|
795
|
+
if (typeof rec.showCountdown === "boolean") {
|
|
796
|
+
config2.showCountdown = rec.showCountdown;
|
|
797
|
+
}
|
|
798
|
+
const startAt = getTrimmedString(rec.startAt);
|
|
799
|
+
if (startAt) config2.startAt = startAt;
|
|
800
|
+
const endAt = getTrimmedString(rec.endAt);
|
|
801
|
+
if (endAt) config2.endAt = endAt;
|
|
802
|
+
return config2;
|
|
803
|
+
}
|
|
804
|
+
function toBoundaryTimestamp(rawIsoOrDate, isEndBoundary) {
|
|
805
|
+
const trimmed = rawIsoOrDate.trim();
|
|
806
|
+
const isoString = /^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? `${trimmed}${isEndBoundary ? "T23:59:59.999Z" : "T00:00:00.000Z"}` : trimmed;
|
|
807
|
+
const ts = new Date(isoString).getTime();
|
|
808
|
+
return Number.isNaN(ts) ? null : ts;
|
|
809
|
+
}
|
|
810
|
+
function toReferenceTimestamp(referenceDate) {
|
|
811
|
+
if (referenceDate instanceof Date) {
|
|
812
|
+
const ts2 = referenceDate.getTime();
|
|
813
|
+
return Number.isNaN(ts2) ? null : ts2;
|
|
814
|
+
}
|
|
815
|
+
const str = getTrimmedString(referenceDate);
|
|
816
|
+
const dt = str ? new Date(str) : /* @__PURE__ */ new Date();
|
|
817
|
+
const ts = dt.getTime();
|
|
818
|
+
return Number.isNaN(ts) ? null : ts;
|
|
819
|
+
}
|
|
820
|
+
function isEventSpotlightActiveOnDate(params) {
|
|
821
|
+
const { spotlightConfig, eventStartDate, eventEndDate, referenceDate } = params;
|
|
822
|
+
if (!spotlightConfig?.enabled) return false;
|
|
823
|
+
const refTs = toReferenceTimestamp(referenceDate);
|
|
824
|
+
if (refTs === null) return false;
|
|
825
|
+
const startTs = toBoundaryTimestamp(
|
|
826
|
+
spotlightConfig.startAt || eventStartDate,
|
|
827
|
+
false
|
|
828
|
+
);
|
|
829
|
+
const endTs = toBoundaryTimestamp(
|
|
830
|
+
spotlightConfig.endAt || eventEndDate || eventStartDate,
|
|
831
|
+
true
|
|
832
|
+
);
|
|
833
|
+
const afterStart = startTs === null || refTs >= startTs;
|
|
834
|
+
const beforeEnd = endTs === null || refTs <= endTs;
|
|
835
|
+
return afterStart && beforeEnd;
|
|
836
|
+
}
|
|
837
|
+
function deduplicateAndSortSpotlights(occurrences) {
|
|
838
|
+
const byDocKey = /* @__PURE__ */ new Map();
|
|
839
|
+
for (const occ of occurrences) {
|
|
840
|
+
if (!occ.spotlightConfig?.enabled || occ.status === "cancelled") {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const key = `${occ.sourceId}:${occ.documentId}`;
|
|
844
|
+
if (!byDocKey.has(key)) {
|
|
845
|
+
byDocKey.set(key, occ);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const unique = Array.from(byDocKey.values());
|
|
849
|
+
unique.sort((a, b) => {
|
|
850
|
+
const prioA = a.spotlightConfig?.priority ?? 0;
|
|
851
|
+
const prioB = b.spotlightConfig?.priority ?? 0;
|
|
852
|
+
if (prioB !== prioA) {
|
|
853
|
+
return prioB - prioA;
|
|
854
|
+
}
|
|
855
|
+
return new Date(a.startDate).getTime() - new Date(b.startDate).getTime();
|
|
856
|
+
});
|
|
857
|
+
return unique;
|
|
858
|
+
}
|
|
859
|
+
const DEFAULT_TIME_SLOTS = {
|
|
860
|
+
morning: { start: "09:00:00", end: "11:00:00" },
|
|
861
|
+
afternoon: { start: "14:00:00", end: "16:00:00" },
|
|
862
|
+
evening: { start: "18:00:00", end: "20:00:00" }
|
|
863
|
+
};
|
|
864
|
+
function buildSafeSlotMap(customSlots) {
|
|
865
|
+
const slotMap = { ...DEFAULT_TIME_SLOTS };
|
|
866
|
+
if (customSlots && typeof customSlots === "object") {
|
|
867
|
+
for (const [k, v] of Object.entries(customSlots)) {
|
|
868
|
+
if (isSafeObjectKey(k) && v && typeof v === "object") {
|
|
869
|
+
slotMap[k] = v;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return slotMap;
|
|
874
|
+
}
|
|
875
|
+
function resolveDateOnlySlotWindow(rawStart, rawSlot, source) {
|
|
876
|
+
if (!isSafeObjectKey(rawSlot)) return null;
|
|
877
|
+
const slotMap = buildSafeSlotMap(source.timeSlotMap);
|
|
878
|
+
const mapped = Object.hasOwn(slotMap, rawSlot) ? slotMap[rawSlot] : void 0;
|
|
879
|
+
if (mapped) {
|
|
880
|
+
return {
|
|
881
|
+
startDate: `${rawStart}T${mapped.start}.000Z`,
|
|
882
|
+
endDate: `${rawStart}T${mapped.end}.000Z`,
|
|
883
|
+
allDay: false
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
if (/^\d{2}:\d{2}(:\d{2})?$/.test(rawSlot)) {
|
|
887
|
+
const normalizedTime = rawSlot.length === 5 ? `${rawSlot}:00` : rawSlot;
|
|
888
|
+
return {
|
|
889
|
+
startDate: `${rawStart}T${normalizedTime}.000Z`,
|
|
890
|
+
endDate: null,
|
|
891
|
+
allDay: false
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
function resolveDateOnlyEnd(rawStart, rawEnd) {
|
|
897
|
+
if (typeof rawEnd === "string" && rawEnd) {
|
|
898
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(rawEnd) ? `${rawEnd}T23:59:59.000Z` : rawEnd;
|
|
899
|
+
}
|
|
900
|
+
return `${rawStart}T23:59:59.000Z`;
|
|
901
|
+
}
|
|
902
|
+
function readOptionalDocField(doc, fieldName) {
|
|
903
|
+
return fieldName && isSafeObjectKey(fieldName) ? doc[fieldName] : null;
|
|
904
|
+
}
|
|
905
|
+
function resolveNonEmptyEnd(rawEnd) {
|
|
906
|
+
return typeof rawEnd === "string" && rawEnd ? rawEnd : null;
|
|
907
|
+
}
|
|
908
|
+
function resolveStartEndFromDocument(doc, source) {
|
|
909
|
+
const startField = source.fields.startDate;
|
|
910
|
+
const rawStart = isSafeObjectKey(startField) ? doc[startField] : void 0;
|
|
911
|
+
if (typeof rawStart !== "string" || !rawStart) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
const rawEnd = readOptionalDocField(doc, source.fields.endDate);
|
|
915
|
+
const rawSlot = readOptionalDocField(doc, source.fields.timeSlot);
|
|
916
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(rawStart)) {
|
|
917
|
+
const slotWindow = resolveDateOnlySlotWindow(rawStart, rawSlot, source);
|
|
918
|
+
if (slotWindow) return slotWindow;
|
|
919
|
+
return {
|
|
920
|
+
startDate: `${rawStart}T00:00:00.000Z`,
|
|
921
|
+
endDate: resolveDateOnlyEnd(rawStart, rawEnd),
|
|
922
|
+
allDay: source.defaultAllDay ?? true
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
const allDay = typeof doc.allDay === "boolean" ? doc.allDay : Boolean(source.defaultAllDay);
|
|
926
|
+
return {
|
|
927
|
+
startDate: rawStart,
|
|
928
|
+
endDate: resolveNonEmptyEnd(rawEnd),
|
|
929
|
+
allDay
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const EVENT_STATUS_ALIAS_MAP = {
|
|
933
|
+
cancelled: "cancelled",
|
|
934
|
+
canceled: "cancelled",
|
|
935
|
+
postponed: "postponed",
|
|
936
|
+
pending: "postponed",
|
|
937
|
+
rescheduled: "rescheduled"
|
|
938
|
+
};
|
|
939
|
+
function mapEventStatus(rawStatus) {
|
|
940
|
+
if (typeof rawStatus !== "string") return "scheduled";
|
|
941
|
+
const lower = rawStatus.toLowerCase();
|
|
942
|
+
return Object.hasOwn(EVENT_STATUS_ALIAS_MAP, lower) ? EVENT_STATUS_ALIAS_MAP[lower] : "scheduled";
|
|
943
|
+
}
|
|
944
|
+
const API_CONTENT_TYPE_UID_RE = /^api::[a-z0-9-]+\.[a-z0-9-]+$/i;
|
|
945
|
+
const SENSITIVE_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
946
|
+
"password",
|
|
947
|
+
"resetpasswordtoken",
|
|
948
|
+
"validationtoken",
|
|
949
|
+
"confirmationtoken",
|
|
950
|
+
"checkintoken",
|
|
951
|
+
"checkintokenhash",
|
|
952
|
+
"canceltoken",
|
|
953
|
+
"canceltokenhash"
|
|
954
|
+
]);
|
|
955
|
+
function isAllowedEventSourceUid(uid) {
|
|
956
|
+
if (typeof uid !== "string") return false;
|
|
957
|
+
const trimmed = uid.trim();
|
|
958
|
+
if (!trimmed) return false;
|
|
959
|
+
return trimmed === EVENT_CONTENT_TYPE_UID || API_CONTENT_TYPE_UID_RE.test(trimmed);
|
|
960
|
+
}
|
|
961
|
+
function extractCustomFields(strapi, sourceUid, doc, customFieldKeys) {
|
|
962
|
+
if (!Array.isArray(customFieldKeys) || customFieldKeys.length === 0) {
|
|
963
|
+
return void 0;
|
|
964
|
+
}
|
|
965
|
+
const schemaAttrs = strapi.contentTypes?.[sourceUid]?.attributes;
|
|
966
|
+
const extracted = {};
|
|
967
|
+
for (const key of customFieldKeys) {
|
|
968
|
+
if (!isSafeObjectKey(key)) continue;
|
|
969
|
+
if (SENSITIVE_FIELD_NAMES.has(key.toLowerCase())) continue;
|
|
970
|
+
const attrDef = schemaAttrs && Object.hasOwn(schemaAttrs, key) ? schemaAttrs[key] : void 0;
|
|
971
|
+
if (attrDef?.private === true) continue;
|
|
972
|
+
if (Object.hasOwn(doc, key) && doc[key] !== void 0) {
|
|
973
|
+
extracted[key] = doc[key];
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return Object.keys(extracted).length > 0 ? extracted : void 0;
|
|
977
|
+
}
|
|
978
|
+
function buildEmptySlotRsvpStats(meta) {
|
|
979
|
+
const capacity = meta.capacity;
|
|
980
|
+
return {
|
|
981
|
+
eventUid: meta.eventUid,
|
|
982
|
+
eventDocumentId: meta.eventDocumentId,
|
|
983
|
+
occurrenceDate: meta.occurrenceDate,
|
|
984
|
+
rsvpEnabled: meta.rsvpEnabled,
|
|
985
|
+
capacity,
|
|
986
|
+
waitlistEnabled: meta.waitlistEnabled,
|
|
987
|
+
confirmedCount: 0,
|
|
988
|
+
confirmedSeats: 0,
|
|
989
|
+
checkedInSeats: 0,
|
|
990
|
+
waitlistedCount: 0,
|
|
991
|
+
waitlistedSeats: 0,
|
|
992
|
+
cancelledCount: 0,
|
|
993
|
+
remainingSeats: capacity !== null ? Math.max(0, capacity) : null,
|
|
994
|
+
isFull: capacity !== null ? capacity <= 0 : false
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
async function attachBatchedRsvpStats(strapi, occurrences, pendingByOccurrenceId) {
|
|
998
|
+
if (pendingByOccurrenceId.size === 0 || occurrences.length === 0) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const rsvpDocIdSet = /* @__PURE__ */ new Set();
|
|
1002
|
+
const occDateSet = /* @__PURE__ */ new Set();
|
|
1003
|
+
for (const meta of pendingByOccurrenceId.values()) {
|
|
1004
|
+
if (meta.rsvpEnabled && meta.eventDocumentId && meta.occurrenceDate) {
|
|
1005
|
+
rsvpDocIdSet.add(meta.eventDocumentId);
|
|
1006
|
+
occDateSet.add(meta.occurrenceDate);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (rsvpDocIdSet.size === 0 || occDateSet.size === 0) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const rsvpDocIds = Array.from(rsvpDocIdSet);
|
|
1013
|
+
const occDates = Array.from(occDateSet);
|
|
1014
|
+
let rows;
|
|
1015
|
+
try {
|
|
1016
|
+
rows = await getDocumentsClient(
|
|
1017
|
+
strapi,
|
|
1018
|
+
REGISTRATION_CONTENT_TYPE_UID
|
|
1019
|
+
).findMany({
|
|
1020
|
+
filters: {
|
|
1021
|
+
eventDocumentId: { $in: rsvpDocIds },
|
|
1022
|
+
occurrenceDate: { $in: occDates }
|
|
1023
|
+
},
|
|
1024
|
+
limit: 1e4
|
|
1025
|
+
});
|
|
1026
|
+
} catch {
|
|
1027
|
+
rows = [];
|
|
1028
|
+
}
|
|
1029
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1030
|
+
for (const row of rows) {
|
|
1031
|
+
const docId = typeof row.eventDocumentId === "string" ? row.eventDocumentId : "";
|
|
1032
|
+
const occDate = typeof row.occurrenceDate === "string" ? row.occurrenceDate : "";
|
|
1033
|
+
if (!docId || !occDate || !rsvpDocIdSet.has(docId)) continue;
|
|
1034
|
+
const slotKey = `${docId}::${occDate}`;
|
|
1035
|
+
const existingBucket = buckets.get(slotKey);
|
|
1036
|
+
if (existingBucket) {
|
|
1037
|
+
existingBucket.push(row);
|
|
1038
|
+
} else {
|
|
1039
|
+
buckets.set(slotKey, [row]);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
for (const occ of occurrences) {
|
|
1043
|
+
const meta = pendingByOccurrenceId.get(occ.occurrenceId);
|
|
1044
|
+
if (!meta || !meta.rsvpEnabled) continue;
|
|
1045
|
+
const slotKey = `${meta.eventDocumentId}::${meta.occurrenceDate}`;
|
|
1046
|
+
const slotRecords = buckets.get(slotKey);
|
|
1047
|
+
if (!slotRecords || slotRecords.length === 0) {
|
|
1048
|
+
occ.rsvp = buildEmptySlotRsvpStats(meta);
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
const counts = aggregateRsvpCounts(slotRecords);
|
|
1052
|
+
const remainingSeats = meta.capacity !== null ? Math.max(0, meta.capacity - counts.confirmedSeats) : null;
|
|
1053
|
+
const isFull = meta.capacity !== null ? counts.confirmedSeats >= meta.capacity : false;
|
|
1054
|
+
occ.rsvp = {
|
|
1055
|
+
eventUid: meta.eventUid,
|
|
1056
|
+
eventDocumentId: meta.eventDocumentId,
|
|
1057
|
+
occurrenceDate: meta.occurrenceDate,
|
|
1058
|
+
rsvpEnabled: meta.rsvpEnabled,
|
|
1059
|
+
capacity: meta.capacity,
|
|
1060
|
+
waitlistEnabled: meta.waitlistEnabled,
|
|
1061
|
+
...counts,
|
|
1062
|
+
remainingSeats,
|
|
1063
|
+
isFull
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function maskEmail(email) {
|
|
1068
|
+
const [local, domain] = (email || "").split("@");
|
|
1069
|
+
if (!local || !domain) return "***";
|
|
1070
|
+
const visible = local.slice(0, Math.min(2, local.length));
|
|
1071
|
+
return `${visible}***@${domain}`;
|
|
1072
|
+
}
|
|
1073
|
+
function readNullablePositiveInt(val) {
|
|
1074
|
+
if (typeof val === "number") {
|
|
1075
|
+
return Number.isFinite(val) && val >= 1 ? Math.floor(val) : null;
|
|
1076
|
+
}
|
|
1077
|
+
if (typeof val === "string") {
|
|
1078
|
+
const trimmed = val.trim();
|
|
1079
|
+
if (!/^\d+$/.test(trimmed)) return null;
|
|
1080
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
1081
|
+
return Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : null;
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
function readBooleanWithDefault(val, fallback) {
|
|
1086
|
+
return typeof val === "boolean" ? val : fallback;
|
|
1087
|
+
}
|
|
1088
|
+
function readMaxGuestsWithDefault(val, fallback) {
|
|
1089
|
+
const parsed = readNullablePositiveInt(val);
|
|
1090
|
+
return parsed !== null ? Math.min(50, parsed) : fallback;
|
|
1091
|
+
}
|
|
1092
|
+
function extractRawDeadline(rawDeadline) {
|
|
1093
|
+
if (rawDeadline === null || rawDeadline === void 0 || rawDeadline === "") {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
if (rawDeadline instanceof Date) {
|
|
1097
|
+
return Number.isNaN(rawDeadline.getTime()) ? "INVALID_DEADLINE" : rawDeadline.toISOString();
|
|
1098
|
+
}
|
|
1099
|
+
if (typeof rawDeadline === "string") {
|
|
1100
|
+
const trimmed = rawDeadline.trim();
|
|
1101
|
+
return trimmed ? trimmed : null;
|
|
1102
|
+
}
|
|
1103
|
+
return "INVALID_DEADLINE";
|
|
1104
|
+
}
|
|
1105
|
+
function buildResolvedTargetEvent(doc, src, eventDocumentId, settings2) {
|
|
1106
|
+
const startEnd = resolveStartEndFromDocument(doc, src);
|
|
1107
|
+
if (!startEnd) {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
const f = src.fields;
|
|
1111
|
+
const title = String(
|
|
1112
|
+
(isSafeObjectKey(f.title) ? doc[f.title] : void 0) ?? doc.title ?? "Event"
|
|
1113
|
+
);
|
|
1114
|
+
const rsvpField = f.rsvpEnabled && isSafeObjectKey(f.rsvpEnabled) ? f.rsvpEnabled : "rsvpEnabled";
|
|
1115
|
+
const capField = f.capacity && isSafeObjectKey(f.capacity) ? f.capacity : "capacity";
|
|
1116
|
+
const statusField = f.status && isSafeObjectKey(f.status) ? f.status : "eventStatus";
|
|
1117
|
+
const rruleField = f.rrule && isSafeObjectKey(f.rrule) ? f.rrule : void 0;
|
|
1118
|
+
const docTz = typeof doc.timezone === "string" && doc.timezone ? doc.timezone : settings2.defaultTimezone;
|
|
1119
|
+
const recurrence = parseRRuleFieldValue(
|
|
1120
|
+
rruleField ? doc[rruleField] : doc.recurrence,
|
|
1121
|
+
docTz
|
|
1122
|
+
);
|
|
1123
|
+
if (startEnd.allDay) {
|
|
1124
|
+
recurrence.allDay = true;
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
uid: src.uid,
|
|
1128
|
+
documentId: eventDocumentId,
|
|
1129
|
+
title,
|
|
1130
|
+
startDate: startEnd.startDate,
|
|
1131
|
+
endDate: startEnd.endDate,
|
|
1132
|
+
allDay: startEnd.allDay,
|
|
1133
|
+
recurrence,
|
|
1134
|
+
eventStatus: mapEventStatus(doc[statusField]),
|
|
1135
|
+
rsvpEnabled: doc[rsvpField] !== void 0 ? Boolean(doc[rsvpField]) : false,
|
|
1136
|
+
capacity: readNullablePositiveInt(doc[capField]),
|
|
1137
|
+
waitlistEnabled: readBooleanWithDefault(
|
|
1138
|
+
doc.waitlistEnabled,
|
|
1139
|
+
settings2.waitlistEnabledByDefault
|
|
1140
|
+
),
|
|
1141
|
+
maxGuestsPerRegistration: readMaxGuestsWithDefault(
|
|
1142
|
+
doc.maxGuestsPerRegistration,
|
|
1143
|
+
settings2.defaultMaxGuestsPerRegistration
|
|
1144
|
+
),
|
|
1145
|
+
registrationDeadline: extractRawDeadline(doc.registrationDeadline)
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
async function resolveTargetEvent(strapi, eventDocumentId, preferredUid, options2) {
|
|
1149
|
+
const includeDrafts = options2?.includeDrafts ?? false;
|
|
1150
|
+
const settings2 = await getSettingsService(strapi).getSettings();
|
|
1151
|
+
const sources = (settings2.sources ?? []).filter(
|
|
1152
|
+
(s) => isAllowedEventSourceUid(s.uid)
|
|
1153
|
+
);
|
|
1154
|
+
const candidateSources = preferredUid ? [
|
|
1155
|
+
...sources.filter((s) => s.uid === preferredUid),
|
|
1156
|
+
...sources.filter((s) => s.uid !== preferredUid)
|
|
1157
|
+
] : sources;
|
|
1158
|
+
for (const src of candidateSources) {
|
|
1159
|
+
try {
|
|
1160
|
+
const doc = await getDocumentsClient(strapi, src.uid).findOne({
|
|
1161
|
+
documentId: eventDocumentId,
|
|
1162
|
+
...includeDrafts ? {} : { status: "published" }
|
|
1163
|
+
});
|
|
1164
|
+
if (!doc) continue;
|
|
1165
|
+
if (!includeDrafts && doc.publishedAt === null) {
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
const resolved = buildResolvedTargetEvent(
|
|
1169
|
+
doc,
|
|
1170
|
+
src,
|
|
1171
|
+
eventDocumentId,
|
|
1172
|
+
settings2
|
|
1173
|
+
);
|
|
1174
|
+
if (resolved) return resolved;
|
|
1175
|
+
} catch {
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
function readOptionalText(val) {
|
|
1181
|
+
return val ? String(val) : void 0;
|
|
1182
|
+
}
|
|
1183
|
+
function readStoredGuestsCount(val) {
|
|
1184
|
+
const parsed = readNullablePositiveInt(val);
|
|
1185
|
+
return parsed ?? 1;
|
|
1186
|
+
}
|
|
1187
|
+
function assertValidGuestsCount(val) {
|
|
1188
|
+
if (val === void 0) {
|
|
1189
|
+
return 1;
|
|
1190
|
+
}
|
|
1191
|
+
if (typeof val !== "number" || !Number.isInteger(val) || val < 1) {
|
|
1192
|
+
throw new PluginValidationError(
|
|
1193
|
+
"Invalid guestsCount: must be an integer >= 1."
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
return val;
|
|
1197
|
+
}
|
|
1198
|
+
function formatRegistrationRecord(raw) {
|
|
1199
|
+
return {
|
|
1200
|
+
documentId: String(raw.documentId ?? ""),
|
|
1201
|
+
eventUid: String(raw.eventUid ?? EVENT_CONTENT_TYPE_UID),
|
|
1202
|
+
eventDocumentId: String(raw.eventDocumentId ?? ""),
|
|
1203
|
+
eventTitle: readOptionalText(raw.eventTitle),
|
|
1204
|
+
occurrenceDate: String(raw.occurrenceDate ?? ""),
|
|
1205
|
+
attendeeName: String(raw.attendeeName ?? ""),
|
|
1206
|
+
attendeeEmail: String(raw.attendeeEmail ?? ""),
|
|
1207
|
+
attendeePhone: readOptionalText(raw.attendeePhone),
|
|
1208
|
+
guestsCount: readStoredGuestsCount(raw.guestsCount),
|
|
1209
|
+
status: raw.status ?? "confirmed",
|
|
1210
|
+
checkedInAt: raw.checkedInAt ? String(raw.checkedInAt) : null,
|
|
1211
|
+
locale: String(raw.locale ?? "es"),
|
|
1212
|
+
notes: readOptionalText(raw.notes),
|
|
1213
|
+
createdAt: readOptionalText(raw.createdAt)
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function aggregateRsvpCounts(records) {
|
|
1217
|
+
let confirmedCount = 0;
|
|
1218
|
+
let confirmedSeats = 0;
|
|
1219
|
+
let checkedInSeats = 0;
|
|
1220
|
+
let waitlistedCount = 0;
|
|
1221
|
+
let waitlistedSeats = 0;
|
|
1222
|
+
let cancelledCount = 0;
|
|
1223
|
+
for (const rec of records) {
|
|
1224
|
+
const guests = readStoredGuestsCount(rec.guestsCount);
|
|
1225
|
+
const st = rec.status;
|
|
1226
|
+
if (st === "confirmed" || st === "checked_in") {
|
|
1227
|
+
confirmedCount++;
|
|
1228
|
+
confirmedSeats += guests;
|
|
1229
|
+
if (st === "checked_in") {
|
|
1230
|
+
checkedInSeats += guests;
|
|
1231
|
+
}
|
|
1232
|
+
} else if (st === "waitlisted") {
|
|
1233
|
+
waitlistedCount++;
|
|
1234
|
+
waitlistedSeats += guests;
|
|
1235
|
+
} else if (st === "cancelled") {
|
|
1236
|
+
cancelledCount++;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
confirmedCount,
|
|
1241
|
+
confirmedSeats,
|
|
1242
|
+
checkedInSeats,
|
|
1243
|
+
waitlistedCount,
|
|
1244
|
+
waitlistedSeats,
|
|
1245
|
+
cancelledCount
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
function assertValidAttendeeIdentity(attendeeName, attendeeEmail) {
|
|
1249
|
+
if (!attendeeName || attendeeName.length > 160) {
|
|
1250
|
+
throw new PluginValidationError(
|
|
1251
|
+
"Invalid attendeeName: must be between 1 and 160 characters."
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
if (!attendeeEmail || attendeeEmail.length > 254 || !isValidEmail(attendeeEmail)) {
|
|
1255
|
+
throw new PluginValidationError(
|
|
1256
|
+
"Invalid attendeeEmail: must be a valid email address."
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function validateRegistrationInputAndEvent(input, targetEvent) {
|
|
1261
|
+
const attendeeName = (input.attendeeName ?? "").trim();
|
|
1262
|
+
const attendeeEmail = (input.attendeeEmail ?? "").trim().toLowerCase();
|
|
1263
|
+
assertValidAttendeeIdentity(attendeeName, attendeeEmail);
|
|
1264
|
+
if (targetEvent.eventStatus === "cancelled" || targetEvent.eventStatus === "postponed") {
|
|
1265
|
+
throw new PluginValidationError(
|
|
1266
|
+
`Registrations are unavailable because this event is ${targetEvent.eventStatus}.`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
if (!targetEvent.rsvpEnabled) {
|
|
1270
|
+
throw new PluginValidationError("Registrations are closed for this event.");
|
|
1271
|
+
}
|
|
1272
|
+
assertRegistrationBeforeDeadline(targetEvent.registrationDeadline);
|
|
1273
|
+
const guestsCount = assertValidGuestsCount(input.guestsCount);
|
|
1274
|
+
if (guestsCount > targetEvent.maxGuestsPerRegistration) {
|
|
1275
|
+
throw new PluginValidationError(
|
|
1276
|
+
`Maximum guests per registration is ${targetEvent.maxGuestsPerRegistration}.`
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
return { attendeeName, attendeeEmail, guestsCount };
|
|
1280
|
+
}
|
|
1281
|
+
function assertRegistrationBeforeDeadline(deadline) {
|
|
1282
|
+
if (deadline === null) return;
|
|
1283
|
+
const deadlineMs = new Date(deadline).getTime();
|
|
1284
|
+
if (!Number.isFinite(deadlineMs) || Date.now() > deadlineMs) {
|
|
1285
|
+
throw new PluginValidationError(
|
|
1286
|
+
"The registration deadline for this event has passed."
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function resolveAssignedRsvpStatus(targetEvent, confirmedSeats, guestsCount) {
|
|
1291
|
+
if (targetEvent.capacity !== null && confirmedSeats + guestsCount > targetEvent.capacity) {
|
|
1292
|
+
if (targetEvent.waitlistEnabled) {
|
|
1293
|
+
return "waitlisted";
|
|
1294
|
+
}
|
|
1295
|
+
throw new PluginValidationError("Event has reached maximum capacity.");
|
|
1296
|
+
}
|
|
1297
|
+
return "confirmed";
|
|
1298
|
+
}
|
|
1299
|
+
async function trySendNotificationEmail(strapi, payload) {
|
|
1300
|
+
try {
|
|
1301
|
+
const settings2 = await getSettingsService(strapi).getSettings();
|
|
1302
|
+
if (!settings2.sendRsvpEmails) return;
|
|
1303
|
+
const emailPlugin = strapi.plugin("email");
|
|
1304
|
+
const emailService = emailPlugin?.service("email");
|
|
1305
|
+
if (!emailService || typeof emailService.send !== "function") return;
|
|
1306
|
+
const statusHeadline = payload.promotedFromWaitlist ? "Your waitlist spot has been confirmed!" : payload.status === "waitlisted" ? "You have been added to the event waitlist" : "Your event registration is confirmed";
|
|
1307
|
+
const safeTitle = payload.eventTitle.replace(/[\r\n]+/g, " ").trim();
|
|
1308
|
+
const safeSubject = `${statusHeadline} — ${safeTitle}`.replace(/[\r\n]+/g, " ").trim();
|
|
1309
|
+
const safeAttendeeName = payload.attendeeName.replace(/[\r\n]+/g, " ").trim();
|
|
1310
|
+
await emailService.send({
|
|
1311
|
+
to: payload.to,
|
|
1312
|
+
subject: safeSubject,
|
|
1313
|
+
text: [
|
|
1314
|
+
`Hello ${safeAttendeeName},`,
|
|
1315
|
+
"",
|
|
1316
|
+
statusHeadline,
|
|
1317
|
+
`Event: ${safeTitle}`,
|
|
1318
|
+
`Date: ${payload.occurrenceDate}`,
|
|
1319
|
+
`Seats: ${payload.guestsCount}`,
|
|
1320
|
+
`Status: ${payload.status.toUpperCase()}`
|
|
1321
|
+
].join("\n")
|
|
1322
|
+
});
|
|
1323
|
+
} catch {
|
|
1324
|
+
strapi.log?.debug?.(
|
|
1325
|
+
`[events] Could not send RSVP notification email to ${maskEmail(payload.to)}`
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function buildRegistrationCreateData(args) {
|
|
1330
|
+
const {
|
|
1331
|
+
targetEvent,
|
|
1332
|
+
eventDocumentId,
|
|
1333
|
+
occurrenceDate,
|
|
1334
|
+
attendeeName,
|
|
1335
|
+
attendeeEmail,
|
|
1336
|
+
guestsCount,
|
|
1337
|
+
assignedStatus,
|
|
1338
|
+
tokenHash,
|
|
1339
|
+
input
|
|
1340
|
+
} = args;
|
|
1341
|
+
return {
|
|
1342
|
+
eventUid: targetEvent.uid,
|
|
1343
|
+
eventDocumentId,
|
|
1344
|
+
eventTitle: targetEvent.title,
|
|
1345
|
+
occurrenceDate,
|
|
1346
|
+
attendeeName,
|
|
1347
|
+
attendeeEmail,
|
|
1348
|
+
attendeePhone: input.attendeePhone ? String(input.attendeePhone).trim().slice(0, 50) : "",
|
|
1349
|
+
guestsCount,
|
|
1350
|
+
status: assignedStatus,
|
|
1351
|
+
checkInToken: tokenHash,
|
|
1352
|
+
locale: input.locale ? String(input.locale).slice(0, 10) : "es",
|
|
1353
|
+
notes: input.notes ? String(input.notes).trim().slice(0, 2e3) : "",
|
|
1354
|
+
metadata: sanitizePlainRecord(input.metadata)
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
const MAX_PUBLIC_PAGE_SIZE = 100;
|
|
1358
|
+
function optStr(val) {
|
|
1359
|
+
return typeof val === "string" && val ? val : void 0;
|
|
1360
|
+
}
|
|
1361
|
+
function optQueryInt(val) {
|
|
1362
|
+
if (typeof val === "number" && Number.isFinite(val)) return val;
|
|
1363
|
+
if (typeof val === "string" && val.trim() !== "") {
|
|
1364
|
+
const parsed = Number(val);
|
|
1365
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1366
|
+
}
|
|
1367
|
+
return void 0;
|
|
1368
|
+
}
|
|
1369
|
+
function clampPublicQueryPagination(q) {
|
|
1370
|
+
const rawPage = optQueryInt(q.page);
|
|
1371
|
+
const rawPageSize = optQueryInt(q.pageSize);
|
|
1372
|
+
const rawLimit = optQueryInt(q.limit);
|
|
1373
|
+
if (rawPage !== void 0 || rawPageSize !== void 0) {
|
|
1374
|
+
const page = rawPage !== void 0 ? Math.max(1, Math.min(1e3, Math.floor(rawPage))) : 1;
|
|
1375
|
+
const sizeCandidate = rawPageSize ?? rawLimit ?? 50;
|
|
1376
|
+
const pageSize = Math.max(
|
|
1377
|
+
1,
|
|
1378
|
+
Math.min(MAX_PUBLIC_PAGE_SIZE, Math.floor(sizeCandidate))
|
|
1379
|
+
);
|
|
1380
|
+
return { page, pageSize };
|
|
1381
|
+
}
|
|
1382
|
+
const limit = rawLimit !== void 0 ? Math.max(1, Math.min(MAX_PUBLIC_PAGE_SIZE, Math.floor(rawLimit))) : MAX_PUBLIC_PAGE_SIZE;
|
|
1383
|
+
return { limit };
|
|
1384
|
+
}
|
|
1385
|
+
function unwrapRequestData(rawBody) {
|
|
1386
|
+
if (rawBody.data && typeof rawBody.data === "object" && !Array.isArray(rawBody.data)) {
|
|
1387
|
+
return rawBody.data;
|
|
1388
|
+
}
|
|
1389
|
+
return rawBody;
|
|
1390
|
+
}
|
|
1391
|
+
function buildRegistrationInput(body) {
|
|
1392
|
+
const guestsCount = assertValidGuestsCount(body.guestsCount);
|
|
1393
|
+
const metadata = body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? body.metadata : void 0;
|
|
1394
|
+
return {
|
|
1395
|
+
eventUid: optStr(body.eventUid),
|
|
1396
|
+
occurrenceDate: optStr(body.occurrenceDate),
|
|
1397
|
+
attendeeName: String(body.attendeeName ?? ""),
|
|
1398
|
+
attendeeEmail: String(body.attendeeEmail ?? ""),
|
|
1399
|
+
attendeePhone: optStr(body.attendeePhone),
|
|
1400
|
+
guestsCount,
|
|
1401
|
+
locale: optStr(body.locale),
|
|
1402
|
+
notes: optStr(body.notes),
|
|
1403
|
+
metadata
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
const contentApi$1 = ({ strapi }) => {
|
|
1407
|
+
return {
|
|
1408
|
+
async listOccurrences(ctx) {
|
|
1409
|
+
const q = ctx.query ?? {};
|
|
1410
|
+
const pagination = clampPublicQueryPagination(q);
|
|
1411
|
+
const data = await getOccurrenceService(strapi).getOccurrences({
|
|
1412
|
+
from: optStr(q.from),
|
|
1413
|
+
to: optStr(q.to),
|
|
1414
|
+
category: optStr(q.category),
|
|
1415
|
+
sourceId: optStr(q.sourceId),
|
|
1416
|
+
locale: optStr(q.locale),
|
|
1417
|
+
includeDrafts: false,
|
|
1418
|
+
...pagination
|
|
1419
|
+
});
|
|
1420
|
+
ctx.body = { data };
|
|
1421
|
+
},
|
|
1422
|
+
async listSpotlights(ctx) {
|
|
1423
|
+
const q = ctx.query ?? {};
|
|
1424
|
+
const pagination = clampPublicQueryPagination(q);
|
|
1425
|
+
const data = await getOccurrenceService(strapi).getActiveSpotlights({
|
|
1426
|
+
date: optStr(q.date),
|
|
1427
|
+
category: optStr(q.category),
|
|
1428
|
+
sourceId: optStr(q.sourceId),
|
|
1429
|
+
locale: optStr(q.locale),
|
|
1430
|
+
includeDrafts: false,
|
|
1431
|
+
...pagination
|
|
1432
|
+
});
|
|
1433
|
+
ctx.body = { data };
|
|
1434
|
+
},
|
|
1435
|
+
async getEventStats(ctx) {
|
|
1436
|
+
const { documentId } = ctx.params ?? {};
|
|
1437
|
+
const q = ctx.query ?? {};
|
|
1438
|
+
const data = await getRsvpService(strapi).getEventStats({
|
|
1439
|
+
eventUid: optStr(q.eventUid),
|
|
1440
|
+
eventDocumentId: String(documentId ?? ""),
|
|
1441
|
+
occurrenceDate: optStr(q.occurrenceDate) ?? null,
|
|
1442
|
+
includeDrafts: false
|
|
1443
|
+
});
|
|
1444
|
+
ctx.body = { data };
|
|
1445
|
+
},
|
|
1446
|
+
async registerAttendee(ctx) {
|
|
1447
|
+
try {
|
|
1448
|
+
const { documentId } = ctx.params ?? {};
|
|
1449
|
+
const body = unwrapRequestData(
|
|
1450
|
+
ctx.request?.body ?? {}
|
|
1451
|
+
);
|
|
1452
|
+
const data = await getRsvpService(strapi).registerAttendee(
|
|
1453
|
+
String(documentId ?? ""),
|
|
1454
|
+
buildRegistrationInput(body)
|
|
1455
|
+
);
|
|
1456
|
+
ctx.status = 201;
|
|
1457
|
+
ctx.body = { data };
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
return handleControllerError(
|
|
1460
|
+
strapi,
|
|
1461
|
+
ctx,
|
|
1462
|
+
err,
|
|
1463
|
+
"Registration could not be completed."
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
async cancelRegistration(ctx) {
|
|
1468
|
+
try {
|
|
1469
|
+
const { token } = ctx.params ?? {};
|
|
1470
|
+
const data = await getRsvpService(strapi).cancelByToken(
|
|
1471
|
+
String(token ?? "")
|
|
1472
|
+
);
|
|
1473
|
+
ctx.body = { data };
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
return handleControllerError(
|
|
1476
|
+
strapi,
|
|
1477
|
+
ctx,
|
|
1478
|
+
err,
|
|
1479
|
+
"Cancellation could not be processed."
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
async downloadFeedIcs(ctx) {
|
|
1484
|
+
const q = ctx.query ?? {};
|
|
1485
|
+
const ics = await getIcalService(strapi).generateFeedIcs({
|
|
1486
|
+
from: optStr(q.from),
|
|
1487
|
+
to: optStr(q.to),
|
|
1488
|
+
category: optStr(q.category),
|
|
1489
|
+
sourceId: optStr(q.sourceId),
|
|
1490
|
+
locale: optStr(q.locale)
|
|
1491
|
+
});
|
|
1492
|
+
ctx.set("Content-Type", "text/calendar; charset=utf-8");
|
|
1493
|
+
ctx.set("Content-Disposition", 'attachment; filename="calendar.ics"');
|
|
1494
|
+
ctx.set("X-Content-Type-Options", "nosniff");
|
|
1495
|
+
ctx.body = ics;
|
|
1496
|
+
},
|
|
1497
|
+
async downloadEventInviteIcs(ctx) {
|
|
1498
|
+
try {
|
|
1499
|
+
const { documentId } = ctx.params ?? {};
|
|
1500
|
+
const q = ctx.query ?? {};
|
|
1501
|
+
const safeDocId = String(documentId ?? "event").replace(/[^a-zA-Z0-9_-]/g, "") || "event";
|
|
1502
|
+
const ics = await getIcalService(strapi).generateEventInviteIcs({
|
|
1503
|
+
documentId: String(documentId ?? ""),
|
|
1504
|
+
occurrenceDate: optStr(q.occurrenceDate),
|
|
1505
|
+
locale: optStr(q.locale)
|
|
1506
|
+
});
|
|
1507
|
+
ctx.set("Content-Type", "text/calendar; charset=utf-8");
|
|
1508
|
+
ctx.set(
|
|
1509
|
+
"Content-Disposition",
|
|
1510
|
+
`attachment; filename="event-${safeDocId}.ics"`
|
|
1511
|
+
);
|
|
1512
|
+
ctx.set("X-Content-Type-Options", "nosniff");
|
|
1513
|
+
ctx.body = ics;
|
|
1514
|
+
} catch (err) {
|
|
1515
|
+
return handleControllerError(
|
|
1516
|
+
strapi,
|
|
1517
|
+
ctx,
|
|
1518
|
+
err,
|
|
1519
|
+
"Invite could not be generated."
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
};
|
|
1525
|
+
const controllers = {
|
|
1526
|
+
admin: admin$1,
|
|
1527
|
+
"content-api": contentApi$1
|
|
1528
|
+
};
|
|
1529
|
+
const utf8Encoder$1 = new TextEncoder();
|
|
1530
|
+
const DAY_MS$1 = 24 * 60 * 60 * 1e3;
|
|
1531
|
+
function generateRawRsvpToken() {
|
|
1532
|
+
return crypto__default.default.randomBytes(24).toString("hex");
|
|
1533
|
+
}
|
|
1534
|
+
function hashRsvpToken(rawToken) {
|
|
1535
|
+
return crypto__default.default.createHash("sha256").update(rawToken, "utf8").digest("hex");
|
|
1536
|
+
}
|
|
1537
|
+
function verifyTokenHashConstantTime(candidateHash, storedValue) {
|
|
1538
|
+
if (typeof storedValue !== "string" || !storedValue) {
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
const a = utf8Encoder$1.encode(candidateHash);
|
|
1542
|
+
const b = utf8Encoder$1.encode(storedValue);
|
|
1543
|
+
if (a.byteLength !== b.byteLength) {
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
return crypto__default.default.timingSafeEqual(a, b);
|
|
1547
|
+
}
|
|
1548
|
+
async function findRegistrationByRawToken(strapi, rawToken) {
|
|
1549
|
+
const cleaned = (rawToken ?? "").trim();
|
|
1550
|
+
if (cleaned.length < 16 || cleaned.length > 256) {
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
const tokenHash = hashRsvpToken(cleaned);
|
|
1554
|
+
const matches = await getDocumentsClient(
|
|
1555
|
+
strapi,
|
|
1556
|
+
REGISTRATION_CONTENT_TYPE_UID
|
|
1557
|
+
).findMany({
|
|
1558
|
+
filters: { checkInToken: { $eq: tokenHash } },
|
|
1559
|
+
limit: 10
|
|
1560
|
+
});
|
|
1561
|
+
for (const item of matches) {
|
|
1562
|
+
if (verifyTokenHashConstantTime(tokenHash, item.checkInToken)) {
|
|
1563
|
+
return { record: item, tokenHash };
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
const strapiSlotLocks = /* @__PURE__ */ new WeakMap();
|
|
1569
|
+
function getSlotLocksMap(strapi) {
|
|
1570
|
+
let map = strapiSlotLocks.get(strapi);
|
|
1571
|
+
if (!map) {
|
|
1572
|
+
map = /* @__PURE__ */ new Map();
|
|
1573
|
+
strapiSlotLocks.set(strapi, map);
|
|
1574
|
+
}
|
|
1575
|
+
return map;
|
|
1576
|
+
}
|
|
1577
|
+
function clearOccurrenceLocks(strapi) {
|
|
1578
|
+
const map = strapiSlotLocks.get(strapi);
|
|
1579
|
+
if (map) {
|
|
1580
|
+
map.clear();
|
|
1581
|
+
strapiSlotLocks.delete(strapi);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
async function withOccurrenceLock(strapi, eventDocumentId, occurrenceDate, fn) {
|
|
1585
|
+
const slotLocks = getSlotLocksMap(strapi);
|
|
1586
|
+
const lockKey = `${eventDocumentId}::${occurrenceDate}`;
|
|
1587
|
+
const previous = slotLocks.get(lockKey) ?? Promise.resolve();
|
|
1588
|
+
let release;
|
|
1589
|
+
const gate = new Promise((resolve) => {
|
|
1590
|
+
release = resolve;
|
|
1591
|
+
});
|
|
1592
|
+
const currentTail = previous.then(() => gate);
|
|
1593
|
+
slotLocks.set(lockKey, currentTail);
|
|
1594
|
+
await previous;
|
|
1595
|
+
try {
|
|
1596
|
+
const txFn = strapi.db?.transaction;
|
|
1597
|
+
if (typeof txFn === "function") {
|
|
1598
|
+
const runInTransaction = txFn.bind(strapi.db);
|
|
1599
|
+
return await runInTransaction(() => fn());
|
|
1600
|
+
}
|
|
1601
|
+
return await fn();
|
|
1602
|
+
} finally {
|
|
1603
|
+
release();
|
|
1604
|
+
if (slotLocks.get(lockKey) === currentTail) {
|
|
1605
|
+
slotLocks.delete(lockKey);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function parseEndTimestamp(endIso, startIso, occurrenceDate, allDay) {
|
|
1610
|
+
if (allDay) {
|
|
1611
|
+
const ms = (/* @__PURE__ */ new Date(`${occurrenceDate}T23:59:59.999Z`)).getTime();
|
|
1612
|
+
return Number.isFinite(ms) ? ms : null;
|
|
1613
|
+
}
|
|
1614
|
+
if (endIso) {
|
|
1615
|
+
const endMs = new Date(endIso).getTime();
|
|
1616
|
+
if (Number.isFinite(endMs)) return endMs;
|
|
1617
|
+
}
|
|
1618
|
+
const startMs = new Date(startIso).getTime();
|
|
1619
|
+
return Number.isFinite(startMs) ? startMs + 60 * 60 * 1e3 : null;
|
|
1620
|
+
}
|
|
1621
|
+
function assertOccurrenceNotEnded(endMs) {
|
|
1622
|
+
if (endMs !== null && Number.isFinite(endMs) && Date.now() > endMs) {
|
|
1623
|
+
throw new PluginValidationError(
|
|
1624
|
+
"Cannot register for an event occurrence that has already ended."
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function assertScheduledOccurrence(targetEvent, occurrenceDate) {
|
|
1629
|
+
if (!isValidCalendarDateKey(occurrenceDate)) {
|
|
1630
|
+
throw new PluginValidationError(
|
|
1631
|
+
"Invalid occurrenceDate: must be a valid YYYY-MM-DD calendar date."
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
const recurrence = targetEvent.recurrence;
|
|
1635
|
+
if (recurrence.exdates?.includes(occurrenceDate)) {
|
|
1636
|
+
throw new PluginValidationError(
|
|
1637
|
+
`Date ${occurrenceDate} is excluded for this event.`
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
const directOverride = isSafeObjectKey(occurrenceDate) && recurrence.overrides && Object.hasOwn(recurrence.overrides, occurrenceDate) ? recurrence.overrides[occurrenceDate] : void 0;
|
|
1641
|
+
if (directOverride?.cancelled) {
|
|
1642
|
+
throw new PluginValidationError(
|
|
1643
|
+
`Occurrence on ${occurrenceDate} has been cancelled.`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
if (!recurrence.rrule) {
|
|
1647
|
+
const baseKey = toDateKey(targetEvent.startDate);
|
|
1648
|
+
if (occurrenceDate !== baseKey) {
|
|
1649
|
+
throw new PluginValidationError(
|
|
1650
|
+
`Date ${occurrenceDate} is not a scheduled occurrence for this event.`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
const effStart = directOverride?.startDate || targetEvent.startDate;
|
|
1654
|
+
const effEnd = directOverride?.endDate || targetEvent.endDate;
|
|
1655
|
+
return parseEndTimestamp(
|
|
1656
|
+
effEnd,
|
|
1657
|
+
effStart,
|
|
1658
|
+
occurrenceDate,
|
|
1659
|
+
targetEvent.allDay
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
const anchorMs = (/* @__PURE__ */ new Date(`${occurrenceDate}T00:00:00.000Z`)).getTime();
|
|
1663
|
+
const slots = expandRuleSlots({
|
|
1664
|
+
baseStartDate: targetEvent.startDate,
|
|
1665
|
+
baseEndDate: targetEvent.endDate,
|
|
1666
|
+
recurrence,
|
|
1667
|
+
windowStart: new Date(anchorMs - 31 * DAY_MS$1).toISOString(),
|
|
1668
|
+
windowEnd: new Date(anchorMs + 32 * DAY_MS$1 - 1).toISOString(),
|
|
1669
|
+
maxInstances: 1e4
|
|
1670
|
+
});
|
|
1671
|
+
const matched = slots.find(
|
|
1672
|
+
(s) => s.occurrenceDate === occurrenceDate || toDateKey(s.startDate) === occurrenceDate
|
|
1673
|
+
);
|
|
1674
|
+
if (!matched) {
|
|
1675
|
+
throw new PluginValidationError(
|
|
1676
|
+
`Date ${occurrenceDate} is not a scheduled occurrence for this event.`
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
return parseEndTimestamp(
|
|
1680
|
+
matched.endDate,
|
|
1681
|
+
matched.startDate,
|
|
1682
|
+
occurrenceDate,
|
|
1683
|
+
Boolean(recurrence.allDay || targetEvent.allDay)
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
function resolveValidatedOccurrenceDate(inputDate, targetEvent) {
|
|
1687
|
+
if (inputDate !== void 0 && inputDate !== "") {
|
|
1688
|
+
const trimmed = String(inputDate).trim();
|
|
1689
|
+
const endMs2 = assertScheduledOccurrence(targetEvent, trimmed);
|
|
1690
|
+
assertOccurrenceNotEnded(endMs2);
|
|
1691
|
+
return trimmed;
|
|
1692
|
+
}
|
|
1693
|
+
if (targetEvent.recurrence.rrule) {
|
|
1694
|
+
const nowMs = Date.now();
|
|
1695
|
+
const isAllDay = Boolean(
|
|
1696
|
+
targetEvent.recurrence.allDay || targetEvent.allDay
|
|
1697
|
+
);
|
|
1698
|
+
const upcomingSlots = expandRuleSlots({
|
|
1699
|
+
baseStartDate: targetEvent.startDate,
|
|
1700
|
+
baseEndDate: targetEvent.endDate,
|
|
1701
|
+
recurrence: targetEvent.recurrence,
|
|
1702
|
+
windowStart: new Date(nowMs - DAY_MS$1).toISOString(),
|
|
1703
|
+
windowEnd: new Date(nowMs + 366 * DAY_MS$1).toISOString(),
|
|
1704
|
+
maxInstances: 1e3
|
|
1705
|
+
});
|
|
1706
|
+
for (const slot of upcomingSlots) {
|
|
1707
|
+
const slotEndMs = parseEndTimestamp(
|
|
1708
|
+
slot.endDate,
|
|
1709
|
+
slot.startDate,
|
|
1710
|
+
slot.occurrenceDate,
|
|
1711
|
+
isAllDay
|
|
1712
|
+
);
|
|
1713
|
+
if (slotEndMs === null || !Number.isFinite(slotEndMs) || slotEndMs > nowMs) {
|
|
1714
|
+
return slot.occurrenceDate;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
throw new PluginValidationError(
|
|
1718
|
+
"Cannot register for an event occurrence that has already ended."
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
const fallbackKey = toDateKey(targetEvent.startDate);
|
|
1722
|
+
const endMs = assertScheduledOccurrence(targetEvent, fallbackKey);
|
|
1723
|
+
assertOccurrenceNotEnded(endMs);
|
|
1724
|
+
return fallbackKey;
|
|
1725
|
+
}
|
|
1726
|
+
const destroy = ({ strapi }) => {
|
|
1727
|
+
clearOccurrenceLocks(strapi);
|
|
1728
|
+
};
|
|
1729
|
+
const middlewares = {};
|
|
1730
|
+
const policies = {};
|
|
1731
|
+
function mergeAttributeIntoTarget(target, attrName, attrDef) {
|
|
1732
|
+
if (!target || typeof target !== "object" || !isSafeObjectKey(attrName)) {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const prev = Object.hasOwn(target, attrName) ? target[attrName] : void 0;
|
|
1736
|
+
const safeDef = {};
|
|
1737
|
+
for (const [k, v] of Object.entries(attrDef)) {
|
|
1738
|
+
if (isSafeObjectKey(k)) {
|
|
1739
|
+
safeDef[k] = v;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
target[attrName] = {
|
|
1743
|
+
...prev && typeof prev === "object" ? prev : {},
|
|
1744
|
+
...safeDef
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
function applyAttributeExtensions(contentType, extraAttributes) {
|
|
1748
|
+
if (!contentType || !extraAttributes || typeof extraAttributes !== "object") {
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
for (const [attrName, attrDef] of Object.entries(extraAttributes)) {
|
|
1752
|
+
if (!isSafeObjectKey(attrName) || !attrDef || typeof attrDef !== "object" || Array.isArray(attrDef)) {
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
mergeAttributeIntoTarget(contentType.attributes, attrName, attrDef);
|
|
1756
|
+
mergeAttributeIntoTarget(
|
|
1757
|
+
contentType.__schema__?.attributes,
|
|
1758
|
+
attrName,
|
|
1759
|
+
attrDef
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
function buildCategoryEnumAttribute(categoryColors) {
|
|
1764
|
+
const colorKeys = Object.keys(categoryColors).filter(isSafeObjectKey);
|
|
1765
|
+
const enumValues = Array.from(/* @__PURE__ */ new Set(["general", ...colorKeys]));
|
|
1766
|
+
return {
|
|
1767
|
+
type: "enumeration",
|
|
1768
|
+
enum: enumValues,
|
|
1769
|
+
default: enumValues.includes("general") ? "general" : enumValues[0],
|
|
1770
|
+
pluginOptions: {
|
|
1771
|
+
i18n: {
|
|
1772
|
+
localized: false
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
const register = ({ strapi }) => {
|
|
1778
|
+
strapi.customFields.register({
|
|
1779
|
+
name: "rrule",
|
|
1780
|
+
plugin: PLUGIN_ID,
|
|
1781
|
+
type: "json",
|
|
1782
|
+
inputSize: {
|
|
1783
|
+
default: 12,
|
|
1784
|
+
isResizable: true
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
const userConfig = strapi.config?.get?.(`plugin::${PLUGIN_ID}`) ?? {};
|
|
1788
|
+
const strapiInternal = strapi;
|
|
1789
|
+
const getCt = (uid) => strapiInternal.contentTypes?.[uid] ?? strapiInternal.contentType?.(uid);
|
|
1790
|
+
const eventSourceWithCategories = userConfig.sources?.find(
|
|
1791
|
+
(s) => s?.uid === EVENT_CONTENT_TYPE_UID && Boolean(s.categoryColors && Object.keys(s.categoryColors).length > 0)
|
|
1792
|
+
);
|
|
1793
|
+
const eventExtensions = {};
|
|
1794
|
+
if (userConfig.schemaExtensions?.event && typeof userConfig.schemaExtensions.event === "object") {
|
|
1795
|
+
for (const [k, v] of Object.entries(userConfig.schemaExtensions.event)) {
|
|
1796
|
+
if (isSafeObjectKey(k) && v && typeof v === "object") {
|
|
1797
|
+
eventExtensions[k] = v;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (eventSourceWithCategories?.categoryColors && !Object.hasOwn(eventExtensions, "category")) {
|
|
1802
|
+
eventExtensions.category = buildCategoryEnumAttribute(
|
|
1803
|
+
eventSourceWithCategories.categoryColors
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
if (Object.keys(eventExtensions).length > 0) {
|
|
1807
|
+
applyAttributeExtensions(getCt(EVENT_CONTENT_TYPE_UID), eventExtensions);
|
|
1808
|
+
}
|
|
1809
|
+
if (userConfig.schemaExtensions?.registration) {
|
|
1810
|
+
applyAttributeExtensions(
|
|
1811
|
+
getCt(REGISTRATION_CONTENT_TYPE_UID),
|
|
1812
|
+
userConfig.schemaExtensions.registration
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
const readPolicies = [
|
|
1817
|
+
"admin::isAuthenticatedAdmin",
|
|
1818
|
+
{
|
|
1819
|
+
name: "admin::hasPermissions",
|
|
1820
|
+
config: { actions: [`plugin::${PLUGIN_ID}.read`] }
|
|
1821
|
+
}
|
|
1822
|
+
];
|
|
1823
|
+
const updatePolicies = [
|
|
1824
|
+
"admin::isAuthenticatedAdmin",
|
|
1825
|
+
{
|
|
1826
|
+
name: "admin::hasPermissions",
|
|
1827
|
+
config: { actions: [`plugin::${PLUGIN_ID}.update`] }
|
|
1828
|
+
}
|
|
1829
|
+
];
|
|
1830
|
+
const settingsPolicies = [
|
|
1831
|
+
"admin::isAuthenticatedAdmin",
|
|
1832
|
+
{
|
|
1833
|
+
name: "admin::hasPermissions",
|
|
1834
|
+
config: { actions: [`plugin::${PLUGIN_ID}.settings.update`] }
|
|
1835
|
+
}
|
|
1836
|
+
];
|
|
1837
|
+
const admin = {
|
|
1838
|
+
type: "admin",
|
|
1839
|
+
routes: [
|
|
1840
|
+
{
|
|
1841
|
+
method: "GET",
|
|
1842
|
+
path: "/settings",
|
|
1843
|
+
handler: "admin.getSettings",
|
|
1844
|
+
config: {
|
|
1845
|
+
policies: readPolicies
|
|
1846
|
+
}
|
|
1847
|
+
},
|
|
1848
|
+
{
|
|
1849
|
+
method: "PUT",
|
|
1850
|
+
path: "/settings",
|
|
1851
|
+
handler: "admin.updateSettings",
|
|
1852
|
+
config: {
|
|
1853
|
+
policies: settingsPolicies
|
|
1854
|
+
}
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
method: "GET",
|
|
1858
|
+
path: "/calendar",
|
|
1859
|
+
handler: "admin.getCalendar",
|
|
1860
|
+
config: {
|
|
1861
|
+
policies: readPolicies
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
method: "POST",
|
|
1866
|
+
path: "/calendar/reschedule",
|
|
1867
|
+
handler: "admin.reschedule",
|
|
1868
|
+
config: {
|
|
1869
|
+
policies: updatePolicies
|
|
1870
|
+
}
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
method: "POST",
|
|
1874
|
+
path: "/calendar/quick-create",
|
|
1875
|
+
handler: "admin.quickCreate",
|
|
1876
|
+
config: {
|
|
1877
|
+
policies: updatePolicies
|
|
1878
|
+
}
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
method: "GET",
|
|
1882
|
+
path: "/stats/:documentId",
|
|
1883
|
+
handler: "admin.getEventStats",
|
|
1884
|
+
config: {
|
|
1885
|
+
policies: readPolicies
|
|
1886
|
+
}
|
|
1887
|
+
},
|
|
1888
|
+
{
|
|
1889
|
+
method: "GET",
|
|
1890
|
+
path: "/registrations",
|
|
1891
|
+
handler: "admin.listRegistrations",
|
|
1892
|
+
config: {
|
|
1893
|
+
policies: readPolicies
|
|
1894
|
+
}
|
|
1895
|
+
},
|
|
1896
|
+
{
|
|
1897
|
+
method: "GET",
|
|
1898
|
+
path: "/registrations/export.csv",
|
|
1899
|
+
handler: "admin.exportRegistrationsCsv",
|
|
1900
|
+
config: {
|
|
1901
|
+
policies: readPolicies
|
|
1902
|
+
}
|
|
1903
|
+
},
|
|
1904
|
+
{
|
|
1905
|
+
method: "POST",
|
|
1906
|
+
path: "/registrations/:documentId/status",
|
|
1907
|
+
handler: "admin.updateRegistrationStatus",
|
|
1908
|
+
config: {
|
|
1909
|
+
policies: updatePolicies
|
|
1910
|
+
}
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
method: "POST",
|
|
1914
|
+
path: "/registrations/:documentId/check-in",
|
|
1915
|
+
handler: "admin.checkIn",
|
|
1916
|
+
config: {
|
|
1917
|
+
policies: updatePolicies
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
]
|
|
1921
|
+
};
|
|
1922
|
+
const contentApi = {
|
|
1923
|
+
type: "content-api",
|
|
1924
|
+
routes: [
|
|
1925
|
+
{
|
|
1926
|
+
method: "GET",
|
|
1927
|
+
path: "/occurrences",
|
|
1928
|
+
handler: "content-api.listOccurrences",
|
|
1929
|
+
config: {
|
|
1930
|
+
policies: []
|
|
1931
|
+
}
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
method: "GET",
|
|
1935
|
+
path: "/spotlights",
|
|
1936
|
+
handler: "content-api.listSpotlights",
|
|
1937
|
+
config: {
|
|
1938
|
+
policies: []
|
|
1939
|
+
}
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
method: "GET",
|
|
1943
|
+
path: "/feed.ics",
|
|
1944
|
+
handler: "content-api.downloadFeedIcs",
|
|
1945
|
+
config: {
|
|
1946
|
+
policies: []
|
|
1947
|
+
}
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
method: "GET",
|
|
1951
|
+
path: "/:documentId/invite.ics",
|
|
1952
|
+
handler: "content-api.downloadEventInviteIcs",
|
|
1953
|
+
config: {
|
|
1954
|
+
policies: []
|
|
1955
|
+
}
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
method: "GET",
|
|
1959
|
+
path: "/:documentId/stats",
|
|
1960
|
+
handler: "content-api.getEventStats",
|
|
1961
|
+
config: {
|
|
1962
|
+
policies: []
|
|
1963
|
+
}
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
method: "POST",
|
|
1967
|
+
path: "/:documentId/register",
|
|
1968
|
+
handler: "content-api.registerAttendee",
|
|
1969
|
+
config: {
|
|
1970
|
+
policies: []
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
method: "POST",
|
|
1975
|
+
path: "/registrations/:token/cancel",
|
|
1976
|
+
handler: "content-api.cancelRegistration",
|
|
1977
|
+
config: {
|
|
1978
|
+
policies: []
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
]
|
|
1982
|
+
};
|
|
1983
|
+
const routes = {
|
|
1984
|
+
admin,
|
|
1985
|
+
"content-api": contentApi
|
|
1986
|
+
};
|
|
1987
|
+
const utf8Encoder = new TextEncoder();
|
|
1988
|
+
function toIcsTimestamp(isoString, allDay = false) {
|
|
1989
|
+
const d = new Date(isoString);
|
|
1990
|
+
if (Number.isNaN(d.getTime())) {
|
|
1991
|
+
return "";
|
|
1992
|
+
}
|
|
1993
|
+
const iso = d.toISOString();
|
|
1994
|
+
if (allDay) {
|
|
1995
|
+
return iso.slice(0, 10).replace(/-/g, "");
|
|
1996
|
+
}
|
|
1997
|
+
return iso.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
1998
|
+
}
|
|
1999
|
+
function resolveExclusiveAllDayEndStamp(startDate, endDate, dtStartStamp) {
|
|
2000
|
+
const rawEndStamp = toIcsTimestamp(endDate, true);
|
|
2001
|
+
if (rawEndStamp && rawEndStamp > dtStartStamp) {
|
|
2002
|
+
return rawEndStamp;
|
|
2003
|
+
}
|
|
2004
|
+
const startObj = new Date(startDate);
|
|
2005
|
+
if (Number.isNaN(startObj.getTime())) {
|
|
2006
|
+
return rawEndStamp;
|
|
2007
|
+
}
|
|
2008
|
+
const nextDay = new Date(
|
|
2009
|
+
Date.UTC(
|
|
2010
|
+
startObj.getUTCFullYear(),
|
|
2011
|
+
startObj.getUTCMonth(),
|
|
2012
|
+
startObj.getUTCDate() + 1
|
|
2013
|
+
)
|
|
2014
|
+
);
|
|
2015
|
+
return nextDay.toISOString().slice(0, 10).replace(/-/g, "");
|
|
2016
|
+
}
|
|
2017
|
+
function escapeIcsText(value) {
|
|
2018
|
+
if (!value) return "";
|
|
2019
|
+
return String(value).replace(/\\/g, "\\\\").replace(/\r\n|\r|\n/g, "\\n").replace(/;/g, "\\;").replace(/,/g, "\\,");
|
|
2020
|
+
}
|
|
2021
|
+
function foldIcsLine(line) {
|
|
2022
|
+
if (utf8Encoder.encode(line).byteLength <= 75) {
|
|
2023
|
+
return line;
|
|
2024
|
+
}
|
|
2025
|
+
const chunks = [];
|
|
2026
|
+
let currentChunk = "";
|
|
2027
|
+
let currentBytes = 0;
|
|
2028
|
+
let maxBytes = 75;
|
|
2029
|
+
for (const codePoint of line) {
|
|
2030
|
+
const byteLen = utf8Encoder.encode(codePoint).byteLength;
|
|
2031
|
+
if (currentBytes + byteLen > maxBytes && currentChunk.length > 0) {
|
|
2032
|
+
chunks.push(chunks.length === 0 ? currentChunk : ` ${currentChunk}`);
|
|
2033
|
+
currentChunk = codePoint;
|
|
2034
|
+
currentBytes = byteLen;
|
|
2035
|
+
maxBytes = 74;
|
|
2036
|
+
} else {
|
|
2037
|
+
currentChunk += codePoint;
|
|
2038
|
+
currentBytes += byteLen;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (currentChunk.length > 0) {
|
|
2042
|
+
chunks.push(chunks.length === 0 ? currentChunk : ` ${currentChunk}`);
|
|
2043
|
+
}
|
|
2044
|
+
return chunks.join("\r\n");
|
|
2045
|
+
}
|
|
2046
|
+
function buildCalendarLinks(params) {
|
|
2047
|
+
const {
|
|
2048
|
+
documentId,
|
|
2049
|
+
title,
|
|
2050
|
+
startDate,
|
|
2051
|
+
endDate,
|
|
2052
|
+
allDay = false,
|
|
2053
|
+
summary = "",
|
|
2054
|
+
location = "",
|
|
2055
|
+
occurrenceDate
|
|
2056
|
+
} = params;
|
|
2057
|
+
const icsStart = toIcsTimestamp(startDate, allDay);
|
|
2058
|
+
const icsEnd = allDay ? resolveExclusiveAllDayEndStamp(startDate, endDate, icsStart) : toIcsTimestamp(endDate, false);
|
|
2059
|
+
const googleParams = new URLSearchParams({
|
|
2060
|
+
action: "TEMPLATE",
|
|
2061
|
+
text: title,
|
|
2062
|
+
dates: `${icsStart}/${icsEnd}`,
|
|
2063
|
+
details: summary,
|
|
2064
|
+
location
|
|
2065
|
+
});
|
|
2066
|
+
const outlookParams = new URLSearchParams({
|
|
2067
|
+
path: "/calendar/action/compose",
|
|
2068
|
+
rru: "addevent",
|
|
2069
|
+
subject: title,
|
|
2070
|
+
startdt: startDate,
|
|
2071
|
+
enddt: endDate,
|
|
2072
|
+
body: summary,
|
|
2073
|
+
location,
|
|
2074
|
+
allday: allDay ? "true" : "false"
|
|
2075
|
+
});
|
|
2076
|
+
const yahooParams = new URLSearchParams({
|
|
2077
|
+
v: "60",
|
|
2078
|
+
title,
|
|
2079
|
+
st: icsStart,
|
|
2080
|
+
et: icsEnd,
|
|
2081
|
+
desc: summary,
|
|
2082
|
+
in_loc: location
|
|
2083
|
+
});
|
|
2084
|
+
const querySuffix = occurrenceDate ? `?occurrenceDate=${encodeURIComponent(occurrenceDate)}` : "";
|
|
2085
|
+
return {
|
|
2086
|
+
google: `https://calendar.google.com/calendar/render?${googleParams.toString()}`,
|
|
2087
|
+
outlook: `https://outlook.live.com/calendar/0/deeplink/compose?${outlookParams.toString()}`,
|
|
2088
|
+
yahoo: `https://calendar.yahoo.com/?${yahooParams.toString()}`,
|
|
2089
|
+
icsPath: `/api/events/${encodeURIComponent(documentId)}/invite.ics${querySuffix}`
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
const ICS_STATUS_MAP = {
|
|
2093
|
+
cancelled: "CANCELLED",
|
|
2094
|
+
postponed: "TENTATIVE"
|
|
2095
|
+
};
|
|
2096
|
+
function resolveIcsStatus(status) {
|
|
2097
|
+
return Object.hasOwn(ICS_STATUS_MAP, status) ? ICS_STATUS_MAP[status] : "CONFIRMED";
|
|
2098
|
+
}
|
|
2099
|
+
function appendVEventDateLines(lines, occ) {
|
|
2100
|
+
const isAllDay = Boolean(occ.allDay);
|
|
2101
|
+
const prefix = isAllDay ? ";VALUE=DATE" : "";
|
|
2102
|
+
const startStamp = toIcsTimestamp(occ.startDate, isAllDay);
|
|
2103
|
+
const endStamp = isAllDay ? resolveExclusiveAllDayEndStamp(occ.startDate, occ.endDate, startStamp) : toIcsTimestamp(occ.endDate, false);
|
|
2104
|
+
lines.push(`DTSTART${prefix}:${startStamp}`);
|
|
2105
|
+
lines.push(`DTEND${prefix}:${endStamp}`);
|
|
2106
|
+
}
|
|
2107
|
+
function appendVEventOptionalLines(lines, occ) {
|
|
2108
|
+
if (occ.summary) {
|
|
2109
|
+
lines.push(foldIcsLine(`DESCRIPTION:${escapeIcsText(occ.summary)}`));
|
|
2110
|
+
}
|
|
2111
|
+
if (occ.location) {
|
|
2112
|
+
lines.push(foldIcsLine(`LOCATION:${escapeIcsText(occ.location)}`));
|
|
2113
|
+
}
|
|
2114
|
+
if (occ.virtualUrl) {
|
|
2115
|
+
lines.push(foldIcsLine(`URL:${escapeIcsText(occ.virtualUrl)}`));
|
|
2116
|
+
}
|
|
2117
|
+
if (occ.isRecurring && occ.rrule) {
|
|
2118
|
+
const normalized = normalizeRRuleString(occ.rrule).replace(/[\r\n]+/g, "");
|
|
2119
|
+
if (normalized) {
|
|
2120
|
+
lines.push(foldIcsLine(`RRULE:${normalized}`));
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
function generateIcsCalendar(occurrences, feedTitle = "Events Calendar") {
|
|
2125
|
+
const nowStamp = toIcsTimestamp((/* @__PURE__ */ new Date()).toISOString(), false);
|
|
2126
|
+
const lines = [
|
|
2127
|
+
"BEGIN:VCALENDAR",
|
|
2128
|
+
"VERSION:2.0",
|
|
2129
|
+
"PRODID:-//Strapi v5//strapi-plugin-events//EN",
|
|
2130
|
+
"CALSCALE:GREGORIAN",
|
|
2131
|
+
"METHOD:PUBLISH",
|
|
2132
|
+
foldIcsLine(`X-WR-CALNAME:${escapeIcsText(feedTitle)}`)
|
|
2133
|
+
];
|
|
2134
|
+
for (const occ of occurrences) {
|
|
2135
|
+
const uid = `${occ.documentId}-${occ.occurrenceDate}@strapi-plugin-events`;
|
|
2136
|
+
lines.push("BEGIN:VEVENT");
|
|
2137
|
+
lines.push(foldIcsLine(`UID:${escapeIcsText(uid)}`));
|
|
2138
|
+
lines.push(`DTSTAMP:${nowStamp}`);
|
|
2139
|
+
appendVEventDateLines(lines, occ);
|
|
2140
|
+
lines.push(foldIcsLine(`SUMMARY:${escapeIcsText(occ.title)}`));
|
|
2141
|
+
appendVEventOptionalLines(lines, occ);
|
|
2142
|
+
lines.push(`STATUS:${resolveIcsStatus(occ.status)}`);
|
|
2143
|
+
lines.push("END:VEVENT");
|
|
2144
|
+
}
|
|
2145
|
+
lines.push("END:VCALENDAR");
|
|
2146
|
+
return `${lines.join("\r\n")}\r
|
|
2147
|
+
`;
|
|
2148
|
+
}
|
|
2149
|
+
function sanitizeCsvCell(raw) {
|
|
2150
|
+
if (raw === null || raw === void 0) {
|
|
2151
|
+
return '""';
|
|
2152
|
+
}
|
|
2153
|
+
let text = String(raw);
|
|
2154
|
+
if (/^[\s\u0085\u2028\u2029]*[=+\-@|]|^[\t\r\n]/.test(text)) {
|
|
2155
|
+
text = `'${text}`;
|
|
2156
|
+
}
|
|
2157
|
+
const escaped = text.replace(/"/g, '""');
|
|
2158
|
+
return `"${escaped}"`;
|
|
2159
|
+
}
|
|
2160
|
+
function generateRegistrationsCsv(records) {
|
|
2161
|
+
const headers = [
|
|
2162
|
+
"Document ID",
|
|
2163
|
+
"Event UID",
|
|
2164
|
+
"Event Document ID",
|
|
2165
|
+
"Event Title",
|
|
2166
|
+
"Occurrence Date",
|
|
2167
|
+
"Attendee Name",
|
|
2168
|
+
"Attendee Email",
|
|
2169
|
+
"Attendee Phone",
|
|
2170
|
+
"Guests Count",
|
|
2171
|
+
"Status",
|
|
2172
|
+
"Checked In At",
|
|
2173
|
+
"Locale",
|
|
2174
|
+
"Notes",
|
|
2175
|
+
"Created At"
|
|
2176
|
+
];
|
|
2177
|
+
const rows = [headers.map(sanitizeCsvCell).join(",")];
|
|
2178
|
+
for (const r of records) {
|
|
2179
|
+
rows.push(
|
|
2180
|
+
[
|
|
2181
|
+
r.documentId,
|
|
2182
|
+
r.eventUid,
|
|
2183
|
+
r.eventDocumentId,
|
|
2184
|
+
r.eventTitle ?? "",
|
|
2185
|
+
r.occurrenceDate,
|
|
2186
|
+
r.attendeeName,
|
|
2187
|
+
r.attendeeEmail,
|
|
2188
|
+
r.attendeePhone ?? "",
|
|
2189
|
+
r.guestsCount,
|
|
2190
|
+
r.status,
|
|
2191
|
+
r.checkedInAt ?? "",
|
|
2192
|
+
r.locale,
|
|
2193
|
+
r.notes ?? "",
|
|
2194
|
+
r.createdAt ?? ""
|
|
2195
|
+
].map(sanitizeCsvCell).join(",")
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
return `${rows.join("\r\n")}\r
|
|
2199
|
+
`;
|
|
2200
|
+
}
|
|
2201
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2202
|
+
const ical = ({ strapi }) => {
|
|
2203
|
+
return {
|
|
2204
|
+
async generateFeedIcs(params) {
|
|
2205
|
+
const settings2 = await getSettingsService(strapi).getSettings();
|
|
2206
|
+
const occurrences = await getOccurrenceService(strapi).getOccurrences({
|
|
2207
|
+
from: params?.from,
|
|
2208
|
+
to: params?.to,
|
|
2209
|
+
category: params?.category,
|
|
2210
|
+
sourceId: params?.sourceId,
|
|
2211
|
+
locale: params?.locale,
|
|
2212
|
+
includeDrafts: false
|
|
2213
|
+
});
|
|
2214
|
+
return generateIcsCalendar(occurrences, settings2.feedTitle);
|
|
2215
|
+
},
|
|
2216
|
+
async generateEventInviteIcs(params) {
|
|
2217
|
+
const docId = (params.documentId ?? "").trim();
|
|
2218
|
+
if (!docId) {
|
|
2219
|
+
throw new PluginValidationError("Event documentId is required.");
|
|
2220
|
+
}
|
|
2221
|
+
const settings2 = await getSettingsService(strapi).getSettings();
|
|
2222
|
+
let from;
|
|
2223
|
+
let to;
|
|
2224
|
+
if (params.occurrenceDate) {
|
|
2225
|
+
if (!isValidCalendarDateKey(params.occurrenceDate)) {
|
|
2226
|
+
throw new PluginValidationError(
|
|
2227
|
+
"Invalid occurrenceDate: must be a valid YYYY-MM-DD calendar date."
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
const anchorMs = (/* @__PURE__ */ new Date(
|
|
2231
|
+
`${params.occurrenceDate}T00:00:00.000Z`
|
|
2232
|
+
)).getTime();
|
|
2233
|
+
from = new Date(anchorMs - 2 * DAY_MS).toISOString();
|
|
2234
|
+
to = new Date(anchorMs + 3 * DAY_MS).toISOString();
|
|
2235
|
+
} else {
|
|
2236
|
+
const nowMs = Date.now();
|
|
2237
|
+
from = new Date(nowMs - 30 * DAY_MS).toISOString();
|
|
2238
|
+
to = new Date(nowMs + 335 * DAY_MS).toISOString();
|
|
2239
|
+
}
|
|
2240
|
+
const occurrences = await getOccurrenceService(strapi).getOccurrences({
|
|
2241
|
+
documentId: docId,
|
|
2242
|
+
from,
|
|
2243
|
+
to,
|
|
2244
|
+
locale: params.locale,
|
|
2245
|
+
includeDrafts: false
|
|
2246
|
+
});
|
|
2247
|
+
const filtered = params.occurrenceDate ? occurrences.filter((o) => o.occurrenceDate === params.occurrenceDate) : occurrences.slice(0, 1);
|
|
2248
|
+
return generateIcsCalendar(filtered, settings2.feedTitle);
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
};
|
|
2252
|
+
const MAX_PUBLIC_WINDOW_MS = 366 * 24 * 60 * 60 * 1e3;
|
|
2253
|
+
const MAX_ADMIN_WINDOW_MS = 5 * 366 * 24 * 60 * 60 * 1e3;
|
|
2254
|
+
function assertValidIsoDateTime(raw, fieldName) {
|
|
2255
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
2256
|
+
throw new PluginValidationError(
|
|
2257
|
+
`Invalid ${fieldName}: must be a non-empty ISO 8601 date or datetime.`
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
const trimmed = raw.trim();
|
|
2261
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
2262
|
+
if (!isValidCalendarDateKey(trimmed)) {
|
|
2263
|
+
throw new PluginValidationError(
|
|
2264
|
+
`Invalid ${fieldName}: "${trimmed}" is not a valid calendar date.`
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
return (/* @__PURE__ */ new Date(`${trimmed}T00:00:00.000Z`)).getTime();
|
|
2268
|
+
}
|
|
2269
|
+
const ms = new Date(trimmed).getTime();
|
|
2270
|
+
if (!Number.isFinite(ms)) {
|
|
2271
|
+
throw new PluginValidationError(
|
|
2272
|
+
`Invalid ${fieldName}: must be a valid ISO 8601 datetime.`
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
return ms;
|
|
2276
|
+
}
|
|
2277
|
+
function isValidIso(val) {
|
|
2278
|
+
if (!val || typeof val !== "string") return false;
|
|
2279
|
+
const trimmed = val.trim();
|
|
2280
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
2281
|
+
return isValidCalendarDateKey(trimmed);
|
|
2282
|
+
}
|
|
2283
|
+
return Number.isFinite(new Date(trimmed).getTime());
|
|
2284
|
+
}
|
|
2285
|
+
function resolveOccurrenceWindow(params) {
|
|
2286
|
+
const now = /* @__PURE__ */ new Date();
|
|
2287
|
+
const defaultFromMs = new Date(
|
|
2288
|
+
now.getFullYear(),
|
|
2289
|
+
now.getMonth() - 1,
|
|
2290
|
+
1
|
|
2291
|
+
).getTime();
|
|
2292
|
+
const defaultToMs = new Date(
|
|
2293
|
+
now.getFullYear(),
|
|
2294
|
+
now.getMonth() + 3,
|
|
2295
|
+
0,
|
|
2296
|
+
23,
|
|
2297
|
+
59,
|
|
2298
|
+
59
|
|
2299
|
+
).getTime();
|
|
2300
|
+
const startMs = isValidIso(params.from) ? new Date(String(params.from)).getTime() : defaultFromMs;
|
|
2301
|
+
const rawEndMs = isValidIso(params.to) ? new Date(String(params.to)).getTime() : defaultToMs;
|
|
2302
|
+
if (rawEndMs < startMs) {
|
|
2303
|
+
return {
|
|
2304
|
+
windowStart: new Date(startMs).toISOString(),
|
|
2305
|
+
windowEnd: new Date(rawEndMs).toISOString(),
|
|
2306
|
+
isEmptyWindow: true
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
const maxSpanMs = params.includeDrafts ? MAX_ADMIN_WINDOW_MS : MAX_PUBLIC_WINDOW_MS;
|
|
2310
|
+
const clampedEndMs = rawEndMs - startMs > maxSpanMs ? startMs + maxSpanMs : rawEndMs;
|
|
2311
|
+
return {
|
|
2312
|
+
windowStart: new Date(startMs).toISOString(),
|
|
2313
|
+
windowEnd: new Date(clampedEndMs).toISOString(),
|
|
2314
|
+
isEmptyWindow: false
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
function resolveQuickCreateSource(settings2, sourceId) {
|
|
2318
|
+
const sources = (settings2.sources ?? []).filter(
|
|
2319
|
+
(s) => isAllowedEventSourceUid(s.uid)
|
|
2320
|
+
);
|
|
2321
|
+
const chosen = sources.find((s) => s.id === sourceId) ?? sources.find((s) => s.uid === EVENT_CONTENT_TYPE_UID) ?? sources[0];
|
|
2322
|
+
if (!chosen) {
|
|
2323
|
+
throw new PluginValidationError("No allowed event source is configured.");
|
|
2324
|
+
}
|
|
2325
|
+
return chosen;
|
|
2326
|
+
}
|
|
2327
|
+
function applyQuickCreateRRuleAndRsvp(data, source, settings2, params) {
|
|
2328
|
+
const rruleField = source.fields.rrule;
|
|
2329
|
+
const cleanRRule = normalizeRRuleString(params.rrule);
|
|
2330
|
+
if (rruleField && isSafeObjectKey(rruleField) && cleanRRule) {
|
|
2331
|
+
data[rruleField] = {
|
|
2332
|
+
rrule: cleanRRule,
|
|
2333
|
+
timezone: settings2.defaultTimezone,
|
|
2334
|
+
allDay: false,
|
|
2335
|
+
exdates: [],
|
|
2336
|
+
overrides: {}
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
if (source.fields.rsvpEnabled && isSafeObjectKey(source.fields.rsvpEnabled) && typeof params.rsvpEnabled === "boolean") {
|
|
2340
|
+
data[source.fields.rsvpEnabled] = params.rsvpEnabled;
|
|
2341
|
+
}
|
|
2342
|
+
if (source.fields.capacity && isSafeObjectKey(source.fields.capacity) && typeof params.capacity === "number" && Number.isFinite(params.capacity) && params.capacity >= 1) {
|
|
2343
|
+
data[source.fields.capacity] = Math.floor(params.capacity);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
function buildQuickCreatePayload(source, settings2, params) {
|
|
2347
|
+
const title = (params.title ?? "").trim();
|
|
2348
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
2349
|
+
const data = {
|
|
2350
|
+
[source.fields.title]: title,
|
|
2351
|
+
[source.fields.startDate]: params.startDate
|
|
2352
|
+
};
|
|
2353
|
+
const optionalStrings = [
|
|
2354
|
+
[source.fields.endDate, params.endDate],
|
|
2355
|
+
[source.fields.location, params.location],
|
|
2356
|
+
[source.fields.category, params.category],
|
|
2357
|
+
[source.fields.summary, params.summary]
|
|
2358
|
+
];
|
|
2359
|
+
for (const [fieldKey, val] of optionalStrings) {
|
|
2360
|
+
if (fieldKey && isSafeObjectKey(fieldKey) && val) {
|
|
2361
|
+
data[fieldKey] = val;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
if (source.fields.slug && isSafeObjectKey(source.fields.slug)) {
|
|
2365
|
+
data[source.fields.slug] = `${slug}-${Date.now().toString(36)}`;
|
|
2366
|
+
}
|
|
2367
|
+
applyQuickCreateRRuleAndRsvp(data, source, settings2, params);
|
|
2368
|
+
return data;
|
|
2369
|
+
}
|
|
2370
|
+
function resolveEffectiveQueryLocale(paramLoc, defaultLoc) {
|
|
2371
|
+
if (paramLoc && paramLoc !== "auto") return paramLoc;
|
|
2372
|
+
return defaultLoc && defaultLoc !== "auto" ? defaultLoc : void 0;
|
|
2373
|
+
}
|
|
2374
|
+
function buildSourceQueryOptions(source, params, settings2) {
|
|
2375
|
+
const posterField = source.fields.poster;
|
|
2376
|
+
const queryOpts = {
|
|
2377
|
+
limit: 500,
|
|
2378
|
+
...posterField ? { populate: [posterField] } : {}
|
|
2379
|
+
};
|
|
2380
|
+
if (params.documentId) {
|
|
2381
|
+
queryOpts.filters = { documentId: { $eq: params.documentId } };
|
|
2382
|
+
}
|
|
2383
|
+
const loc = resolveEffectiveQueryLocale(
|
|
2384
|
+
params.locale,
|
|
2385
|
+
settings2.defaultLocale
|
|
2386
|
+
);
|
|
2387
|
+
if (loc) queryOpts.locale = loc;
|
|
2388
|
+
if (!params.includeDrafts) queryOpts.status = "published";
|
|
2389
|
+
return queryOpts;
|
|
2390
|
+
}
|
|
2391
|
+
function filterOutExplicitDrafts(docs, includeDrafts) {
|
|
2392
|
+
if (includeDrafts) return docs;
|
|
2393
|
+
return docs.filter((doc) => doc.publishedAt !== null);
|
|
2394
|
+
}
|
|
2395
|
+
async function fetchDocumentsForSource(strapi, source, params, settings2) {
|
|
2396
|
+
if (!isAllowedEventSourceUid(source.uid)) {
|
|
2397
|
+
return [];
|
|
2398
|
+
}
|
|
2399
|
+
const client = getDocumentsClient(strapi, source.uid);
|
|
2400
|
+
try {
|
|
2401
|
+
const queryOpts = buildSourceQueryOptions(source, params, settings2);
|
|
2402
|
+
const docs = await client.findMany(queryOpts);
|
|
2403
|
+
if (docs.length === 0 && queryOpts.locale) {
|
|
2404
|
+
const { locale: _ignoredLocale, ...withoutLocaleOpts } = queryOpts;
|
|
2405
|
+
const fallbackLocaleDocs = await client.findMany(withoutLocaleOpts);
|
|
2406
|
+
return filterOutExplicitDrafts(fallbackLocaleDocs, params.includeDrafts);
|
|
2407
|
+
}
|
|
2408
|
+
return filterOutExplicitDrafts(docs, params.includeDrafts);
|
|
2409
|
+
} catch {
|
|
2410
|
+
return fetchDocumentsFallback(client, params);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
async function fetchDocumentsFallback(client, params) {
|
|
2414
|
+
try {
|
|
2415
|
+
const fallbackOpts = { limit: 500 };
|
|
2416
|
+
if (params.documentId) {
|
|
2417
|
+
fallbackOpts.filters = { documentId: { $eq: params.documentId } };
|
|
2418
|
+
}
|
|
2419
|
+
if (!params.includeDrafts) {
|
|
2420
|
+
fallbackOpts.status = "published";
|
|
2421
|
+
}
|
|
2422
|
+
const docs = await client.findMany(fallbackOpts);
|
|
2423
|
+
return filterOutExplicitDrafts(docs, params.includeDrafts);
|
|
2424
|
+
} catch {
|
|
2425
|
+
return [];
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
function resolveDocColor(doc, source, categoryVal) {
|
|
2429
|
+
const colors = source.categoryColors;
|
|
2430
|
+
const categoryColor = categoryVal && isSafeObjectKey(categoryVal) && colors && Object.hasOwn(colors, categoryVal) ? colors[categoryVal] : void 0;
|
|
2431
|
+
if (categoryColor) return categoryColor;
|
|
2432
|
+
if (typeof doc.color === "string" && doc.color.startsWith("#")) {
|
|
2433
|
+
return doc.color;
|
|
2434
|
+
}
|
|
2435
|
+
return source.color;
|
|
2436
|
+
}
|
|
2437
|
+
const readOptionalFieldString = (doc, key) => key && isSafeObjectKey(key) ? String(doc[key] ?? "") : "";
|
|
2438
|
+
function extractDocMetadata(strapi, doc, source, settings2, docId, baseDateKey) {
|
|
2439
|
+
const f = source.fields;
|
|
2440
|
+
const categoryVal = readOptionalFieldString(doc, f.category);
|
|
2441
|
+
const baseTitle = String(
|
|
2442
|
+
(isSafeObjectKey(f.title) ? doc[f.title] : void 0) ?? doc.title ?? "Untitled"
|
|
2443
|
+
);
|
|
2444
|
+
const slugVal = readOptionalFieldString(doc, f.slug);
|
|
2445
|
+
const rsvpField = f.rsvpEnabled && isSafeObjectKey(f.rsvpEnabled) ? f.rsvpEnabled : "rsvpEnabled";
|
|
2446
|
+
const capField = f.capacity && isSafeObjectKey(f.capacity) ? f.capacity : "capacity";
|
|
2447
|
+
return {
|
|
2448
|
+
docId,
|
|
2449
|
+
baseTitle,
|
|
2450
|
+
baseLocation: readOptionalFieldString(doc, f.location),
|
|
2451
|
+
summary: readOptionalFieldString(doc, f.summary),
|
|
2452
|
+
slug: slugVal || void 0,
|
|
2453
|
+
categoryVal,
|
|
2454
|
+
statusVal: mapEventStatus(
|
|
2455
|
+
f.status && isSafeObjectKey(f.status) ? doc[f.status] : doc.eventStatus
|
|
2456
|
+
),
|
|
2457
|
+
color: resolveDocColor(doc, source, categoryVal),
|
|
2458
|
+
rsvpEnabled: doc[rsvpField] !== void 0 ? Boolean(doc[rsvpField]) : false,
|
|
2459
|
+
capacity: readNullablePositiveInt(doc[capField]),
|
|
2460
|
+
waitlistEnabled: typeof doc.waitlistEnabled === "boolean" ? doc.waitlistEnabled : settings2.waitlistEnabledByDefault,
|
|
2461
|
+
baseDateKey,
|
|
2462
|
+
virtualUrl: sanitizeEventUrl(doc.virtualUrl, { allowMailto: false }) ?? void 0,
|
|
2463
|
+
poster: normalizeEventPoster(
|
|
2464
|
+
f.poster && isSafeObjectKey(f.poster) ? doc[f.poster] : doc.poster,
|
|
2465
|
+
baseTitle
|
|
2466
|
+
),
|
|
2467
|
+
actionLinks: normalizeEventActionLinks(doc[f.actionLinks ?? "actionLinks"]),
|
|
2468
|
+
spotlightConfig: normalizeEventSpotlightConfig(
|
|
2469
|
+
doc[f.spotlightConfig ?? "spotlightConfig"]
|
|
2470
|
+
),
|
|
2471
|
+
extractedCustomFields: extractCustomFields(
|
|
2472
|
+
strapi,
|
|
2473
|
+
source.uid,
|
|
2474
|
+
doc,
|
|
2475
|
+
f.customFields
|
|
2476
|
+
)
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
const matchesCategoryFilter = (cat, filter) => !filter || filter === "all" || cat.toLowerCase() === filter.toLowerCase();
|
|
2480
|
+
function resolveDocumentRecurrence(doc, source, defaultTimezone, allDay) {
|
|
2481
|
+
const rruleField = source.fields.rrule;
|
|
2482
|
+
const rawRecurrence = rruleField && isSafeObjectKey(rruleField) ? doc[rruleField] : null;
|
|
2483
|
+
const docTz = typeof doc.timezone === "string" && doc.timezone ? doc.timezone : defaultTimezone;
|
|
2484
|
+
const recurrence = parseRRuleFieldValue(rawRecurrence, docTz);
|
|
2485
|
+
if (allDay) recurrence.allDay = true;
|
|
2486
|
+
return recurrence;
|
|
2487
|
+
}
|
|
2488
|
+
function expandDocumentOccurrencesSync(args) {
|
|
2489
|
+
const {
|
|
2490
|
+
strapi,
|
|
2491
|
+
doc,
|
|
2492
|
+
source,
|
|
2493
|
+
params,
|
|
2494
|
+
settings: settings2,
|
|
2495
|
+
windowStart,
|
|
2496
|
+
windowEnd,
|
|
2497
|
+
pendingRsvpMap
|
|
2498
|
+
} = args;
|
|
2499
|
+
if (!isAllowedEventSourceUid(source.uid)) return [];
|
|
2500
|
+
const docId = String(doc.documentId ?? doc.id ?? "");
|
|
2501
|
+
if (!docId) return [];
|
|
2502
|
+
const startEnd = resolveStartEndFromDocument(doc, source);
|
|
2503
|
+
if (!startEnd) return [];
|
|
2504
|
+
const meta = extractDocMetadata(
|
|
2505
|
+
strapi,
|
|
2506
|
+
doc,
|
|
2507
|
+
source,
|
|
2508
|
+
settings2,
|
|
2509
|
+
docId,
|
|
2510
|
+
startEnd.startDate.slice(0, 10)
|
|
2511
|
+
);
|
|
2512
|
+
if (params.onlySpotlights && !meta.spotlightConfig?.enabled) {
|
|
2513
|
+
return [];
|
|
2514
|
+
}
|
|
2515
|
+
if (!matchesCategoryFilter(meta.categoryVal, params.category)) return [];
|
|
2516
|
+
const recurrence = resolveDocumentRecurrence(
|
|
2517
|
+
doc,
|
|
2518
|
+
source,
|
|
2519
|
+
settings2.defaultTimezone,
|
|
2520
|
+
startEnd.allDay
|
|
2521
|
+
);
|
|
2522
|
+
const slots = expandRuleSlots({
|
|
2523
|
+
baseStartDate: startEnd.startDate,
|
|
2524
|
+
baseEndDate: startEnd.endDate,
|
|
2525
|
+
recurrence,
|
|
2526
|
+
windowStart,
|
|
2527
|
+
windowEnd
|
|
2528
|
+
});
|
|
2529
|
+
const results = [];
|
|
2530
|
+
for (const slot of slots) {
|
|
2531
|
+
const occ = buildSingleExpandedOccurrence({
|
|
2532
|
+
doc,
|
|
2533
|
+
source,
|
|
2534
|
+
meta,
|
|
2535
|
+
slot,
|
|
2536
|
+
recurrence,
|
|
2537
|
+
settings: settings2,
|
|
2538
|
+
paramLocale: params.locale
|
|
2539
|
+
});
|
|
2540
|
+
if (meta.rsvpEnabled) {
|
|
2541
|
+
pendingRsvpMap.set(occ.occurrenceId, {
|
|
2542
|
+
eventUid: source.uid,
|
|
2543
|
+
eventDocumentId: meta.docId,
|
|
2544
|
+
occurrenceDate: slot.occurrenceDate,
|
|
2545
|
+
capacity: meta.capacity,
|
|
2546
|
+
waitlistEnabled: meta.waitlistEnabled,
|
|
2547
|
+
rsvpEnabled: meta.rsvpEnabled
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
results.push(occ);
|
|
2551
|
+
}
|
|
2552
|
+
return results;
|
|
2553
|
+
}
|
|
2554
|
+
const toUndefinedIfEmpty = (val) => val ? val : void 0;
|
|
2555
|
+
const readOptionalDocString = (doc, key) => typeof doc[key] === "string" && doc[key] ? doc[key] : void 0;
|
|
2556
|
+
function buildSingleExpandedOccurrence(args) {
|
|
2557
|
+
const { doc, source, meta, slot, recurrence, settings: settings2, paramLocale } = args;
|
|
2558
|
+
const effectiveTitle = slot.override?.title || meta.baseTitle;
|
|
2559
|
+
const effectiveLocation = slot.override?.location || meta.baseLocation;
|
|
2560
|
+
const calendarLinks = buildCalendarLinks({
|
|
2561
|
+
documentId: meta.docId,
|
|
2562
|
+
title: effectiveTitle,
|
|
2563
|
+
startDate: slot.startDate,
|
|
2564
|
+
endDate: slot.endDate,
|
|
2565
|
+
allDay: Boolean(recurrence.allDay),
|
|
2566
|
+
summary: meta.summary,
|
|
2567
|
+
location: effectiveLocation,
|
|
2568
|
+
occurrenceDate: slot.occurrenceDate
|
|
2569
|
+
});
|
|
2570
|
+
const alternateDates = shared.resolveOccurrenceAlternateDates({
|
|
2571
|
+
occurrenceDate: slot.occurrenceDate,
|
|
2572
|
+
baseDateKey: meta.baseDateKey,
|
|
2573
|
+
isRecurring: slot.isRecurring,
|
|
2574
|
+
doc,
|
|
2575
|
+
source,
|
|
2576
|
+
secondaryCalendars: settings2.secondaryCalendars ?? [],
|
|
2577
|
+
locale: paramLocale && paramLocale !== "auto" ? paramLocale : "en"
|
|
2578
|
+
});
|
|
2579
|
+
return {
|
|
2580
|
+
occurrenceId: `${source.id}:${meta.docId}:${slot.occurrenceDate}`,
|
|
2581
|
+
sourceId: source.id,
|
|
2582
|
+
sourceLabel: source.label,
|
|
2583
|
+
uid: source.uid,
|
|
2584
|
+
documentId: meta.docId,
|
|
2585
|
+
locale: readOptionalDocString(doc, "locale"),
|
|
2586
|
+
occurrenceDate: slot.occurrenceDate,
|
|
2587
|
+
startDate: slot.startDate,
|
|
2588
|
+
endDate: slot.endDate,
|
|
2589
|
+
allDay: Boolean(recurrence.allDay),
|
|
2590
|
+
timezone: recurrence.timezone,
|
|
2591
|
+
title: effectiveTitle,
|
|
2592
|
+
slug: meta.slug,
|
|
2593
|
+
summary: toUndefinedIfEmpty(meta.summary),
|
|
2594
|
+
location: toUndefinedIfEmpty(effectiveLocation),
|
|
2595
|
+
virtualUrl: meta.virtualUrl,
|
|
2596
|
+
category: toUndefinedIfEmpty(meta.categoryVal),
|
|
2597
|
+
status: meta.statusVal,
|
|
2598
|
+
color: meta.color,
|
|
2599
|
+
isRecurring: slot.isRecurring,
|
|
2600
|
+
rrule: toUndefinedIfEmpty(recurrence.rrule),
|
|
2601
|
+
isOverridden: slot.isOverridden,
|
|
2602
|
+
calendarLinks,
|
|
2603
|
+
alternateDates: Object.keys(alternateDates).length > 0 ? alternateDates : void 0,
|
|
2604
|
+
poster: meta.poster,
|
|
2605
|
+
actionLinks: meta.actionLinks,
|
|
2606
|
+
spotlightConfig: meta.spotlightConfig,
|
|
2607
|
+
customFields: meta.extractedCustomFields
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
function validateRescheduleDates(params) {
|
|
2611
|
+
if (params.cancelOccurrence && !params.newStartDate) {
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
const startMs = assertValidIsoDateTime(params.newStartDate, "newStartDate");
|
|
2615
|
+
if (params.newEndDate !== void 0 && params.newEndDate !== "") {
|
|
2616
|
+
const endMs = assertValidIsoDateTime(params.newEndDate, "newEndDate");
|
|
2617
|
+
if (endMs <= startMs) {
|
|
2618
|
+
throw new PluginValidationError(
|
|
2619
|
+
"Invalid newEndDate: must be after newStartDate."
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
function buildReschedulePayload(source, params) {
|
|
2625
|
+
if (params.cancelOccurrence && source.fields.status && isSafeObjectKey(source.fields.status)) {
|
|
2626
|
+
return { [source.fields.status]: "cancelled" };
|
|
2627
|
+
}
|
|
2628
|
+
const updatePayload = {
|
|
2629
|
+
[source.fields.startDate]: params.newStartDate
|
|
2630
|
+
};
|
|
2631
|
+
if (params.newEndDate && source.fields.endDate && isSafeObjectKey(source.fields.endDate)) {
|
|
2632
|
+
updatePayload[source.fields.endDate] = params.newEndDate;
|
|
2633
|
+
}
|
|
2634
|
+
return updatePayload;
|
|
2635
|
+
}
|
|
2636
|
+
function resolveRescheduleSource(sources, sourceId, uid) {
|
|
2637
|
+
const list = (sources ?? []).filter((s) => isAllowedEventSourceUid(s.uid));
|
|
2638
|
+
return list.find((s) => s.id === sourceId || s.uid === uid) ?? list[0];
|
|
2639
|
+
}
|
|
2640
|
+
function buildSingleOccurrenceOverridePatch(rruleField, recurrence, params) {
|
|
2641
|
+
if (!isSafeObjectKey(params.occurrenceDate) || !isValidCalendarDateKey(params.occurrenceDate)) {
|
|
2642
|
+
throw new PluginValidationError(
|
|
2643
|
+
"Invalid occurrenceDate: must be a valid YYYY-MM-DD calendar date."
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
const nextOverrides = {};
|
|
2647
|
+
if (recurrence.overrides && typeof recurrence.overrides === "object") {
|
|
2648
|
+
for (const [k, v] of Object.entries(recurrence.overrides)) {
|
|
2649
|
+
if (isSafeObjectKey(k) && v && typeof v === "object") {
|
|
2650
|
+
nextOverrides[k] = v;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
const prevOverride = Object.hasOwn(nextOverrides, params.occurrenceDate) ? nextOverrides[params.occurrenceDate] : void 0;
|
|
2655
|
+
nextOverrides[params.occurrenceDate] = {
|
|
2656
|
+
...prevOverride,
|
|
2657
|
+
originalDate: params.occurrenceDate,
|
|
2658
|
+
cancelled: Boolean(params.cancelOccurrence),
|
|
2659
|
+
startDate: params.newStartDate || void 0,
|
|
2660
|
+
endDate: params.newEndDate || void 0
|
|
2661
|
+
};
|
|
2662
|
+
return { [rruleField]: { ...recurrence, overrides: nextOverrides } };
|
|
2663
|
+
}
|
|
2664
|
+
const occurrence = ({ strapi }) => {
|
|
2665
|
+
return {
|
|
2666
|
+
registerCalendarFormatter(calendarId, formatter) {
|
|
2667
|
+
shared.registerCalendarSystemFormatter(calendarId, formatter);
|
|
2668
|
+
},
|
|
2669
|
+
unregisterCalendarFormatter(calendarId) {
|
|
2670
|
+
shared.unregisterCalendarSystemFormatter(calendarId);
|
|
2671
|
+
},
|
|
2672
|
+
formatDateInCalendarSystem(isoOrDateKey, system, locale = "en", options2) {
|
|
2673
|
+
return shared.formatDateInCalendarSystem(isoOrDateKey, system, locale, options2);
|
|
2674
|
+
},
|
|
2675
|
+
async getOccurrences(params) {
|
|
2676
|
+
const settings2 = await getSettingsService(strapi).getSettings();
|
|
2677
|
+
const { windowStart, windowEnd, isEmptyWindow } = resolveOccurrenceWindow(params);
|
|
2678
|
+
if (isEmptyWindow) {
|
|
2679
|
+
return [];
|
|
2680
|
+
}
|
|
2681
|
+
const activeSources = (settings2.sources ?? []).filter(
|
|
2682
|
+
(s) => s.enabled && isAllowedEventSourceUid(s.uid) && (!params.sourceId || s.id === params.sourceId || s.uid === params.sourceId)
|
|
2683
|
+
);
|
|
2684
|
+
const allOccurrences = [];
|
|
2685
|
+
const pendingRsvpMap = /* @__PURE__ */ new Map();
|
|
2686
|
+
for (const source of activeSources) {
|
|
2687
|
+
const docs = await fetchDocumentsForSource(
|
|
2688
|
+
strapi,
|
|
2689
|
+
source,
|
|
2690
|
+
params,
|
|
2691
|
+
settings2
|
|
2692
|
+
);
|
|
2693
|
+
for (const doc of docs) {
|
|
2694
|
+
const docOccurrences = expandDocumentOccurrencesSync({
|
|
2695
|
+
strapi,
|
|
2696
|
+
doc,
|
|
2697
|
+
source,
|
|
2698
|
+
params,
|
|
2699
|
+
settings: settings2,
|
|
2700
|
+
windowStart,
|
|
2701
|
+
windowEnd,
|
|
2702
|
+
pendingRsvpMap
|
|
2703
|
+
});
|
|
2704
|
+
allOccurrences.push(...docOccurrences);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
allOccurrences.sort(
|
|
2708
|
+
(a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
|
2709
|
+
);
|
|
2710
|
+
const sliced = clampPaginatedSlice(allOccurrences, {
|
|
2711
|
+
limit: params.limit,
|
|
2712
|
+
page: params.page,
|
|
2713
|
+
pageSize: params.pageSize,
|
|
2714
|
+
defaultLimit: params.includeDrafts ? 2e3 : 500,
|
|
2715
|
+
maxLimit: params.includeDrafts ? 2e3 : 500
|
|
2716
|
+
});
|
|
2717
|
+
await attachBatchedRsvpStats(strapi, sliced, pendingRsvpMap);
|
|
2718
|
+
return sliced;
|
|
2719
|
+
},
|
|
2720
|
+
async getActiveSpotlights(params) {
|
|
2721
|
+
const refDate = params?.date && !Number.isNaN(new Date(params.date).getTime()) ? new Date(params.date) : /* @__PURE__ */ new Date();
|
|
2722
|
+
const fromWindow = new Date(
|
|
2723
|
+
refDate.getTime() - 60 * 24 * 60 * 60 * 1e3
|
|
2724
|
+
).toISOString();
|
|
2725
|
+
const toWindow = new Date(
|
|
2726
|
+
refDate.getTime() + 180 * 24 * 60 * 60 * 1e3
|
|
2727
|
+
).toISOString();
|
|
2728
|
+
const occurrences = await this.getOccurrences({
|
|
2729
|
+
from: fromWindow,
|
|
2730
|
+
to: toWindow,
|
|
2731
|
+
locale: params?.locale,
|
|
2732
|
+
sourceId: params?.sourceId,
|
|
2733
|
+
category: params?.category,
|
|
2734
|
+
includeDrafts: params?.includeDrafts ?? false,
|
|
2735
|
+
onlySpotlights: true
|
|
2736
|
+
});
|
|
2737
|
+
const active = occurrences.filter(
|
|
2738
|
+
(occ) => isEventSpotlightActiveOnDate({
|
|
2739
|
+
spotlightConfig: occ.spotlightConfig,
|
|
2740
|
+
eventStartDate: occ.startDate,
|
|
2741
|
+
eventEndDate: occ.endDate,
|
|
2742
|
+
referenceDate: refDate
|
|
2743
|
+
})
|
|
2744
|
+
);
|
|
2745
|
+
const sorted = deduplicateAndSortSpotlights(active);
|
|
2746
|
+
return clampPaginatedSlice(sorted, {
|
|
2747
|
+
limit: params?.limit,
|
|
2748
|
+
page: params?.page,
|
|
2749
|
+
pageSize: params?.pageSize,
|
|
2750
|
+
defaultLimit: 100,
|
|
2751
|
+
maxLimit: 100
|
|
2752
|
+
});
|
|
2753
|
+
},
|
|
2754
|
+
async rescheduleOccurrence(params) {
|
|
2755
|
+
const docId = (params.documentId ?? "").trim();
|
|
2756
|
+
if (!docId) {
|
|
2757
|
+
throw new PluginValidationError("Event documentId is required.");
|
|
2758
|
+
}
|
|
2759
|
+
validateRescheduleDates(params);
|
|
2760
|
+
const settings2 = await getSettingsService(strapi).getSettings();
|
|
2761
|
+
const source = resolveRescheduleSource(
|
|
2762
|
+
settings2.sources,
|
|
2763
|
+
params.sourceId,
|
|
2764
|
+
params.uid
|
|
2765
|
+
);
|
|
2766
|
+
if (!source || !isAllowedEventSourceUid(source.uid)) {
|
|
2767
|
+
throw new PluginValidationError(
|
|
2768
|
+
"Target event source is not configured."
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
const client = getDocumentsClient(strapi, source.uid);
|
|
2772
|
+
const existing = await client.findOne({ documentId: docId });
|
|
2773
|
+
if (!existing) {
|
|
2774
|
+
throw new PluginNotFoundError(
|
|
2775
|
+
`Document ${docId} not found in ${source.uid}.`
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
const rruleField = source.fields.rrule;
|
|
2779
|
+
const recurrence = rruleField && isSafeObjectKey(rruleField) ? parseRRuleFieldValue(existing[rruleField], settings2.defaultTimezone) : null;
|
|
2780
|
+
if (recurrence?.rrule && !params.applyToSeries && rruleField && isSafeObjectKey(rruleField)) {
|
|
2781
|
+
return client.update({
|
|
2782
|
+
documentId: docId,
|
|
2783
|
+
data: buildSingleOccurrenceOverridePatch(
|
|
2784
|
+
rruleField,
|
|
2785
|
+
recurrence,
|
|
2786
|
+
params
|
|
2787
|
+
)
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
return client.update({
|
|
2791
|
+
documentId: docId,
|
|
2792
|
+
data: buildReschedulePayload(source, params)
|
|
2793
|
+
});
|
|
2794
|
+
},
|
|
2795
|
+
async quickCreateEvent(params) {
|
|
2796
|
+
const title = (params.title ?? "").trim();
|
|
2797
|
+
if (!title) {
|
|
2798
|
+
throw new PluginValidationError("Event title is required.");
|
|
2799
|
+
}
|
|
2800
|
+
const startMs = assertValidIsoDateTime(params.startDate, "startDate");
|
|
2801
|
+
if (params.endDate !== void 0 && params.endDate !== "") {
|
|
2802
|
+
const endMs = assertValidIsoDateTime(params.endDate, "endDate");
|
|
2803
|
+
if (endMs <= startMs) {
|
|
2804
|
+
throw new PluginValidationError(
|
|
2805
|
+
"Invalid endDate: must be after startDate."
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
const settings2 = await getSettingsService(strapi).getSettings();
|
|
2810
|
+
const source = resolveQuickCreateSource(settings2, params.sourceId);
|
|
2811
|
+
const data = buildQuickCreatePayload(source, settings2, {
|
|
2812
|
+
...params,
|
|
2813
|
+
title
|
|
2814
|
+
});
|
|
2815
|
+
return getDocumentsClient(strapi, source.uid).create({
|
|
2816
|
+
data,
|
|
2817
|
+
status: "published"
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
};
|
|
2822
|
+
async function fetchRegistrationRecordsSafe(strapi, eventDocumentId, occurrenceDate) {
|
|
2823
|
+
const filters = {
|
|
2824
|
+
eventDocumentId: { $eq: eventDocumentId },
|
|
2825
|
+
...occurrenceDate ? { occurrenceDate: { $eq: occurrenceDate } } : {}
|
|
2826
|
+
};
|
|
2827
|
+
try {
|
|
2828
|
+
return await getDocumentsClient(
|
|
2829
|
+
strapi,
|
|
2830
|
+
REGISTRATION_CONTENT_TYPE_UID
|
|
2831
|
+
).findMany({
|
|
2832
|
+
filters,
|
|
2833
|
+
limit: 5e3
|
|
2834
|
+
});
|
|
2835
|
+
} catch {
|
|
2836
|
+
return [];
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
async function promoteFromWaitlistUnlocked(strapi, params) {
|
|
2840
|
+
const targetEvent = params.targetEventOverride !== void 0 ? params.targetEventOverride : await resolveTargetEvent(
|
|
2841
|
+
strapi,
|
|
2842
|
+
params.eventDocumentId,
|
|
2843
|
+
params.eventUid,
|
|
2844
|
+
{ includeDrafts: true }
|
|
2845
|
+
);
|
|
2846
|
+
if (!targetEvent || targetEvent.capacity === null || targetEvent.eventStatus === "cancelled") {
|
|
2847
|
+
return { promoted: [], emails: [] };
|
|
2848
|
+
}
|
|
2849
|
+
const slotRecords = await fetchRegistrationRecordsSafe(
|
|
2850
|
+
strapi,
|
|
2851
|
+
params.eventDocumentId,
|
|
2852
|
+
params.occurrenceDate
|
|
2853
|
+
);
|
|
2854
|
+
const counts = aggregateRsvpCounts(slotRecords);
|
|
2855
|
+
let availableSeats = Math.max(
|
|
2856
|
+
0,
|
|
2857
|
+
targetEvent.capacity - counts.confirmedSeats
|
|
2858
|
+
);
|
|
2859
|
+
if (availableSeats <= 0) {
|
|
2860
|
+
return { promoted: [], emails: [] };
|
|
2861
|
+
}
|
|
2862
|
+
const client = getDocumentsClient(strapi, REGISTRATION_CONTENT_TYPE_UID);
|
|
2863
|
+
const waitlisted = await client.findMany({
|
|
2864
|
+
filters: {
|
|
2865
|
+
eventDocumentId: { $eq: params.eventDocumentId },
|
|
2866
|
+
occurrenceDate: { $eq: params.occurrenceDate },
|
|
2867
|
+
status: { $eq: "waitlisted" }
|
|
2868
|
+
},
|
|
2869
|
+
sort: "createdAt:asc",
|
|
2870
|
+
limit: 200
|
|
2871
|
+
});
|
|
2872
|
+
const promoted = [];
|
|
2873
|
+
const emails = [];
|
|
2874
|
+
for (const candidate of waitlisted) {
|
|
2875
|
+
if (availableSeats <= 0) break;
|
|
2876
|
+
if (candidate.status !== "waitlisted") continue;
|
|
2877
|
+
const needed = readStoredGuestsCount(candidate.guestsCount);
|
|
2878
|
+
if (needed > availableSeats) continue;
|
|
2879
|
+
const updated = await client.update({
|
|
2880
|
+
documentId: String(candidate.documentId),
|
|
2881
|
+
data: { status: "confirmed" }
|
|
2882
|
+
});
|
|
2883
|
+
availableSeats -= needed;
|
|
2884
|
+
promoted.push(formatRegistrationRecord(updated));
|
|
2885
|
+
emails.push({
|
|
2886
|
+
to: String(candidate.attendeeEmail),
|
|
2887
|
+
attendeeName: String(candidate.attendeeName),
|
|
2888
|
+
eventTitle: String(candidate.eventTitle ?? targetEvent.title),
|
|
2889
|
+
occurrenceDate: params.occurrenceDate,
|
|
2890
|
+
guestsCount: needed,
|
|
2891
|
+
status: "confirmed",
|
|
2892
|
+
promotedFromWaitlist: true
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
return { promoted, emails };
|
|
2896
|
+
}
|
|
2897
|
+
async function executeCancelByToken(strapi, token) {
|
|
2898
|
+
const cleanedToken = (token ?? "").trim();
|
|
2899
|
+
if (!cleanedToken || cleanedToken.length < 16) {
|
|
2900
|
+
throw new PluginValidationError("Invalid cancellation token.");
|
|
2901
|
+
}
|
|
2902
|
+
const initialMatch = await findRegistrationByRawToken(strapi, cleanedToken);
|
|
2903
|
+
if (!initialMatch) {
|
|
2904
|
+
throw new PluginNotFoundError(
|
|
2905
|
+
"Registration not found for the provided token."
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
const regDocId = String(initialMatch.record.documentId);
|
|
2909
|
+
const eventDocId = String(initialMatch.record.eventDocumentId);
|
|
2910
|
+
const occDate = String(initialMatch.record.occurrenceDate);
|
|
2911
|
+
const eventUid = String(
|
|
2912
|
+
initialMatch.record.eventUid ?? EVENT_CONTENT_TYPE_UID
|
|
2913
|
+
);
|
|
2914
|
+
const targetEvent = await resolveTargetEvent(strapi, eventDocId, eventUid, {
|
|
2915
|
+
includeDrafts: true
|
|
2916
|
+
});
|
|
2917
|
+
if (targetEvent) {
|
|
2918
|
+
try {
|
|
2919
|
+
const endMs = assertScheduledOccurrence(targetEvent, occDate);
|
|
2920
|
+
if (endMs !== null && Number.isFinite(endMs) && Date.now() > endMs) {
|
|
2921
|
+
throw new PluginValidationError(
|
|
2922
|
+
"Cannot cancel registration after the event has ended."
|
|
2923
|
+
);
|
|
2924
|
+
}
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
if (err instanceof PluginValidationError && err.message.includes("ended")) {
|
|
2927
|
+
throw err;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
const { cancelledRecord, emails, promotedCount } = await withOccurrenceLock(
|
|
2932
|
+
strapi,
|
|
2933
|
+
eventDocId,
|
|
2934
|
+
occDate,
|
|
2935
|
+
async () => {
|
|
2936
|
+
const client = getDocumentsClient(strapi, REGISTRATION_CONTENT_TYPE_UID);
|
|
2937
|
+
const fresh = await client.findOne({ documentId: regDocId });
|
|
2938
|
+
if (!fresh || !verifyTokenHashConstantTime(initialMatch.tokenHash, fresh.checkInToken)) {
|
|
2939
|
+
throw new PluginValidationError(
|
|
2940
|
+
"Invalid or already used cancellation token."
|
|
2941
|
+
);
|
|
2942
|
+
}
|
|
2943
|
+
if (fresh.status === "cancelled") {
|
|
2944
|
+
throw new PluginValidationError(
|
|
2945
|
+
"Registration has already been cancelled."
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
if (fresh.status === "checked_in") {
|
|
2949
|
+
throw new PluginValidationError(
|
|
2950
|
+
"Checked-in registrations cannot be cancelled."
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
const wasConfirmed = fresh.status === "confirmed";
|
|
2954
|
+
const updated = await client.update({
|
|
2955
|
+
documentId: regDocId,
|
|
2956
|
+
data: { status: "cancelled", checkInToken: null }
|
|
2957
|
+
});
|
|
2958
|
+
const promo = wasConfirmed ? await promoteFromWaitlistUnlocked(strapi, {
|
|
2959
|
+
eventUid,
|
|
2960
|
+
eventDocumentId: eventDocId,
|
|
2961
|
+
occurrenceDate: occDate,
|
|
2962
|
+
targetEventOverride: targetEvent
|
|
2963
|
+
}) : { promoted: [], emails: [] };
|
|
2964
|
+
return {
|
|
2965
|
+
cancelledRecord: formatRegistrationRecord(updated),
|
|
2966
|
+
promotedCount: promo.promoted.length,
|
|
2967
|
+
emails: promo.emails
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
);
|
|
2971
|
+
for (const email of emails) {
|
|
2972
|
+
await trySendNotificationEmail(strapi, email);
|
|
2973
|
+
}
|
|
2974
|
+
return {
|
|
2975
|
+
cancelled: cancelledRecord,
|
|
2976
|
+
promotedCount
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
async function executeUpdateRegistrationStatus(strapi, registrationDocumentId, newStatus) {
|
|
2980
|
+
const validStatuses = [
|
|
2981
|
+
"confirmed",
|
|
2982
|
+
"waitlisted",
|
|
2983
|
+
"cancelled",
|
|
2984
|
+
"checked_in"
|
|
2985
|
+
];
|
|
2986
|
+
if (!validStatuses.includes(newStatus)) {
|
|
2987
|
+
throw new PluginValidationError(`Invalid status: ${String(newStatus)}`);
|
|
2988
|
+
}
|
|
2989
|
+
const client = getDocumentsClient(strapi, REGISTRATION_CONTENT_TYPE_UID);
|
|
2990
|
+
const initial = await client.findOne({
|
|
2991
|
+
documentId: registrationDocumentId
|
|
2992
|
+
});
|
|
2993
|
+
if (!initial) {
|
|
2994
|
+
throw new PluginNotFoundError(
|
|
2995
|
+
`Registration ${registrationDocumentId} not found.`
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2998
|
+
const eventDocId = String(initial.eventDocumentId);
|
|
2999
|
+
const occDate = String(initial.occurrenceDate);
|
|
3000
|
+
const eventUid = String(initial.eventUid ?? EVENT_CONTENT_TYPE_UID);
|
|
3001
|
+
const { registration, promotedCount, emails } = await withOccurrenceLock(
|
|
3002
|
+
strapi,
|
|
3003
|
+
eventDocId,
|
|
3004
|
+
occDate,
|
|
3005
|
+
async () => {
|
|
3006
|
+
const existing = await client.findOne({
|
|
3007
|
+
documentId: registrationDocumentId
|
|
3008
|
+
});
|
|
3009
|
+
if (!existing) {
|
|
3010
|
+
throw new PluginNotFoundError(
|
|
3011
|
+
`Registration ${registrationDocumentId} not found.`
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
const wasOccupyingSeat = existing.status === "confirmed" || existing.status === "checked_in";
|
|
3015
|
+
const nowOccupyingSeat = newStatus === "confirmed" || newStatus === "checked_in";
|
|
3016
|
+
const targetEvent = await resolveTargetEvent(
|
|
3017
|
+
strapi,
|
|
3018
|
+
eventDocId,
|
|
3019
|
+
eventUid,
|
|
3020
|
+
{ includeDrafts: true }
|
|
3021
|
+
);
|
|
3022
|
+
if (nowOccupyingSeat && targetEvent?.eventStatus === "cancelled") {
|
|
3023
|
+
throw new PluginValidationError(
|
|
3024
|
+
"Cannot confirm or check in registration for a cancelled event."
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
if (!wasOccupyingSeat && nowOccupyingSeat && targetEvent && targetEvent.capacity !== null) {
|
|
3028
|
+
const slotRecords = await fetchRegistrationRecordsSafe(
|
|
3029
|
+
strapi,
|
|
3030
|
+
eventDocId,
|
|
3031
|
+
occDate
|
|
3032
|
+
);
|
|
3033
|
+
const counts = aggregateRsvpCounts(slotRecords);
|
|
3034
|
+
const neededSeats = readStoredGuestsCount(existing.guestsCount);
|
|
3035
|
+
if (counts.confirmedSeats + neededSeats > targetEvent.capacity) {
|
|
3036
|
+
throw new PluginValidationError(
|
|
3037
|
+
"Cannot confirm registration: event has reached maximum capacity."
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
const dataPatch = {
|
|
3042
|
+
status: newStatus,
|
|
3043
|
+
...newStatus === "checked_in" && !existing.checkedInAt ? { checkedInAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
|
|
3044
|
+
...newStatus === "cancelled" ? { checkInToken: null } : {}
|
|
3045
|
+
};
|
|
3046
|
+
const updated = await client.update({
|
|
3047
|
+
documentId: registrationDocumentId,
|
|
3048
|
+
data: dataPatch
|
|
3049
|
+
});
|
|
3050
|
+
const promo = wasOccupyingSeat && !nowOccupyingSeat ? await promoteFromWaitlistUnlocked(strapi, {
|
|
3051
|
+
eventUid,
|
|
3052
|
+
eventDocumentId: eventDocId,
|
|
3053
|
+
occurrenceDate: occDate,
|
|
3054
|
+
targetEventOverride: targetEvent
|
|
3055
|
+
}) : { promoted: [], emails: [] };
|
|
3056
|
+
return {
|
|
3057
|
+
registration: formatRegistrationRecord(updated),
|
|
3058
|
+
promotedCount: promo.promoted.length,
|
|
3059
|
+
emails: promo.emails
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
);
|
|
3063
|
+
for (const email of emails) {
|
|
3064
|
+
await trySendNotificationEmail(strapi, email);
|
|
3065
|
+
}
|
|
3066
|
+
return { registration, promotedCount };
|
|
3067
|
+
}
|
|
3068
|
+
async function executeCheckInAttendee(strapi, identifier) {
|
|
3069
|
+
let targetDocId = identifier.documentId?.trim();
|
|
3070
|
+
let providedTokenHash = null;
|
|
3071
|
+
if (identifier.token !== void 0) {
|
|
3072
|
+
const cleanedToken = String(identifier.token).trim();
|
|
3073
|
+
if (cleanedToken.length < 16 || cleanedToken.length > 256) {
|
|
3074
|
+
throw new PluginValidationError("Invalid check-in token.");
|
|
3075
|
+
}
|
|
3076
|
+
providedTokenHash = hashRsvpToken(cleanedToken);
|
|
3077
|
+
if (!targetDocId) {
|
|
3078
|
+
const match = await findRegistrationByRawToken(strapi, cleanedToken);
|
|
3079
|
+
if (!match) {
|
|
3080
|
+
throw new PluginValidationError("Invalid check-in token.");
|
|
3081
|
+
}
|
|
3082
|
+
targetDocId = String(match.record.documentId);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
if (!targetDocId) {
|
|
3086
|
+
throw new PluginNotFoundError("Registration not found for check-in.");
|
|
3087
|
+
}
|
|
3088
|
+
const client = getDocumentsClient(strapi, REGISTRATION_CONTENT_TYPE_UID);
|
|
3089
|
+
const initial = await client.findOne({ documentId: targetDocId });
|
|
3090
|
+
if (!initial) {
|
|
3091
|
+
throw new PluginNotFoundError("Registration not found for check-in.");
|
|
3092
|
+
}
|
|
3093
|
+
const eventDocId = String(initial.eventDocumentId);
|
|
3094
|
+
const occDate = String(initial.occurrenceDate);
|
|
3095
|
+
const eventUid = String(initial.eventUid ?? EVENT_CONTENT_TYPE_UID);
|
|
3096
|
+
return withOccurrenceLock(strapi, eventDocId, occDate, async () => {
|
|
3097
|
+
const existing = await client.findOne({ documentId: targetDocId });
|
|
3098
|
+
if (!existing) {
|
|
3099
|
+
throw new PluginNotFoundError("Registration not found for check-in.");
|
|
3100
|
+
}
|
|
3101
|
+
if (providedTokenHash !== null && !verifyTokenHashConstantTime(providedTokenHash, existing.checkInToken)) {
|
|
3102
|
+
throw new PluginValidationError("Invalid check-in token.");
|
|
3103
|
+
}
|
|
3104
|
+
const targetEvent = await resolveTargetEvent(strapi, eventDocId, eventUid, {
|
|
3105
|
+
includeDrafts: true
|
|
3106
|
+
});
|
|
3107
|
+
if (targetEvent?.eventStatus === "cancelled") {
|
|
3108
|
+
throw new PluginValidationError(
|
|
3109
|
+
"Cannot check in attendee for a cancelled event."
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3112
|
+
if (existing.status === "checked_in") {
|
|
3113
|
+
return formatRegistrationRecord(existing);
|
|
3114
|
+
}
|
|
3115
|
+
if (existing.status !== "confirmed") {
|
|
3116
|
+
throw new PluginValidationError(
|
|
3117
|
+
`Cannot check in attendee with status "${String(existing.status)}": only confirmed registrations can be checked in.`
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
const updated = await client.update({
|
|
3121
|
+
documentId: targetDocId,
|
|
3122
|
+
data: {
|
|
3123
|
+
status: "checked_in",
|
|
3124
|
+
checkedInAt: existing.checkedInAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
3127
|
+
return formatRegistrationRecord(updated);
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
function buildListRegistrationsQueryFilters(filters) {
|
|
3131
|
+
const queryFilters = {};
|
|
3132
|
+
if (filters?.eventDocumentId) {
|
|
3133
|
+
queryFilters.eventDocumentId = { $eq: filters.eventDocumentId };
|
|
3134
|
+
}
|
|
3135
|
+
if (filters?.occurrenceDate) {
|
|
3136
|
+
queryFilters.occurrenceDate = { $eq: filters.occurrenceDate };
|
|
3137
|
+
}
|
|
3138
|
+
if (filters?.status) {
|
|
3139
|
+
queryFilters.status = { $eq: filters.status };
|
|
3140
|
+
}
|
|
3141
|
+
const q = filters?.search?.trim();
|
|
3142
|
+
if (q) {
|
|
3143
|
+
queryFilters.$or = [
|
|
3144
|
+
{ attendeeName: { $containsi: q } },
|
|
3145
|
+
{ attendeeEmail: { $containsi: q } },
|
|
3146
|
+
{ eventTitle: { $containsi: q } }
|
|
3147
|
+
];
|
|
3148
|
+
}
|
|
3149
|
+
return queryFilters;
|
|
3150
|
+
}
|
|
3151
|
+
async function resolveStatsEventConfig(strapi, params) {
|
|
3152
|
+
if (params.capacityOverride === void 0) {
|
|
3153
|
+
const resolved = await resolveTargetEvent(
|
|
3154
|
+
strapi,
|
|
3155
|
+
params.eventDocumentId,
|
|
3156
|
+
params.eventUid,
|
|
3157
|
+
{ includeDrafts: params.includeDrafts ?? false }
|
|
3158
|
+
);
|
|
3159
|
+
if (resolved) {
|
|
3160
|
+
return {
|
|
3161
|
+
eventUid: resolved.uid,
|
|
3162
|
+
capacity: resolved.capacity,
|
|
3163
|
+
waitlistEnabled: resolved.waitlistEnabled,
|
|
3164
|
+
rsvpEnabled: resolved.rsvpEnabled
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
return {
|
|
3169
|
+
eventUid: params.eventUid ?? EVENT_CONTENT_TYPE_UID,
|
|
3170
|
+
capacity: params.capacityOverride ?? null,
|
|
3171
|
+
waitlistEnabled: params.waitlistOverride ?? true,
|
|
3172
|
+
rsvpEnabled: params.rsvpEnabledOverride ?? false
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
const rsvp = ({ strapi }) => {
|
|
3176
|
+
return {
|
|
3177
|
+
async getEventStats(params) {
|
|
3178
|
+
const { eventDocumentId, occurrenceDate = null } = params;
|
|
3179
|
+
const { eventUid, capacity, waitlistEnabled, rsvpEnabled } = await resolveStatsEventConfig(strapi, params);
|
|
3180
|
+
const records = await fetchRegistrationRecordsSafe(
|
|
3181
|
+
strapi,
|
|
3182
|
+
eventDocumentId,
|
|
3183
|
+
occurrenceDate
|
|
3184
|
+
);
|
|
3185
|
+
const counts = aggregateRsvpCounts(records);
|
|
3186
|
+
const remainingSeats = capacity !== null ? Math.max(0, capacity - counts.confirmedSeats) : null;
|
|
3187
|
+
const isFull = capacity !== null ? counts.confirmedSeats >= capacity : false;
|
|
3188
|
+
return {
|
|
3189
|
+
eventUid,
|
|
3190
|
+
eventDocumentId,
|
|
3191
|
+
occurrenceDate,
|
|
3192
|
+
rsvpEnabled,
|
|
3193
|
+
capacity,
|
|
3194
|
+
waitlistEnabled,
|
|
3195
|
+
...counts,
|
|
3196
|
+
remainingSeats,
|
|
3197
|
+
isFull
|
|
3198
|
+
};
|
|
3199
|
+
},
|
|
3200
|
+
async registerAttendee(eventDocumentId, input) {
|
|
3201
|
+
const docId = (eventDocumentId ?? "").trim();
|
|
3202
|
+
if (!docId) {
|
|
3203
|
+
throw new PluginValidationError("Event documentId is required.");
|
|
3204
|
+
}
|
|
3205
|
+
const targetEvent = await resolveTargetEvent(
|
|
3206
|
+
strapi,
|
|
3207
|
+
docId,
|
|
3208
|
+
input.eventUid,
|
|
3209
|
+
{ includeDrafts: false }
|
|
3210
|
+
);
|
|
3211
|
+
if (!targetEvent) {
|
|
3212
|
+
throw new PluginNotFoundError(
|
|
3213
|
+
`Event not found for documentId: ${docId}`
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
const { attendeeName, attendeeEmail, guestsCount } = validateRegistrationInputAndEvent(input, targetEvent);
|
|
3217
|
+
const occurrenceDate = resolveValidatedOccurrenceDate(
|
|
3218
|
+
input.occurrenceDate,
|
|
3219
|
+
targetEvent
|
|
3220
|
+
);
|
|
3221
|
+
const rawToken = generateRawRsvpToken();
|
|
3222
|
+
const tokenHash = hashRsvpToken(rawToken);
|
|
3223
|
+
const { formatted, assignedStatus } = await withOccurrenceLock(
|
|
3224
|
+
strapi,
|
|
3225
|
+
docId,
|
|
3226
|
+
occurrenceDate,
|
|
3227
|
+
async () => {
|
|
3228
|
+
const client = getDocumentsClient(
|
|
3229
|
+
strapi,
|
|
3230
|
+
REGISTRATION_CONTENT_TYPE_UID
|
|
3231
|
+
);
|
|
3232
|
+
const existing = await client.findMany({
|
|
3233
|
+
filters: {
|
|
3234
|
+
eventDocumentId: { $eq: docId },
|
|
3235
|
+
occurrenceDate: { $eq: occurrenceDate },
|
|
3236
|
+
attendeeEmail: { $eq: attendeeEmail },
|
|
3237
|
+
status: { $in: ["confirmed", "waitlisted", "checked_in"] }
|
|
3238
|
+
},
|
|
3239
|
+
limit: 1
|
|
3240
|
+
});
|
|
3241
|
+
if (existing.length > 0) {
|
|
3242
|
+
throw new PluginValidationError(
|
|
3243
|
+
"An active registration already exists for this email and date."
|
|
3244
|
+
);
|
|
3245
|
+
}
|
|
3246
|
+
const slotRecords = await fetchRegistrationRecordsSafe(
|
|
3247
|
+
strapi,
|
|
3248
|
+
docId,
|
|
3249
|
+
occurrenceDate
|
|
3250
|
+
);
|
|
3251
|
+
const counts = aggregateRsvpCounts(slotRecords);
|
|
3252
|
+
const status = resolveAssignedRsvpStatus(
|
|
3253
|
+
targetEvent,
|
|
3254
|
+
counts.confirmedSeats,
|
|
3255
|
+
guestsCount
|
|
3256
|
+
);
|
|
3257
|
+
const created = await client.create({
|
|
3258
|
+
data: buildRegistrationCreateData({
|
|
3259
|
+
targetEvent,
|
|
3260
|
+
eventDocumentId: docId,
|
|
3261
|
+
occurrenceDate,
|
|
3262
|
+
attendeeName,
|
|
3263
|
+
attendeeEmail,
|
|
3264
|
+
guestsCount,
|
|
3265
|
+
assignedStatus: status,
|
|
3266
|
+
tokenHash,
|
|
3267
|
+
input
|
|
3268
|
+
})
|
|
3269
|
+
});
|
|
3270
|
+
return {
|
|
3271
|
+
assignedStatus: status,
|
|
3272
|
+
formatted: formatRegistrationRecord({
|
|
3273
|
+
...created,
|
|
3274
|
+
eventUid: targetEvent.uid,
|
|
3275
|
+
eventDocumentId: docId,
|
|
3276
|
+
eventTitle: targetEvent.title,
|
|
3277
|
+
occurrenceDate,
|
|
3278
|
+
attendeeName,
|
|
3279
|
+
attendeeEmail,
|
|
3280
|
+
guestsCount,
|
|
3281
|
+
status
|
|
3282
|
+
})
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
);
|
|
3286
|
+
await trySendNotificationEmail(strapi, {
|
|
3287
|
+
to: attendeeEmail,
|
|
3288
|
+
attendeeName,
|
|
3289
|
+
eventTitle: targetEvent.title,
|
|
3290
|
+
occurrenceDate,
|
|
3291
|
+
guestsCount,
|
|
3292
|
+
status: assignedStatus
|
|
3293
|
+
});
|
|
3294
|
+
return {
|
|
3295
|
+
...formatted,
|
|
3296
|
+
attendeePhone: formatted.attendeePhone ?? "",
|
|
3297
|
+
notes: formatted.notes ?? "",
|
|
3298
|
+
cancellationToken: rawToken
|
|
3299
|
+
};
|
|
3300
|
+
},
|
|
3301
|
+
async promoteFromWaitlist(params) {
|
|
3302
|
+
const { promoted, emails } = await withOccurrenceLock(
|
|
3303
|
+
strapi,
|
|
3304
|
+
params.eventDocumentId,
|
|
3305
|
+
params.occurrenceDate,
|
|
3306
|
+
() => promoteFromWaitlistUnlocked(strapi, params)
|
|
3307
|
+
);
|
|
3308
|
+
for (const email of emails) {
|
|
3309
|
+
await trySendNotificationEmail(strapi, email);
|
|
3310
|
+
}
|
|
3311
|
+
return promoted;
|
|
3312
|
+
},
|
|
3313
|
+
cancelByToken(token) {
|
|
3314
|
+
return executeCancelByToken(strapi, token);
|
|
3315
|
+
},
|
|
3316
|
+
updateRegistrationStatus(registrationDocumentId, newStatus) {
|
|
3317
|
+
return executeUpdateRegistrationStatus(
|
|
3318
|
+
strapi,
|
|
3319
|
+
registrationDocumentId,
|
|
3320
|
+
newStatus
|
|
3321
|
+
);
|
|
3322
|
+
},
|
|
3323
|
+
checkInAttendee(identifier) {
|
|
3324
|
+
return executeCheckInAttendee(strapi, identifier);
|
|
3325
|
+
},
|
|
3326
|
+
async listRegistrations(filters) {
|
|
3327
|
+
const rows = await getDocumentsClient(
|
|
3328
|
+
strapi,
|
|
3329
|
+
REGISTRATION_CONTENT_TYPE_UID
|
|
3330
|
+
).findMany({
|
|
3331
|
+
filters: buildListRegistrationsQueryFilters(filters),
|
|
3332
|
+
sort: "createdAt:desc",
|
|
3333
|
+
limit: 1e3
|
|
3334
|
+
});
|
|
3335
|
+
const formatted = rows.map((r) => formatRegistrationRecord(r));
|
|
3336
|
+
return clampPaginatedSlice(formatted, {
|
|
3337
|
+
limit: filters?.limit,
|
|
3338
|
+
page: filters?.page,
|
|
3339
|
+
pageSize: filters?.pageSize,
|
|
3340
|
+
defaultLimit: 1e3,
|
|
3341
|
+
maxLimit: 1e3
|
|
3342
|
+
});
|
|
3343
|
+
},
|
|
3344
|
+
async exportRegistrationsCsv(filters) {
|
|
3345
|
+
const records = await this.listRegistrations(filters);
|
|
3346
|
+
return generateRegistrationsCsv(records);
|
|
3347
|
+
},
|
|
3348
|
+
formatRegistrationRecord
|
|
3349
|
+
};
|
|
3350
|
+
};
|
|
3351
|
+
const STORE_KEY = "settings";
|
|
3352
|
+
function sanitizeStringRecord(raw) {
|
|
3353
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
3354
|
+
return void 0;
|
|
3355
|
+
}
|
|
3356
|
+
const clean = {};
|
|
3357
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
3358
|
+
if (isSafeObjectKey(k) && typeof v === "string") {
|
|
3359
|
+
clean[k] = v;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
return Object.keys(clean).length > 0 ? clean : void 0;
|
|
3363
|
+
}
|
|
3364
|
+
function sanitizeTimeSlotMap(raw) {
|
|
3365
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
3366
|
+
return void 0;
|
|
3367
|
+
}
|
|
3368
|
+
const clean = {};
|
|
3369
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
3370
|
+
if (isSafeObjectKey(k) && v && typeof v === "object" && typeof v.start === "string" && typeof v.end === "string") {
|
|
3371
|
+
clean[k] = { start: v.start, end: v.end };
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
return Object.keys(clean).length > 0 ? clean : void 0;
|
|
3375
|
+
}
|
|
3376
|
+
function sanitizeSourceFields(fields) {
|
|
3377
|
+
const safeFields = {
|
|
3378
|
+
title: typeof fields?.title === "string" && isSafeObjectKey(fields.title) ? fields.title : "title",
|
|
3379
|
+
startDate: typeof fields?.startDate === "string" && isSafeObjectKey(fields.startDate) ? fields.startDate : "startDate"
|
|
3380
|
+
};
|
|
3381
|
+
const optionalKeys = [
|
|
3382
|
+
"endDate",
|
|
3383
|
+
"rrule",
|
|
3384
|
+
"location",
|
|
3385
|
+
"category",
|
|
3386
|
+
"status",
|
|
3387
|
+
"summary",
|
|
3388
|
+
"slug",
|
|
3389
|
+
"capacity",
|
|
3390
|
+
"rsvpEnabled",
|
|
3391
|
+
"timeSlot",
|
|
3392
|
+
"poster",
|
|
3393
|
+
"actionLinks",
|
|
3394
|
+
"spotlightConfig"
|
|
3395
|
+
];
|
|
3396
|
+
for (const key of optionalKeys) {
|
|
3397
|
+
const val = fields?.[key];
|
|
3398
|
+
if (typeof val === "string" && isSafeObjectKey(val)) {
|
|
3399
|
+
safeFields[key] = val;
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
const altFields = sanitizeStringRecord(fields?.alternateDateFields);
|
|
3403
|
+
if (altFields) {
|
|
3404
|
+
safeFields.alternateDateFields = altFields;
|
|
3405
|
+
}
|
|
3406
|
+
if (Array.isArray(fields?.customFields)) {
|
|
3407
|
+
safeFields.customFields = fields.customFields.filter(isSafeObjectKey);
|
|
3408
|
+
}
|
|
3409
|
+
return safeFields;
|
|
3410
|
+
}
|
|
3411
|
+
function sanitizeSourceMapping(src) {
|
|
3412
|
+
if (!src || typeof src !== "object") return null;
|
|
3413
|
+
if (!isSafeObjectKey(src.id) || !isAllowedEventSourceUid(src.uid)) {
|
|
3414
|
+
return null;
|
|
3415
|
+
}
|
|
3416
|
+
return {
|
|
3417
|
+
id: src.id.trim(),
|
|
3418
|
+
label: typeof src.label === "string" && src.label ? src.label : src.id,
|
|
3419
|
+
uid: src.uid.trim(),
|
|
3420
|
+
color: typeof src.color === "string" && src.color.startsWith("#") ? src.color : "#0D9488",
|
|
3421
|
+
enabled: typeof src.enabled === "boolean" ? src.enabled : true,
|
|
3422
|
+
fields: sanitizeSourceFields(src.fields),
|
|
3423
|
+
...sanitizeTimeSlotMap(src.timeSlotMap) ? { timeSlotMap: sanitizeTimeSlotMap(src.timeSlotMap) } : {},
|
|
3424
|
+
...sanitizeStringRecord(src.categoryColors) ? { categoryColors: sanitizeStringRecord(src.categoryColors) } : {},
|
|
3425
|
+
...typeof src.defaultAllDay === "boolean" ? { defaultAllDay: src.defaultAllDay } : {}
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
function mergeSourcesList(defaults, userSources) {
|
|
3429
|
+
const merged = defaults.map((d) => sanitizeSourceMapping(d)).filter((d) => d !== null);
|
|
3430
|
+
if (!Array.isArray(userSources)) return merged;
|
|
3431
|
+
for (const rawSrc of userSources) {
|
|
3432
|
+
const src = sanitizeSourceMapping(rawSrc);
|
|
3433
|
+
if (!src) continue;
|
|
3434
|
+
const existingIdx = merged.findIndex(
|
|
3435
|
+
(s) => s.id === src.id || s.uid === src.uid
|
|
3436
|
+
);
|
|
3437
|
+
if (existingIdx >= 0) {
|
|
3438
|
+
merged[existingIdx] = {
|
|
3439
|
+
...merged[existingIdx],
|
|
3440
|
+
...src,
|
|
3441
|
+
fields: { ...merged[existingIdx].fields, ...src.fields }
|
|
3442
|
+
};
|
|
3443
|
+
} else {
|
|
3444
|
+
merged.push(src);
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
return merged;
|
|
3448
|
+
}
|
|
3449
|
+
function sanitizeCalendarConfig(cal) {
|
|
3450
|
+
if (!cal || typeof cal !== "object") return null;
|
|
3451
|
+
if (!isSafeObjectKey(cal.id) || typeof cal.intlCalendar !== "string") {
|
|
3452
|
+
return null;
|
|
3453
|
+
}
|
|
3454
|
+
const intlCal = cal.intlCalendar.trim();
|
|
3455
|
+
if (!intlCal) return null;
|
|
3456
|
+
return {
|
|
3457
|
+
id: cal.id.trim(),
|
|
3458
|
+
label: typeof cal.label === "string" && cal.label ? cal.label : cal.id,
|
|
3459
|
+
intlCalendar: intlCal,
|
|
3460
|
+
...typeof cal.locale === "string" && cal.locale ? { locale: cal.locale } : {},
|
|
3461
|
+
...typeof cal.dayOffset === "number" && Number.isFinite(cal.dayOffset) ? { dayOffset: Math.max(-2, Math.min(2, Math.round(cal.dayOffset))) } : {},
|
|
3462
|
+
...isSafeObjectKey(cal.overrideField) ? { overrideField: cal.overrideField } : {},
|
|
3463
|
+
enabled: Boolean(cal.enabled)
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
function mergeCalendarsList(defaults, userCalendars) {
|
|
3467
|
+
const merged = (defaults ?? []).map((c) => sanitizeCalendarConfig(c)).filter((c) => c !== null);
|
|
3468
|
+
if (!Array.isArray(userCalendars)) return merged;
|
|
3469
|
+
for (const rawCal of userCalendars) {
|
|
3470
|
+
const cal = sanitizeCalendarConfig(rawCal);
|
|
3471
|
+
if (!cal) continue;
|
|
3472
|
+
const idx = merged.findIndex((c) => c.id === cal.id);
|
|
3473
|
+
if (idx >= 0) {
|
|
3474
|
+
merged[idx] = { ...merged[idx], ...cal };
|
|
3475
|
+
} else {
|
|
3476
|
+
merged.push(cal);
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
return merged;
|
|
3480
|
+
}
|
|
3481
|
+
function sanitizeRecurrencePreset(item) {
|
|
3482
|
+
if (!item || typeof item !== "object") return null;
|
|
3483
|
+
if (!isSafeObjectKey(item.id) || typeof item.rrule !== "string") {
|
|
3484
|
+
return null;
|
|
3485
|
+
}
|
|
3486
|
+
const cleanRule = normalizeRRuleString(item.rrule);
|
|
3487
|
+
if (!cleanRule) return null;
|
|
3488
|
+
return {
|
|
3489
|
+
id: item.id.trim(),
|
|
3490
|
+
label: typeof item.label === "string" && item.label ? item.label : item.id,
|
|
3491
|
+
rrule: cleanRule
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
function mergePresetLists(defaults, stored) {
|
|
3495
|
+
const result = (defaults ?? []).map((p) => sanitizeRecurrencePreset(p)).filter((p) => p !== null);
|
|
3496
|
+
if (!Array.isArray(stored)) return result;
|
|
3497
|
+
for (const rawItem of stored) {
|
|
3498
|
+
const item = sanitizeRecurrencePreset(rawItem);
|
|
3499
|
+
if (!item) continue;
|
|
3500
|
+
const idx = result.findIndex(
|
|
3501
|
+
(p) => p.id === item.id || p.rrule === item.rrule
|
|
3502
|
+
);
|
|
3503
|
+
if (idx >= 0) {
|
|
3504
|
+
result[idx] = { ...result[idx], ...item };
|
|
3505
|
+
} else {
|
|
3506
|
+
result.push(item);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
return result;
|
|
3510
|
+
}
|
|
3511
|
+
function resolveNonEmptyString(patchVal, fallback, maxLen) {
|
|
3512
|
+
if (typeof patchVal === "string" && patchVal.trim()) {
|
|
3513
|
+
const trimmed = patchVal.trim();
|
|
3514
|
+
return maxLen ? trimmed.slice(0, maxLen) : trimmed;
|
|
3515
|
+
}
|
|
3516
|
+
return fallback;
|
|
3517
|
+
}
|
|
3518
|
+
function resolvePositiveInt(patchVal, fallback, maxLimit) {
|
|
3519
|
+
if (typeof patchVal === "number" && Number.isFinite(patchVal) && patchVal >= 1) {
|
|
3520
|
+
const floored = Math.floor(patchVal);
|
|
3521
|
+
return maxLimit ? Math.min(maxLimit, floored) : floored;
|
|
3522
|
+
}
|
|
3523
|
+
return fallback;
|
|
3524
|
+
}
|
|
3525
|
+
function resolveBoolean(patchVal, fallback) {
|
|
3526
|
+
return typeof patchVal === "boolean" ? patchVal : fallback;
|
|
3527
|
+
}
|
|
3528
|
+
function validateAndSanitizeSourcesPatch(patchSources, fallback) {
|
|
3529
|
+
if (patchSources === void 0) return fallback;
|
|
3530
|
+
if (!Array.isArray(patchSources)) {
|
|
3531
|
+
throw new PluginValidationError("Invalid sources: must be an array.");
|
|
3532
|
+
}
|
|
3533
|
+
const validated = [];
|
|
3534
|
+
for (const rawItem of patchSources) {
|
|
3535
|
+
const rawObj = rawItem;
|
|
3536
|
+
if (!rawObj || !isAllowedEventSourceUid(rawObj.uid)) {
|
|
3537
|
+
throw new PluginValidationError(
|
|
3538
|
+
`Disallowed event source UID "${String(rawObj?.uid ?? "")}". Only api::* and plugin::events.event are permitted.`
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
const sanitized = sanitizeSourceMapping(rawObj);
|
|
3542
|
+
if (!sanitized) {
|
|
3543
|
+
throw new PluginValidationError("Invalid event source configuration.");
|
|
3544
|
+
}
|
|
3545
|
+
validated.push(sanitized);
|
|
3546
|
+
}
|
|
3547
|
+
return validated;
|
|
3548
|
+
}
|
|
3549
|
+
const settings = ({ strapi }) => {
|
|
3550
|
+
const getStore = () => strapi.store({
|
|
3551
|
+
type: "plugin",
|
|
3552
|
+
name: PLUGIN_ID
|
|
3553
|
+
});
|
|
3554
|
+
const getConfigDefaults = () => {
|
|
3555
|
+
const userConfig = strapi.config.get(`plugin::${PLUGIN_ID}`) ?? {};
|
|
3556
|
+
return {
|
|
3557
|
+
...DEFAULT_PLUGIN_SETTINGS,
|
|
3558
|
+
...userConfig,
|
|
3559
|
+
sources: mergeSourcesList(
|
|
3560
|
+
DEFAULT_PLUGIN_SETTINGS.sources,
|
|
3561
|
+
userConfig.sources
|
|
3562
|
+
),
|
|
3563
|
+
secondaryCalendars: mergeCalendarsList(
|
|
3564
|
+
DEFAULT_PLUGIN_SETTINGS.secondaryCalendars,
|
|
3565
|
+
userConfig.secondaryCalendars
|
|
3566
|
+
),
|
|
3567
|
+
recurrencePresets: mergePresetLists(
|
|
3568
|
+
DEFAULT_PLUGIN_SETTINGS.recurrencePresets,
|
|
3569
|
+
userConfig.recurrencePresets
|
|
3570
|
+
)
|
|
3571
|
+
};
|
|
3572
|
+
};
|
|
3573
|
+
return {
|
|
3574
|
+
async ensureInitialized() {
|
|
3575
|
+
const store = getStore();
|
|
3576
|
+
const existing = await store.get({
|
|
3577
|
+
key: STORE_KEY
|
|
3578
|
+
});
|
|
3579
|
+
const defaults = getConfigDefaults();
|
|
3580
|
+
if (!existing) {
|
|
3581
|
+
await store.set({ key: STORE_KEY, value: defaults });
|
|
3582
|
+
return defaults;
|
|
3583
|
+
}
|
|
3584
|
+
const storedSources = Array.isArray(existing.sources) ? existing.sources.map((s) => sanitizeSourceMapping(s)).filter((s) => s !== null) : [];
|
|
3585
|
+
for (const defSource of defaults.sources) {
|
|
3586
|
+
if (!storedSources.some(
|
|
3587
|
+
(s) => s.id === defSource.id || s.uid === defSource.uid
|
|
3588
|
+
)) {
|
|
3589
|
+
storedSources.push(defSource);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
const storedCalendars = Array.isArray(existing.secondaryCalendars) ? existing.secondaryCalendars.map((c) => sanitizeCalendarConfig(c)).filter((c) => c !== null) : [];
|
|
3593
|
+
for (const defCal of defaults.secondaryCalendars ?? []) {
|
|
3594
|
+
if (!storedCalendars.some((c) => c.id === defCal.id)) {
|
|
3595
|
+
storedCalendars.push(defCal);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
const merged = {
|
|
3599
|
+
...defaults,
|
|
3600
|
+
...existing,
|
|
3601
|
+
sources: storedSources,
|
|
3602
|
+
secondaryCalendars: storedCalendars,
|
|
3603
|
+
recurrencePresets: mergePresetLists(
|
|
3604
|
+
defaults.recurrencePresets,
|
|
3605
|
+
existing.recurrencePresets
|
|
3606
|
+
)
|
|
3607
|
+
};
|
|
3608
|
+
await store.set({ key: STORE_KEY, value: merged });
|
|
3609
|
+
return merged;
|
|
3610
|
+
},
|
|
3611
|
+
async getSettings() {
|
|
3612
|
+
const store = getStore();
|
|
3613
|
+
const stored = await store.get({
|
|
3614
|
+
key: STORE_KEY
|
|
3615
|
+
});
|
|
3616
|
+
const defaults = getConfigDefaults();
|
|
3617
|
+
if (!stored) {
|
|
3618
|
+
return defaults;
|
|
3619
|
+
}
|
|
3620
|
+
const cleanSources = Array.isArray(stored.sources) ? stored.sources.map((s) => sanitizeSourceMapping(s)).filter((s) => s !== null) : [];
|
|
3621
|
+
const cleanCalendars = Array.isArray(stored.secondaryCalendars) ? stored.secondaryCalendars.map((c) => sanitizeCalendarConfig(c)).filter((c) => c !== null) : [];
|
|
3622
|
+
return {
|
|
3623
|
+
...defaults,
|
|
3624
|
+
...stored,
|
|
3625
|
+
sources: cleanSources.length > 0 ? cleanSources : defaults.sources,
|
|
3626
|
+
secondaryCalendars: cleanCalendars.length > 0 ? cleanCalendars : defaults.secondaryCalendars ?? [],
|
|
3627
|
+
recurrencePresets: mergePresetLists(
|
|
3628
|
+
defaults.recurrencePresets,
|
|
3629
|
+
stored.recurrencePresets
|
|
3630
|
+
)
|
|
3631
|
+
};
|
|
3632
|
+
},
|
|
3633
|
+
async updateSettings(patch) {
|
|
3634
|
+
const current = await this.getSettings();
|
|
3635
|
+
const nextSources = validateAndSanitizeSourcesPatch(
|
|
3636
|
+
patch.sources,
|
|
3637
|
+
current.sources
|
|
3638
|
+
);
|
|
3639
|
+
const nextCalendars = Array.isArray(patch.secondaryCalendars) ? patch.secondaryCalendars.map((c) => sanitizeCalendarConfig(c)).filter((c) => c !== null) : current.secondaryCalendars ?? [];
|
|
3640
|
+
const nextPresets = Array.isArray(patch.recurrencePresets) ? patch.recurrencePresets.map((p) => sanitizeRecurrencePreset(p)).filter((p) => p !== null) : current.recurrencePresets ?? [];
|
|
3641
|
+
const next = {
|
|
3642
|
+
...current,
|
|
3643
|
+
defaultTimezone: resolveNonEmptyString(
|
|
3644
|
+
patch.defaultTimezone,
|
|
3645
|
+
current.defaultTimezone
|
|
3646
|
+
),
|
|
3647
|
+
defaultCapacity: resolvePositiveInt(
|
|
3648
|
+
patch.defaultCapacity,
|
|
3649
|
+
current.defaultCapacity
|
|
3650
|
+
),
|
|
3651
|
+
waitlistEnabledByDefault: resolveBoolean(
|
|
3652
|
+
patch.waitlistEnabledByDefault,
|
|
3653
|
+
current.waitlistEnabledByDefault
|
|
3654
|
+
),
|
|
3655
|
+
defaultMaxGuestsPerRegistration: resolvePositiveInt(
|
|
3656
|
+
patch.defaultMaxGuestsPerRegistration,
|
|
3657
|
+
current.defaultMaxGuestsPerRegistration,
|
|
3658
|
+
50
|
|
3659
|
+
),
|
|
3660
|
+
sendRsvpEmails: resolveBoolean(
|
|
3661
|
+
patch.sendRsvpEmails,
|
|
3662
|
+
current.sendRsvpEmails
|
|
3663
|
+
),
|
|
3664
|
+
feedTitle: resolveNonEmptyString(
|
|
3665
|
+
patch.feedTitle,
|
|
3666
|
+
current.feedTitle,
|
|
3667
|
+
140
|
|
3668
|
+
),
|
|
3669
|
+
sources: nextSources,
|
|
3670
|
+
secondaryCalendars: nextCalendars,
|
|
3671
|
+
recurrencePresets: nextPresets
|
|
3672
|
+
};
|
|
3673
|
+
const store = getStore();
|
|
3674
|
+
await store.set({ key: STORE_KEY, value: next });
|
|
3675
|
+
return next;
|
|
3676
|
+
}
|
|
3677
|
+
};
|
|
3678
|
+
};
|
|
3679
|
+
const services = {
|
|
3680
|
+
settings,
|
|
3681
|
+
occurrence,
|
|
3682
|
+
rsvp,
|
|
3683
|
+
ical
|
|
3684
|
+
};
|
|
3685
|
+
const index = {
|
|
3686
|
+
register,
|
|
3687
|
+
bootstrap,
|
|
3688
|
+
destroy,
|
|
3689
|
+
config,
|
|
3690
|
+
controllers,
|
|
3691
|
+
routes,
|
|
3692
|
+
services,
|
|
3693
|
+
contentTypes,
|
|
3694
|
+
policies,
|
|
3695
|
+
middlewares
|
|
3696
|
+
};
|
|
3697
|
+
Object.defineProperty(exports, "parseRRule", {
|
|
3698
|
+
enumerable: true,
|
|
3699
|
+
get: () => shared.parseRRule
|
|
3700
|
+
});
|
|
3701
|
+
exports.buildCalendarLinks = buildCalendarLinks;
|
|
3702
|
+
exports.buildRRule = buildRRule;
|
|
3703
|
+
exports.default = index;
|
|
3704
|
+
exports.describeRRule = describeRRule;
|
|
3705
|
+
exports.expandRuleSlots = expandRuleSlots;
|
|
3706
|
+
exports.generateIcsCalendar = generateIcsCalendar;
|
|
3707
|
+
exports.generateRegistrationsCsv = generateRegistrationsCsv;
|
|
3708
|
+
exports.parseRRuleFieldValue = parseRRuleFieldValue;
|
|
3709
|
+
exports.sanitizeCsvCell = sanitizeCsvCell;
|