nodebb-plugin-onekite-calendar 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -27
- package/lib/admin.js +155 -15
- package/lib/api.js +103 -158
- package/lib/controllers.js +2 -2
- package/lib/db.js +5 -5
- package/lib/discord.js +5 -6
- package/lib/helloasso.js +3 -4
- package/lib/helloassoWebhook.js +7 -8
- package/lib/scheduler.js +11 -8
- package/lib/widgets.js +8 -128
- package/library.js +63 -39
- package/package.json +1 -1
- package/plugin.json +11 -5
- package/public/admin.js +91 -19
- package/public/client.js +23 -23
- package/templates/admin/plugins/{onekite-calendar.tpl → calendar-onekite.tpl} +23 -4
- package/templates/{onekite-calendar.tpl → calendar-onekite.tpl} +0 -11
- package/lib/constants.js +0 -7
- package/lib/email.js +0 -26
- package/lib/log.js +0 -29
- /package/templates/emails/{onekite-calendar_approved.tpl → calendar-onekite_approved.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_expired.tpl → calendar-onekite_expired.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_paid.tpl → calendar-onekite_paid.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_pending.tpl → calendar-onekite_pending.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_refused.tpl → calendar-onekite_refused.tpl} +0 -0
- /package/templates/emails/{onekite-calendar_reminder.tpl → calendar-onekite_reminder.tpl} +0 -0
package/lib/scheduler.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const meta = require.main.require('./src/meta');
|
|
4
4
|
const db = require.main.require('./src/database');
|
|
5
5
|
const dbLayer = require('./db');
|
|
6
|
-
const log = require('./log');
|
|
7
6
|
|
|
8
7
|
let timer = null;
|
|
9
8
|
|
|
@@ -16,7 +15,7 @@ function getSetting(settings, key, fallback) {
|
|
|
16
15
|
|
|
17
16
|
// Pending holds: short lock after a user creates a request (defaults to 5 minutes)
|
|
18
17
|
async function expirePending() {
|
|
19
|
-
const settings = await meta.settings.get('onekite
|
|
18
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
20
19
|
const holdMins = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
|
|
21
20
|
const now = Date.now();
|
|
22
21
|
|
|
@@ -44,7 +43,7 @@ async function expirePending() {
|
|
|
44
43
|
// - We send a reminder after `paymentHoldMinutes` (default 60)
|
|
45
44
|
// - We expire (and remove) after `2 * paymentHoldMinutes`
|
|
46
45
|
async function processAwaitingPayment() {
|
|
47
|
-
const settings = await meta.settings.get('onekite
|
|
46
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
48
47
|
const holdMins = parseInt(
|
|
49
48
|
getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')),
|
|
50
49
|
10
|
|
@@ -84,7 +83,11 @@ async function processAwaitingPayment() {
|
|
|
84
83
|
}
|
|
85
84
|
} catch (err) {
|
|
86
85
|
// eslint-disable-next-line no-console
|
|
87
|
-
|
|
86
|
+
console.warn('[calendar-onekite] Failed to send email (scheduler)', {
|
|
87
|
+
template,
|
|
88
|
+
toEmail,
|
|
89
|
+
err: String((err && err.message) || err),
|
|
90
|
+
});
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -108,7 +111,7 @@ async function processAwaitingPayment() {
|
|
|
108
111
|
|
|
109
112
|
if (!r.reminderSent && now >= reminderAt && now < expireAt) {
|
|
110
113
|
// Send reminder once (guarded across clustered NodeBB processes)
|
|
111
|
-
const reminderKey = 'onekite
|
|
114
|
+
const reminderKey = 'calendar-onekite:email:reminderSent';
|
|
112
115
|
const first = await db.setAdd(reminderKey, rid);
|
|
113
116
|
if (!first) {
|
|
114
117
|
// another process already sent it
|
|
@@ -119,7 +122,7 @@ async function processAwaitingPayment() {
|
|
|
119
122
|
}
|
|
120
123
|
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
121
124
|
if (u && u.email) {
|
|
122
|
-
await sendEmail('
|
|
125
|
+
await sendEmail('calendar-onekite_reminder', u.email, 'Location matériel - Rappel', {
|
|
123
126
|
username: u.username,
|
|
124
127
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
125
128
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
@@ -138,12 +141,12 @@ async function processAwaitingPayment() {
|
|
|
138
141
|
if (now >= expireAt) {
|
|
139
142
|
// Expire: remove reservation so it disappears from calendar and frees items
|
|
140
143
|
// Guard email send across clustered NodeBB processes
|
|
141
|
-
const expiredKey = 'onekite
|
|
144
|
+
const expiredKey = 'calendar-onekite:email:expiredSent';
|
|
142
145
|
const firstExpired = await db.setAdd(expiredKey, rid);
|
|
143
146
|
const shouldEmail = !!firstExpired;
|
|
144
147
|
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
145
148
|
if (shouldEmail && u && u.email) {
|
|
146
|
-
await sendEmail('
|
|
149
|
+
await sendEmail('calendar-onekite_expired', u.email, 'Location matériel - Rappel', {
|
|
147
150
|
username: u.username,
|
|
148
151
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
149
152
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
package/lib/widgets.js
CHANGED
|
@@ -40,8 +40,8 @@ widgets.defineWidgets = async function (widgetData) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
list.push({
|
|
43
|
-
widget: 'onekite-
|
|
44
|
-
name: 'Calendrier
|
|
43
|
+
widget: 'calendar-onekite-twoweeks',
|
|
44
|
+
name: 'Calendrier OneKite (2 semaines)',
|
|
45
45
|
description: 'Affiche la semaine courante + la semaine suivante (FullCalendar via CDN).',
|
|
46
46
|
content: '',
|
|
47
47
|
});
|
|
@@ -54,7 +54,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
54
54
|
const id = makeDomId();
|
|
55
55
|
const calUrl = widgetCalendarUrl();
|
|
56
56
|
const apiBase = forumBaseUrl();
|
|
57
|
-
const eventsEndpoint = `${apiBase}/api/v3/plugins/onekite
|
|
57
|
+
const eventsEndpoint = `${apiBase}/api/v3/plugins/calendar-onekite/events`;
|
|
58
58
|
|
|
59
59
|
const idJson = JSON.stringify(id);
|
|
60
60
|
const calUrlJson = JSON.stringify(calUrl);
|
|
@@ -63,7 +63,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
63
63
|
const html = `
|
|
64
64
|
<div class="onekite-twoweeks">
|
|
65
65
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
66
|
-
<div style="font-weight: 600;">Calendrier</div>
|
|
66
|
+
<div style="font-weight: 600;">Calendrier (2 semaines)</div>
|
|
67
67
|
<a href="${escapeHtml(calUrl)}" class="btn btn-sm btn-outline-secondary" style="line-height: 1.1;">Ouvrir</a>
|
|
68
68
|
</div>
|
|
69
69
|
<div id="${escapeHtml(id)}"></div>
|
|
@@ -137,130 +137,21 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
137
137
|
},
|
|
138
138
|
navLinks: false,
|
|
139
139
|
eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
|
|
140
|
-
eventContent: function(arg) {
|
|
141
|
-
// Render a simple dot instead of text.
|
|
142
|
-
const wrap = document.createElement('span');
|
|
143
|
-
wrap.className = 'onekite-dot';
|
|
144
|
-
wrap.setAttribute('aria-label', arg.event.title || '');
|
|
145
|
-
// Mark the node so eventDidMount can find it.
|
|
146
|
-
wrap.setAttribute('data-onekite-dot', '1');
|
|
147
|
-
return { domNodes: [wrap] };
|
|
148
|
-
},
|
|
149
140
|
events: function(info, successCallback, failureCallback) {
|
|
150
141
|
const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
|
|
151
|
-
|
|
152
|
-
fetch(url1, { credentials: 'same-origin' })
|
|
142
|
+
fetch(eventsEndpoint + '?' + qs.toString(), { credentials: 'same-origin' })
|
|
153
143
|
.then((r) => r.json())
|
|
154
144
|
.then((json) => successCallback(json || []))
|
|
155
145
|
.catch((e) => failureCallback(e));
|
|
156
146
|
},
|
|
157
|
-
eventDidMount: function(info) {
|
|
158
|
-
// Native tooltip + click popup (Bootbox when available).
|
|
159
|
-
const title = info.event && info.event.title ? String(info.event.title) : '';
|
|
160
|
-
const start = info.event && info.event.start ? info.event.start : null;
|
|
161
|
-
const end = info.event && info.event.end ? info.event.end : null;
|
|
162
|
-
const fmt = (d) => {
|
|
163
|
-
try {
|
|
164
|
-
return new Intl.DateTimeFormat('fr-FR', { dateStyle: 'medium' }).format(d);
|
|
165
|
-
} catch (e) {
|
|
166
|
-
return d ? d.toISOString().slice(0, 10) : '';
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
const period = start ? (end ? (fmt(start) + ' → ' + fmt(end)) : fmt(start)) : '';
|
|
170
|
-
const body = [title, period].filter(Boolean).join('\n');
|
|
171
|
-
if (info.el) {
|
|
172
|
-
// Make dot color match the FullCalendar event colors.
|
|
173
|
-
try {
|
|
174
|
-
const dot = info.el.querySelector && info.el.querySelector('[data-onekite-dot="1"]');
|
|
175
|
-
if (dot) {
|
|
176
|
-
// Prefer explicit event colors, otherwise fall back to computed styles.
|
|
177
|
-
const bg = (info.event && (info.event.backgroundColor || info.event.borderColor)) || '';
|
|
178
|
-
if (bg) {
|
|
179
|
-
dot.style.backgroundColor = bg;
|
|
180
|
-
} else {
|
|
181
|
-
const cs = window.getComputedStyle(info.el);
|
|
182
|
-
const bgs = cs && cs.backgroundColor;
|
|
183
|
-
if (bgs && bgs !== 'rgba(0, 0, 0, 0)' && bgs !== 'transparent') {
|
|
184
|
-
dot.style.backgroundColor = bgs;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
} catch (e) {
|
|
189
|
-
// ignore
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
info.el.title = body;
|
|
193
|
-
info.el.style.cursor = 'pointer';
|
|
194
|
-
info.el.addEventListener('click', function(ev) {
|
|
195
|
-
ev.preventDefault();
|
|
196
|
-
ev.stopPropagation();
|
|
197
|
-
const html = '<div style="white-space:pre-line;">' + (body || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') + '</div>';
|
|
198
|
-
if (window.bootbox && typeof window.bootbox.alert === 'function') {
|
|
199
|
-
window.bootbox.alert({ message: html });
|
|
200
|
-
} else {
|
|
201
|
-
// Fallback
|
|
202
|
-
alert(body);
|
|
203
|
-
}
|
|
204
|
-
}, { passive: false });
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
// On mobile, users can tap the calendar background to open the full page.
|
|
208
147
|
dateClick: function() {
|
|
209
148
|
window.location.href = calUrl;
|
|
210
149
|
},
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
try { if (info && info.jsEvent) info.jsEvent.preventDefault(); } catch (e) {}
|
|
150
|
+
eventClick: function() {
|
|
151
|
+
window.location.href = calUrl;
|
|
214
152
|
},
|
|
215
153
|
});
|
|
216
154
|
|
|
217
|
-
// Mobile: swipe left/right to change week (2-week view).
|
|
218
|
-
(function enableSwipe() {
|
|
219
|
-
try {
|
|
220
|
-
if (!('ontouchstart' in window)) return;
|
|
221
|
-
const target = el; // calendar root
|
|
222
|
-
let sx = 0;
|
|
223
|
-
let sy = 0;
|
|
224
|
-
let st = 0;
|
|
225
|
-
let tracking = false;
|
|
226
|
-
|
|
227
|
-
target.addEventListener('touchstart', function (e) {
|
|
228
|
-
if (!e.touches || e.touches.length !== 1) return;
|
|
229
|
-
const t = e.touches[0];
|
|
230
|
-
sx = t.clientX;
|
|
231
|
-
sy = t.clientY;
|
|
232
|
-
st = Date.now();
|
|
233
|
-
tracking = true;
|
|
234
|
-
}, { passive: true });
|
|
235
|
-
|
|
236
|
-
target.addEventListener('touchend', function (e) {
|
|
237
|
-
if (!tracking) return;
|
|
238
|
-
tracking = false;
|
|
239
|
-
const changed = e.changedTouches && e.changedTouches[0];
|
|
240
|
-
if (!changed) return;
|
|
241
|
-
const dx = changed.clientX - sx;
|
|
242
|
-
const dy = changed.clientY - sy;
|
|
243
|
-
const adx = Math.abs(dx);
|
|
244
|
-
const ady = Math.abs(dy);
|
|
245
|
-
const dt = Date.now() - st;
|
|
246
|
-
|
|
247
|
-
// Must be a quick-ish horizontal swipe.
|
|
248
|
-
if (dt > 800) return;
|
|
249
|
-
if (adx < 50) return;
|
|
250
|
-
if (ady > adx * 0.75) return;
|
|
251
|
-
|
|
252
|
-
// Move by one week per swipe, even though the view spans 2 weeks.
|
|
253
|
-
if (dx < 0) {
|
|
254
|
-
calendar.incrementDate({ weeks: 1 });
|
|
255
|
-
} else {
|
|
256
|
-
calendar.incrementDate({ weeks: -1 });
|
|
257
|
-
}
|
|
258
|
-
}, { passive: true });
|
|
259
|
-
} catch (e) {
|
|
260
|
-
// ignore
|
|
261
|
-
}
|
|
262
|
-
})();
|
|
263
|
-
|
|
264
155
|
calendar.render();
|
|
265
156
|
}
|
|
266
157
|
|
|
@@ -274,18 +165,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
274
165
|
.onekite-twoweeks .fc .fc-button { padding: .2rem .35rem; font-size: .75rem; }
|
|
275
166
|
.onekite-twoweeks .fc .fc-daygrid-day-number { font-size: .75rem; padding: 2px; }
|
|
276
167
|
.onekite-twoweeks .fc .fc-col-header-cell-cushion { font-size: .75rem; }
|
|
277
|
-
.onekite-twoweeks .fc .fc-event-title {
|
|
278
|
-
.onekite-twoweeks .fc .fc-event-time { display: none; }
|
|
279
|
-
.onekite-twoweeks .onekite-dot {
|
|
280
|
-
display: inline-block;
|
|
281
|
-
width: 8px;
|
|
282
|
-
height: 8px;
|
|
283
|
-
border-radius: 50%;
|
|
284
|
-
background: var(--fc-event-bg-color, currentColor);
|
|
285
|
-
border: 1px solid var(--fc-event-border-color, transparent);
|
|
286
|
-
opacity: 0.85;
|
|
287
|
-
margin: 0 2px;
|
|
288
|
-
}
|
|
168
|
+
.onekite-twoweeks .fc .fc-event-title { font-size: .72rem; }
|
|
289
169
|
</style>
|
|
290
170
|
`;
|
|
291
171
|
|
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,6 @@ 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');
|
|
12
16
|
const bodyParser = require('body-parser');
|
|
13
17
|
|
|
14
18
|
const Plugin = {};
|
|
@@ -19,8 +23,16 @@ const mw = (...fns) => fns.filter(isFn);
|
|
|
19
23
|
Plugin.init = async function (params) {
|
|
20
24
|
const { router, middleware } = params;
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
// Build middleware arrays safely and always spread them into Express route methods.
|
|
27
|
+
// Express will throw if any callback is undefined, so we filter strictly.
|
|
28
|
+
// Auth middlewares differ slightly depending on NodeBB configuration.
|
|
29
|
+
// In v4, some installs rely on middleware.authenticate rather than exposeUid.
|
|
30
|
+
const baseExpose = mw(middleware && (middleware.authenticate || middleware.exposeUid));
|
|
31
|
+
const publicExpose = baseExpose;
|
|
32
|
+
const publicAuth = mw(middleware && (middleware.authenticate || middleware.exposeUid), middleware && middleware.ensureLoggedIn);
|
|
23
33
|
|
|
34
|
+
// Robust admin guard: avoid middleware.admin.checkPrivileges() signature differences
|
|
35
|
+
// across NodeBB versions. We treat membership in the 'administrators' group as admin.
|
|
24
36
|
const Groups = require.main.require('./src/groups');
|
|
25
37
|
async function adminOnly(req, res, next) {
|
|
26
38
|
try {
|
|
@@ -37,7 +49,6 @@ Plugin.init = async function (params) {
|
|
|
37
49
|
return next(err);
|
|
38
50
|
}
|
|
39
51
|
}
|
|
40
|
-
|
|
41
52
|
const adminMws = mw(
|
|
42
53
|
middleware && (middleware.authenticate || middleware.exposeUid),
|
|
43
54
|
middleware && middleware.ensureLoggedIn,
|
|
@@ -45,58 +56,67 @@ Plugin.init = async function (params) {
|
|
|
45
56
|
);
|
|
46
57
|
|
|
47
58
|
// Page routes (HTML)
|
|
59
|
+
// IMPORTANT: pass an ARRAY for middlewares (even if empty), otherwise
|
|
60
|
+
// setupPageRoute will throw "middlewares is not iterable".
|
|
48
61
|
routeHelpers.setupPageRoute(router, '/calendar', mw(), controllers.renderCalendar);
|
|
62
|
+
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/calendar-onekite', mw(), admin.renderAdmin);
|
|
49
63
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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);
|
|
64
|
+
// Public API (JSON) — NodeBB 4.x only (v3 API)
|
|
65
|
+
router.get('/api/v3/plugins/calendar-onekite/events', ...publicExpose, api.getEvents);
|
|
66
|
+
router.get('/api/v3/plugins/calendar-onekite/items', ...publicExpose, api.getItems);
|
|
67
|
+
router.get('/api/v3/plugins/calendar-onekite/capabilities', ...publicExpose, api.getCapabilities);
|
|
57
68
|
|
|
58
|
-
router.post(
|
|
59
|
-
router.get(
|
|
60
|
-
router.put(
|
|
61
|
-
router.put(
|
|
62
|
-
router.put(
|
|
69
|
+
router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose, api.createReservation);
|
|
70
|
+
router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose, api.getReservationDetails);
|
|
71
|
+
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose, api.approveReservation);
|
|
72
|
+
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose, api.refuseReservation);
|
|
73
|
+
router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose, api.cancelReservation);
|
|
63
74
|
|
|
64
|
-
router.post(
|
|
65
|
-
router.get(
|
|
66
|
-
router.delete(
|
|
75
|
+
router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, api.createSpecialEvent);
|
|
76
|
+
router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.getSpecialEventDetails);
|
|
77
|
+
router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
|
|
67
78
|
|
|
68
79
|
// Admin API (JSON)
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
router.get(`${base}/settings`, ...adminMws, admin.getSettings);
|
|
72
|
-
router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
|
|
80
|
+
const adminBases = ['/api/v3/admin/plugins/calendar-onekite'];
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
adminBases.forEach((base) => {
|
|
83
|
+
router.get(`${base}/settings`, ...adminMws, admin.getSettings);
|
|
84
|
+
router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
router.get(`${base}/pending`, ...adminMws, admin.listPending);
|
|
87
|
+
router.put(`${base}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
|
|
88
|
+
router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
|
|
91
|
+
router.get(`${base}/debug`, ...adminMws, admin.debugHelloAsso);
|
|
92
|
+
// Accounting / exports
|
|
93
|
+
router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
|
|
94
|
+
router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
|
|
95
|
+
router.post(`${base}/accounting/purge`, ...adminMws, admin.purgeAccounting);
|
|
83
96
|
|
|
84
|
-
|
|
97
|
+
// Purge special events by year
|
|
98
|
+
router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
|
|
99
|
+
});
|
|
85
100
|
|
|
86
|
-
// HelloAsso
|
|
101
|
+
// HelloAsso callback endpoint (hardened)
|
|
102
|
+
// - Only accepts POST
|
|
103
|
+
// - Verifies x-ha-signature (HMAC SHA-256) using the configured client secret
|
|
104
|
+
// - Basic replay protection
|
|
105
|
+
// NOTE: we capture the raw body for signature verification.
|
|
87
106
|
const helloassoJson = bodyParser.json({
|
|
88
107
|
verify: (req, _res, buf) => {
|
|
89
108
|
req.rawBody = buf;
|
|
90
109
|
},
|
|
91
110
|
type: ['application/json', 'application/*+json'],
|
|
92
111
|
});
|
|
93
|
-
|
|
112
|
+
// Accept webhook on both legacy root path and namespaced plugin path.
|
|
113
|
+
// Some reverse proxies block unknown root paths, so /plugins/... is recommended.
|
|
94
114
|
router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
95
|
-
router.post(
|
|
115
|
+
router.post('/plugins/calendar-onekite/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
96
116
|
|
|
97
|
-
// Optional health checks
|
|
117
|
+
// Optional: health checks
|
|
98
118
|
router.get('/helloasso', (req, res) => res.json({ ok: true }));
|
|
99
|
-
router.get(
|
|
119
|
+
router.get('/plugins/calendar-onekite/helloasso', (req, res) => res.json({ ok: true }));
|
|
100
120
|
|
|
101
121
|
scheduler.start();
|
|
102
122
|
};
|
|
@@ -104,20 +124,24 @@ Plugin.init = async function (params) {
|
|
|
104
124
|
Plugin.addAdminNavigation = async function (header) {
|
|
105
125
|
header.plugins = header.plugins || [];
|
|
106
126
|
header.plugins.push({
|
|
107
|
-
route:
|
|
127
|
+
route: '/plugins/calendar-onekite',
|
|
108
128
|
icon: 'fa-calendar',
|
|
109
|
-
name: '
|
|
129
|
+
name: 'Calendar OneKite',
|
|
110
130
|
});
|
|
111
131
|
return header;
|
|
112
132
|
};
|
|
113
133
|
|
|
134
|
+
|
|
114
135
|
// Ensure our transactional emails always get a subject.
|
|
136
|
+
// NodeBB's Emailer.sendToEmail signature expects (template, email, language, params),
|
|
137
|
+
// so plugins typically inject/modify the subject via this hook.
|
|
115
138
|
Plugin.emailModify = async function (data) {
|
|
116
139
|
try {
|
|
117
140
|
if (!data || !data.template) return data;
|
|
118
141
|
const tpl = String(data.template);
|
|
119
|
-
if (!tpl.startsWith('
|
|
142
|
+
if (!tpl.startsWith('calendar-onekite_')) return data;
|
|
120
143
|
|
|
144
|
+
// If the caller provided a subject (we pass it in params.subject), copy it to data.subject.
|
|
121
145
|
const provided = data.params && data.params.subject ? String(data.params.subject) : '';
|
|
122
146
|
if (provided && (!data.subject || !String(data.subject).trim())) {
|
|
123
147
|
data.subject = provided;
|
|
@@ -130,4 +154,4 @@ Plugin.emailModify = async function (data) {
|
|
|
130
154
|
Plugin.defineWidgets = widgets.defineWidgets;
|
|
131
155
|
Plugin.renderTwoWeeksWidget = widgets.renderTwoWeeksWidget;
|
|
132
156
|
|
|
133
|
-
module.exports = Plugin;
|
|
157
|
+
module.exports = Plugin;
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "nodebb-plugin-onekite-calendar",
|
|
3
|
-
"name": "
|
|
3
|
+
"name": "OneKite Calendar",
|
|
4
4
|
"description": "Equipment reservation calendar (FullCalendar) with admin approval & HelloAsso checkout",
|
|
5
5
|
"url": "https://www.onekite.com/calendar",
|
|
6
6
|
"hooks": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"method": "defineWidgets"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
"hook": "filter:widget.render:onekite-
|
|
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
|
|
34
|
-
"
|
|
33
|
+
"../admin/plugins/calendar-onekite.js": "./public/admin.js",
|
|
34
|
+
"admin/plugins/calendar-onekite": "./public/admin.js"
|
|
35
35
|
},
|
|
36
|
-
"
|
|
36
|
+
"scripts": [
|
|
37
|
+
"public/client.js"
|
|
38
|
+
],
|
|
39
|
+
"acpScripts": [
|
|
40
|
+
"public/admin.js"
|
|
41
|
+
],
|
|
42
|
+
"version": "1.0.3"
|
|
37
43
|
}
|