nodebb-plugin-onekite-calendar 2.0.93 → 2.0.95
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/lib/api.js +0 -6
- package/lib/scheduler.js +2 -352
- package/lib/shared.js +0 -1
- package/library.js +2 -10
- package/package.json +1 -1
- package/public/client.js +2 -14
- package/templates/admin/plugins/calendar-onekite.tpl +0 -17
package/lib/api.js
CHANGED
|
@@ -793,8 +793,6 @@ api.leaveSpecialEvent = async function (req, res) {
|
|
|
793
793
|
api.getCapabilities = async function (req, res) {
|
|
794
794
|
const settings = await getSettings();
|
|
795
795
|
const uid = req.uid || 0;
|
|
796
|
-
const pendingHoldMinutes = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
|
|
797
|
-
const paymentHoldMinutes = parseInt(getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')), 10) || 60;
|
|
798
796
|
if (!uid) {
|
|
799
797
|
return res.json({
|
|
800
798
|
canModerate: false,
|
|
@@ -803,8 +801,6 @@ api.getCapabilities = async function (req, res) {
|
|
|
803
801
|
canCreateOuting: false,
|
|
804
802
|
canCreateReservation: false,
|
|
805
803
|
specialEventCategoryCid: 0,
|
|
806
|
-
pendingHoldMinutes,
|
|
807
|
-
paymentHoldMinutes,
|
|
808
804
|
});
|
|
809
805
|
}
|
|
810
806
|
const [canMod, canSpecialC, canSpecialD, canReq] = await Promise.all([
|
|
@@ -821,8 +817,6 @@ api.getCapabilities = async function (req, res) {
|
|
|
821
817
|
canCreateOuting: canMod || canReq,
|
|
822
818
|
canCreateReservation: canReq,
|
|
823
819
|
specialEventCategoryCid: parseInt(settings && settings.specialEventCategoryId, 10) || 0,
|
|
824
|
-
pendingHoldMinutes,
|
|
825
|
-
paymentHoldMinutes,
|
|
826
820
|
});
|
|
827
821
|
};
|
|
828
822
|
|
package/lib/scheduler.js
CHANGED
|
@@ -1,338 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const nconf = require.main.require('nconf');
|
|
4
|
-
const db = require.main.require('./src/database');
|
|
5
|
-
const user = require.main.require('./src/user');
|
|
6
|
-
const dbLayer = require('./db');
|
|
7
|
-
const discord = require('./discord');
|
|
8
|
-
const realtime = require('./realtime');
|
|
9
|
-
const { getSettings } = require('./settings');
|
|
10
4
|
|
|
11
|
-
const shared = require('./shared');
|
|
12
|
-
const {
|
|
13
|
-
getSetting,
|
|
14
|
-
formatFR,
|
|
15
|
-
arrayifyNames,
|
|
16
|
-
forumBaseUrl,
|
|
17
|
-
normalizeAllowedGroups,
|
|
18
|
-
normalizeUids,
|
|
19
|
-
getMembersByGroupIdentifier,
|
|
20
|
-
sendEmail,
|
|
21
|
-
} = shared;
|
|
22
5
|
let timer = null;
|
|
23
6
|
|
|
24
|
-
// Some NodeBB database adapters don't expose setAdd/setRemove helpers.
|
|
25
|
-
// Use a safe "add once" guard to avoid crashing the scheduler.
|
|
26
|
-
async function addOnce(key, value) {
|
|
27
|
-
const v = String(value);
|
|
28
|
-
if (!v) return false;
|
|
29
|
-
|
|
30
|
-
// Best-effort atomic guard across multi-process / multi-instance.
|
|
31
|
-
// incrObjectField is implemented by most NodeBB DB adapters (Redis/Mongo).
|
|
32
|
-
// If it exists, it gives us a reliable "first winner" (value === 1).
|
|
33
|
-
if (typeof db.incrObjectField === 'function') {
|
|
34
|
-
try {
|
|
35
|
-
const n = await db.incrObjectField(key, v);
|
|
36
|
-
return Number(n) === 1;
|
|
37
|
-
} catch (e) {
|
|
38
|
-
// fall through
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Preferred atomic helper (Redis + some adapters)
|
|
43
|
-
if (typeof db.setAdd === 'function') {
|
|
44
|
-
try {
|
|
45
|
-
return !!(await db.setAdd(key, v));
|
|
46
|
-
} catch (e) {
|
|
47
|
-
// fall through
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Fallback: store a marker in an object map.
|
|
52
|
-
// Not perfectly atomic across clustered processes, but avoids a hard failure.
|
|
53
|
-
try {
|
|
54
|
-
const existing = await db.getObjectField(key, v);
|
|
55
|
-
if (existing) return false;
|
|
56
|
-
await db.setObjectField(key, v, 1);
|
|
57
|
-
return true;
|
|
58
|
-
} catch (e) {
|
|
59
|
-
// Last resort: allow sending rather than silently doing nothing.
|
|
60
|
-
// This may duplicate emails in edge cases, but avoids "expires with no email".
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Helpers imported from shared.js above
|
|
66
|
-
async function getValidatorUids(settings) {
|
|
67
|
-
const out = new Set();
|
|
68
|
-
// Always include administrators
|
|
69
|
-
try {
|
|
70
|
-
const admins = await getMembersByGroupIdentifier('administrators');
|
|
71
|
-
normalizeUids(admins).forEach((u) => out.add(u));
|
|
72
|
-
} catch (e) {}
|
|
73
|
-
|
|
74
|
-
const groupsCsv = normalizeAllowedGroups(settings && settings.validatorGroups);
|
|
75
|
-
for (const g of groupsCsv) {
|
|
76
|
-
try {
|
|
77
|
-
const members = await getMembersByGroupIdentifier(g);
|
|
78
|
-
normalizeUids(members).forEach((u) => out.add(u));
|
|
79
|
-
} catch (e) {}
|
|
80
|
-
}
|
|
81
|
-
return Array.from(out);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
async function getNotifyUids(settings) {
|
|
86
|
-
const out = new Set();
|
|
87
|
-
const groupsCsv = normalizeAllowedGroups(settings && settings.notifyGroups);
|
|
88
|
-
for (const g of groupsCsv) {
|
|
89
|
-
try {
|
|
90
|
-
const members = await getMembersByGroupIdentifier(g);
|
|
91
|
-
normalizeUids(members).forEach((u) => out.add(u));
|
|
92
|
-
} catch (e) {}
|
|
93
|
-
}
|
|
94
|
-
return Array.from(out);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function getNotifiedValidatorUids(settings) {
|
|
98
|
-
const [validators, notified] = await Promise.all([
|
|
99
|
-
getValidatorUids(settings),
|
|
100
|
-
getNotifyUids(settings),
|
|
101
|
-
]);
|
|
102
|
-
const notifiedSet = new Set((notified || []).map((u) => parseInt(u, 10)).filter(Number.isFinite));
|
|
103
|
-
return (validators || []).filter((u) => notifiedSet.has(parseInt(u, 10)));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Pending holds: short lock after a user creates a request (defaults to 5 minutes)
|
|
107
|
-
async function expirePending(preIds, preReservations) {
|
|
108
|
-
const settings = await getSettings();
|
|
109
|
-
const holdMins = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
|
|
110
|
-
const validatorReminderMins = parseInt(getSetting(settings, 'validatorReminderMinutesPending', '0'), 10) || 0;
|
|
111
|
-
const now = Date.now();
|
|
112
|
-
|
|
113
|
-
const adminUrl = (() => {
|
|
114
|
-
const base = forumBaseUrl();
|
|
115
|
-
return base ? `${base}/admin/plugins/calendar-onekite` : '/admin/plugins/calendar-onekite';
|
|
116
|
-
})();
|
|
117
|
-
|
|
118
|
-
const validatorUids = validatorReminderMins > 0 ? await getNotifiedValidatorUids(settings) : [];
|
|
119
|
-
|
|
120
|
-
const ids = preIds || await dbLayer.listAllReservationIds(5000);
|
|
121
|
-
if (!ids || !ids.length) return;
|
|
122
|
-
|
|
123
|
-
const reservations = preReservations || await dbLayer.getReservations(ids);
|
|
124
|
-
|
|
125
|
-
for (let i = 0; i < ids.length; i += 1) {
|
|
126
|
-
const rid = ids[i];
|
|
127
|
-
const resv = reservations[i];
|
|
128
|
-
if (!resv || resv.status !== 'pending') {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const createdAt = parseInt(resv.createdAt, 10) || 0;
|
|
132
|
-
const expiresAt = createdAt + holdMins * 60 * 1000;
|
|
133
|
-
|
|
134
|
-
// Reminder to validators while still pending
|
|
135
|
-
if (validatorReminderMins > 0 && createdAt && now >= (createdAt + validatorReminderMins * 60 * 1000) && now < expiresAt) {
|
|
136
|
-
const reminderKey = 'calendar-onekite:email:validatorReminder:pending';
|
|
137
|
-
const first = await addOnce(reminderKey, rid);
|
|
138
|
-
if (first && validatorUids.length) {
|
|
139
|
-
const requesterUid = parseInt(resv.uid, 10) || 0;
|
|
140
|
-
const requester = requesterUid ? await user.getUserFields(requesterUid, ['username']) : null;
|
|
141
|
-
const requesterUsername = requester && requester.username ? requester.username : (resv.username || '');
|
|
142
|
-
for (const vuid of validatorUids) {
|
|
143
|
-
await sendEmail('calendar-onekite_validator_reminder', vuid, 'Location matériel - Demande en attente', {
|
|
144
|
-
rid,
|
|
145
|
-
requesterUsername,
|
|
146
|
-
itemNames: arrayifyNames(resv),
|
|
147
|
-
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
148
|
-
adminUrl,
|
|
149
|
-
kind: 'pending',
|
|
150
|
-
delayMinutes: validatorReminderMins,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (now > expiresAt) {
|
|
157
|
-
// Expire (remove from calendar) + notify requester + validators
|
|
158
|
-
const expiredKey = 'calendar-onekite:email:expiredSent:pending';
|
|
159
|
-
const firstExpired = await addOnce(expiredKey, rid);
|
|
160
|
-
|
|
161
|
-
const requesterUid = parseInt(resv.uid, 10) || 0;
|
|
162
|
-
const requester = requesterUid ? await user.getUserFields(requesterUid, ['username']) : null;
|
|
163
|
-
const requesterUsername = requester && requester.username ? requester.username : (resv.username || '');
|
|
164
|
-
|
|
165
|
-
const reason = 'Demande non prise en charge dans le temps imparti.';
|
|
166
|
-
|
|
167
|
-
if (firstExpired && requesterUid) {
|
|
168
|
-
await sendEmail('calendar-onekite_expired', requesterUid, 'Location matériel - Demande expirée', {
|
|
169
|
-
uid: requesterUid,
|
|
170
|
-
username: requesterUsername,
|
|
171
|
-
itemNames: arrayifyNames(resv),
|
|
172
|
-
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
173
|
-
delayMinutes: holdMins,
|
|
174
|
-
reason,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Validators info email (best-effort)
|
|
179
|
-
if (firstExpired) {
|
|
180
|
-
const validators = await getNotifiedValidatorUids(settings);
|
|
181
|
-
for (const vuid of validators) {
|
|
182
|
-
await sendEmail('calendar-onekite_validator_expired', vuid, 'Location matériel - Demande expirée', {
|
|
183
|
-
rid,
|
|
184
|
-
requesterUsername,
|
|
185
|
-
itemNames: arrayifyNames(resv),
|
|
186
|
-
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
187
|
-
adminUrl,
|
|
188
|
-
reason,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
await dbLayer.removeReservation(rid);
|
|
194
|
-
|
|
195
|
-
// Real-time refresh for all viewers
|
|
196
|
-
realtime.emitCalendarUpdated({ kind: 'reservation', action: 'expired', rid: String(rid), status: 'expired' });
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Payment window logic:
|
|
202
|
-
// - When a reservation is validated it becomes awaiting_payment
|
|
203
|
-
// - We send a reminder after `paymentHoldMinutes` (default 60)
|
|
204
|
-
// - We expire (and remove) after `2 * paymentHoldMinutes`
|
|
205
|
-
async function processAwaitingPayment(preIds, preReservations) {
|
|
206
|
-
const settings = await getSettings();
|
|
207
|
-
const holdMins = parseInt(
|
|
208
|
-
getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')),
|
|
209
|
-
10
|
|
210
|
-
) || 60;
|
|
211
|
-
const now = Date.now();
|
|
212
|
-
|
|
213
|
-
const ids = preIds || await dbLayer.listAllReservationIds(5000);
|
|
214
|
-
if (!ids || !ids.length) return;
|
|
215
|
-
|
|
216
|
-
const adminUrl = (() => {
|
|
217
|
-
const base = forumBaseUrl();
|
|
218
|
-
return base ? `${base}/admin/plugins/calendar-onekite` : '/admin/plugins/calendar-onekite';
|
|
219
|
-
})();
|
|
220
|
-
|
|
221
|
-
const reservations = preReservations || await dbLayer.getReservations(ids);
|
|
222
|
-
|
|
223
|
-
for (let i = 0; i < ids.length; i += 1) {
|
|
224
|
-
const rid = ids[i];
|
|
225
|
-
const r = reservations[i];
|
|
226
|
-
if (!r || r.status !== 'awaiting_payment') continue;
|
|
227
|
-
|
|
228
|
-
const approvedAt = parseInt(r.approvedAt || r.validatedAt || 0, 10) || 0;
|
|
229
|
-
if (!approvedAt) continue;
|
|
230
|
-
|
|
231
|
-
const reminderAt = approvedAt + holdMins * 60 * 1000;
|
|
232
|
-
const expireAt = approvedAt + 2 * holdMins * 60 * 1000;
|
|
233
|
-
|
|
234
|
-
if (!r.reminderSent && now >= reminderAt && now < expireAt) {
|
|
235
|
-
// Send reminder once (guarded across clustered NodeBB processes)
|
|
236
|
-
const reminderKey = 'calendar-onekite:email:reminderSent';
|
|
237
|
-
const first = await addOnce(reminderKey, rid);
|
|
238
|
-
if (!first) {
|
|
239
|
-
// another process already sent it
|
|
240
|
-
r.reminderSent = true;
|
|
241
|
-
r.reminderAt = now;
|
|
242
|
-
await dbLayer.saveReservation(r);
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const toUid = parseInt(r.uid, 10);
|
|
247
|
-
const u = await user.getUserFields(toUid, ['username']);
|
|
248
|
-
if (toUid) {
|
|
249
|
-
await sendEmail('calendar-onekite_reminder', toUid, 'Location matériel - Rappel', {
|
|
250
|
-
uid: toUid,
|
|
251
|
-
username: (u && u.username) ? u.username : '',
|
|
252
|
-
itemName: arrayifyNames(r).join(', '),
|
|
253
|
-
itemNames: arrayifyNames(r),
|
|
254
|
-
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
255
|
-
paymentUrl: r.paymentUrl || '',
|
|
256
|
-
calendarUrl: `${forumBaseUrl()}/calendar`,
|
|
257
|
-
delayMinutes: holdMins,
|
|
258
|
-
pickupLine: r.pickupTime ? (r.adminNote ? `${r.pickupTime} à ${r.adminNote}` : r.pickupTime) : '',
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
r.reminderSent = true;
|
|
263
|
-
r.reminderAt = now;
|
|
264
|
-
await dbLayer.saveReservation(r);
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (now >= expireAt) {
|
|
269
|
-
// Expire: remove reservation so it disappears from calendar and frees items
|
|
270
|
-
// Guard email send across clustered NodeBB processes
|
|
271
|
-
const expiredKey = 'calendar-onekite:email:expiredSent';
|
|
272
|
-
const firstExpired = await addOnce(expiredKey, rid);
|
|
273
|
-
const shouldEmail = !!firstExpired;
|
|
274
|
-
|
|
275
|
-
// Guard Discord notification across clustered NodeBB processes
|
|
276
|
-
const discordKey = 'calendar-onekite:discord:cancelledSent';
|
|
277
|
-
const firstDiscord = await addOnce(discordKey, rid);
|
|
278
|
-
const shouldDiscord = !!firstDiscord;
|
|
279
|
-
|
|
280
|
-
const toUid = parseInt(r.uid, 10);
|
|
281
|
-
const u = await user.getUserFields(toUid, ['username']);
|
|
282
|
-
|
|
283
|
-
if (shouldEmail && toUid) {
|
|
284
|
-
await sendEmail('calendar-onekite_expired', toUid, 'Location matériel - Demande expirée', {
|
|
285
|
-
uid: toUid,
|
|
286
|
-
username: (u && u.username) ? u.username : '',
|
|
287
|
-
itemName: arrayifyNames(r).join(', '),
|
|
288
|
-
itemNames: arrayifyNames(r),
|
|
289
|
-
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
290
|
-
delayMinutes: holdMins,
|
|
291
|
-
reason: 'Paiement non reçu dans le temps imparti.',
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Validators info email (best-effort)
|
|
296
|
-
if (shouldEmail) {
|
|
297
|
-
const validators = await getNotifiedValidatorUids(settings);
|
|
298
|
-
for (const vuid of validators) {
|
|
299
|
-
await sendEmail('calendar-onekite_validator_expired', vuid, 'Location matériel - Demande expirée', {
|
|
300
|
-
rid: r.rid || rid,
|
|
301
|
-
requesterUsername: (u && u.username) ? u.username : (r.username || ''),
|
|
302
|
-
itemNames: arrayifyNames(r),
|
|
303
|
-
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
304
|
-
adminUrl,
|
|
305
|
-
reason: 'Paiement non reçu dans le temps imparti.',
|
|
306
|
-
paymentUrl: r.paymentUrl || '',
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (shouldDiscord) {
|
|
312
|
-
try {
|
|
313
|
-
await discord.notifyReservationCancelled(settings, {
|
|
314
|
-
rid: r.rid || rid,
|
|
315
|
-
uid: r.uid,
|
|
316
|
-
username: (u && u.username) ? u.username : (r.username || ''),
|
|
317
|
-
itemIds: r.itemIds || [],
|
|
318
|
-
itemNames: r.itemNames || [],
|
|
319
|
-
start: r.start,
|
|
320
|
-
end: r.end,
|
|
321
|
-
status: 'cancelled',
|
|
322
|
-
cancelledAt: now,
|
|
323
|
-
cancelledBy: 'system',
|
|
324
|
-
});
|
|
325
|
-
} catch (e) {}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
await dbLayer.removeReservation(rid);
|
|
329
|
-
|
|
330
|
-
// Real-time refresh for all viewers
|
|
331
|
-
realtime.emitCalendarUpdated({ kind: 'reservation', action: 'expired', rid: String(rid), status: 'expired' });
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
7
|
function start() {
|
|
337
8
|
const runJobs = nconf.get('runJobs');
|
|
338
9
|
if (runJobs === false || runJobs === 'false' || runJobs === 0 || runJobs === '0') {
|
|
@@ -340,24 +11,8 @@ function start() {
|
|
|
340
11
|
console.info('[calendar-onekite] Scheduler disabled (runJobs=false)');
|
|
341
12
|
return;
|
|
342
13
|
}
|
|
343
|
-
if (timer) return;
|
|
344
14
|
// eslint-disable-next-line no-console
|
|
345
|
-
console.info('[calendar-onekite] Scheduler
|
|
346
|
-
timer = setInterval(async () => {
|
|
347
|
-
try {
|
|
348
|
-
// Single DB fetch shared between both jobs (avoids duplicate listAll + getReservations)
|
|
349
|
-
const ids = await dbLayer.listAllReservationIds(5000);
|
|
350
|
-
const reservations = ids && ids.length ? await dbLayer.getReservations(ids) : [];
|
|
351
|
-
await expirePending(ids, reservations).catch((err) => {
|
|
352
|
-
console.warn('[calendar-onekite] Scheduler error in expirePending', err && err.message ? err.message : err);
|
|
353
|
-
});
|
|
354
|
-
await processAwaitingPayment(ids, reservations).catch((err) => {
|
|
355
|
-
console.warn('[calendar-onekite] Scheduler error in processAwaitingPayment', err && err.message ? err.message : err);
|
|
356
|
-
});
|
|
357
|
-
} catch (err) {
|
|
358
|
-
console.warn('[calendar-onekite] Scheduler tick error', err && err.message ? err.message : err);
|
|
359
|
-
}
|
|
360
|
-
}, 60 * 1000);
|
|
15
|
+
console.info('[calendar-onekite] Scheduler started (no expiry timers configured)');
|
|
361
16
|
}
|
|
362
17
|
|
|
363
18
|
function stop() {
|
|
@@ -367,9 +22,4 @@ function stop() {
|
|
|
367
22
|
}
|
|
368
23
|
}
|
|
369
24
|
|
|
370
|
-
module.exports = {
|
|
371
|
-
start,
|
|
372
|
-
stop,
|
|
373
|
-
expirePending,
|
|
374
|
-
processAwaitingPayment,
|
|
375
|
-
};
|
|
25
|
+
module.exports = { start, stop };
|
package/lib/shared.js
CHANGED
package/library.js
CHANGED
|
@@ -127,24 +127,16 @@ Plugin.init = async function (params) {
|
|
|
127
127
|
// Purge outings by year
|
|
128
128
|
router.post(`${adminBase}/outings/purge`, ...adminMws, admin.purgeOutingsByYear);
|
|
129
129
|
|
|
130
|
-
// HelloAsso
|
|
131
|
-
// - Only accepts POST
|
|
130
|
+
// HelloAsso webhook endpoint (hardened)
|
|
132
131
|
// - Verifies x-ha-signature (HMAC SHA-256) using the configured client secret
|
|
133
|
-
// - Basic replay protection
|
|
134
|
-
// NOTE: we capture the raw body for signature verification.
|
|
132
|
+
// - Basic replay protection; raw body captured for signature verification
|
|
135
133
|
const helloassoJson = bodyParser.json({
|
|
136
134
|
verify: (req, _res, buf) => {
|
|
137
135
|
req.rawBody = buf;
|
|
138
136
|
},
|
|
139
137
|
type: ['application/json', 'application/*+json'],
|
|
140
138
|
});
|
|
141
|
-
// Accept webhook on both legacy root path and namespaced plugin path.
|
|
142
|
-
// Some reverse proxies block unknown root paths, so /plugins/... is recommended.
|
|
143
|
-
router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
144
139
|
router.post('/plugins/calendar-onekite/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
145
|
-
|
|
146
|
-
// Optional: health checks
|
|
147
|
-
router.get('/helloasso', (req, res) => res.json({ ok: true }));
|
|
148
140
|
router.get('/plugins/calendar-onekite/helloasso', (req, res) => res.json({ ok: true }));
|
|
149
141
|
|
|
150
142
|
scheduler.start();
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1244,14 +1244,6 @@ function toDatetimeLocalValue(date) {
|
|
|
1244
1244
|
</div>
|
|
1245
1245
|
` : '';
|
|
1246
1246
|
|
|
1247
|
-
const holdPending = parseInt((opts && opts.pendingHoldMinutes), 10) || 5;
|
|
1248
|
-
const holdPayment = parseInt((opts && opts.paymentHoldMinutes), 10) || 60;
|
|
1249
|
-
const delayBannerHtml = `
|
|
1250
|
-
<div class="mt-2 p-2 rounded" style="font-size: 12px; background: var(--bs-info-bg-subtle, #cff4fc); border: 1px solid var(--bs-info-border-subtle, #9eeaf9); color: var(--bs-info-text-emphasis, #055160);">
|
|
1251
|
-
<strong>Délais :</strong>
|
|
1252
|
-
validation sous <strong>${fmtDuration(holdPending)}</strong> · paiement sous <strong>${fmtDuration(holdPayment)}</strong> après validation
|
|
1253
|
-
</div>
|
|
1254
|
-
`;
|
|
1255
1247
|
|
|
1256
1248
|
const messageHtml = `
|
|
1257
1249
|
<div class="mb-2" id="onekite-period"><strong>Période</strong><br>${formatDt(start)} → ${formatDt(endDisplay)} <span class="text-muted" id="onekite-days">(${days} jour${days > 1 ? 's' : ''})</span></div>
|
|
@@ -1269,7 +1261,6 @@ function toDatetimeLocalValue(date) {
|
|
|
1269
1261
|
<div id="onekite-total" style="font-size: 18px;"><strong>0,00 €</strong></div>
|
|
1270
1262
|
</div>
|
|
1271
1263
|
<div class="text-muted" style="font-size: 12px;">Les matériels grisés sont déjà réservés ou en attente.</div>
|
|
1272
|
-
${delayBannerHtml}
|
|
1273
1264
|
`;
|
|
1274
1265
|
|
|
1275
1266
|
return new Promise((resolve) => {
|
|
@@ -1446,9 +1437,6 @@ function toDatetimeLocalValue(date) {
|
|
|
1446
1437
|
const canCreateReservation = !!caps.canCreateReservation;
|
|
1447
1438
|
const isValidator = !!caps.isValidator;
|
|
1448
1439
|
const specialEventCategoryCid = parseInt(caps.specialEventCategoryCid, 10) || 0;
|
|
1449
|
-
const pendingHoldMinutes = parseInt(caps.pendingHoldMinutes, 10) || 5;
|
|
1450
|
-
const paymentHoldMinutes = parseInt(caps.paymentHoldMinutes, 10) || 60;
|
|
1451
|
-
|
|
1452
1440
|
// Creation chooser: Location / Prévision de sortie / Évènement (si autorisé).
|
|
1453
1441
|
|
|
1454
1442
|
// Inject lightweight responsive CSS once.
|
|
@@ -1596,7 +1584,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1596
1584
|
showAlert('error', 'Aucun matériel disponible (items HelloAsso non chargés).');
|
|
1597
1585
|
return;
|
|
1598
1586
|
}
|
|
1599
|
-
const chosen = await openReservationDialog(sel, items, { isValidator
|
|
1587
|
+
const chosen = await openReservationDialog(sel, items, { isValidator });
|
|
1600
1588
|
if (!chosen || !chosen.itemIds || !chosen.itemIds.length) return;
|
|
1601
1589
|
const startDate = toLocalYmd(sel.start);
|
|
1602
1590
|
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(sel.end);
|
|
@@ -1688,7 +1676,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1688
1676
|
showAlert('error', 'Aucun matériel disponible (items HelloAsso non chargés).');
|
|
1689
1677
|
return;
|
|
1690
1678
|
}
|
|
1691
|
-
const chosen = await openReservationDialog(sel, items, { isValidator
|
|
1679
|
+
const chosen = await openReservationDialog(sel, items, { isValidator });
|
|
1692
1680
|
if (!chosen || !chosen.itemIds || !chosen.itemIds.length) return;
|
|
1693
1681
|
const startDate = toLocalYmd(sel.start);
|
|
1694
1682
|
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(sel.end);
|
|
@@ -51,23 +51,6 @@
|
|
|
51
51
|
<input class="form-control" name="notifyGroups" placeholder="ex: administrators">
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
|
-
<div class="mb-3">
|
|
55
|
-
<label class="form-label">Durée de blocage en attente (minutes)</label>
|
|
56
|
-
<input class="form-control" name="pendingHoldMinutes" placeholder="5">
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<div class="mb-3">
|
|
60
|
-
<label class="form-label">Rappel validateurs (demande en attente) après (minutes)</label>
|
|
61
|
-
<input class="form-control" name="validatorReminderMinutesPending" placeholder="0">
|
|
62
|
-
<div class="form-text">Si > 0, un email de rappel est envoyé aux validateurs après ce délai tant que la demande est toujours en attente.</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div class="mb-3">
|
|
66
|
-
<label class="form-label">Délai rappel paiement (minutes)</label>
|
|
67
|
-
<input class="form-control" name="paymentHoldMinutes" placeholder="60">
|
|
68
|
-
<div class="form-text">Après validation (statut <code>paiement en attente</code>), un rappel est envoyé après ce délai. La réservation est ensuite expirée après <strong>2×</strong> ce délai.</div>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
54
|
<div class="mb-3">
|
|
72
55
|
<label class="form-label">Location longue durée (jours) pour validateurs</label>
|
|
73
56
|
<input class="form-control" name="validatorFreeMaxDays" placeholder="0">
|