nodebb-plugin-onekite-calendar 1.0.4 → 1.0.6

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 CHANGED
@@ -1,19 +1,8 @@
1
- # Changelog
2
-
3
- ## 1.1.0
4
- - Ultra perf clean : JS chargé uniquement sur les pages/plugin concernées (plus de scripts globaux)
5
- - Suppression legacy supplémentaire : clés DB renommées en onekite-calendar:* (aucune donnée conservée)
6
- - Nettoyages internes (variables globales JS, routes/paramètres uniformisés)
7
-
8
-
9
- ## 1.0.8
10
- - Suppression du legacy (namespace unique onekite-calendar, ACP/API/widgets)
11
- - Suppression des fallbacks inutiles
12
-
1
+ # Changelog – nodebb-plugin-onekite-calendar
13
2
 
14
3
  ## 1.0.6
15
4
  - Refactor interne : centralisation de l’envoi d’emails et du logging (opt-in)
16
- - Nettoyage : suppression des alias legacy (API + ACP)
5
+ - Compatibilité : ajout d’alias de routes "onekite-calendar" en plus de "calendar-onekite" (API + ACP)
17
6
 
18
7
  ## 1.0.5
19
8
  - Widget : points colorés selon le type (mêmes couleurs que le calendrier)
@@ -43,7 +32,7 @@
43
32
  - Correctifs divers de stabilité/performance (dont crash au démarrage)
44
33
 
45
34
  ## 1.0.0
46
- - Première version stable du plugin onekite-calendar
35
+ - Première version stable du plugin calendar-onekite
47
36
  - Gestion des réservations de matériel avec contrôle de disponibilité
48
37
  - Calendrier FullCalendar via CDN
49
38
  - Validation / refus des demandes depuis l’ACP
package/lib/admin.js CHANGED
@@ -50,18 +50,18 @@ const ADMIN_PRIV = 'admin:settings';
50
50
  const admin = {};
51
51
 
52
52
  admin.renderAdmin = async function (req, res) {
53
- res.render('admin/plugins/onekite-calendar', {
53
+ res.render('admin/plugins/calendar-onekite', {
54
54
  title: 'Calendar Onekite',
55
55
  });
56
56
  };
57
57
 
58
58
  admin.getSettings = async function (req, res) {
59
- const settings = await meta.settings.get('onekite-calendar');
59
+ const settings = await meta.settings.get('calendar-onekite');
60
60
  res.json(settings || {});
61
61
  };
62
62
 
63
63
  admin.saveSettings = async function (req, res) {
64
- await meta.settings.set('onekite-calendar', req.body || {});
64
+ await meta.settings.set('calendar-onekite', req.body || {});
65
65
  res.json({ ok: true });
66
66
  };
67
67
 
@@ -97,7 +97,7 @@ admin.approveReservation = async function (req, res) {
97
97
  }
98
98
 
99
99
  // Create HelloAsso payment link if configured
100
- const settings = await meta.settings.get('onekite-calendar');
100
+ const settings = await meta.settings.get('calendar-onekite');
101
101
  const env = settings.helloassoEnv || 'prod';
102
102
  const token = await helloasso.getAccessToken({
103
103
  env,
@@ -111,7 +111,7 @@ admin.approveReservation = async function (req, res) {
111
111
  const totalAmount = Math.max(0, Math.round((Number(r.total) || 0) * 100));
112
112
  const base = forumBaseUrl();
113
113
  const returnUrl = base ? `${base}/calendar` : '';
114
- const webhookUrl = base ? `${base}/plugins/onekite-calendar/helloasso` : '';
114
+ const webhookUrl = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
115
115
  const year = new Date(Number(r.start)).getFullYear();
116
116
  const intent = await helloasso.createCheckoutIntent({
117
117
  env,
@@ -154,7 +154,7 @@ admin.approveReservation = async function (req, res) {
154
154
  const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
155
155
  ? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
156
156
  : '';
157
- await sendEmail('onekite-calendar_approved', requester.email, 'Location matériel - Réservation validée', {
157
+ await sendEmail('calendar-onekite_approved', requester.email, 'Location matériel - Réservation validée', {
158
158
  uid: parseInt(r.uid, 10),
159
159
  username: requester.username,
160
160
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
@@ -189,7 +189,7 @@ admin.refuseReservation = async function (req, res) {
189
189
  try {
190
190
  const requester = await user.getUserFields(r.uid, ['username', 'email']);
191
191
  if (requester && requester.email) {
192
- await sendEmail('onekite-calendar_refused', requester.email, 'Location matériel - Réservation refusée', {
192
+ await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Réservation refusée', {
193
193
  uid: parseInt(r.uid, 10),
194
194
  username: requester.username,
195
195
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
@@ -360,7 +360,7 @@ admin.exportAccountingCsv = async function (req, res) {
360
360
  }
361
361
  const csv = lines.join('\n');
362
362
  res.setHeader('Content-Type', 'text/csv; charset=utf-8');
363
- res.setHeader('Content-Disposition', 'attachment; filename="onekite-calendar-accounting.csv"');
363
+ res.setHeader('Content-Disposition', 'attachment; filename="calendar-onekite-accounting.csv"');
364
364
  return res.send(csv);
365
365
  };
366
366
 
package/lib/api.js CHANGED
@@ -13,41 +13,6 @@ const dbLayer = require('./db');
13
13
  const { sendEmail } = require('./email');
14
14
  const log = require('./log');
15
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
-
51
16
  // Fast membership check without N calls to groups.isMember.
52
17
  // NodeBB's groups.getUserGroups([uid]) returns an array (per uid) of group objects.
53
18
  // We compare against both group slugs and names to be tolerant with older settings.
@@ -336,7 +301,7 @@ api.getEvents = async function (req, res) {
336
301
  const startTs = toTs(req.query.start) || 0;
337
302
  const endTs = toTs(req.query.end) || (Date.now() + 365 * 24 * 3600 * 1000);
338
303
 
339
- const settings = await meta.settings.get('onekite-calendar');
304
+ const settings = await meta.settings.get('calendar-onekite');
340
305
  const canMod = req.uid ? await canValidate(req.uid, settings) : false;
341
306
  const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
342
307
  const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings) : false;
@@ -344,89 +309,20 @@ api.getEvents = async function (req, res) {
344
309
  // Fetch a wider window because an event can start before the query range
345
310
  // and still overlap.
346
311
  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
-
312
+ const ids = await dbLayer.listReservationIdsByStartRange(wideStart, endTs, 5000);
423
313
  const out = [];
424
- const uidStr = (req.uid || req.uid === 0) ? String(req.uid) : '';
425
- for (const ev of (base || [])) {
426
- if (!ev) continue;
427
- const p = ev.extendedProps || {};
428
- if (p.type === 'reservation') {
429
- const ownerOrMod = !!(p.uid && uidStr && String(p.uid) === uidStr) || canMod;
314
+ // Batch fetch = major perf win when there are many reservations.
315
+ const reservations = await dbLayer.getReservations(ids);
316
+ for (const r of (reservations || [])) {
317
+ if (!r) continue;
318
+ // Only show active statuses
319
+ if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
320
+ const rStart = parseInt(r.start, 10);
321
+ const rEnd = parseInt(r.end, 10);
322
+ if (!(rStart < endTs && startTs < rEnd)) continue; // overlap check
323
+ const evs = eventsFor(r);
324
+ for (const ev of evs) {
325
+ const p = ev.extendedProps || {};
430
326
  const minimal = {
431
327
  id: ev.id,
432
328
  title: ev.title,
@@ -444,30 +340,53 @@ api.getEvents = async function (req, res) {
444
340
  canModerate: canMod,
445
341
  },
446
342
  };
447
- if (p._username && ownerOrMod) minimal.extendedProps.username = String(p._username);
448
- if (p._hasPayment && ownerOrMod) minimal.extendedProps.hasPayment = true;
343
+ // Only expose username on the event list to owner/moderators.
344
+ if (r.username && ((req.uid && String(req.uid) === String(r.uid)) || canMod)) {
345
+ minimal.extendedProps.username = String(r.username);
346
+ }
347
+ // Let the UI decide if a "Payer" button might exist, without exposing the URL in list.
348
+ if (r.status === 'awaiting_payment' && r.paymentUrl && (/^https?:\/\//i).test(String(r.paymentUrl))) {
349
+ if ((req.uid && String(req.uid) === String(r.uid)) || canMod) {
350
+ minimal.extendedProps.hasPayment = true;
351
+ }
352
+ }
449
353
  out.push(minimal);
450
- } else if (p.type === 'special') {
451
- const ownerOrPriv = canMod || canSpecialDelete || (p.uid && uidStr && String(p.uid) === uidStr);
354
+ }
355
+ }
356
+
357
+ // Special events
358
+ try {
359
+ const specialIds = await dbLayer.listSpecialIdsByStartRange(wideStart, endTs, 5000);
360
+ const specials = await dbLayer.getSpecialEvents(specialIds);
361
+ for (const sev of (specials || [])) {
362
+ if (!sev) continue;
363
+ const sStart = parseInt(sev.start, 10);
364
+ const sEnd = parseInt(sev.end, 10);
365
+ if (!(sStart < endTs && startTs < sEnd)) continue;
366
+ const full = eventsForSpecial(sev);
452
367
  const minimal = {
453
- id: ev.id,
454
- title: ev.title,
455
- allDay: ev.allDay,
456
- start: ev.start,
457
- end: ev.end,
458
- backgroundColor: ev.backgroundColor,
459
- borderColor: ev.borderColor,
460
- textColor: ev.textColor,
368
+ id: full.id,
369
+ title: full.title,
370
+ allDay: full.allDay,
371
+ start: full.start,
372
+ end: full.end,
373
+ backgroundColor: full.backgroundColor,
374
+ borderColor: full.borderColor,
375
+ textColor: full.textColor,
461
376
  extendedProps: {
462
377
  type: 'special',
463
- eid: p.eid,
378
+ eid: sev.eid,
464
379
  canCreateSpecial: canSpecialCreate,
465
380
  canDeleteSpecial: canSpecialDelete,
466
381
  },
467
382
  };
468
- if (p._username && ownerOrPriv) minimal.extendedProps.username = String(p._username);
383
+ if (sev.username && (canMod || canSpecialDelete || (req.uid && String(req.uid) === String(sev.uid)))) {
384
+ minimal.extendedProps.username = String(sev.username);
385
+ }
469
386
  out.push(minimal);
470
387
  }
388
+ } catch (e) {
389
+ // ignore
471
390
  }
472
391
 
473
392
  // Stable ordering -> stable ETag
@@ -482,8 +401,7 @@ api.getEvents = async function (req, res) {
482
401
 
483
402
  const etag = computeEtag(out);
484
403
  res.setHeader('ETag', etag);
485
- // Short caching + ETag revalidation keeps the feed snappy and reduces bandwidth.
486
- res.setHeader('Cache-Control', 'private, max-age=15, must-revalidate');
404
+ res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
487
405
  if (String(req.headers['if-none-match'] || '') === etag) {
488
406
  return res.status(304).end();
489
407
  }
@@ -495,7 +413,7 @@ api.getReservationDetails = async function (req, res) {
495
413
  const uid = req.uid;
496
414
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
497
415
 
498
- const settings = await meta.settings.get('onekite-calendar');
416
+ const settings = await meta.settings.get('calendar-onekite');
499
417
  const canMod = await canValidate(uid, settings);
500
418
 
501
419
  const rid = String(req.params.rid || '').trim();
@@ -537,7 +455,7 @@ api.getSpecialEventDetails = async function (req, res) {
537
455
  const uid = req.uid;
538
456
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
539
457
 
540
- const settings = await meta.settings.get('onekite-calendar');
458
+ const settings = await meta.settings.get('calendar-onekite');
541
459
  const canMod = await canValidate(uid, settings);
542
460
  const canSpecialDelete = await canDeleteSpecial(uid, settings);
543
461
 
@@ -566,7 +484,7 @@ api.getSpecialEventDetails = async function (req, res) {
566
484
  };
567
485
 
568
486
  api.getCapabilities = async function (req, res) {
569
- const settings = await meta.settings.get('onekite-calendar');
487
+ const settings = await meta.settings.get('calendar-onekite');
570
488
  const uid = req.uid || 0;
571
489
  const canMod = uid ? await canValidate(uid, settings) : false;
572
490
  res.json({
@@ -577,7 +495,7 @@ api.getCapabilities = async function (req, res) {
577
495
  };
578
496
 
579
497
  api.createSpecialEvent = async function (req, res) {
580
- const settings = await meta.settings.get('onekite-calendar');
498
+ const settings = await meta.settings.get('calendar-onekite');
581
499
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
582
500
  const ok = await canCreateSpecial(req.uid, settings);
583
501
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
@@ -613,7 +531,7 @@ api.createSpecialEvent = async function (req, res) {
613
531
  };
614
532
 
615
533
  api.deleteSpecialEvent = async function (req, res) {
616
- const settings = await meta.settings.get('onekite-calendar');
534
+ const settings = await meta.settings.get('calendar-onekite');
617
535
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
618
536
  const ok = await canDeleteSpecial(req.uid, settings);
619
537
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
@@ -624,7 +542,7 @@ api.deleteSpecialEvent = async function (req, res) {
624
542
  };
625
543
 
626
544
  api.getItems = async function (req, res) {
627
- const settings = await meta.settings.get('onekite-calendar');
545
+ const settings = await meta.settings.get('calendar-onekite');
628
546
 
629
547
  const env = settings.helloassoEnv || 'prod';
630
548
  const token = await helloasso.getAccessToken({
@@ -665,7 +583,7 @@ api.createReservation = async function (req, res) {
665
583
  const uid = req.uid;
666
584
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
667
585
 
668
- const settings = await meta.settings.get('onekite-calendar');
586
+ const settings = await meta.settings.get('calendar-onekite');
669
587
  const startPreview = toTs(req.body.start);
670
588
  const ok = await canRequest(uid, settings, startPreview);
671
589
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
@@ -762,7 +680,7 @@ api.createReservation = async function (req, res) {
762
680
 
763
681
  for (const md of (usersData || [])) {
764
682
  if (md && md.email) {
765
- await sendEmail('onekite-calendar_pending', md.email, 'Location matériel - Demande de réservation', {
683
+ await sendEmail('calendar-onekite_pending', md.email, 'Location matériel - Demande de réservation', {
766
684
  username: md.username,
767
685
  requester: requester.username,
768
686
  itemName: itemsLabel,
@@ -801,7 +719,7 @@ api.createReservation = async function (req, res) {
801
719
  api.approveReservation = async function (req, res) {
802
720
  const uid = req.uid;
803
721
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
804
- const settings = await meta.settings.get('onekite-calendar');
722
+ const settings = await meta.settings.get('calendar-onekite');
805
723
  const ok = await canValidate(uid, settings);
806
724
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
807
725
 
@@ -828,7 +746,7 @@ api.approveReservation = async function (req, res) {
828
746
  }
829
747
  // Create HelloAsso payment link on validation
830
748
  try {
831
- const settings2 = await meta.settings.get('onekite-calendar');
749
+ const settings2 = await meta.settings.get('calendar-onekite');
832
750
  const token = await helloasso.getAccessToken({ env: settings2.helloassoEnv || 'prod', clientId: settings2.helloassoClientId, clientSecret: settings2.helloassoClientSecret });
833
751
  const payer = await user.getUserFields(r.uid, ['email']);
834
752
  const year = yearFromTs(r.start);
@@ -880,7 +798,7 @@ api.approveReservation = async function (req, res) {
880
798
  const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
881
799
  ? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
882
800
  : '';
883
- await sendEmail('onekite-calendar_approved', requester.email, 'Location matériel - Réservation validée', {
801
+ await sendEmail('calendar-onekite_approved', requester.email, 'Location matériel - Réservation validée', {
884
802
  username: requester.username,
885
803
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
886
804
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
@@ -904,7 +822,7 @@ api.approveReservation = async function (req, res) {
904
822
  api.refuseReservation = async function (req, res) {
905
823
  const uid = req.uid;
906
824
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
907
- const settings = await meta.settings.get('onekite-calendar');
825
+ const settings = await meta.settings.get('calendar-onekite');
908
826
  const ok = await canValidate(uid, settings);
909
827
  if (!ok) return res.status(403).json({ error: 'not-allowed' });
910
828
 
@@ -919,7 +837,7 @@ api.refuseReservation = async function (req, res) {
919
837
 
920
838
  const requester = await user.getUserFields(r.uid, ['username', 'email']);
921
839
  if (requester && requester.email) {
922
- await sendEmail('onekite-calendar_refused', requester.email, 'Location matériel - Demande de réservation', {
840
+ await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Demande de réservation', {
923
841
  username: requester.username,
924
842
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
925
843
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
@@ -939,7 +857,7 @@ api.cancelReservation = async function (req, res) {
939
857
  const uid = req.uid;
940
858
  if (!uid) return res.status(401).json({ error: 'not-logged-in' });
941
859
 
942
- const settings = await meta.settings.get('onekite-calendar');
860
+ const settings = await meta.settings.get('calendar-onekite');
943
861
  const rid = String(req.params.rid || '').trim();
944
862
  if (!rid) return res.status(400).json({ error: 'missing-rid' });
945
863
 
package/lib/constants.js CHANGED
@@ -1,7 +1,10 @@
1
1
  'use strict';
2
2
 
3
- // Primary namespace for this plugin (NodeBB 4.x)
3
+ // Keep backward compatibility with historical route namespaces.
4
+ // New plugin name: nodebb-plugin-onekite-calendar
5
+
4
6
  module.exports = {
7
+ LEGACY_NAMESPACE: 'calendar-onekite',
5
8
  NAMESPACE: 'onekite-calendar',
6
- WIDGET_ID: 'onekite-calendar-twoweeks',
9
+ WIDGET_ID: 'calendar-onekite-twoweeks',
7
10
  };
@@ -3,8 +3,8 @@
3
3
  const controllers = {};
4
4
 
5
5
  controllers.renderCalendar = async function (req, res) {
6
- res.render('onekite-calendar', {
7
- title: 'Calendrier',
6
+ res.render('calendar-onekite', {
7
+ title: 'Calendar',
8
8
  });
9
9
  };
10
10
 
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-calendar:reservations';
6
- const KEY_OBJ = (rid) => `onekite-calendar:reservation:${rid}`;
7
- const KEY_CHECKOUT_INTENT_TO_RID = 'onekite-calendar:helloasso:checkoutIntentToRid';
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-calendar:special';
11
- const KEY_SPECIAL_OBJ = (eid) => `onekite-calendar:special:${eid}`;
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
@@ -37,7 +37,7 @@ function postWebhook(webhookUrl, payload) {
37
37
  headers: {
38
38
  'Content-Type': 'application/json',
39
39
  'Content-Length': body.length,
40
- 'User-Agent': 'nodebb-plugin-onekite-calendar',
40
+ 'User-Agent': 'nodebb-plugin-calendar-onekite',
41
41
  },
42
42
  }, (res) => {
43
43
  const ok = res.statusCode && res.statusCode >= 200 && res.statusCode < 300;
package/lib/email.js CHANGED
@@ -8,17 +8,30 @@ function defaultLanguage() {
8
8
  }
9
9
 
10
10
  /**
11
- * Send a transactional email using NodeBB's emailer (NodeBB 4.x).
11
+ * Send a transactional email using NodeBB's emailer.
12
+ *
13
+ * We intentionally do not log here (per project preferences).
12
14
  */
13
15
  async function sendEmail(template, toEmail, subject, data) {
14
16
  if (!toEmail) return;
15
- if (!emailer || typeof emailer.sendToEmail !== 'function') {
16
- throw new Error('Emailer not available (sendToEmail missing)');
17
- }
18
17
 
19
18
  const language = defaultLanguage();
20
19
  const params = Object.assign({}, data || {}, subject ? { subject } : {});
21
- await emailer.sendToEmail(template, toEmail, language, params);
20
+
21
+ // Common signature: sendToEmail(template, email, language, params)
22
+ if (emailer && typeof emailer.sendToEmail === 'function') {
23
+ await emailer.sendToEmail(template, toEmail, language, params);
24
+ return;
25
+ }
26
+
27
+ // Fallbacks for older builds
28
+ if (emailer && typeof emailer.send === 'function') {
29
+ if (emailer.send.length >= 4) {
30
+ await emailer.send(template, toEmail, language, params);
31
+ return;
32
+ }
33
+ await emailer.send(template, toEmail, params);
34
+ }
22
35
  }
23
36
 
24
37
  module.exports = {
@@ -20,10 +20,10 @@ const dbLayer = require('./db');
20
20
  const helloasso = require('./helloasso');
21
21
  const discord = require('./discord');
22
22
 
23
- const SETTINGS_KEY = 'onekite-calendar';
23
+ const SETTINGS_KEY = 'calendar-onekite';
24
24
 
25
25
  // Replay protection: store processed payment ids.
26
- const PROCESSED_KEY = 'onekite-calendar:helloasso:processedPayments';
26
+ const PROCESSED_KEY = 'calendar-onekite:helloasso:processedPayments';
27
27
 
28
28
  async function sendEmail(template, toEmail, subject, data) {
29
29
  const uidFromData = data && Number.isInteger(data.uid) ? data.uid : null;
@@ -348,14 +348,14 @@ async function handler(req, res, next) {
348
348
  // Real-time notify: refresh calendars for all viewers (owner + validators/admins)
349
349
  try {
350
350
  if (io && io.sockets && typeof io.sockets.emit === 'function') {
351
- io.sockets.emit('event:onekite-calendar.reservationUpdated', { rid: r.rid, status: r.status });
351
+ io.sockets.emit('event:calendar-onekite.reservationUpdated', { rid: r.rid, status: r.status });
352
352
  }
353
353
  } catch (e) {}
354
354
 
355
355
  // Notify requester
356
356
  const requester = await user.getUserFields(r.uid, ['username', 'email']);
357
357
  if (requester && requester.email) {
358
- await sendEmail('onekite-calendar_paid', requester.email, 'Location matériel - Paiement reçu', {
358
+ await sendEmail('calendar-onekite_paid', requester.email, 'Location matériel - Paiement reçu', {
359
359
  uid: parseInt(r.uid, 10),
360
360
  username: requester.username,
361
361
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
package/lib/scheduler.js CHANGED
@@ -16,7 +16,7 @@ function getSetting(settings, key, fallback) {
16
16
 
17
17
  // Pending holds: short lock after a user creates a request (defaults to 5 minutes)
18
18
  async function expirePending() {
19
- const settings = await meta.settings.get('onekite-calendar');
19
+ const settings = await meta.settings.get('calendar-onekite');
20
20
  const holdMins = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
21
21
  const now = Date.now();
22
22
 
@@ -44,7 +44,7 @@ async function expirePending() {
44
44
  // - We send a reminder after `paymentHoldMinutes` (default 60)
45
45
  // - We expire (and remove) after `2 * paymentHoldMinutes`
46
46
  async function processAwaitingPayment() {
47
- const settings = await meta.settings.get('onekite-calendar');
47
+ const settings = await meta.settings.get('calendar-onekite');
48
48
  const holdMins = parseInt(
49
49
  getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')),
50
50
  10
@@ -108,7 +108,7 @@ async function processAwaitingPayment() {
108
108
 
109
109
  if (!r.reminderSent && now >= reminderAt && now < expireAt) {
110
110
  // Send reminder once (guarded across clustered NodeBB processes)
111
- const reminderKey = 'onekite-calendar:email:reminderSent';
111
+ const reminderKey = 'calendar-onekite:email:reminderSent';
112
112
  const first = await db.setAdd(reminderKey, rid);
113
113
  if (!first) {
114
114
  // another process already sent it
@@ -119,7 +119,7 @@ async function processAwaitingPayment() {
119
119
  }
120
120
  const u = await user.getUserFields(r.uid, ['username', 'email']);
121
121
  if (u && u.email) {
122
- await sendEmail('onekite-calendar_reminder', u.email, 'Location matériel - Rappel', {
122
+ await sendEmail('calendar-onekite_reminder', u.email, 'Location matériel - Rappel', {
123
123
  username: u.username,
124
124
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
125
125
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
@@ -138,12 +138,12 @@ async function processAwaitingPayment() {
138
138
  if (now >= expireAt) {
139
139
  // Expire: remove reservation so it disappears from calendar and frees items
140
140
  // Guard email send across clustered NodeBB processes
141
- const expiredKey = 'onekite-calendar:email:expiredSent';
141
+ const expiredKey = 'calendar-onekite:email:expiredSent';
142
142
  const firstExpired = await db.setAdd(expiredKey, rid);
143
143
  const shouldEmail = !!firstExpired;
144
144
  const u = await user.getUserFields(r.uid, ['username', 'email']);
145
145
  if (shouldEmail && u && u.email) {
146
- await sendEmail('onekite-calendar_expired', u.email, 'Location matériel - Rappel', {
146
+ await sendEmail('calendar-onekite_expired', u.email, 'Location matériel - Rappel', {
147
147
  username: u.username,
148
148
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
149
149
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
package/lib/widgets.js CHANGED
@@ -40,7 +40,7 @@ widgets.defineWidgets = async function (widgetData) {
40
40
  }
41
41
 
42
42
  list.push({
43
- widget: 'onekite-calendar-twoweeks',
43
+ widget: 'calendar-onekite-twoweeks',
44
44
  name: 'Calendrier Onekite',
45
45
  description: 'Affiche la semaine courante + la semaine suivante (FullCalendar via CDN).',
46
46
  content: '',
@@ -54,11 +54,14 @@ widgets.renderTwoWeeksWidget = async function (data) {
54
54
  const id = makeDomId();
55
55
  const calUrl = widgetCalendarUrl();
56
56
  const apiBase = forumBaseUrl();
57
+ // Prefer the new namespace, but keep a transparent fallback to the legacy one.
57
58
  const eventsEndpoint = `${apiBase}/api/v3/plugins/onekite-calendar/events`;
59
+ const legacyEventsEndpoint = `${apiBase}/api/v3/plugins/calendar-onekite/events`;
58
60
 
59
61
  const idJson = JSON.stringify(id);
60
62
  const calUrlJson = JSON.stringify(calUrl);
61
63
  const eventsEndpointJson = JSON.stringify(eventsEndpoint);
64
+ const legacyEventsEndpointJson = JSON.stringify(legacyEventsEndpoint);
62
65
 
63
66
  const html = `
64
67
  <div class="onekite-twoweeks">
@@ -75,6 +78,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
75
78
  const containerId = ${idJson};
76
79
  const calUrl = ${calUrlJson};
77
80
  const eventsEndpoint = ${eventsEndpointJson};
81
+ const legacyEventsEndpoint = ${legacyEventsEndpointJson};
78
82
 
79
83
  function loadOnce(tag, attrs) {
80
84
  return new Promise((resolve, reject) => {
@@ -150,6 +154,13 @@ widgets.renderTwoWeeksWidget = async function (data) {
150
154
  const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
151
155
  const url1 = eventsEndpoint + '?' + qs.toString();
152
156
  fetch(url1, { credentials: 'same-origin' })
157
+ .then((r) => {
158
+ if (r && r.status === 404) {
159
+ const url2 = legacyEventsEndpoint + '?' + qs.toString();
160
+ return fetch(url2, { credentials: 'same-origin' });
161
+ }
162
+ return r;
163
+ })
153
164
  .then((r) => r.json())
154
165
  .then((json) => successCallback(json || []))
155
166
  .catch((e) => failureCallback(e));
package/library.js CHANGED
@@ -1,5 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ // We use NodeBB's route helpers for page routes so the rendered page includes
4
+ // the normal client bundle (ajaxify/requirejs). The helper signatures differ
5
+ // across NodeBB versions, but for NodeBB v4.x the order is:
6
+ // setupPageRoute(router, path, middlewaresArray, handler)
7
+ // setupAdminPageRoute(router, path, middlewaresArray, handler)
3
8
  const routeHelpers = require.main.require('./src/routes/helpers');
4
9
 
5
10
  const controllers = require('./lib/controllers.js');
@@ -8,7 +13,7 @@ const admin = require('./lib/admin');
8
13
  const scheduler = require('./lib/scheduler');
9
14
  const helloassoWebhook = require('./lib/helloassoWebhook');
10
15
  const widgets = require('./lib/widgets');
11
- const { NAMESPACE } = require('./lib/constants');
16
+ const { LEGACY_NAMESPACE, NAMESPACE } = require('./lib/constants');
12
17
  const bodyParser = require('body-parser');
13
18
 
14
19
  const Plugin = {};
@@ -19,8 +24,16 @@ const mw = (...fns) => fns.filter(isFn);
19
24
  Plugin.init = async function (params) {
20
25
  const { router, middleware } = params;
21
26
 
22
- const publicExpose = mw(middleware && (middleware.authenticate || middleware.exposeUid));
27
+ // Build middleware arrays safely and always spread them into Express route methods.
28
+ // Express will throw if any callback is undefined, so we filter strictly.
29
+ // Auth middlewares differ slightly depending on NodeBB configuration.
30
+ // In v4, some installs rely on middleware.authenticate rather than exposeUid.
31
+ const baseExpose = mw(middleware && (middleware.authenticate || middleware.exposeUid));
32
+ const publicExpose = baseExpose;
33
+ const publicAuth = mw(middleware && (middleware.authenticate || middleware.exposeUid), middleware && middleware.ensureLoggedIn);
23
34
 
35
+ // Robust admin guard: avoid middleware.admin.checkPrivileges() signature differences
36
+ // across NodeBB versions. We treat membership in the 'administrators' group as admin.
24
37
  const Groups = require.main.require('./src/groups');
25
38
  async function adminOnly(req, res, next) {
26
39
  try {
@@ -37,7 +50,6 @@ Plugin.init = async function (params) {
37
50
  return next(err);
38
51
  }
39
52
  }
40
-
41
53
  const adminMws = mw(
42
54
  middleware && (middleware.authenticate || middleware.exposeUid),
43
55
  middleware && middleware.ensureLoggedIn,
@@ -45,57 +57,72 @@ Plugin.init = async function (params) {
45
57
  );
46
58
 
47
59
  // Page routes (HTML)
60
+ // IMPORTANT: pass an ARRAY for middlewares (even if empty), otherwise
61
+ // setupPageRoute will throw "middlewares is not iterable".
48
62
  routeHelpers.setupPageRoute(router, '/calendar', mw(), controllers.renderCalendar);
49
-
50
- // Admin page route
63
+ // Admin page route (keep legacy + add new)
64
+ routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${LEGACY_NAMESPACE}`, mw(), admin.renderAdmin);
51
65
  routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${NAMESPACE}`, mw(), admin.renderAdmin);
52
66
 
53
- // Public API (JSON) — NodeBB 4.x (v3 API)
54
- router.get(`/api/v3/plugins/${NAMESPACE}/events`, ...publicExpose, api.getEvents);
55
- router.get(`/api/v3/plugins/${NAMESPACE}/items`, ...publicExpose, api.getItems);
56
- router.get(`/api/v3/plugins/${NAMESPACE}/capabilities`, ...publicExpose, api.getCapabilities);
57
-
58
- router.post(`/api/v3/plugins/${NAMESPACE}/reservations`, ...publicExpose, api.createReservation);
59
- router.get(`/api/v3/plugins/${NAMESPACE}/reservations/:rid`, ...publicExpose, api.getReservationDetails);
60
- router.put(`/api/v3/plugins/${NAMESPACE}/reservations/:rid/approve`, ...publicExpose, api.approveReservation);
61
- router.put(`/api/v3/plugins/${NAMESPACE}/reservations/:rid/refuse`, ...publicExpose, api.refuseReservation);
62
- router.put(`/api/v3/plugins/${NAMESPACE}/reservations/:rid/cancel`, ...publicExpose, api.cancelReservation);
63
-
64
- router.post(`/api/v3/plugins/${NAMESPACE}/special-events`, ...publicExpose, api.createSpecialEvent);
65
- router.get(`/api/v3/plugins/${NAMESPACE}/special-events/:eid`, ...publicExpose, api.getSpecialEventDetails);
66
- router.delete(`/api/v3/plugins/${NAMESPACE}/special-events/:eid`, ...publicExpose, api.deleteSpecialEvent);
67
+ // Public API (JSON) — NodeBB 4.x only (v3 API)
68
+ const pluginBases = [LEGACY_NAMESPACE, NAMESPACE];
69
+ pluginBases.forEach((ns) => {
70
+ router.get(`/api/v3/plugins/${ns}/events`, ...publicExpose, api.getEvents);
71
+ router.get(`/api/v3/plugins/${ns}/items`, ...publicExpose, api.getItems);
72
+ router.get(`/api/v3/plugins/${ns}/capabilities`, ...publicExpose, api.getCapabilities);
73
+
74
+ router.post(`/api/v3/plugins/${ns}/reservations`, ...publicExpose, api.createReservation);
75
+ router.get(`/api/v3/plugins/${ns}/reservations/:rid`, ...publicExpose, api.getReservationDetails);
76
+ router.put(`/api/v3/plugins/${ns}/reservations/:rid/approve`, ...publicExpose, api.approveReservation);
77
+ router.put(`/api/v3/plugins/${ns}/reservations/:rid/refuse`, ...publicExpose, api.refuseReservation);
78
+ router.put(`/api/v3/plugins/${ns}/reservations/:rid/cancel`, ...publicExpose, api.cancelReservation);
79
+
80
+ router.post(`/api/v3/plugins/${ns}/special-events`, ...publicExpose, api.createSpecialEvent);
81
+ router.get(`/api/v3/plugins/${ns}/special-events/:eid`, ...publicExpose, api.getSpecialEventDetails);
82
+ router.delete(`/api/v3/plugins/${ns}/special-events/:eid`, ...publicExpose, api.deleteSpecialEvent);
83
+ });
67
84
 
68
85
  // Admin API (JSON)
69
- const base = `/api/v3/admin/plugins/${NAMESPACE}`;
86
+ const adminBases = [`/api/v3/admin/plugins/${LEGACY_NAMESPACE}`, `/api/v3/admin/plugins/${NAMESPACE}`];
70
87
 
71
- router.get(`${base}/settings`, ...adminMws, admin.getSettings);
72
- router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
88
+ adminBases.forEach((base) => {
89
+ router.get(`${base}/settings`, ...adminMws, admin.getSettings);
90
+ router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
73
91
 
74
- router.get(`${base}/pending`, ...adminMws, admin.listPending);
75
- router.put(`${base}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
76
- router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
92
+ router.get(`${base}/pending`, ...adminMws, admin.listPending);
93
+ router.put(`${base}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
94
+ router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
77
95
 
78
- router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
96
+ router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
97
+ // Accounting / exports
98
+ router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
99
+ router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
100
+ router.post(`${base}/accounting/purge`, ...adminMws, admin.purgeAccounting);
79
101
 
80
- router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
81
- router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
82
- router.post(`${base}/accounting/purge`, ...adminMws, admin.purgeAccounting);
83
-
84
- router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
102
+ // Purge special events by year
103
+ router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
104
+ });
85
105
 
86
- // HelloAsso webhook endpoint (signature verified)
106
+ // HelloAsso callback endpoint (hardened)
107
+ // - Only accepts POST
108
+ // - Verifies x-ha-signature (HMAC SHA-256) using the configured client secret
109
+ // - Basic replay protection
110
+ // NOTE: we capture the raw body for signature verification.
87
111
  const helloassoJson = bodyParser.json({
88
112
  verify: (req, _res, buf) => {
89
113
  req.rawBody = buf;
90
114
  },
91
115
  type: ['application/json', 'application/*+json'],
92
116
  });
93
-
117
+ // Accept webhook on both legacy root path and namespaced plugin path.
118
+ // Some reverse proxies block unknown root paths, so /plugins/... is recommended.
94
119
  router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
120
+ router.post(`/plugins/${LEGACY_NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
95
121
  router.post(`/plugins/${NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
96
122
 
97
- // Optional health checks
123
+ // Optional: health checks
98
124
  router.get('/helloasso', (req, res) => res.json({ ok: true }));
125
+ router.get(`/plugins/${LEGACY_NAMESPACE}/helloasso`, (req, res) => res.json({ ok: true }));
99
126
  router.get(`/plugins/${NAMESPACE}/helloasso`, (req, res) => res.json({ ok: true }));
100
127
 
101
128
  scheduler.start();
@@ -104,20 +131,24 @@ Plugin.init = async function (params) {
104
131
  Plugin.addAdminNavigation = async function (header) {
105
132
  header.plugins = header.plugins || [];
106
133
  header.plugins.push({
107
- route: `/plugins/${NAMESPACE}`,
134
+ route: `/plugins/${LEGACY_NAMESPACE}`,
108
135
  icon: 'fa-calendar',
109
- name: 'Onekite Calendar',
136
+ name: 'Calendar Onekite',
110
137
  });
111
138
  return header;
112
139
  };
113
140
 
141
+
114
142
  // Ensure our transactional emails always get a subject.
143
+ // NodeBB's Emailer.sendToEmail signature expects (template, email, language, params),
144
+ // so plugins typically inject/modify the subject via this hook.
115
145
  Plugin.emailModify = async function (data) {
116
146
  try {
117
147
  if (!data || !data.template) return data;
118
148
  const tpl = String(data.template);
119
- if (!tpl.startsWith('onekite-calendar_')) return data;
149
+ if (!tpl.startsWith('calendar-onekite_')) return data;
120
150
 
151
+ // If the caller provided a subject (we pass it in params.subject), copy it to data.subject.
121
152
  const provided = data.params && data.params.subject ? String(data.params.subject) : '';
122
153
  if (provided && (!data.subject || !String(data.subject).trim())) {
123
154
  data.subject = provided;
@@ -130,4 +161,4 @@ Plugin.emailModify = async function (data) {
130
161
  Plugin.defineWidgets = widgets.defineWidgets;
131
162
  Plugin.renderTwoWeeksWidget = widgets.renderTwoWeeksWidget;
132
163
 
133
- module.exports = Plugin;
164
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "method": "defineWidgets"
22
22
  },
23
23
  {
24
- "hook": "filter:widget.render:onekite-calendar-twoweeks",
24
+ "hook": "filter:widget.render:calendar-onekite-twoweeks",
25
25
  "method": "renderTwoWeeksWidget"
26
26
  }
27
27
  ],
@@ -30,8 +30,14 @@
30
30
  },
31
31
  "templates": "./templates",
32
32
  "modules": {
33
- "admin/plugins/onekite-calendar": "./public/admin.js",
34
- "forum/onekite-calendar": "./public/client.js"
33
+ "../admin/plugins/calendar-onekite.js": "./public/admin.js",
34
+ "admin/plugins/calendar-onekite": "./public/admin.js"
35
35
  },
36
- "version": "1.1.0"
36
+ "scripts": [
37
+ "public/client.js"
38
+ ],
39
+ "acpScripts": [
40
+ "public/admin.js"
41
+ ],
42
+ "version": "1.0.7"
37
43
  }
package/public/admin.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts, bootbox) {
2
+ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts, bootbox) {
3
3
  'use strict';
4
4
 
5
5
  // Cache of pending reservations keyed by rid so delegated click handlers
@@ -11,11 +11,11 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
11
11
  // by NodeBB ACP save buttons/hooks across ajaxify navigations.
12
12
  try {
13
13
  const now = Date.now();
14
- const last = window.onekiteCalendarLastAlert;
14
+ const last = window.oneKiteCalendarLastAlert;
15
15
  if (last && last.type === type && last.msg === msg && (now - last.ts) < 1200) {
16
16
  return;
17
17
  }
18
- window.onekiteCalendarLastAlert = { type, msg, ts: now };
18
+ window.oneKiteCalendarLastAlert = { type, msg, ts: now };
19
19
  } catch (e) {}
20
20
  try {
21
21
  if (alerts && typeof alerts[type] === 'function') {
@@ -46,7 +46,30 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
46
46
 
47
47
 
48
48
  if (!res.ok) {
49
- const text = await res.text().catch(() => '');
49
+ // NodeBB versions differ: some expose admin APIs under /api/admin instead of /api/v3/admin
50
+ if (res.status === 404 && typeof url === 'string' && url.includes('/api/v3/admin/')) {
51
+ const altUrl = url.replace('/api/v3/admin/', '/api/admin/');
52
+ const res2 = await fetch(altUrl, {
53
+ credentials: 'same-origin',
54
+ headers: (() => {
55
+ const headers = { 'Content-Type': 'application/json' };
56
+ const token =
57
+ (window.config && (window.config.csrf_token || window.config.csrfToken)) ||
58
+ (window.ajaxify && window.ajaxify.data && window.ajaxify.data.csrf_token) ||
59
+ (document.querySelector('meta[name="csrf-token"]') && document.querySelector('meta[name="csrf-token"]').getAttribute('content')) ||
60
+ (document.querySelector('meta[name="csrf_token"]') && document.querySelector('meta[name="csrf_token"]').getAttribute('content')) ||
61
+ (typeof app !== 'undefined' && app && app.csrfToken) ||
62
+ null;
63
+ if (token) headers['x-csrf-token'] = token;
64
+ return headers;
65
+ })(),
66
+ ...opts,
67
+ });
68
+ if (res2.ok) {
69
+ return await res2.json();
70
+ }
71
+ }
72
+ const text = await res.text().catch(() => '');
50
73
  throw new Error(`${res.status} ${text}`);
51
74
  }
52
75
  return await res.json();
@@ -244,36 +267,36 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
244
267
  }
245
268
 
246
269
  async function loadSettings() {
247
- return await fetchJson('/api/v3/admin/plugins/onekite-calendar/settings');
270
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/settings');
248
271
  }
249
272
 
250
273
  async function saveSettings(payload) {
251
- return await fetchJson('/api/v3/admin/plugins/onekite-calendar/settings', {
274
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/settings', {
252
275
  method: 'PUT',
253
276
  body: JSON.stringify(payload),
254
277
  });
255
278
  }
256
279
 
257
280
  async function loadPending() {
258
- return await fetchJson('/api/v3/admin/plugins/onekite-calendar/pending');
281
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/pending');
259
282
  }
260
283
 
261
284
  async function approve(rid, payload) {
262
- return await fetchJson(`/api/v3/admin/plugins/onekite-calendar/reservations/${rid}/approve`, {
285
+ return await fetchJson(`/api/v3/admin/plugins/calendar-onekite/reservations/${rid}/approve`, {
263
286
  method: 'PUT',
264
287
  body: JSON.stringify(payload || {}),
265
288
  });
266
289
  }
267
290
 
268
291
  async function refuse(rid, payload) {
269
- return await fetchJson(`/api/v3/admin/plugins/onekite-calendar/reservations/${rid}/refuse`, {
292
+ return await fetchJson(`/api/v3/admin/plugins/calendar-onekite/reservations/${rid}/refuse`, {
270
293
  method: 'PUT',
271
294
  body: JSON.stringify(payload || {}),
272
295
  });
273
296
  }
274
297
 
275
298
  async function purge(year) {
276
- return await fetchJson('/api/v3/admin/plugins/onekite-calendar/purge', {
299
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/purge', {
277
300
  method: 'POST',
278
301
  body: JSON.stringify({ year }),
279
302
  });
@@ -281,12 +304,12 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
281
304
 
282
305
  async function purgeSpecialEvents(year) {
283
306
  try {
284
- return await fetchJson('/api/v3/admin/plugins/onekite-calendar/special-events/purge', {
307
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/special-events/purge', {
285
308
  method: 'POST',
286
309
  body: JSON.stringify({ year }),
287
310
  });
288
311
  } catch (e) {
289
- return await fetchJson('/api/v3/admin/plugins/onekite-calendar/special-events/purge', {
312
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/special-events/purge', {
290
313
  method: 'POST',
291
314
  body: JSON.stringify({ year }),
292
315
  });
@@ -298,7 +321,7 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
298
321
  if (from) params.set('from', from);
299
322
  if (to) params.set('to', to);
300
323
  const qs = params.toString();
301
- return await fetchJson(`/api/v3/admin/plugins/onekite-calendar/accounting${qs ? `?${qs}` : ''}`);
324
+ return await fetchJson(`/api/v3/admin/plugins/calendar-onekite/accounting${qs ? `?${qs}` : ''}`);
302
325
  }
303
326
 
304
327
  async function init() {
@@ -361,19 +384,19 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
361
384
 
362
385
  // Expose the latest save handler so the global delegated listener (bound once)
363
386
  // can always call the current instance tied to the current form.
364
- window.onekiteCalendarAdminDoSave = doSave;
387
+ window.oneKiteCalendarAdminDoSave = doSave;
365
388
 
366
389
  // Save buttons (NodeBB header/footer "Enregistrer" + floppy icon)
367
390
  // Bind a SINGLE delegated listener for the entire admin session.
368
391
  const SAVE_SELECTOR = '#save, .save, [data-action="save"], .settings-save, .floating-save, .btn[data-action="save"]';
369
- if (!window.onekiteCalendarAdminBound) {
370
- window.onekiteCalendarAdminBound = true;
392
+ if (!window.oneKiteCalendarAdminBound) {
393
+ window.oneKiteCalendarAdminBound = true;
371
394
  document.addEventListener('click', (ev) => {
372
395
  const btn = ev.target && ev.target.closest && ev.target.closest(SAVE_SELECTOR);
373
396
  if (!btn) return;
374
397
  // Only handle clicks while we're on this plugin page
375
398
  if (!document.getElementById('onekite-settings-form')) return;
376
- const fn = window.onekiteCalendarAdminDoSave;
399
+ const fn = window.oneKiteCalendarAdminDoSave;
377
400
  if (typeof fn === 'function') fn(ev);
378
401
  });
379
402
  }
@@ -697,7 +720,7 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
697
720
  if (accFrom && accFrom.value) params.set('from', accFrom.value);
698
721
  if (accTo && accTo.value) params.set('to', accTo.value);
699
722
  const qs = params.toString();
700
- const url = `/api/v3/admin/plugins/onekite-calendar/accounting.csv${qs ? `?${qs}` : ''}`;
723
+ const url = `/api/v3/admin/plugins/calendar-onekite/accounting.csv${qs ? `?${qs}` : ''}`;
701
724
  window.open(url, '_blank');
702
725
  });
703
726
 
@@ -710,7 +733,7 @@ define('admin/plugins/onekite-calendar', ['alerts', 'bootbox'], function (alerts
710
733
  if (accFrom && accFrom.value) params.set('from', accFrom.value);
711
734
  if (accTo && accTo.value) params.set('to', accTo.value);
712
735
  const qs = params.toString();
713
- const url = `/api/v3/admin/plugins/onekite-calendar/accounting/purge${qs ? `?${qs}` : ''}`;
736
+ const url = `/api/v3/admin/plugins/calendar-onekite/accounting/purge${qs ? `?${qs}` : ''}`;
714
737
  const res = await fetchJson(url, { method: 'POST' });
715
738
  if (res && res.ok) {
716
739
  showAlert('success', `Compta purgée : ${res.purged || 0} réservation(s).`);
package/public/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* global FullCalendar, ajaxify */
2
2
 
3
- define('forum/onekite-calendar', ['alerts', 'bootbox', 'hooks'], function (alerts, bootbox, hooks) {
3
+ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alerts, bootbox, hooks) {
4
4
  'use strict';
5
5
 
6
6
  // Ensure small UI tweaks are applied even when themes override bootstrap defaults.
@@ -498,7 +498,7 @@ define('forum/onekite-calendar', ['alerts', 'bootbox', 'hooks'], function (alert
498
498
  }
499
499
 
500
500
  async function loadCapabilities() {
501
- return await fetchJson('/api/v3/plugins/onekite-calendar/capabilities');
501
+ return await fetchJson('/api/v3/plugins/calendar-onekite/capabilities');
502
502
  }
503
503
 
504
504
  // Leaflet (OpenStreetMap) helpers - loaded lazily only when needed.
@@ -706,34 +706,34 @@ function attachAddressAutocomplete(inputEl, onPick) {
706
706
 
707
707
  async function loadItems() {
708
708
  try {
709
- return await fetchJson('/api/v3/plugins/onekite-calendar/items');
709
+ return await fetchJson('/api/v3/plugins/calendar-onekite/items');
710
710
  } catch (e) {
711
711
  return [];
712
712
  }
713
713
  }
714
714
 
715
715
  async function requestReservation(payload) {
716
- return await fetchJson('/api/v3/plugins/onekite-calendar/reservations', {
716
+ return await fetchJson('/api/v3/plugins/calendar-onekite/reservations', {
717
717
  method: 'POST',
718
718
  body: JSON.stringify(payload),
719
719
  });
720
720
  }
721
721
 
722
722
  async function approveReservation(rid, payload) {
723
- return await fetchJson(`/api/v3/plugins/onekite-calendar/reservations/${rid}/approve`, {
723
+ return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/approve`, {
724
724
  method: 'PUT',
725
725
  body: JSON.stringify(payload || {}),
726
726
  });
727
727
  }
728
728
 
729
729
  async function cancelReservation(rid) {
730
- return await fetchJson(`/api/v3/plugins/onekite-calendar/reservations/${rid}/cancel`, {
730
+ return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/cancel`, {
731
731
  method: 'PUT',
732
732
  });
733
733
  }
734
734
 
735
735
  async function refuseReservation(rid, payload) {
736
- return await fetchJson(`/api/v3/plugins/onekite-calendar/reservations/${rid}/refuse`, {
736
+ return await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${rid}/refuse`, {
737
737
  method: 'PUT',
738
738
  body: JSON.stringify(payload || {}),
739
739
  });
@@ -783,7 +783,7 @@ function toDatetimeLocalValue(date) {
783
783
  let blocked = new Set();
784
784
  try {
785
785
  const qs = new URLSearchParams({ start: selectionInfo.startStr, end: selectionInfo.endStr });
786
- const evs = await fetchJson(`/api/v3/plugins/onekite-calendar/events?${qs.toString()}`);
786
+ const evs = await fetchJson(`/api/v3/plugins/calendar-onekite/events?${qs.toString()}`);
787
787
  (evs || []).forEach((ev) => {
788
788
  const st = (ev.extendedProps && ev.extendedProps.status) || '';
789
789
  if (!['pending', 'awaiting_payment', 'approved', 'paid'].includes(st)) return;
@@ -1094,7 +1094,7 @@ function toDatetimeLocalValue(date) {
1094
1094
  window.__onekiteEventsAbort = abort;
1095
1095
 
1096
1096
  const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
1097
- const url = `/api/v3/plugins/onekite-calendar/events?${qs.toString()}`;
1097
+ const url = `/api/v3/plugins/calendar-onekite/events?${qs.toString()}`;
1098
1098
  const data = await fetchJsonCached(url, { signal: abort.signal });
1099
1099
 
1100
1100
  // Prefetch adjacent range (previous/next) for snappier navigation.
@@ -1108,8 +1108,8 @@ function toDatetimeLocalValue(date) {
1108
1108
  const toStr = (d) => new Date(d.getTime()).toISOString();
1109
1109
  const qPrev = new URLSearchParams({ start: toStr(prevStart), end: toStr(prevEnd) });
1110
1110
  const qNext = new URLSearchParams({ start: toStr(nextStart), end: toStr(nextEnd) });
1111
- fetchJsonCached(`/api/v3/plugins/onekite-calendar/events?${qPrev.toString()}`).catch(() => {});
1112
- fetchJsonCached(`/api/v3/plugins/onekite-calendar/events?${qNext.toString()}`).catch(() => {});
1111
+ fetchJsonCached(`/api/v3/plugins/calendar-onekite/events?${qPrev.toString()}`).catch(() => {});
1112
+ fetchJsonCached(`/api/v3/plugins/calendar-onekite/events?${qNext.toString()}`).catch(() => {});
1113
1113
  }
1114
1114
  } catch (e) {}
1115
1115
 
@@ -1171,11 +1171,11 @@ function toDatetimeLocalValue(date) {
1171
1171
  isDialogOpen = false;
1172
1172
  return;
1173
1173
  }
1174
- await fetchJson('/api/v3/plugins/onekite-calendar/special-events', {
1174
+ await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1175
1175
  method: 'POST',
1176
1176
  body: JSON.stringify(payload),
1177
1177
  }).catch(async () => {
1178
- return await fetchJson('/api/v3/plugins/onekite-calendar/special-events', {
1178
+ return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1179
1179
  method: 'POST',
1180
1180
  body: JSON.stringify(payload),
1181
1181
  });
@@ -1265,11 +1265,11 @@ function toDatetimeLocalValue(date) {
1265
1265
  isDialogOpen = false;
1266
1266
  return;
1267
1267
  }
1268
- await fetchJson('/api/v3/plugins/onekite-calendar/special-events', {
1268
+ await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1269
1269
  method: 'POST',
1270
1270
  body: JSON.stringify(payload),
1271
1271
  }).catch(async () => {
1272
- return await fetchJson('/api/v3/plugins/onekite-calendar/special-events', {
1272
+ return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1273
1273
  method: 'POST',
1274
1274
  body: JSON.stringify(payload),
1275
1275
  });
@@ -1296,10 +1296,10 @@ function toDatetimeLocalValue(date) {
1296
1296
  let p = p0;
1297
1297
  try {
1298
1298
  if (p0.type === 'reservation' && p0.rid) {
1299
- const details = await fetchJson(`/api/v3/plugins/onekite-calendar/reservations/${encodeURIComponent(String(p0.rid))}`);
1299
+ const details = await fetchJson(`/api/v3/plugins/calendar-onekite/reservations/${encodeURIComponent(String(p0.rid))}`);
1300
1300
  p = Object.assign({}, p0, details);
1301
1301
  } else if (p0.type === 'special' && p0.eid) {
1302
- const details = await fetchJson(`/api/v3/plugins/onekite-calendar/special-events/${encodeURIComponent(String(p0.eid))}`);
1302
+ const details = await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(String(p0.eid))}`);
1303
1303
  p = Object.assign({}, p0, details, {
1304
1304
  // keep backward compat with older field names used by templates below
1305
1305
  pickupAddress: details.address || details.pickupAddress || p0.pickupAddress,
@@ -1348,7 +1348,7 @@ function toDatetimeLocalValue(date) {
1348
1348
  callback: async () => {
1349
1349
  try {
1350
1350
  const eid = String(p.eid || ev.id).replace(/^special:/, '');
1351
- await fetchJson(`/api/v3/plugins/onekite-calendar/special-events/${encodeURIComponent(eid)}`, { method: 'DELETE' });
1351
+ await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, { method: 'DELETE' });
1352
1352
  showAlert('success', 'Évènement supprimé.');
1353
1353
  calendar.refetchEvents();
1354
1354
  } catch (e) {
@@ -1667,7 +1667,7 @@ function toDatetimeLocalValue(date) {
1667
1667
  });
1668
1668
 
1669
1669
  // Expose for live updates
1670
- try { window.onekiteCalendar = calendar; } catch (e) {}
1670
+ try { window.oneKiteCalendar = calendar; } catch (e) {}
1671
1671
 
1672
1672
  calendar.render();
1673
1673
 
@@ -1766,7 +1766,7 @@ function toDatetimeLocalValue(date) {
1766
1766
  function autoInit(data) {
1767
1767
  try {
1768
1768
  const tpl = data && data.template ? data.template.name : (ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '');
1769
- if (tpl === 'onekite-calendar') {
1769
+ if (tpl === 'calendar-onekite') {
1770
1770
  init('#onekite-calendar');
1771
1771
  }
1772
1772
  } catch (e) {}
@@ -1786,9 +1786,9 @@ function toDatetimeLocalValue(date) {
1786
1786
  try {
1787
1787
  if (!window.__oneKiteSocketBound && typeof socket !== 'undefined' && socket && typeof socket.on === 'function') {
1788
1788
  window.__oneKiteSocketBound = true;
1789
- socket.on('event:onekite-calendar.reservationUpdated', function () {
1789
+ socket.on('event:calendar-onekite.reservationUpdated', function () {
1790
1790
  try {
1791
- const cal = window.onekiteCalendar;
1791
+ const cal = window.oneKiteCalendar;
1792
1792
  scheduleRefetch(cal);
1793
1793
  } catch (e) {}
1794
1794
  });
@@ -186,7 +186,7 @@
186
186
  </div>
187
187
 
188
188
  <script>
189
- require(['admin/plugins/onekite-calendar'], function (mod) {
189
+ require(['admin/plugins/calendar-onekite'], function (mod) {
190
190
  if (mod && mod.init) {
191
191
  mod.init();
192
192
  }
@@ -21,17 +21,6 @@
21
21
  The plugin's forum script auto-initialises on the calendar page via ajaxify.
22
22
  -->
23
23
 
24
- <script>
25
- // Ultra-perf: load the forum JS only on the calendar page.
26
- require(['forum/onekite-calendar'], function (mod) {
27
- try {
28
- if (mod && typeof mod.init === 'function') {
29
- mod.init('#onekite-calendar');
30
- }
31
- } catch (e) {}
32
- });
33
- </script>
34
-
35
24
 
36
25
  <style>
37
26
  /* Make the custom "Évènement" button distinct from view buttons */