nodebb-plugin-equipment-calendar 0.7.1 → 0.8.1

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/library.js CHANGED
@@ -60,20 +60,23 @@ function parseLocationMap(locationMapJson) {
60
60
 
61
61
  let haTokenCache = null; // { accessToken, refreshToken, expMs }
62
62
 
63
- async function getHelloAssoAccessToken(settings) {
63
+ async function getHelloAssoAccessToken(settings, opts = {}) {
64
64
  const now = Date.now();
65
- if (haTokenCache && haTokenCache.accessToken && haTokenCache.expMs && now < haTokenCache.expMs - 30_000) {
65
+ if (!opts.force && haTokenCache && haTokenCache.accessToken && haTokenCache.expMs && now < haTokenCache.expMs - 30_000) {
66
66
  return haTokenCache.accessToken;
67
67
  }
68
68
 
69
69
  const tokenKey = 'equipmentCalendar:ha:token';
70
+ if (opts.clearStored) {
71
+ try { await db.delete(tokenKey); } catch (e) {}
72
+ }
70
73
  let stored = null;
71
74
  try {
72
75
  stored = await db.getObject(tokenKey);
73
76
  } catch (e) {}
74
77
 
75
78
  // If refresh token exists and not expired locally, try refresh flow first
76
- const canRefresh = stored && stored.refresh_token;
79
+ const canRefresh = !opts.force && stored && stored.refresh_token;
77
80
  const useRefresh = canRefresh && stored.refresh_expires_at && now < parseInt(stored.refresh_expires_at, 10);
78
81
 
79
82
  const formBody = new URLSearchParams();
@@ -158,7 +161,11 @@ async function createHelloAssoCheckoutIntent(settings, bookingId, reservations)
158
161
  },
159
162
  };
160
163
 
161
- const token = await getHelloAssoAccessToken(settings);
164
+ const token = const force = String(req.query.force || '') === '1';
165
+ const clear = String(req.query.clear || '') === '1';
166
+ // force=1 skips in-memory cache and refresh_token; clear=1 wipes stored refresh token
167
+ haTokenCache = null;
168
+ await getHelloAssoAccessToken(settings, { force, clearStored: clear });
162
169
  const url = `https://api.helloasso.com/v5/organizations/${encodeURIComponent(org)}/checkout-intents`;
163
170
  const resp = await fetchFn(url, {
164
171
  method: 'POST',
@@ -179,7 +186,11 @@ async function createHelloAssoCheckoutIntent(settings, bookingId, reservations)
179
186
 
180
187
  async function fetchHelloAssoCheckoutIntent(settings, checkoutIntentId) {
181
188
  const org = String(settings.ha_organizationSlug || '').trim();
182
- const token = await getHelloAssoAccessToken(settings);
189
+ const token = const force = String(req.query.force || '') === '1';
190
+ const clear = String(req.query.clear || '') === '1';
191
+ // force=1 skips in-memory cache and refresh_token; clear=1 wipes stored refresh token
192
+ haTokenCache = null;
193
+ await getHelloAssoAccessToken(settings, { force, clearStored: clear });
183
194
  const url = `https://api.helloasso.com/v5/organizations/${encodeURIComponent(org)}/checkout-intents/${encodeURIComponent(checkoutIntentId)}`;
184
195
  const resp = await fetchFn(url, { headers: { authorization: `Bearer ${token}`, accept: 'application/json' } });
185
196
  if (!resp.ok) {
@@ -219,7 +230,11 @@ async function fetchHelloAssoItems(settings) {
219
230
  } catch (e) {}
220
231
  }
221
232
 
222
- const token = await getHelloAssoAccessToken(settings);
233
+ const token = const force = String(req.query.force || '') === '1';
234
+ const clear = String(req.query.clear || '') === '1';
235
+ // force=1 skips in-memory cache and refresh_token; clear=1 wipes stored refresh token
236
+ haTokenCache = null;
237
+ await getHelloAssoAccessToken(settings, { force, clearStored: clear });
223
238
  const url = `https://api.helloasso.com/v5/organizations/${encodeURIComponent(org)}/forms/${encodeURIComponent(formType)}/${encodeURIComponent(formSlug)}/items`;
224
239
  const resp = await fetchFn(url, { headers: { authorization: `Bearer ${token}` } });
225
240
  if (!resp.ok) {
@@ -564,6 +579,8 @@ plugin.init = async function (params) {
564
579
  if (mid && mid.admin) {
565
580
  router.get('/admin/plugins/equipment-calendar', middleware.applyCSRF, mid.admin.buildHeader, renderAdminPage);
566
581
  router.get('/admin/plugins/equipment-calendar/reservations', middleware.applyCSRF, mid.admin.buildHeader, renderAdminReservationsPage);
582
+ router.get('/admin/plugins/equipment-calendar/helloasso-test', middleware.applyCSRF, mid.admin.buildHeader, handleHelloAssoTest);
583
+ router.get('/api/admin/plugins/equipment-calendar/helloasso-test', middleware.applyCSRF, handleHelloAssoTest);
567
584
  router.get('/api/admin/plugins/equipment-calendar/reservations', middleware.applyCSRF, renderAdminReservationsPage);
568
585
  router.get('/api/admin/plugins/equipment-calendar', middleware.applyCSRF, renderAdminPage);
569
586
  router.post('/admin/plugins/equipment-calendar/save', middleware.applyCSRF, handleAdminSave);
@@ -652,6 +669,8 @@ plugin.addAdminRoutes = async function (params) {
652
669
  const { router, middleware: mid } = params;
653
670
  router.get('/admin/plugins/equipment-calendar', middleware.applyCSRF, mid.admin.buildHeader, renderAdminPage);
654
671
  router.get('/admin/plugins/equipment-calendar/reservations', middleware.applyCSRF, mid.admin.buildHeader, renderAdminReservationsPage);
672
+ router.get('/admin/plugins/equipment-calendar/helloasso-test', middleware.applyCSRF, mid.admin.buildHeader, handleHelloAssoTest);
673
+ router.get('/api/admin/plugins/equipment-calendar/helloasso-test', middleware.applyCSRF, handleHelloAssoTest);
655
674
  router.get('/api/admin/plugins/equipment-calendar/reservations', middleware.applyCSRF, renderAdminReservationsPage);
656
675
  router.get('/api/admin/plugins/equipment-calendar', middleware.applyCSRF, renderAdminPage);
657
676
  };
@@ -781,13 +800,53 @@ async function handleAdminDelete(req, res) {
781
800
  return res.redirect('/admin/plugins/equipment-calendar/reservations?updated=1');
782
801
  }
783
802
 
803
+
804
+ async function handleHelloAssoTest(req, res) {
805
+ const isAdmin = req.uid ? await groups.isMember(req.uid, 'administrators') : false;
806
+ if (!isAdmin) return helpers.notAllowed(req, res);
807
+
808
+ const settings = await getSettings();
809
+ let ok = false;
810
+ let message = '';
811
+ let count = 0;
812
+
813
+ try {
814
+ const force = String(req.query.force || '') === '1';
815
+ const clear = String(req.query.clear || '') === '1';
816
+ // force=1 skips in-memory cache and refresh_token; clear=1 wipes stored refresh token
817
+ haTokenCache = null;
818
+ await getHelloAssoAccessToken(settings, { force, clearStored: clear });
819
+ const items = await fetchHelloAssoItems(settings);
820
+ const list = Array.isArray(items) ? items : (Array.isArray(items.data) ? items.data : []);
821
+ count = list.length;
822
+ const sampleItems = list.slice(0, 10).map(it => ({
823
+ id: String(it.id || it.itemId || it.reference || it.slug || it.name || '').trim(),
824
+ name: String(it.name || it.label || it.title || '').trim(),
825
+ rawName: String(it.name || it.label || it.title || it.id || '').trim(),
826
+ }));
827
+ ok = true;
828
+ message = `OK: token valide. Items récupérés: ${count}.`;
829
+ } catch (e) {
830
+ ok = false;
831
+ message = (e && e.message) ? e.message : String(e);
832
+ }
833
+
834
+ res.render('admin/plugins/equipment-calendar-helloasso-test', {
835
+ title: 'Equipment Calendar - Test HelloAsso',
836
+ ok,
837
+ message,
838
+ count,
839
+ settings,
840
+ sampleItems,
841
+ hasSampleItems: sampleItems && sampleItems.length > 0,
842
+ });
843
+ }
844
+
784
845
  async function renderAdminPage(req, res) {
785
846
  const settings = await getSettings();
786
847
  res.render('admin/plugins/equipment-calendar', {
787
848
  title: 'Equipment Calendar',
788
849
  settings,
789
- view_itemsSourceManual: String(settings.itemsSource || 'manual') !== 'helloasso',
790
- view_itemsSourceHelloasso: String(settings.itemsSource || 'manual') === 'helloasso',
791
850
  saved: req.query && String(req.query.saved || '') === '1',
792
851
  purged: req.query && parseInt(req.query.purged, 10) || 0,
793
852
  view_dayGridMonth: (settings.defaultView || 'dayGridMonth') === 'dayGridMonth',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Equipment reservation calendar for NodeBB (FullCalendar, approvals, HelloAsso payments)",
5
5
  "main": "library.js",
6
6
  "scripts": {
package/plugin.json CHANGED
@@ -26,6 +26,6 @@
26
26
  "scripts": [
27
27
  "public/js/client.js"
28
28
  ],
29
- "version": "0.4.2",
29
+ "version": "0.4.7",
30
30
  "minver": "4.7.1"
31
31
  }
@@ -0,0 +1,54 @@
1
+ <div class="acp-page-container">
2
+ <div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
3
+ <h1 class="mb-0">Equipment Calendar</h1>
4
+ <div class="btn-group">
5
+ <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar">Paramètres</a>
6
+ <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations">Réservations</a>
7
+ <a class="btn btn-secondary active" href="/admin/plugins/equipment-calendar/helloasso-test">Test HelloAsso</a>
8
+ </div>
9
+ </div>
10
+
11
+ <div class="card card-body mt-3">
12
+ <div class="d-flex flex-wrap gap-2 mb-2">
13
+ <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test">Test (cache OK)</a>
14
+ <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test?force=1">Test (forcer nouveau token)</a>
15
+ <a class="btn btn-outline-danger" href="/admin/plugins/equipment-calendar/helloasso-test?force=1&clear=1" onclick="return confirm('Supprimer le token stocké et retester ?');">Vider token + retester</a>
16
+ </div>
17
+ {{{ if ok }}}
18
+ <div class="alert alert-success">{message}</div>
19
+ {{{ else }}}
20
+ <div class="alert alert-danger">Échec : {message}</div>
21
+ {{{ end }}}
22
+
23
+ <div class="small text-muted">
24
+ Form: <code>{settings.ha_itemsFormType}</code> / <code>{settings.ha_itemsFormSlug}</code> — Orga: <code>{settings.ha_organizationSlug}</code>
25
+ </div>
26
+ {{{ if hasSampleItems }}}
27
+ <div class="card card-body mt-3">
28
+ <div class="d-flex flex-wrap gap-2 mb-2">
29
+ <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test">Test (cache OK)</a>
30
+ <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test?force=1">Test (forcer nouveau token)</a>
31
+ <a class="btn btn-outline-danger" href="/admin/plugins/equipment-calendar/helloasso-test?force=1&clear=1" onclick="return confirm('Supprimer le token stocké et retester ?');">Vider token + retester</a>
32
+ </div>
33
+ <h5 class="mb-2">Aperçu (10 premiers articles)</h5>
34
+ <div class="table-responsive">
35
+ <table class="table table-striped align-middle">
36
+ <thead>
37
+ <tr>
38
+ <th>ID</th>
39
+ <th>Nom</th>
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ {{{ each sampleItems }}}
44
+ <tr>
45
+ <td><code>{sampleItems.id}</code></td>
46
+ <td>{sampleItems.rawName}</td>
47
+ </tr>
48
+ {{{ end }}}
49
+ </tbody>
50
+ </table>
51
+ </div>
52
+ </div>
53
+ {{{ end }}}
54
+ </div>
@@ -4,6 +4,7 @@
4
4
  <div class="btn-group">
5
5
  <a class="btn btn-secondary active" href="/admin/plugins/equipment-calendar">Paramètres</a>
6
6
  <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations">Réservations</a>
7
+ <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/helloasso-test">Test HelloAsso</a>
7
8
  </div>
8
9
  </div>
9
10
 
@@ -58,8 +59,8 @@
58
59
  <div class="mb-3">
59
60
  <label class="form-label">Source</label>
60
61
  <select class="form-select" name="itemsSource">
61
- <option value="manual" {{{ if view_itemsSourceManual }}}selected{{{ end }}}>Manuel (JSON)</option>
62
- <option value="helloasso" {{{ if view_itemsSourceHelloasso }}}selected{{{ end }}}>HelloAsso (articles d’un formulaire)</option>
62
+ <option value="manual">Manuel (JSON)</option>
63
+ <option value="helloasso">HelloAsso (articles d’un formulaire)</option>
63
64
  </select>
64
65
  <div class="form-text">Si HelloAsso est choisi, la liste du matériel est récupérée via l’API HelloAsso (items d’un formulaire).</div>
65
66
  </div>