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 =
|
|
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 =
|
|
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 =
|
|
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
package/plugin.json
CHANGED
|
@@ -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"
|
|
62
|
-
<option value="helloasso"
|
|
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>
|