nodebb-plugin-onekite-calendar 1.0.4 → 1.0.5
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/CHANGELOG.md +2 -27
- package/lib/admin.js +155 -15
- package/lib/api.js +103 -158
- package/lib/controllers.js +2 -2
- package/lib/db.js +5 -5
- package/lib/discord.js +5 -6
- package/lib/helloasso.js +3 -4
- package/lib/helloassoWebhook.js +7 -8
- package/lib/scheduler.js +11 -8
- package/lib/widgets.js +8 -128
- package/library.js +63 -39
- package/package.json +1 -1
- package/plugin.json +11 -5
- package/public/admin.js +91 -19
- package/public/client.js +23 -23
- package/templates/admin/plugins/{onekite-calendar.tpl → calendar-onekite.tpl} +23 -4
- package/templates/{onekite-calendar.tpl → calendar-onekite.tpl} +0 -11
- package/lib/constants.js +0 -7
- package/lib/email.js +0 -26
- package/lib/log.js +0 -29
- /package/templates/emails/{onekite-calendar_approved.tpl → calendar-onekite_approved.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_expired.tpl → calendar-onekite_expired.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_paid.tpl → calendar-onekite_paid.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_pending.tpl → calendar-onekite_pending.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_refused.tpl → calendar-onekite_refused.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_reminder.tpl → calendar-onekite_reminder.tpl} +0 -0
package/lib/api.js
CHANGED
|
@@ -3,50 +3,14 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
|
|
5
5
|
const meta = require.main.require('./src/meta');
|
|
6
|
+
const emailer = require.main.require('./src/emailer');
|
|
6
7
|
const nconf = require.main.require('nconf');
|
|
7
8
|
const user = require.main.require('./src/user');
|
|
8
9
|
const groups = require.main.require('./src/groups');
|
|
9
10
|
const db = require.main.require('./src/database');
|
|
10
|
-
|
|
11
|
+
const logger = require.main.require('./src/logger');
|
|
11
12
|
|
|
12
13
|
const dbLayer = require('./db');
|
|
13
|
-
const { sendEmail } = require('./email');
|
|
14
|
-
const log = require('./log');
|
|
15
|
-
|
|
16
|
-
// Ultra-perf: short-lived in-memory cache for the events feed.
|
|
17
|
-
// We cache a "base" event list without per-user privileges, then decorate
|
|
18
|
-
// it per request. This reduces DB load when many users/widgets refresh.
|
|
19
|
-
const EVENTS_CACHE = new Map();
|
|
20
|
-
const EVENTS_CACHE_TTL_MS = 30 * 1000;
|
|
21
|
-
const EVENTS_CACHE_MAX = 50;
|
|
22
|
-
|
|
23
|
-
function eventsCacheKey(startTs, endTs) {
|
|
24
|
-
// Keep the key stable and short.
|
|
25
|
-
return `${Math.max(0, Number(startTs) || 0)}:${Math.max(0, Number(endTs) || 0)}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getEventsCache(key) {
|
|
29
|
-
const hit = EVENTS_CACHE.get(key);
|
|
30
|
-
if (!hit) return null;
|
|
31
|
-
if ((Date.now() - hit.ts) > EVENTS_CACHE_TTL_MS) {
|
|
32
|
-
EVENTS_CACHE.delete(key);
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return hit.data;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function setEventsCache(key, data) {
|
|
39
|
-
EVENTS_CACHE.set(key, { ts: Date.now(), data });
|
|
40
|
-
if (EVENTS_CACHE.size > EVENTS_CACHE_MAX) {
|
|
41
|
-
// Drop oldest entries.
|
|
42
|
-
const entries = Array.from(EVENTS_CACHE.entries());
|
|
43
|
-
entries.sort((a, b) => (a[1]?.ts || 0) - (b[1]?.ts || 0));
|
|
44
|
-
const toDrop = Math.max(1, EVENTS_CACHE.size - EVENTS_CACHE_MAX);
|
|
45
|
-
for (let i = 0; i < toDrop; i++) {
|
|
46
|
-
EVENTS_CACHE.delete(entries[i][0]);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
14
|
|
|
51
15
|
// Fast membership check without N calls to groups.isMember.
|
|
52
16
|
// NodeBB's groups.getUserGroups([uid]) returns an array (per uid) of group objects.
|
|
@@ -88,7 +52,36 @@ function normalizeAllowedGroups(raw) {
|
|
|
88
52
|
const helloasso = require('./helloasso');
|
|
89
53
|
const discord = require('./discord');
|
|
90
54
|
|
|
91
|
-
// Email helper
|
|
55
|
+
// Email helper: NodeBB's Emailer signature differs across versions.
|
|
56
|
+
// We try the common forms. Any failure is logged for debugging.
|
|
57
|
+
async function sendEmail(template, toEmail, subject, data) {
|
|
58
|
+
if (!toEmail) return;
|
|
59
|
+
try {
|
|
60
|
+
// NodeBB core signature (historically):
|
|
61
|
+
// Emailer.sendToEmail(template, email, language, params[, callback])
|
|
62
|
+
// Subject is not a positional arg; it must be injected (either by NodeBB itself
|
|
63
|
+
// or via filter:email.modify). We always pass it in params.subject.
|
|
64
|
+
const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
65
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
66
|
+
if (typeof emailer.sendToEmail === 'function') {
|
|
67
|
+
await emailer.sendToEmail(template, toEmail, language, params);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Fallback for older/unusual builds (rare)
|
|
71
|
+
if (typeof emailer.send === 'function') {
|
|
72
|
+
// Some builds accept (template, email, language, params)
|
|
73
|
+
if (emailer.send.length >= 4) {
|
|
74
|
+
await emailer.send(template, toEmail, language, params);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Some builds accept (template, email, params)
|
|
78
|
+
await emailer.send(template, toEmail, params);
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
92
85
|
|
|
93
86
|
function normalizeBaseUrl(meta) {
|
|
94
87
|
// Prefer meta.config.url, fallback to nconf.get('url')
|
|
@@ -109,8 +102,7 @@ function normalizeCallbackUrl(configured, meta) {
|
|
|
109
102
|
let url = (configured || '').trim();
|
|
110
103
|
if (!url) {
|
|
111
104
|
// Default webhook endpoint (recommended): namespaced under /plugins
|
|
112
|
-
|
|
113
|
-
url = base ? `${base}/plugins/onekite-calendar/helloasso` : '';
|
|
105
|
+
url = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
|
|
114
106
|
}
|
|
115
107
|
if (url && url.startsWith('/') && base) {
|
|
116
108
|
url = `${base}${url}`;
|
|
@@ -336,7 +328,7 @@ api.getEvents = async function (req, res) {
|
|
|
336
328
|
const startTs = toTs(req.query.start) || 0;
|
|
337
329
|
const endTs = toTs(req.query.end) || (Date.now() + 365 * 24 * 3600 * 1000);
|
|
338
330
|
|
|
339
|
-
const settings = await meta.settings.get('onekite
|
|
331
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
340
332
|
const canMod = req.uid ? await canValidate(req.uid, settings) : false;
|
|
341
333
|
const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
|
|
342
334
|
const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings) : false;
|
|
@@ -344,89 +336,20 @@ api.getEvents = async function (req, res) {
|
|
|
344
336
|
// Fetch a wider window because an event can start before the query range
|
|
345
337
|
// and still overlap.
|
|
346
338
|
const wideStart = Math.max(0, startTs - 366 * 24 * 3600 * 1000);
|
|
347
|
-
|
|
348
|
-
const cacheKey = eventsCacheKey(startTs, endTs);
|
|
349
|
-
let base = getEventsCache(cacheKey);
|
|
350
|
-
if (!base) {
|
|
351
|
-
const baseOut = [];
|
|
352
|
-
|
|
353
|
-
const ids = await dbLayer.listReservationIdsByStartRange(wideStart, endTs, 5000);
|
|
354
|
-
// Batch fetch = major perf win when there are many reservations.
|
|
355
|
-
const reservations = await dbLayer.getReservations(ids);
|
|
356
|
-
for (const r of (reservations || [])) {
|
|
357
|
-
if (!r) continue;
|
|
358
|
-
if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
|
|
359
|
-
const rStart = parseInt(r.start, 10);
|
|
360
|
-
const rEnd = parseInt(r.end, 10);
|
|
361
|
-
if (!(rStart < endTs && startTs < rEnd)) continue;
|
|
362
|
-
|
|
363
|
-
const evs = eventsFor(r);
|
|
364
|
-
const hasPay = (r.status === 'awaiting_payment' && r.paymentUrl && (/^https?:\/\//i).test(String(r.paymentUrl)));
|
|
365
|
-
for (const ev of (evs || [])) {
|
|
366
|
-
const p = ev.extendedProps || {};
|
|
367
|
-
baseOut.push({
|
|
368
|
-
id: ev.id,
|
|
369
|
-
title: ev.title,
|
|
370
|
-
backgroundColor: ev.backgroundColor,
|
|
371
|
-
borderColor: ev.borderColor,
|
|
372
|
-
textColor: ev.textColor,
|
|
373
|
-
allDay: ev.allDay,
|
|
374
|
-
start: ev.start,
|
|
375
|
-
end: ev.end,
|
|
376
|
-
extendedProps: {
|
|
377
|
-
type: 'reservation',
|
|
378
|
-
rid: p.rid,
|
|
379
|
-
status: p.status,
|
|
380
|
-
uid: p.uid,
|
|
381
|
-
_username: r.username ? String(r.username) : '',
|
|
382
|
-
_hasPayment: !!hasPay,
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Special events
|
|
389
|
-
try {
|
|
390
|
-
const specialIds = await dbLayer.listSpecialIdsByStartRange(wideStart, endTs, 5000);
|
|
391
|
-
const specials = await dbLayer.getSpecialEvents(specialIds);
|
|
392
|
-
for (const sev of (specials || [])) {
|
|
393
|
-
if (!sev) continue;
|
|
394
|
-
const sStart = parseInt(sev.start, 10);
|
|
395
|
-
const sEnd = parseInt(sev.end, 10);
|
|
396
|
-
if (!(sStart < endTs && startTs < sEnd)) continue;
|
|
397
|
-
const full = eventsForSpecial(sev);
|
|
398
|
-
baseOut.push({
|
|
399
|
-
id: full.id,
|
|
400
|
-
title: full.title,
|
|
401
|
-
allDay: full.allDay,
|
|
402
|
-
start: full.start,
|
|
403
|
-
end: full.end,
|
|
404
|
-
backgroundColor: full.backgroundColor,
|
|
405
|
-
borderColor: full.borderColor,
|
|
406
|
-
textColor: full.textColor,
|
|
407
|
-
extendedProps: {
|
|
408
|
-
type: 'special',
|
|
409
|
-
eid: sev.eid,
|
|
410
|
-
uid: sev.uid,
|
|
411
|
-
_username: sev.username ? String(sev.username) : '',
|
|
412
|
-
},
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
} catch (e) {
|
|
416
|
-
// ignore
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
base = baseOut;
|
|
420
|
-
setEventsCache(cacheKey, baseOut);
|
|
421
|
-
}
|
|
422
|
-
|
|
339
|
+
const ids = await dbLayer.listReservationIdsByStartRange(wideStart, endTs, 5000);
|
|
423
340
|
const out = [];
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
341
|
+
// Batch fetch = major perf win when there are many reservations.
|
|
342
|
+
const reservations = await dbLayer.getReservations(ids);
|
|
343
|
+
for (const r of (reservations || [])) {
|
|
344
|
+
if (!r) continue;
|
|
345
|
+
// Only show active statuses
|
|
346
|
+
if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
|
|
347
|
+
const rStart = parseInt(r.start, 10);
|
|
348
|
+
const rEnd = parseInt(r.end, 10);
|
|
349
|
+
if (!(rStart < endTs && startTs < rEnd)) continue; // overlap check
|
|
350
|
+
const evs = eventsFor(r);
|
|
351
|
+
for (const ev of evs) {
|
|
352
|
+
const p = ev.extendedProps || {};
|
|
430
353
|
const minimal = {
|
|
431
354
|
id: ev.id,
|
|
432
355
|
title: ev.title,
|
|
@@ -444,30 +367,53 @@ api.getEvents = async function (req, res) {
|
|
|
444
367
|
canModerate: canMod,
|
|
445
368
|
},
|
|
446
369
|
};
|
|
447
|
-
|
|
448
|
-
if (
|
|
370
|
+
// Only expose username on the event list to owner/moderators.
|
|
371
|
+
if (r.username && ((req.uid && String(req.uid) === String(r.uid)) || canMod)) {
|
|
372
|
+
minimal.extendedProps.username = String(r.username);
|
|
373
|
+
}
|
|
374
|
+
// Let the UI decide if a "Payer" button might exist, without exposing the URL in list.
|
|
375
|
+
if (r.status === 'awaiting_payment' && r.paymentUrl && (/^https?:\/\//i).test(String(r.paymentUrl))) {
|
|
376
|
+
if ((req.uid && String(req.uid) === String(r.uid)) || canMod) {
|
|
377
|
+
minimal.extendedProps.hasPayment = true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
449
380
|
out.push(minimal);
|
|
450
|
-
}
|
|
451
|
-
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Special events
|
|
385
|
+
try {
|
|
386
|
+
const specialIds = await dbLayer.listSpecialIdsByStartRange(wideStart, endTs, 5000);
|
|
387
|
+
const specials = await dbLayer.getSpecialEvents(specialIds);
|
|
388
|
+
for (const sev of (specials || [])) {
|
|
389
|
+
if (!sev) continue;
|
|
390
|
+
const sStart = parseInt(sev.start, 10);
|
|
391
|
+
const sEnd = parseInt(sev.end, 10);
|
|
392
|
+
if (!(sStart < endTs && startTs < sEnd)) continue;
|
|
393
|
+
const full = eventsForSpecial(sev);
|
|
452
394
|
const minimal = {
|
|
453
|
-
id:
|
|
454
|
-
title:
|
|
455
|
-
allDay:
|
|
456
|
-
start:
|
|
457
|
-
end:
|
|
458
|
-
backgroundColor:
|
|
459
|
-
borderColor:
|
|
460
|
-
textColor:
|
|
395
|
+
id: full.id,
|
|
396
|
+
title: full.title,
|
|
397
|
+
allDay: full.allDay,
|
|
398
|
+
start: full.start,
|
|
399
|
+
end: full.end,
|
|
400
|
+
backgroundColor: full.backgroundColor,
|
|
401
|
+
borderColor: full.borderColor,
|
|
402
|
+
textColor: full.textColor,
|
|
461
403
|
extendedProps: {
|
|
462
404
|
type: 'special',
|
|
463
|
-
eid:
|
|
405
|
+
eid: sev.eid,
|
|
464
406
|
canCreateSpecial: canSpecialCreate,
|
|
465
407
|
canDeleteSpecial: canSpecialDelete,
|
|
466
408
|
},
|
|
467
409
|
};
|
|
468
|
-
if (
|
|
410
|
+
if (sev.username && (canMod || canSpecialDelete || (req.uid && String(req.uid) === String(sev.uid)))) {
|
|
411
|
+
minimal.extendedProps.username = String(sev.username);
|
|
412
|
+
}
|
|
469
413
|
out.push(minimal);
|
|
470
414
|
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
// ignore
|
|
471
417
|
}
|
|
472
418
|
|
|
473
419
|
// Stable ordering -> stable ETag
|
|
@@ -482,8 +428,7 @@ api.getEvents = async function (req, res) {
|
|
|
482
428
|
|
|
483
429
|
const etag = computeEtag(out);
|
|
484
430
|
res.setHeader('ETag', etag);
|
|
485
|
-
|
|
486
|
-
res.setHeader('Cache-Control', 'private, max-age=15, must-revalidate');
|
|
431
|
+
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
|
|
487
432
|
if (String(req.headers['if-none-match'] || '') === etag) {
|
|
488
433
|
return res.status(304).end();
|
|
489
434
|
}
|
|
@@ -495,7 +440,7 @@ api.getReservationDetails = async function (req, res) {
|
|
|
495
440
|
const uid = req.uid;
|
|
496
441
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
497
442
|
|
|
498
|
-
const settings = await meta.settings.get('onekite
|
|
443
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
499
444
|
const canMod = await canValidate(uid, settings);
|
|
500
445
|
|
|
501
446
|
const rid = String(req.params.rid || '').trim();
|
|
@@ -537,7 +482,7 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
537
482
|
const uid = req.uid;
|
|
538
483
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
539
484
|
|
|
540
|
-
const settings = await meta.settings.get('onekite
|
|
485
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
541
486
|
const canMod = await canValidate(uid, settings);
|
|
542
487
|
const canSpecialDelete = await canDeleteSpecial(uid, settings);
|
|
543
488
|
|
|
@@ -566,7 +511,7 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
566
511
|
};
|
|
567
512
|
|
|
568
513
|
api.getCapabilities = async function (req, res) {
|
|
569
|
-
const settings = await meta.settings.get('onekite
|
|
514
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
570
515
|
const uid = req.uid || 0;
|
|
571
516
|
const canMod = uid ? await canValidate(uid, settings) : false;
|
|
572
517
|
res.json({
|
|
@@ -577,7 +522,7 @@ api.getCapabilities = async function (req, res) {
|
|
|
577
522
|
};
|
|
578
523
|
|
|
579
524
|
api.createSpecialEvent = async function (req, res) {
|
|
580
|
-
const settings = await meta.settings.get('onekite
|
|
525
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
581
526
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
582
527
|
const ok = await canCreateSpecial(req.uid, settings);
|
|
583
528
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
@@ -613,7 +558,7 @@ api.createSpecialEvent = async function (req, res) {
|
|
|
613
558
|
};
|
|
614
559
|
|
|
615
560
|
api.deleteSpecialEvent = async function (req, res) {
|
|
616
|
-
const settings = await meta.settings.get('onekite
|
|
561
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
617
562
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
618
563
|
const ok = await canDeleteSpecial(req.uid, settings);
|
|
619
564
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
@@ -624,7 +569,7 @@ api.deleteSpecialEvent = async function (req, res) {
|
|
|
624
569
|
};
|
|
625
570
|
|
|
626
571
|
api.getItems = async function (req, res) {
|
|
627
|
-
const settings = await meta.settings.get('onekite
|
|
572
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
628
573
|
|
|
629
574
|
const env = settings.helloassoEnv || 'prod';
|
|
630
575
|
const token = await helloasso.getAccessToken({
|
|
@@ -665,7 +610,7 @@ api.createReservation = async function (req, res) {
|
|
|
665
610
|
const uid = req.uid;
|
|
666
611
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
667
612
|
|
|
668
|
-
const settings = await meta.settings.get('onekite
|
|
613
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
669
614
|
const startPreview = toTs(req.body.start);
|
|
670
615
|
const ok = await canRequest(uid, settings, startPreview);
|
|
671
616
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
@@ -762,7 +707,7 @@ api.createReservation = async function (req, res) {
|
|
|
762
707
|
|
|
763
708
|
for (const md of (usersData || [])) {
|
|
764
709
|
if (md && md.email) {
|
|
765
|
-
await sendEmail('
|
|
710
|
+
await sendEmail('calendar-onekite_pending', md.email, 'Location matériel - Demande de réservation', {
|
|
766
711
|
username: md.username,
|
|
767
712
|
requester: requester.username,
|
|
768
713
|
itemName: itemsLabel,
|
|
@@ -777,7 +722,7 @@ api.createReservation = async function (req, res) {
|
|
|
777
722
|
}
|
|
778
723
|
}
|
|
779
724
|
} catch (e) {
|
|
780
|
-
|
|
725
|
+
console.warn('[calendar-onekite] Failed to send pending email', e && e.message ? e.message : e);
|
|
781
726
|
}
|
|
782
727
|
|
|
783
728
|
// Discord webhook (optional)
|
|
@@ -801,7 +746,7 @@ api.createReservation = async function (req, res) {
|
|
|
801
746
|
api.approveReservation = async function (req, res) {
|
|
802
747
|
const uid = req.uid;
|
|
803
748
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
804
|
-
const settings = await meta.settings.get('onekite
|
|
749
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
805
750
|
const ok = await canValidate(uid, settings);
|
|
806
751
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
807
752
|
|
|
@@ -828,7 +773,7 @@ api.approveReservation = async function (req, res) {
|
|
|
828
773
|
}
|
|
829
774
|
// Create HelloAsso payment link on validation
|
|
830
775
|
try {
|
|
831
|
-
const settings2 = await meta.settings.get('onekite
|
|
776
|
+
const settings2 = await meta.settings.get('calendar-onekite');
|
|
832
777
|
const token = await helloasso.getAccessToken({ env: settings2.helloassoEnv || 'prod', clientId: settings2.helloassoClientId, clientSecret: settings2.helloassoClientSecret });
|
|
833
778
|
const payer = await user.getUserFields(r.uid, ['email']);
|
|
834
779
|
const year = yearFromTs(r.start);
|
|
@@ -843,7 +788,7 @@ api.approveReservation = async function (req, res) {
|
|
|
843
788
|
totalAmount: (() => {
|
|
844
789
|
const cents = Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
845
790
|
if (!cents) {
|
|
846
|
-
|
|
791
|
+
console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
|
|
847
792
|
}
|
|
848
793
|
return cents;
|
|
849
794
|
})(),
|
|
@@ -852,7 +797,7 @@ api.approveReservation = async function (req, res) {
|
|
|
852
797
|
// Can be overridden via ACP setting `helloassoCallbackUrl`.
|
|
853
798
|
callbackUrl: normalizeReturnUrl(meta),
|
|
854
799
|
webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
|
|
855
|
-
itemName: 'Réservation matériel
|
|
800
|
+
itemName: 'Réservation matériel OneKite',
|
|
856
801
|
containsDonation: false,
|
|
857
802
|
metadata: { reservationId: String(rid) },
|
|
858
803
|
});
|
|
@@ -864,7 +809,7 @@ api.approveReservation = async function (req, res) {
|
|
|
864
809
|
r.checkoutIntentId = checkoutIntentId;
|
|
865
810
|
}
|
|
866
811
|
} else {
|
|
867
|
-
|
|
812
|
+
console.warn('[calendar-onekite] HelloAsso payment link not created (approve API)', { rid });
|
|
868
813
|
}
|
|
869
814
|
} catch (e) {
|
|
870
815
|
// ignore payment link errors, admin can retry
|
|
@@ -880,7 +825,7 @@ api.approveReservation = async function (req, res) {
|
|
|
880
825
|
const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
|
|
881
826
|
? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
|
|
882
827
|
: '';
|
|
883
|
-
await sendEmail('
|
|
828
|
+
await sendEmail('calendar-onekite_approved', requester.email, 'Location matériel - Réservation validée', {
|
|
884
829
|
username: requester.username,
|
|
885
830
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
886
831
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
@@ -904,7 +849,7 @@ api.approveReservation = async function (req, res) {
|
|
|
904
849
|
api.refuseReservation = async function (req, res) {
|
|
905
850
|
const uid = req.uid;
|
|
906
851
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
907
|
-
const settings = await meta.settings.get('onekite
|
|
852
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
908
853
|
const ok = await canValidate(uid, settings);
|
|
909
854
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
910
855
|
|
|
@@ -919,7 +864,7 @@ api.refuseReservation = async function (req, res) {
|
|
|
919
864
|
|
|
920
865
|
const requester = await user.getUserFields(r.uid, ['username', 'email']);
|
|
921
866
|
if (requester && requester.email) {
|
|
922
|
-
await sendEmail('
|
|
867
|
+
await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Demande de réservation', {
|
|
923
868
|
username: requester.username,
|
|
924
869
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
925
870
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
@@ -939,7 +884,7 @@ api.cancelReservation = async function (req, res) {
|
|
|
939
884
|
const uid = req.uid;
|
|
940
885
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
941
886
|
|
|
942
|
-
const settings = await meta.settings.get('onekite
|
|
887
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
943
888
|
const rid = String(req.params.rid || '').trim();
|
|
944
889
|
if (!rid) return res.status(400).json({ error: 'missing-rid' });
|
|
945
890
|
|
package/lib/controllers.js
CHANGED
package/lib/db.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const db = require.main.require('./src/database');
|
|
4
4
|
|
|
5
|
-
const KEY_ZSET = 'onekite
|
|
6
|
-
const KEY_OBJ = (rid) => `onekite
|
|
7
|
-
const KEY_CHECKOUT_INTENT_TO_RID = 'onekite
|
|
5
|
+
const KEY_ZSET = 'calendar-onekite:reservations';
|
|
6
|
+
const KEY_OBJ = (rid) => `calendar-onekite:reservation:${rid}`;
|
|
7
|
+
const KEY_CHECKOUT_INTENT_TO_RID = 'calendar-onekite:helloasso:checkoutIntentToRid';
|
|
8
8
|
|
|
9
9
|
// Special events (non-reservation events shown in a different colour)
|
|
10
|
-
const KEY_SPECIAL_ZSET = 'onekite
|
|
11
|
-
const KEY_SPECIAL_OBJ = (eid) => `onekite
|
|
10
|
+
const KEY_SPECIAL_ZSET = 'calendar-onekite:special';
|
|
11
|
+
const KEY_SPECIAL_OBJ = (eid) => `calendar-onekite:special:${eid}`;
|
|
12
12
|
|
|
13
13
|
// Helpers
|
|
14
14
|
function reservationKey(rid) {
|
package/lib/discord.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const https = require('https');
|
|
4
|
-
const log = require('./log');
|
|
5
4
|
const { URL } = require('url');
|
|
6
5
|
|
|
7
6
|
function isEnabled(v, defaultValue) {
|
|
@@ -37,7 +36,7 @@ function postWebhook(webhookUrl, payload) {
|
|
|
37
36
|
headers: {
|
|
38
37
|
'Content-Type': 'application/json',
|
|
39
38
|
'Content-Length': body.length,
|
|
40
|
-
'User-Agent': 'nodebb-plugin-onekite
|
|
39
|
+
'User-Agent': 'nodebb-plugin-calendar-onekite',
|
|
41
40
|
},
|
|
42
41
|
}, (res) => {
|
|
43
42
|
const ok = res.statusCode && res.statusCode >= 200 && res.statusCode < 300;
|
|
@@ -89,7 +88,7 @@ function buildReservationMessage(kind, reservation) {
|
|
|
89
88
|
function buildWebhookPayload(kind, reservation) {
|
|
90
89
|
// Discord "regroupe" visuellement les messages consécutifs d'un même auteur.
|
|
91
90
|
// En utilisant un username différent par action, on obtient un message bien distinct.
|
|
92
|
-
const webhookUsername = kind === 'paid' ? '
|
|
91
|
+
const webhookUsername = kind === 'paid' ? 'OneKite • Paiement' : 'OneKite • Réservation';
|
|
93
92
|
|
|
94
93
|
const calUrl = 'https://www.onekite.com/calendar';
|
|
95
94
|
const username = reservation && reservation.username ? String(reservation.username) : '';
|
|
@@ -125,7 +124,7 @@ function buildWebhookPayload(kind, reservation) {
|
|
|
125
124
|
? 'Un paiement a été reçu pour une réservation.'
|
|
126
125
|
: 'Une nouvelle demande de réservation a été créée.',
|
|
127
126
|
fields,
|
|
128
|
-
footer: { text: '
|
|
127
|
+
footer: { text: 'OneKite • Calendrier' },
|
|
129
128
|
timestamp: new Date().toISOString(),
|
|
130
129
|
},
|
|
131
130
|
],
|
|
@@ -141,7 +140,7 @@ async function notifyReservationRequested(settings, reservation) {
|
|
|
141
140
|
await postWebhook(url, buildWebhookPayload('request', reservation));
|
|
142
141
|
} catch (e) {
|
|
143
142
|
// eslint-disable-next-line no-console
|
|
144
|
-
|
|
143
|
+
console.warn('[calendar-onekite] Discord webhook failed (request)', e && e.message ? e.message : String(e));
|
|
145
144
|
}
|
|
146
145
|
}
|
|
147
146
|
|
|
@@ -154,7 +153,7 @@ async function notifyPaymentReceived(settings, reservation) {
|
|
|
154
153
|
await postWebhook(url, buildWebhookPayload('paid', reservation));
|
|
155
154
|
} catch (e) {
|
|
156
155
|
// eslint-disable-next-line no-console
|
|
157
|
-
|
|
156
|
+
console.warn('[calendar-onekite] Discord webhook failed (paid)', e && e.message ? e.message : String(e));
|
|
158
157
|
}
|
|
159
158
|
}
|
|
160
159
|
|
package/lib/helloasso.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const https = require('https');
|
|
4
|
-
const log = require('./log');
|
|
5
4
|
|
|
6
5
|
function requestJson(method, url, headers = {}, bodyObj = null) {
|
|
7
6
|
return new Promise((resolve) => {
|
|
@@ -285,11 +284,11 @@ async function listCatalogItems({ env, token, organizationSlug, formType, formSl
|
|
|
285
284
|
async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail, callbackUrl, webhookUrl, itemName, containsDonation, metadata }) {
|
|
286
285
|
if (!token || !organizationSlug) return null;
|
|
287
286
|
if (!callbackUrl || !/^https?:\/\//i.test(String(callbackUrl))) {
|
|
288
|
-
|
|
287
|
+
console.warn('[calendar-onekite] HelloAsso invalid return/back/error URL', { callbackUrl });
|
|
289
288
|
return null;
|
|
290
289
|
}
|
|
291
290
|
if (webhookUrl && !/^https?:\/\//i.test(String(webhookUrl))) {
|
|
292
|
-
|
|
291
|
+
console.warn('[calendar-onekite] HelloAsso invalid webhook URL', { webhookUrl });
|
|
293
292
|
}
|
|
294
293
|
// Checkout intents are created at organization level.
|
|
295
294
|
const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/checkout-intents`;
|
|
@@ -312,7 +311,7 @@ async function createCheckoutIntent({ env, token, organizationSlug, formType, fo
|
|
|
312
311
|
// Log the error payload to help diagnose configuration issues (slug, env, urls, amount, etc.)
|
|
313
312
|
try {
|
|
314
313
|
// eslint-disable-next-line no-console
|
|
315
|
-
|
|
314
|
+
console.warn('[calendar-onekite] HelloAsso checkout-intent failed', { status, json });
|
|
316
315
|
} catch (e) { /* ignore */ }
|
|
317
316
|
return null;
|
|
318
317
|
}
|
package/lib/helloassoWebhook.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
-
const log = require('./log');
|
|
5
4
|
|
|
6
5
|
const db = require.main.require('./src/database');
|
|
7
6
|
const meta = require.main.require('./src/meta');
|
|
@@ -20,10 +19,10 @@ const dbLayer = require('./db');
|
|
|
20
19
|
const helloasso = require('./helloasso');
|
|
21
20
|
const discord = require('./discord');
|
|
22
21
|
|
|
23
|
-
const SETTINGS_KEY = 'onekite
|
|
22
|
+
const SETTINGS_KEY = 'calendar-onekite';
|
|
24
23
|
|
|
25
24
|
// Replay protection: store processed payment ids.
|
|
26
|
-
const PROCESSED_KEY = 'onekite
|
|
25
|
+
const PROCESSED_KEY = 'calendar-onekite:helloasso:processedPayments';
|
|
27
26
|
|
|
28
27
|
async function sendEmail(template, toEmail, subject, data) {
|
|
29
28
|
const uidFromData = data && Number.isInteger(data.uid) ? data.uid : null;
|
|
@@ -68,7 +67,7 @@ async function sendEmail(template, toEmail, subject, data) {
|
|
|
68
67
|
// fall back to sendToEmail only.
|
|
69
68
|
} catch (err) {
|
|
70
69
|
// eslint-disable-next-line no-console
|
|
71
|
-
|
|
70
|
+
console.warn('[calendar-onekite] Failed to send email (webhook)', { template, toEmail, err: String(err && err.message || err) });
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
73
|
|
|
@@ -282,7 +281,7 @@ async function handler(req, res, next) {
|
|
|
282
281
|
|
|
283
282
|
if (allowedIps.length && clientIp && !allowedIps.includes(clientIp)) {
|
|
284
283
|
// eslint-disable-next-line no-console
|
|
285
|
-
|
|
284
|
+
console.warn('[calendar-onekite] HelloAsso webhook blocked by IP allowlist', { ip: clientIp, allowed: allowedIps });
|
|
286
285
|
return res.status(403).json({ ok: false, error: 'ip-not-allowed' });
|
|
287
286
|
}
|
|
288
287
|
|
|
@@ -325,7 +324,7 @@ async function handler(req, res, next) {
|
|
|
325
324
|
}
|
|
326
325
|
if (!resolvedRid) {
|
|
327
326
|
// eslint-disable-next-line no-console
|
|
328
|
-
|
|
327
|
+
console.warn('[calendar-onekite] HelloAsso webhook missing reservationId in metadata', { eventType: payload && payload.eventType, paymentId, checkoutIntentId });
|
|
329
328
|
// Do NOT mark as processed: if metadata/config is fixed later, a manual replay may be possible.
|
|
330
329
|
return res.json({ ok: true, processed: false, missingReservationId: true });
|
|
331
330
|
}
|
|
@@ -348,14 +347,14 @@ async function handler(req, res, next) {
|
|
|
348
347
|
// Real-time notify: refresh calendars for all viewers (owner + validators/admins)
|
|
349
348
|
try {
|
|
350
349
|
if (io && io.sockets && typeof io.sockets.emit === 'function') {
|
|
351
|
-
io.sockets.emit('event:onekite
|
|
350
|
+
io.sockets.emit('event:calendar-onekite.reservationUpdated', { rid: r.rid, status: r.status });
|
|
352
351
|
}
|
|
353
352
|
} catch (e) {}
|
|
354
353
|
|
|
355
354
|
// Notify requester
|
|
356
355
|
const requester = await user.getUserFields(r.uid, ['username', 'email']);
|
|
357
356
|
if (requester && requester.email) {
|
|
358
|
-
await sendEmail('
|
|
357
|
+
await sendEmail('calendar-onekite_paid', requester.email, 'Location matériel - Paiement reçu', {
|
|
359
358
|
uid: parseInt(r.uid, 10),
|
|
360
359
|
username: requester.username,
|
|
361
360
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|