nodebb-plugin-onekite-calendar 1.0.2 → 1.0.4
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 +31 -2
- package/lib/admin.js +15 -155
- package/lib/api.js +158 -103
- package/lib/constants.js +7 -0
- package/lib/controllers.js +2 -2
- package/lib/db.js +5 -5
- package/lib/discord.js +6 -5
- package/lib/email.js +26 -0
- package/lib/helloasso.js +4 -3
- package/lib/helloassoWebhook.js +8 -7
- package/lib/log.js +29 -0
- package/lib/scheduler.js +8 -11
- package/lib/widgets.js +128 -8
- package/library.js +39 -63
- package/package.json +1 -1
- package/plugin.json +5 -11
- package/public/admin.js +20 -92
- package/public/client.js +23 -23
- package/templates/admin/plugins/{calendar-onekite.tpl → onekite-calendar.tpl} +4 -23
- package/templates/{calendar-onekite.tpl → onekite-calendar.tpl} +11 -0
- /package/templates/emails/{calendar-onekite_approved.tpl → onekite-calendar_approved.tpl} +0 -0
- /package/templates/emails/{calendar-onekite_expired.tpl → onekite-calendar_expired.tpl} +0 -0
- /package/templates/emails/{calendar-onekite_paid.tpl → onekite-calendar_paid.tpl} +0 -0
- /package/templates/emails/{calendar-onekite_pending.tpl → onekite-calendar_pending.tpl} +0 -0
- /package/templates/emails/{calendar-onekite_refused.tpl → onekite-calendar_refused.tpl} +0 -0
- /package/templates/emails/{calendar-onekite_reminder.tpl → onekite-calendar_reminder.tpl} +0 -0
package/lib/log.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Minimal, opt-in logging.
|
|
4
|
+
// Enable by setting ONEKITE_CALENDAR_DEBUG=1 in the NodeBB process env.
|
|
5
|
+
|
|
6
|
+
let logger;
|
|
7
|
+
try {
|
|
8
|
+
logger = require.main.require('./src/logger');
|
|
9
|
+
} catch (e) {
|
|
10
|
+
logger = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const enabled = () => !!process.env.ONEKITE_CALENDAR_DEBUG;
|
|
14
|
+
|
|
15
|
+
function warn(msg, meta) {
|
|
16
|
+
if (!enabled()) return;
|
|
17
|
+
if (logger && typeof logger.warn === 'function') {
|
|
18
|
+
logger.warn(`[onekite-calendar] ${msg}`, meta || '');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function error(msg, meta) {
|
|
23
|
+
if (!enabled()) return;
|
|
24
|
+
if (logger && typeof logger.error === 'function') {
|
|
25
|
+
logger.error(`[onekite-calendar] ${msg}`, meta || '');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { warn, error };
|
package/lib/scheduler.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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');
|
|
6
7
|
|
|
7
8
|
let timer = null;
|
|
8
9
|
|
|
@@ -15,7 +16,7 @@ function getSetting(settings, key, fallback) {
|
|
|
15
16
|
|
|
16
17
|
// Pending holds: short lock after a user creates a request (defaults to 5 minutes)
|
|
17
18
|
async function expirePending() {
|
|
18
|
-
const settings = await meta.settings.get('calendar
|
|
19
|
+
const settings = await meta.settings.get('onekite-calendar');
|
|
19
20
|
const holdMins = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
|
|
20
21
|
const now = Date.now();
|
|
21
22
|
|
|
@@ -43,7 +44,7 @@ async function expirePending() {
|
|
|
43
44
|
// - We send a reminder after `paymentHoldMinutes` (default 60)
|
|
44
45
|
// - We expire (and remove) after `2 * paymentHoldMinutes`
|
|
45
46
|
async function processAwaitingPayment() {
|
|
46
|
-
const settings = await meta.settings.get('calendar
|
|
47
|
+
const settings = await meta.settings.get('onekite-calendar');
|
|
47
48
|
const holdMins = parseInt(
|
|
48
49
|
getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')),
|
|
49
50
|
10
|
|
@@ -83,11 +84,7 @@ async function processAwaitingPayment() {
|
|
|
83
84
|
}
|
|
84
85
|
} catch (err) {
|
|
85
86
|
// eslint-disable-next-line no-console
|
|
86
|
-
|
|
87
|
-
template,
|
|
88
|
-
toEmail,
|
|
89
|
-
err: String((err && err.message) || err),
|
|
90
|
-
});
|
|
87
|
+
log.warn('Failed to send email (scheduler)', { template, toEmail, err: String((err && err.message) || err) });
|
|
91
88
|
}
|
|
92
89
|
}
|
|
93
90
|
|
|
@@ -111,7 +108,7 @@ async function processAwaitingPayment() {
|
|
|
111
108
|
|
|
112
109
|
if (!r.reminderSent && now >= reminderAt && now < expireAt) {
|
|
113
110
|
// Send reminder once (guarded across clustered NodeBB processes)
|
|
114
|
-
const reminderKey = 'calendar
|
|
111
|
+
const reminderKey = 'onekite-calendar:email:reminderSent';
|
|
115
112
|
const first = await db.setAdd(reminderKey, rid);
|
|
116
113
|
if (!first) {
|
|
117
114
|
// another process already sent it
|
|
@@ -122,7 +119,7 @@ async function processAwaitingPayment() {
|
|
|
122
119
|
}
|
|
123
120
|
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
124
121
|
if (u && u.email) {
|
|
125
|
-
await sendEmail('
|
|
122
|
+
await sendEmail('onekite-calendar_reminder', u.email, 'Location matériel - Rappel', {
|
|
126
123
|
username: u.username,
|
|
127
124
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
128
125
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
@@ -141,12 +138,12 @@ async function processAwaitingPayment() {
|
|
|
141
138
|
if (now >= expireAt) {
|
|
142
139
|
// Expire: remove reservation so it disappears from calendar and frees items
|
|
143
140
|
// Guard email send across clustered NodeBB processes
|
|
144
|
-
const expiredKey = 'calendar
|
|
141
|
+
const expiredKey = 'onekite-calendar:email:expiredSent';
|
|
145
142
|
const firstExpired = await db.setAdd(expiredKey, rid);
|
|
146
143
|
const shouldEmail = !!firstExpired;
|
|
147
144
|
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
148
145
|
if (shouldEmail && u && u.email) {
|
|
149
|
-
await sendEmail('
|
|
146
|
+
await sendEmail('onekite-calendar_expired', u.email, 'Location matériel - Rappel', {
|
|
150
147
|
username: u.username,
|
|
151
148
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
152
149
|
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: 'calendar-
|
|
44
|
-
name: 'Calendrier
|
|
43
|
+
widget: 'onekite-calendar-twoweeks',
|
|
44
|
+
name: 'Calendrier Onekite',
|
|
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/calendar
|
|
57
|
+
const eventsEndpoint = `${apiBase}/api/v3/plugins/onekite-calendar/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
|
|
66
|
+
<div style="font-weight: 600;">Calendrier</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,21 +137,130 @@ 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
|
+
},
|
|
140
149
|
events: function(info, successCallback, failureCallback) {
|
|
141
150
|
const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
|
|
142
|
-
|
|
151
|
+
const url1 = eventsEndpoint + '?' + qs.toString();
|
|
152
|
+
fetch(url1, { credentials: 'same-origin' })
|
|
143
153
|
.then((r) => r.json())
|
|
144
154
|
.then((json) => successCallback(json || []))
|
|
145
155
|
.catch((e) => failureCallback(e));
|
|
146
156
|
},
|
|
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.
|
|
147
208
|
dateClick: function() {
|
|
148
209
|
window.location.href = calUrl;
|
|
149
210
|
},
|
|
150
|
-
eventClick:
|
|
151
|
-
|
|
211
|
+
// Do not redirect on eventClick: we show a popup in eventDidMount.
|
|
212
|
+
eventClick: function(info) {
|
|
213
|
+
try { if (info && info.jsEvent) info.jsEvent.preventDefault(); } catch (e) {}
|
|
152
214
|
},
|
|
153
215
|
});
|
|
154
216
|
|
|
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
|
+
|
|
155
264
|
calendar.render();
|
|
156
265
|
}
|
|
157
266
|
|
|
@@ -165,7 +274,18 @@ widgets.renderTwoWeeksWidget = async function (data) {
|
|
|
165
274
|
.onekite-twoweeks .fc .fc-button { padding: .2rem .35rem; font-size: .75rem; }
|
|
166
275
|
.onekite-twoweeks .fc .fc-daygrid-day-number { font-size: .75rem; padding: 2px; }
|
|
167
276
|
.onekite-twoweeks .fc .fc-col-header-cell-cushion { font-size: .75rem; }
|
|
168
|
-
.onekite-twoweeks .fc .fc-event-title {
|
|
277
|
+
.onekite-twoweeks .fc .fc-event-title { display: none; }
|
|
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
|
+
}
|
|
169
289
|
</style>
|
|
170
290
|
`;
|
|
171
291
|
|
package/library.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
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)
|
|
8
3
|
const routeHelpers = require.main.require('./src/routes/helpers');
|
|
9
4
|
|
|
10
5
|
const controllers = require('./lib/controllers.js');
|
|
@@ -13,6 +8,7 @@ const admin = require('./lib/admin');
|
|
|
13
8
|
const scheduler = require('./lib/scheduler');
|
|
14
9
|
const helloassoWebhook = require('./lib/helloassoWebhook');
|
|
15
10
|
const widgets = require('./lib/widgets');
|
|
11
|
+
const { NAMESPACE } = require('./lib/constants');
|
|
16
12
|
const bodyParser = require('body-parser');
|
|
17
13
|
|
|
18
14
|
const Plugin = {};
|
|
@@ -23,16 +19,8 @@ const mw = (...fns) => fns.filter(isFn);
|
|
|
23
19
|
Plugin.init = async function (params) {
|
|
24
20
|
const { router, middleware } = params;
|
|
25
21
|
|
|
26
|
-
|
|
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);
|
|
22
|
+
const publicExpose = mw(middleware && (middleware.authenticate || middleware.exposeUid));
|
|
33
23
|
|
|
34
|
-
// Robust admin guard: avoid middleware.admin.checkPrivileges() signature differences
|
|
35
|
-
// across NodeBB versions. We treat membership in the 'administrators' group as admin.
|
|
36
24
|
const Groups = require.main.require('./src/groups');
|
|
37
25
|
async function adminOnly(req, res, next) {
|
|
38
26
|
try {
|
|
@@ -49,6 +37,7 @@ Plugin.init = async function (params) {
|
|
|
49
37
|
return next(err);
|
|
50
38
|
}
|
|
51
39
|
}
|
|
40
|
+
|
|
52
41
|
const adminMws = mw(
|
|
53
42
|
middleware && (middleware.authenticate || middleware.exposeUid),
|
|
54
43
|
middleware && middleware.ensureLoggedIn,
|
|
@@ -56,67 +45,58 @@ Plugin.init = async function (params) {
|
|
|
56
45
|
);
|
|
57
46
|
|
|
58
47
|
// Page routes (HTML)
|
|
59
|
-
// IMPORTANT: pass an ARRAY for middlewares (even if empty), otherwise
|
|
60
|
-
// setupPageRoute will throw "middlewares is not iterable".
|
|
61
48
|
routeHelpers.setupPageRoute(router, '/calendar', mw(), controllers.renderCalendar);
|
|
62
|
-
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/calendar-onekite', mw(), admin.renderAdmin);
|
|
63
49
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
// Admin page route
|
|
51
|
+
routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${NAMESPACE}`, mw(), admin.renderAdmin);
|
|
52
|
+
|
|
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);
|
|
68
57
|
|
|
69
|
-
router.post(
|
|
70
|
-
router.get(
|
|
71
|
-
router.put(
|
|
72
|
-
router.put(
|
|
73
|
-
router.put(
|
|
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);
|
|
74
63
|
|
|
75
|
-
router.post(
|
|
76
|
-
router.get(
|
|
77
|
-
router.delete(
|
|
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);
|
|
78
67
|
|
|
79
68
|
// Admin API (JSON)
|
|
80
|
-
const
|
|
69
|
+
const base = `/api/v3/admin/plugins/${NAMESPACE}`;
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
|
|
71
|
+
router.get(`${base}/settings`, ...adminMws, admin.getSettings);
|
|
72
|
+
router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
|
|
85
73
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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);
|
|
89
77
|
|
|
90
|
-
|
|
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);
|
|
78
|
+
router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
|
|
96
79
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
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);
|
|
100
83
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
// - Basic replay protection
|
|
105
|
-
// NOTE: we capture the raw body for signature verification.
|
|
84
|
+
router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
|
|
85
|
+
|
|
86
|
+
// HelloAsso webhook endpoint (signature verified)
|
|
106
87
|
const helloassoJson = bodyParser.json({
|
|
107
88
|
verify: (req, _res, buf) => {
|
|
108
89
|
req.rawBody = buf;
|
|
109
90
|
},
|
|
110
91
|
type: ['application/json', 'application/*+json'],
|
|
111
92
|
});
|
|
112
|
-
|
|
113
|
-
// Some reverse proxies block unknown root paths, so /plugins/... is recommended.
|
|
93
|
+
|
|
114
94
|
router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
|
|
115
|
-
router.post(
|
|
95
|
+
router.post(`/plugins/${NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
|
|
116
96
|
|
|
117
|
-
// Optional
|
|
97
|
+
// Optional health checks
|
|
118
98
|
router.get('/helloasso', (req, res) => res.json({ ok: true }));
|
|
119
|
-
router.get(
|
|
99
|
+
router.get(`/plugins/${NAMESPACE}/helloasso`, (req, res) => res.json({ ok: true }));
|
|
120
100
|
|
|
121
101
|
scheduler.start();
|
|
122
102
|
};
|
|
@@ -124,24 +104,20 @@ Plugin.init = async function (params) {
|
|
|
124
104
|
Plugin.addAdminNavigation = async function (header) {
|
|
125
105
|
header.plugins = header.plugins || [];
|
|
126
106
|
header.plugins.push({
|
|
127
|
-
route:
|
|
107
|
+
route: `/plugins/${NAMESPACE}`,
|
|
128
108
|
icon: 'fa-calendar',
|
|
129
|
-
name: 'Calendar
|
|
109
|
+
name: 'Onekite Calendar',
|
|
130
110
|
});
|
|
131
111
|
return header;
|
|
132
112
|
};
|
|
133
113
|
|
|
134
|
-
|
|
135
114
|
// 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.
|
|
138
115
|
Plugin.emailModify = async function (data) {
|
|
139
116
|
try {
|
|
140
117
|
if (!data || !data.template) return data;
|
|
141
118
|
const tpl = String(data.template);
|
|
142
|
-
if (!tpl.startsWith('
|
|
119
|
+
if (!tpl.startsWith('onekite-calendar_')) return data;
|
|
143
120
|
|
|
144
|
-
// If the caller provided a subject (we pass it in params.subject), copy it to data.subject.
|
|
145
121
|
const provided = data.params && data.params.subject ? String(data.params.subject) : '';
|
|
146
122
|
if (provided && (!data.subject || !String(data.subject).trim())) {
|
|
147
123
|
data.subject = provided;
|
|
@@ -154,4 +130,4 @@ Plugin.emailModify = async function (data) {
|
|
|
154
130
|
Plugin.defineWidgets = widgets.defineWidgets;
|
|
155
131
|
Plugin.renderTwoWeeksWidget = widgets.renderTwoWeeksWidget;
|
|
156
132
|
|
|
157
|
-
module.exports = Plugin;
|
|
133
|
+
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": "Calendar
|
|
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:calendar-
|
|
24
|
+
"hook": "filter:widget.render:onekite-calendar-twoweeks",
|
|
25
25
|
"method": "renderTwoWeeksWidget"
|
|
26
26
|
}
|
|
27
27
|
],
|
|
@@ -30,14 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"templates": "./templates",
|
|
32
32
|
"modules": {
|
|
33
|
-
"
|
|
34
|
-
"
|
|
33
|
+
"admin/plugins/onekite-calendar": "./public/admin.js",
|
|
34
|
+
"forum/onekite-calendar": "./public/client.js"
|
|
35
35
|
},
|
|
36
|
-
"
|
|
37
|
-
"public/client.js"
|
|
38
|
-
],
|
|
39
|
-
"acpScripts": [
|
|
40
|
-
"public/admin.js"
|
|
41
|
-
],
|
|
42
|
-
"version": "1.0.2"
|
|
36
|
+
"version": "1.1.0"
|
|
43
37
|
}
|