nodebb-plugin-calendar-onekite 11.1.46 → 11.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/admin.js +25 -1
- package/lib/api.js +62 -3
- package/lib/helloasso.js +15 -2
- package/package.json +1 -1
- package/public/admin.js +6 -3
- package/public/client.js +12 -4
- package/templates/emails/calendar-onekite_approved.tpl +6 -1
package/lib/admin.js
CHANGED
|
@@ -25,7 +25,8 @@ async function sendEmail(template, toEmail, subject, data) {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
if (emailer.send.length === 3) {
|
|
28
|
-
|
|
28
|
+
const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
29
|
+
await emailer.send(template, toEmail, dataWithSubject);
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
await emailer.send(template, toEmail, subject, data);
|
|
@@ -35,6 +36,26 @@ async function sendEmail(template, toEmail, subject, data) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function normalizeCallbackUrl(configured, meta) {
|
|
40
|
+
const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
|
|
41
|
+
let url = (configured || '').trim();
|
|
42
|
+
if (!url) {
|
|
43
|
+
url = base ? `${base}/helloasso` : '';
|
|
44
|
+
}
|
|
45
|
+
if (url && url.startsWith('/') && base) {
|
|
46
|
+
url = `${base}${url}`;
|
|
47
|
+
}
|
|
48
|
+
return url;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeReturnUrl(meta) {
|
|
52
|
+
const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
|
|
53
|
+
const b = String(base || '').trim().replace(/\/$/, '');
|
|
54
|
+
if (!b) return '';
|
|
55
|
+
return `${b}/calendar`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
38
59
|
const dbLayer = require('./db');
|
|
39
60
|
const helloasso = require('./helloasso');
|
|
40
61
|
|
|
@@ -89,6 +110,9 @@ admin.approveReservation = async function (req, res) {
|
|
|
89
110
|
clientId: settings.helloassoClientId,
|
|
90
111
|
clientSecret: settings.helloassoClientSecret,
|
|
91
112
|
});
|
|
113
|
+
if (!token) {
|
|
114
|
+
console.warn('[calendar-onekite] HelloAsso access token not obtained (approve ACP)', { rid: r && r.rid });
|
|
115
|
+
}
|
|
92
116
|
|
|
93
117
|
let paymentUrl = null;
|
|
94
118
|
if (token) {
|
package/lib/api.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
|
|
5
5
|
const meta = require.main.require('./src/meta');
|
|
6
|
+
const nconf = require.main.require('nconf');
|
|
6
7
|
const user = require.main.require('./src/user');
|
|
7
8
|
const groups = require.main.require('./src/groups');
|
|
8
9
|
|
|
@@ -17,6 +18,16 @@ async function sendEmail(template, toEmail, subject, data) {
|
|
|
17
18
|
try {
|
|
18
19
|
// Newer NodeBB builds expose sendToEmail
|
|
19
20
|
if (typeof emailer.sendToEmail === 'function') {
|
|
21
|
+
if (emailer.sendToEmail.length >= 4) {
|
|
22
|
+
await emailer.sendToEmail(template, toEmail, subject, data);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (emailer.sendToEmail.length === 3) {
|
|
26
|
+
const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
27
|
+
await emailer.sendToEmail(template, toEmail, dataWithSubject);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Fallback
|
|
20
31
|
await emailer.sendToEmail(template, toEmail, subject, data);
|
|
21
32
|
return;
|
|
22
33
|
}
|
|
@@ -27,8 +38,10 @@ async function sendEmail(template, toEmail, subject, data) {
|
|
|
27
38
|
return;
|
|
28
39
|
}
|
|
29
40
|
// Some builds: (template, email, data)
|
|
41
|
+
// In that case, subject is expected inside `data.subject`.
|
|
30
42
|
if (emailer.send.length === 3) {
|
|
31
|
-
|
|
43
|
+
const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
44
|
+
await emailer.send(template, toEmail, dataWithSubject);
|
|
32
45
|
return;
|
|
33
46
|
}
|
|
34
47
|
// Fallback: try 4-args anyway
|
|
@@ -41,6 +54,42 @@ async function sendEmail(template, toEmail, subject, data) {
|
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
56
|
|
|
57
|
+
function normalizeBaseUrl(meta) {
|
|
58
|
+
// Prefer meta.config.url, fallback to nconf.get('url')
|
|
59
|
+
let base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
|
|
60
|
+
if (!base) {
|
|
61
|
+
base = String(nconf.get('url') || '').trim();
|
|
62
|
+
}
|
|
63
|
+
base = String(base || '').trim().replace(/\/$/, '');
|
|
64
|
+
// Ensure absolute with scheme
|
|
65
|
+
if (base && !/^https?:\/\//i.test(base)) {
|
|
66
|
+
base = `https://${base.replace(/^\/\//, '')}`;
|
|
67
|
+
}
|
|
68
|
+
return base;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeCallbackUrl(configured, meta) {
|
|
72
|
+
const base = normalizeBaseUrl(meta);
|
|
73
|
+
let url = (configured || '').trim();
|
|
74
|
+
if (!url) {
|
|
75
|
+
url = base ? `${base}/helloasso` : '';
|
|
76
|
+
}
|
|
77
|
+
if (url && url.startsWith('/') && base) {
|
|
78
|
+
url = `${base}${url}`;
|
|
79
|
+
}
|
|
80
|
+
// Ensure scheme for absolute URLs
|
|
81
|
+
if (url && !/^https?:\/\//i.test(url)) {
|
|
82
|
+
url = `https://${url.replace(/^\/\//, '')}`;
|
|
83
|
+
}
|
|
84
|
+
return url;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeReturnUrl(meta) {
|
|
88
|
+
const base = normalizeBaseUrl(meta);
|
|
89
|
+
return base ? `${base}/calendar` : '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
44
93
|
function overlap(aStart, aEnd, bStart, bEnd) {
|
|
45
94
|
return aStart < bEnd && bStart < aEnd;
|
|
46
95
|
}
|
|
@@ -168,6 +217,9 @@ api.getItems = async function (req, res) {
|
|
|
168
217
|
clientId: settings.helloassoClientId,
|
|
169
218
|
clientSecret: settings.helloassoClientSecret,
|
|
170
219
|
});
|
|
220
|
+
if (!token) {
|
|
221
|
+
console.warn('[calendar-onekite] HelloAsso access token not obtained (approve API)', { rid });
|
|
222
|
+
}
|
|
171
223
|
|
|
172
224
|
if (!token) {
|
|
173
225
|
return res.json([]);
|
|
@@ -321,11 +373,18 @@ api.approveReservation = async function (req, res) {
|
|
|
321
373
|
formType: settings2.helloassoFormType,
|
|
322
374
|
formSlug: settings2.helloassoFormSlug,
|
|
323
375
|
// r.total is stored as an estimated total in euros; HelloAsso expects cents.
|
|
324
|
-
totalAmount:
|
|
376
|
+
totalAmount: (() => {
|
|
377
|
+
const cents = Math.max(0, Math.round((Number(r.total) || 0) * 100));
|
|
378
|
+
if (!cents) {
|
|
379
|
+
console.warn('[calendar-onekite] HelloAsso totalAmount is 0 (approve API)', { rid, total: r.total });
|
|
380
|
+
}
|
|
381
|
+
return cents;
|
|
382
|
+
})(),
|
|
325
383
|
payerEmail: payer && payer.email ? payer.email : '',
|
|
326
384
|
// By default, point to the forum base url so the webhook hits this NodeBB instance.
|
|
327
385
|
// Can be overridden via ACP setting `helloassoCallbackUrl`.
|
|
328
|
-
callbackUrl: (
|
|
386
|
+
callbackUrl: normalizeReturnUrl(meta),
|
|
387
|
+
webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
|
|
329
388
|
itemName: 'Réservation matériel OneKite',
|
|
330
389
|
containsDonation: false,
|
|
331
390
|
metadata: { reservationId: String(rid) },
|
package/lib/helloasso.js
CHANGED
|
@@ -174,8 +174,15 @@ async function listCatalogItems({ env, token, organizationSlug, formType, formSl
|
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail, callbackUrl, itemName, containsDonation, metadata }) {
|
|
177
|
+
async function createCheckoutIntent({ env, token, organizationSlug, formType, formSlug, totalAmount, payerEmail, callbackUrl, webhookUrl, itemName, containsDonation, metadata }) {
|
|
178
178
|
if (!token || !organizationSlug) return null;
|
|
179
|
+
if (!callbackUrl || !/^https?:\/\//i.test(String(callbackUrl))) {
|
|
180
|
+
console.warn('[calendar-onekite] HelloAsso invalid return/back/error URL', { callbackUrl });
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (webhookUrl && !/^https?:\/\//i.test(String(webhookUrl))) {
|
|
184
|
+
console.warn('[calendar-onekite] HelloAsso invalid webhook URL', { webhookUrl });
|
|
185
|
+
}
|
|
179
186
|
// Checkout intents are created at organization level.
|
|
180
187
|
const url = `${baseUrl(env)}/v5/organizations/${encodeURIComponent(organizationSlug)}/checkout-intents`;
|
|
181
188
|
const payload = {
|
|
@@ -188,15 +195,21 @@ async function createCheckoutIntent({ env, token, organizationSlug, formType, fo
|
|
|
188
195
|
backUrl: callbackUrl || '',
|
|
189
196
|
errorUrl: callbackUrl || '',
|
|
190
197
|
returnUrl: callbackUrl || '',
|
|
191
|
-
notificationUrl: callbackUrl || '',
|
|
198
|
+
notificationUrl: webhookUrl || callbackUrl || '',
|
|
192
199
|
};
|
|
193
200
|
const { status, json } = await requestJson('POST', url, { Authorization: `Bearer ${token}` }, payload);
|
|
194
201
|
if (status >= 200 && status < 300 && json) {
|
|
195
202
|
return json.redirectUrl || json.checkoutUrl || json.url || null;
|
|
196
203
|
}
|
|
204
|
+
// Log the error payload to help diagnose configuration issues (slug, env, urls, amount, etc.)
|
|
205
|
+
try {
|
|
206
|
+
// eslint-disable-next-line no-console
|
|
207
|
+
console.warn('[calendar-onekite] HelloAsso checkout-intent failed', { status, json });
|
|
208
|
+
} catch (e) { /* ignore */ }
|
|
197
209
|
return null;
|
|
198
210
|
}
|
|
199
211
|
|
|
212
|
+
|
|
200
213
|
module.exports = {
|
|
201
214
|
getAccessToken,
|
|
202
215
|
listItems,
|
package/package.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -77,7 +77,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
77
77
|
for (const r of list) {
|
|
78
78
|
const created = r.createdAt ? fmtFR(r.createdAt) : '';
|
|
79
79
|
const itemNames = Array.isArray(r.itemNames) && r.itemNames.length ? r.itemNames : [r.itemName || r.itemId].filter(Boolean);
|
|
80
|
-
const itemsHtml = itemNames.map(n => `<
|
|
80
|
+
const itemsHtml = `<ul style="margin: 0 0 10px 18px;">${itemNames.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul>`;
|
|
81
81
|
const div = document.createElement('div');
|
|
82
82
|
div.className = 'list-group-item';
|
|
83
83
|
div.innerHTML = `
|
|
@@ -107,9 +107,12 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
approveBtn.addEventListener('click', async () => {
|
|
110
|
+
const itemNamesModal = Array.isArray(r.itemNames) && r.itemNames.length ? r.itemNames : [r.itemName || r.itemId].filter(Boolean);
|
|
111
|
+
const itemsHtmlModal = `<ul style="margin: 0 0 10px 18px;">${itemNamesModal.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul>`;
|
|
112
|
+
|
|
110
113
|
const opts = (() => {
|
|
111
114
|
const out = [];
|
|
112
|
-
for (let h =
|
|
115
|
+
for (let h = 7; h < 24; h++) {
|
|
113
116
|
for (let m = 0; m < 60; m += 5) {
|
|
114
117
|
out.push(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
|
|
115
118
|
}
|
|
@@ -162,7 +165,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
162
165
|
function timeOptions(stepMinutes) {
|
|
163
166
|
const step = stepMinutes || 5;
|
|
164
167
|
const out = [];
|
|
165
|
-
for (let h =
|
|
168
|
+
for (let h = 7; h < 24; h++) {
|
|
166
169
|
for (let m = 0; m < 60; m += step) {
|
|
167
170
|
const hh = String(h).padStart(2, '0');
|
|
168
171
|
const mm = String(m).padStart(2, '0');
|
package/public/client.js
CHANGED
|
@@ -286,14 +286,20 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
286
286
|
const p = ev.extendedProps || {};
|
|
287
287
|
const rid = p.rid || ev.id;
|
|
288
288
|
const status = p.status || '';
|
|
289
|
-
const
|
|
289
|
+
const itemsHtml = (() => {
|
|
290
|
+
const names = Array.isArray(p.itemNames) ? p.itemNames : (p.itemName ? [p.itemName] : []);
|
|
291
|
+
if (names.length) {
|
|
292
|
+
return `<ul style="margin:0 0 0 1.1rem; padding:0;">${names.map(n => `<li>${String(n).replace(/</g,'<').replace(/>/g,'>')}</li>`).join('')}</ul>`;
|
|
293
|
+
}
|
|
294
|
+
return String(ev.title || '').replace(/</g,'<').replace(/>/g,'>');
|
|
295
|
+
})();
|
|
290
296
|
const period = `${formatDt(ev.start)} → ${formatDt(ev.end)}`;
|
|
291
297
|
|
|
292
298
|
const canModerate = !!p.canModerate;
|
|
293
299
|
const isPending = status === 'pending';
|
|
294
300
|
|
|
295
301
|
const baseHtml = `
|
|
296
|
-
<div class="mb-2"><strong>Matériel</strong><br>${
|
|
302
|
+
<div class="mb-2"><strong>Matériel</strong><br>${itemsHtml}</div>
|
|
297
303
|
<div class="mb-2"><strong>Période</strong><br>${period}</div>
|
|
298
304
|
<div class="text-muted" style="font-size: 12px;">Statut: ${statusLabel(status)}</div>
|
|
299
305
|
`;
|
|
@@ -339,9 +345,11 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
339
345
|
label: 'Valider',
|
|
340
346
|
className: 'btn-success',
|
|
341
347
|
callback: async () => {
|
|
348
|
+
const itemNames = Array.isArray(p.itemNames) && p.itemNames.length ? p.itemNames : String(itemsLabel || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
349
|
+
const itemsHtml = itemNames.map(n => `<li>${String(n).replace(/</g, '<').replace(/>/g, '>')}</li>`).join('');
|
|
342
350
|
const opts = (() => {
|
|
343
351
|
const out = [];
|
|
344
|
-
for (let h =
|
|
352
|
+
for (let h = 7; h < 24; h++) {
|
|
345
353
|
for (let m = 0; m < 60; m += 5) {
|
|
346
354
|
out.push(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
|
|
347
355
|
}
|
|
@@ -416,4 +424,4 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
416
424
|
return {
|
|
417
425
|
init,
|
|
418
426
|
};
|
|
419
|
-
});
|
|
427
|
+
});
|
|
@@ -19,5 +19,10 @@
|
|
|
19
19
|
<!-- ENDIF pickupTime -->
|
|
20
20
|
|
|
21
21
|
<!-- IF paymentUrl -->
|
|
22
|
-
<p
|
|
22
|
+
<p>
|
|
23
|
+
<a href="{paymentUrl}" style="display:inline-block;padding:12px 18px;border-radius:6px;text-decoration:none;border:1px solid #333;">
|
|
24
|
+
Payer maintenant
|
|
25
|
+
</a>
|
|
26
|
+
</p>
|
|
27
|
+
<p style="font-size: 12px; color: #666;">Si le bouton ne fonctionne pas, utilisez ce lien : <a href="{paymentUrl}">{paymentUrl}</a></p>
|
|
23
28
|
<!-- ENDIF paymentUrl -->
|