nodebb-plugin-calendar-onekite 12.0.5 → 12.0.7
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 +77 -19
- package/lib/api.js +144 -18
- package/lib/db.js +0 -15
- package/lib/scheduler.js +55 -20
- package/package.json +1 -1
- package/public/admin.js +1 -4
- package/public/client.js +1 -36
- package/lib/utils/mailer.js +0 -59
- package/lib/utils/permissions.js +0 -73
- package/lib/utils/time.js +0 -32
- package/lib/utils/url.js +0 -36
package/lib/admin.js
CHANGED
|
@@ -4,18 +4,85 @@ const meta = require.main.require('./src/meta');
|
|
|
4
4
|
const user = require.main.require('./src/user');
|
|
5
5
|
const emailer = require.main.require('./src/emailer');
|
|
6
6
|
const nconf = require.main.require('nconf');
|
|
7
|
-
const { sendTemplateEmail } = require('./utils/mailer');
|
|
8
|
-
const { formatFR } = require('./utils/time');
|
|
9
|
-
const { normalizeReturnUrl, normalizeCallbackUrl } = require('./utils/url');
|
|
10
7
|
|
|
11
8
|
function forumBaseUrl() {
|
|
12
9
|
const base = String(nconf.get('url') || '').trim().replace(/\/$/, '');
|
|
13
10
|
return base;
|
|
14
11
|
}
|
|
15
12
|
|
|
13
|
+
function formatFR(tsOrIso) {
|
|
14
|
+
const d = new Date(typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso);
|
|
15
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
16
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
17
|
+
const yyyy = d.getFullYear();
|
|
18
|
+
return `${dd}/${mm}/${yyyy}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
async function sendEmail(template, toEmail, subject, data) {
|
|
22
|
+
// Prefer sending by uid (NodeBB core expects uid in various places)
|
|
17
23
|
const uid = data && Number.isInteger(data.uid) ? data.uid : null;
|
|
18
|
-
|
|
24
|
+
if (!toEmail && !uid) return;
|
|
25
|
+
|
|
26
|
+
const settings = await meta.settings.get('calendar-onekite').catch(() => ({}));
|
|
27
|
+
const lang = (settings && settings.defaultLang) || (meta && meta.config && meta.config.defaultLang) || 'fr';
|
|
28
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
29
|
+
|
|
30
|
+
// If we have a uid, use the native uid-based sender first.
|
|
31
|
+
try {
|
|
32
|
+
if (uid && typeof emailer.send === 'function') {
|
|
33
|
+
// NodeBB: send(template, uid, params)
|
|
34
|
+
if (emailer.send.length >= 3) {
|
|
35
|
+
await emailer.send(template, uid, params);
|
|
36
|
+
} else {
|
|
37
|
+
await emailer.send(template, uid, params);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.warn('[calendar-onekite] Failed to send email', {
|
|
43
|
+
template,
|
|
44
|
+
toEmail,
|
|
45
|
+
err: err && err.message ? err.message : String(err),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
if (typeof emailer.sendToEmail === 'function') {
|
|
51
|
+
// NodeBB: sendToEmail(template, email, language, params)
|
|
52
|
+
if (emailer.sendToEmail.length >= 4) {
|
|
53
|
+
await emailer.sendToEmail(template, toEmail, lang, params);
|
|
54
|
+
} else {
|
|
55
|
+
// Older signature: sendToEmail(template, email, params)
|
|
56
|
+
await emailer.sendToEmail(template, toEmail, params);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.warn('[calendar-onekite] Failed to send email', {
|
|
62
|
+
template,
|
|
63
|
+
toEmail,
|
|
64
|
+
err: err && err.message ? err.message : String(err),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeCallbackUrl(configured, meta) {
|
|
70
|
+
const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
|
|
71
|
+
let url = (configured || '').trim();
|
|
72
|
+
if (!url) {
|
|
73
|
+
url = base ? `${base}/helloasso` : '';
|
|
74
|
+
}
|
|
75
|
+
if (url && url.startsWith('/') && base) {
|
|
76
|
+
url = `${base}${url}`;
|
|
77
|
+
}
|
|
78
|
+
return url;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeReturnUrl(meta) {
|
|
82
|
+
const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
|
|
83
|
+
const b = String(base || '').trim().replace(/\/$/, '');
|
|
84
|
+
if (!b) return '';
|
|
85
|
+
return `${b}/calendar`;
|
|
19
86
|
}
|
|
20
87
|
|
|
21
88
|
|
|
@@ -45,13 +112,10 @@ admin.saveSettings = async function (req, res) {
|
|
|
45
112
|
admin.listPending = async function (req, res) {
|
|
46
113
|
const ids = await dbLayer.listAllReservationIds(5000);
|
|
47
114
|
const pending = [];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const rows = await dbLayer.getReservations(slice);
|
|
53
|
-
for (const r of rows) {
|
|
54
|
-
if (r && r.status === 'pending') pending.push(r);
|
|
115
|
+
for (const rid of ids) {
|
|
116
|
+
const r = await dbLayer.getReservation(rid);
|
|
117
|
+
if (r && r.status === 'pending') {
|
|
118
|
+
pending.push(r);
|
|
55
119
|
}
|
|
56
120
|
}
|
|
57
121
|
pending.sort((a, b) => parseInt(a.start, 10) - parseInt(b.start, 10));
|
|
@@ -202,20 +266,15 @@ admin.purgeByYear = async function (req, res) {
|
|
|
202
266
|
const startTs = new Date(Date.UTC(y, 0, 1)).getTime();
|
|
203
267
|
const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
|
|
204
268
|
|
|
205
|
-
// IMPORTANT:
|
|
206
|
-
// Purging "locations" (reservations) must NOT wipe accounting.
|
|
207
|
-
// Accounting is computed from paid reservations, so for paid ones we keep the
|
|
208
|
-
// reservation object but mark it as hidden from the calendar.
|
|
209
|
-
// Non-paid reservations can be safely removed.
|
|
210
269
|
const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 100000);
|
|
211
|
-
const ts = Date.now();
|
|
212
270
|
let removed = 0;
|
|
213
271
|
let archivedForAccounting = 0;
|
|
214
|
-
|
|
272
|
+
const ts = Date.now();
|
|
215
273
|
for (const rid of ids) {
|
|
216
274
|
const r = await dbLayer.getReservation(rid);
|
|
217
275
|
if (!r) continue;
|
|
218
276
|
|
|
277
|
+
// Keep paid reservations for accounting; just hide them from the calendar.
|
|
219
278
|
if (String(r.status) === 'paid') {
|
|
220
279
|
if (!r.calendarPurgedAt) {
|
|
221
280
|
r.calendarPurgedAt = ts;
|
|
@@ -228,7 +287,6 @@ admin.purgeByYear = async function (req, res) {
|
|
|
228
287
|
await dbLayer.removeReservation(rid);
|
|
229
288
|
removed++;
|
|
230
289
|
}
|
|
231
|
-
|
|
232
290
|
res.json({ ok: true, removed, archivedForAccounting });
|
|
233
291
|
};
|
|
234
292
|
|
package/lib/api.js
CHANGED
|
@@ -9,23 +9,82 @@ const user = require.main.require('./src/user');
|
|
|
9
9
|
const groups = require.main.require('./src/groups');
|
|
10
10
|
|
|
11
11
|
const dbLayer = require('./db');
|
|
12
|
+
|
|
13
|
+
async function userInAnyGroup(uid, allowed) {
|
|
14
|
+
if (!uid || !allowed || !allowed.length) return false;
|
|
15
|
+
const ug = await groups.getUserGroups([uid]);
|
|
16
|
+
const slugs = ug[0] || [];
|
|
17
|
+
return allowed.some(g => slugs.includes(g));
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
const helloasso = require('./helloasso');
|
|
13
|
-
const { sendTemplateEmail } = require('./utils/mailer');
|
|
14
|
-
const urlUtils = require('./utils/url');
|
|
15
|
-
const timeUtils = require('./utils/time');
|
|
16
|
-
const permUtils = require('./utils/permissions');
|
|
17
21
|
|
|
18
22
|
// Email helper: NodeBB's Emailer signature differs across versions.
|
|
19
23
|
// We try the common forms. Any failure is logged for debugging.
|
|
20
24
|
async function sendEmail(template, toEmail, subject, data) {
|
|
21
|
-
|
|
25
|
+
if (!toEmail) return;
|
|
26
|
+
try {
|
|
27
|
+
// NodeBB core signature (historically):
|
|
28
|
+
// Emailer.sendToEmail(template, email, language, params[, callback])
|
|
29
|
+
// Subject is not a positional arg; it must be injected (either by NodeBB itself
|
|
30
|
+
// or via filter:email.modify). We always pass it in params.subject.
|
|
31
|
+
const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
32
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
33
|
+
if (typeof emailer.sendToEmail === 'function') {
|
|
34
|
+
await emailer.sendToEmail(template, toEmail, language, params);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Fallback for older/unusual builds (rare)
|
|
38
|
+
if (typeof emailer.send === 'function') {
|
|
39
|
+
// Some builds accept (template, email, language, params)
|
|
40
|
+
if (emailer.send.length >= 4) {
|
|
41
|
+
await emailer.send(template, toEmail, language, params);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Some builds accept (template, email, params)
|
|
45
|
+
await emailer.send(template, toEmail, params);
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err) });
|
|
50
|
+
}
|
|
22
51
|
}
|
|
23
52
|
|
|
24
|
-
function normalizeBaseUrl(meta) {
|
|
53
|
+
function normalizeBaseUrl(meta) {
|
|
54
|
+
// Prefer meta.config.url, fallback to nconf.get('url')
|
|
55
|
+
let base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
|
|
56
|
+
if (!base) {
|
|
57
|
+
base = String(nconf.get('url') || '').trim();
|
|
58
|
+
}
|
|
59
|
+
base = String(base || '').trim().replace(/\/$/, '');
|
|
60
|
+
// Ensure absolute with scheme
|
|
61
|
+
if (base && !/^https?:\/\//i.test(base)) {
|
|
62
|
+
base = `https://${base.replace(/^\/\//, '')}`;
|
|
63
|
+
}
|
|
64
|
+
return base;
|
|
65
|
+
}
|
|
25
66
|
|
|
26
|
-
function normalizeCallbackUrl(configured, meta) {
|
|
67
|
+
function normalizeCallbackUrl(configured, meta) {
|
|
68
|
+
const base = normalizeBaseUrl(meta);
|
|
69
|
+
let url = (configured || '').trim();
|
|
70
|
+
if (!url) {
|
|
71
|
+
// Default webhook endpoint (recommended): namespaced under /plugins
|
|
72
|
+
url = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
|
|
73
|
+
}
|
|
74
|
+
if (url && url.startsWith('/') && base) {
|
|
75
|
+
url = `${base}${url}`;
|
|
76
|
+
}
|
|
77
|
+
// Ensure scheme for absolute URLs
|
|
78
|
+
if (url && !/^https?:\/\//i.test(url)) {
|
|
79
|
+
url = `https://${url.replace(/^\/\//, '')}`;
|
|
80
|
+
}
|
|
81
|
+
return url;
|
|
82
|
+
}
|
|
27
83
|
|
|
28
|
-
function normalizeReturnUrl(meta) {
|
|
84
|
+
function normalizeReturnUrl(meta) {
|
|
85
|
+
const base = normalizeBaseUrl(meta);
|
|
86
|
+
return base ? `${base}/calendar` : '';
|
|
87
|
+
}
|
|
29
88
|
|
|
30
89
|
|
|
31
90
|
function overlap(aStart, aEnd, bStart, bEnd) {
|
|
@@ -33,32 +92,100 @@ function overlap(aStart, aEnd, bStart, bEnd) {
|
|
|
33
92
|
}
|
|
34
93
|
|
|
35
94
|
|
|
36
|
-
function formatFR(tsOrIso) {
|
|
95
|
+
function formatFR(tsOrIso) {
|
|
96
|
+
const d = new Date(typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso);
|
|
97
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
98
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
99
|
+
const yyyy = d.getFullYear();
|
|
100
|
+
return `${dd}/${mm}/${yyyy}`;
|
|
101
|
+
}
|
|
37
102
|
|
|
38
|
-
function toTs(v) {
|
|
103
|
+
function toTs(v) {
|
|
104
|
+
if (v === undefined || v === null || v === '') return NaN;
|
|
105
|
+
// Accept milliseconds timestamps passed as strings or numbers.
|
|
106
|
+
if (typeof v === 'number') return v;
|
|
107
|
+
const s = String(v).trim();
|
|
108
|
+
if (/^[0-9]+$/.test(s)) return parseInt(s, 10);
|
|
109
|
+
const d = new Date(s);
|
|
110
|
+
return d.getTime();
|
|
111
|
+
}
|
|
39
112
|
|
|
40
|
-
function yearFromTs(ts) {
|
|
113
|
+
function yearFromTs(ts) {
|
|
114
|
+
const d = new Date(Number(ts));
|
|
115
|
+
return Number.isFinite(d.getTime()) ? d.getFullYear() : new Date().getFullYear();
|
|
116
|
+
}
|
|
41
117
|
|
|
42
|
-
function autoCreatorGroupForYear(year) {
|
|
118
|
+
function autoCreatorGroupForYear(year) {
|
|
119
|
+
return `onekite-ffvl-${year}`;
|
|
120
|
+
}
|
|
43
121
|
|
|
44
122
|
function autoFormSlugForYear(year) {
|
|
45
123
|
return `locations-materiel-${year}`;
|
|
46
124
|
}
|
|
47
125
|
|
|
48
126
|
async function canRequest(uid, settings, startTs) {
|
|
49
|
-
|
|
127
|
+
const year = yearFromTs(startTs);
|
|
128
|
+
const defaultGroup = autoCreatorGroupForYear(year);
|
|
129
|
+
const extras = (settings.creatorGroups || settings.allowedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
130
|
+
const allowed = [defaultGroup, ...extras].filter(Boolean);
|
|
131
|
+
// If only the default group exists, enforce membership (do not open access to all).
|
|
132
|
+
if (!allowed.length) return true;
|
|
133
|
+
for (const g of allowed) {
|
|
134
|
+
const isMember = await groups.isMember(uid, g);
|
|
135
|
+
if (isMember) return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
50
138
|
}
|
|
51
139
|
|
|
52
140
|
async function canValidate(uid, settings) {
|
|
53
|
-
|
|
141
|
+
// Always allow forum administrators (and global moderators) to validate,
|
|
142
|
+
// even if validatorGroups is empty.
|
|
143
|
+
try {
|
|
144
|
+
const isAdmin = await groups.isMember(uid, 'administrators');
|
|
145
|
+
if (isAdmin) return true;
|
|
146
|
+
const isGlobalMod = await groups.isMember(uid, 'Global Moderators');
|
|
147
|
+
if (isGlobalMod) return true;
|
|
148
|
+
} catch (e) {}
|
|
149
|
+
|
|
150
|
+
const allowed = (settings.validatorGroups || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
151
|
+
if (!allowed.length) return false;
|
|
152
|
+
for (const g of allowed) {
|
|
153
|
+
const isMember = await groups.isMember(uid, g);
|
|
154
|
+
if (isMember) return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
54
157
|
}
|
|
55
158
|
|
|
56
159
|
async function canCreateSpecial(uid, settings) {
|
|
57
|
-
|
|
160
|
+
if (!uid) return false;
|
|
161
|
+
try {
|
|
162
|
+
const isAdmin = await groups.isMember(uid, 'administrators');
|
|
163
|
+
if (isAdmin) return true;
|
|
164
|
+
const isGlobalMod = await groups.isMember(uid, 'Global Moderators');
|
|
165
|
+
if (isGlobalMod) return true;
|
|
166
|
+
} catch (e) {}
|
|
167
|
+
const allowed = (settings.specialCreatorGroups || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
168
|
+
if (!allowed.length) return false;
|
|
169
|
+
for (const g of allowed) {
|
|
170
|
+
if (await groups.isMember(uid, g)) return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
58
173
|
}
|
|
59
174
|
|
|
60
175
|
async function canDeleteSpecial(uid, settings) {
|
|
61
|
-
|
|
176
|
+
if (!uid) return false;
|
|
177
|
+
try {
|
|
178
|
+
const isAdmin = await groups.isMember(uid, 'administrators');
|
|
179
|
+
if (isAdmin) return true;
|
|
180
|
+
const isGlobalMod = await groups.isMember(uid, 'Global Moderators');
|
|
181
|
+
if (isGlobalMod) return true;
|
|
182
|
+
} catch (e) {}
|
|
183
|
+
const allowed = (settings.specialDeleterGroups || settings.specialCreatorGroups || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
184
|
+
if (!allowed.length) return false;
|
|
185
|
+
for (const g of allowed) {
|
|
186
|
+
if (await groups.isMember(uid, g)) return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
62
189
|
}
|
|
63
190
|
|
|
64
191
|
function eventsFor(resv) {
|
|
@@ -156,8 +283,7 @@ api.getEvents = async function (req, res) {
|
|
|
156
283
|
for (const rid of ids) {
|
|
157
284
|
const r = await dbLayer.getReservation(rid);
|
|
158
285
|
if (!r) continue;
|
|
159
|
-
//
|
|
160
|
-
// do not show it in the calendar UI.
|
|
286
|
+
// Purged from calendar view (kept for accounting)
|
|
161
287
|
if (r.calendarPurgedAt) continue;
|
|
162
288
|
// Only show active statuses
|
|
163
289
|
if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
|
package/lib/db.js
CHANGED
|
@@ -14,19 +14,6 @@ async function getReservation(rid) {
|
|
|
14
14
|
return await db.getObject(KEY_OBJ(rid));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
async function getReservations(rids) {
|
|
19
|
-
if (!Array.isArray(rids) || !rids.length) return [];
|
|
20
|
-
const keys = rids.map((rid) => KEY_OBJ(rid));
|
|
21
|
-
return await db.getObjects(keys);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function getSpecialEvents(eids) {
|
|
25
|
-
if (!Array.isArray(eids) || !eids.length) return [];
|
|
26
|
-
const keys = eids.map((eid) => KEY_SPECIAL_OBJ(eid));
|
|
27
|
-
return await db.getObjects(keys);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
17
|
async function saveReservation(resv) {
|
|
31
18
|
await db.setObject(KEY_OBJ(resv.rid), resv);
|
|
32
19
|
// score = start timestamp
|
|
@@ -63,8 +50,6 @@ module.exports = {
|
|
|
63
50
|
KEY_SPECIAL_ZSET,
|
|
64
51
|
KEY_CHECKOUT_INTENT_TO_RID,
|
|
65
52
|
getReservation,
|
|
66
|
-
getReservations,
|
|
67
|
-
getSpecialEvents,
|
|
68
53
|
saveReservation,
|
|
69
54
|
removeReservation,
|
|
70
55
|
// Special events
|
package/lib/scheduler.js
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const meta = require.main.require('./src/meta');
|
|
4
4
|
const dbLayer = require('./db');
|
|
5
|
-
const { sendTemplateEmail } = require('./utils/mailer');
|
|
6
|
-
const { formatFR } = require('./utils/time');
|
|
7
5
|
|
|
8
6
|
let timer = null;
|
|
9
7
|
|
|
@@ -25,12 +23,8 @@ async function expirePending() {
|
|
|
25
23
|
return;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const slice = ids.slice(i, i + chunkSize);
|
|
31
|
-
const rows = await dbLayer.getReservations(slice);
|
|
32
|
-
for (const resv of rows) {
|
|
33
|
-
const rid = resv && resv.rid;
|
|
26
|
+
for (const rid of ids) {
|
|
27
|
+
const resv = await dbLayer.getReservation(rid);
|
|
34
28
|
if (!resv || resv.status !== 'pending') {
|
|
35
29
|
continue;
|
|
36
30
|
}
|
|
@@ -40,7 +34,6 @@ async function expirePending() {
|
|
|
40
34
|
// Expire (remove from calendar)
|
|
41
35
|
await dbLayer.removeReservation(rid);
|
|
42
36
|
}
|
|
43
|
-
}
|
|
44
37
|
}
|
|
45
38
|
}
|
|
46
39
|
|
|
@@ -58,12 +51,55 @@ async function processAwaitingPayment() {
|
|
|
58
51
|
|
|
59
52
|
const ids = await dbLayer.listAllReservationIds(5000);
|
|
60
53
|
if (!ids || !ids.length) return;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
|
|
55
|
+
const emailer = require.main.require('./src/emailer');
|
|
56
|
+
const user = require.main.require('./src/user');
|
|
57
|
+
|
|
58
|
+
async function sendEmail(template, toEmail, subject, data) {
|
|
59
|
+
if (!toEmail) return;
|
|
60
|
+
try {
|
|
61
|
+
// NodeBB core signature:
|
|
62
|
+
// Emailer.sendToEmail(template, email, language, params[, callback])
|
|
63
|
+
// Subject is NOT a positional argument; it must be provided in params.subject
|
|
64
|
+
// and optionally copied into the final email by filter:email.modify.
|
|
65
|
+
const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
66
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
67
|
+
|
|
68
|
+
if (typeof emailer.sendToEmail === 'function') {
|
|
69
|
+
await emailer.sendToEmail(template, toEmail, language, params);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fallbacks for older/unusual builds
|
|
74
|
+
if (typeof emailer.send === 'function') {
|
|
75
|
+
// Some builds accept (template, email, language, params)
|
|
76
|
+
if (emailer.send.length >= 4) {
|
|
77
|
+
await emailer.send(template, toEmail, language, params);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Some builds accept (template, email, params)
|
|
81
|
+
await emailer.send(template, toEmail, params);
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.warn('[calendar-onekite] Failed to send email (scheduler)', {
|
|
86
|
+
template,
|
|
87
|
+
toEmail,
|
|
88
|
+
err: String((err && err.message) || err),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatFR(ts) {
|
|
94
|
+
const d = new Date(ts);
|
|
95
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
96
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
97
|
+
const yyyy = d.getFullYear();
|
|
98
|
+
return `${dd}/${mm}/${yyyy}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const rid of ids) {
|
|
102
|
+
const r = await dbLayer.getReservation(rid);
|
|
67
103
|
if (!r || r.status !== 'awaiting_payment') continue;
|
|
68
104
|
|
|
69
105
|
const approvedAt = parseInt(r.approvedAt || r.validatedAt || 0, 10) || 0;
|
|
@@ -76,7 +112,7 @@ async function processAwaitingPayment() {
|
|
|
76
112
|
// Send reminder once
|
|
77
113
|
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
78
114
|
if (u && u.email) {
|
|
79
|
-
await
|
|
115
|
+
await sendEmail('calendar-onekite_reminder', u.email, 'Location matériel - Rappel', {
|
|
80
116
|
username: u.username,
|
|
81
117
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
82
118
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
@@ -84,7 +120,7 @@ async function processAwaitingPayment() {
|
|
|
84
120
|
paymentUrl: r.paymentUrl || '',
|
|
85
121
|
delayMinutes: holdMins,
|
|
86
122
|
pickupLine: r.pickupTime ? (r.adminNote ? `${r.pickupTime} à ${r.adminNote}` : r.pickupTime) : '',
|
|
87
|
-
}
|
|
123
|
+
});
|
|
88
124
|
}
|
|
89
125
|
r.reminderSent = true;
|
|
90
126
|
r.reminderAt = now;
|
|
@@ -96,17 +132,16 @@ async function processAwaitingPayment() {
|
|
|
96
132
|
// Expire: remove reservation so it disappears from calendar and frees items
|
|
97
133
|
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
98
134
|
if (u && u.email) {
|
|
99
|
-
await
|
|
135
|
+
await sendEmail('calendar-onekite_expired', u.email, 'Location matériel - Rappel', {
|
|
100
136
|
username: u.username,
|
|
101
137
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
102
138
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
103
139
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
104
140
|
delayMinutes: holdMins,
|
|
105
|
-
}
|
|
141
|
+
});
|
|
106
142
|
}
|
|
107
143
|
await dbLayer.removeReservation(rid);
|
|
108
144
|
}
|
|
109
|
-
}
|
|
110
145
|
}
|
|
111
146
|
}
|
|
112
147
|
|
package/package.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -636,10 +636,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
636
636
|
if (!ok) return;
|
|
637
637
|
try {
|
|
638
638
|
const r = await purge(year);
|
|
639
|
-
|
|
640
|
-
const archived = r.archivedForAccounting || 0;
|
|
641
|
-
const extra = archived ? `, ${archived} archivée(s) (compta conservée)` : '';
|
|
642
|
-
showAlert('success', `Purge OK (${removed} supprimée(s)${extra}).`);
|
|
639
|
+
showAlert('success', `Purge OK (${r.removed || 0} supprimées, ${r.archivedForAccounting || 0} archivées — compta conservée).`);
|
|
643
640
|
await refreshPending();
|
|
644
641
|
} catch (e) {
|
|
645
642
|
showAlert('error', 'Purge impossible.');
|
package/public/client.js
CHANGED
|
@@ -886,25 +886,6 @@ function toDatetimeLocalValue(date) {
|
|
|
886
886
|
}
|
|
887
887
|
|
|
888
888
|
async function init(selector) {
|
|
889
|
-
|
|
890
|
-
const container = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
891
|
-
if (!container) return;
|
|
892
|
-
|
|
893
|
-
// Prevent double-init (ajaxify + initial load, or return from payment)
|
|
894
|
-
if (container.dataset && container.dataset.onekiteCalendarInit === '1') {
|
|
895
|
-
// Still refresh the custom button label in case FC rerendered
|
|
896
|
-
try { refreshDesktopModeButton(); } catch (e) {}
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
if (container.dataset) container.dataset.onekiteCalendarInit = '1';
|
|
900
|
-
|
|
901
|
-
// If a previous instance exists (shouldn't, but happens in some navigation flows), destroy it.
|
|
902
|
-
try {
|
|
903
|
-
if (window.oneKiteCalendar && typeof window.oneKiteCalendar.destroy === 'function') {
|
|
904
|
-
window.oneKiteCalendar.destroy();
|
|
905
|
-
}
|
|
906
|
-
window.oneKiteCalendar = null;
|
|
907
|
-
} catch (e) {}
|
|
908
889
|
const el = document.querySelector(selector);
|
|
909
890
|
if (!el) {
|
|
910
891
|
return;
|
|
@@ -1082,7 +1063,6 @@ try {
|
|
|
1082
1063
|
|
|
1083
1064
|
const calendar = new FullCalendar.Calendar(el, {
|
|
1084
1065
|
initialView: 'dayGridMonth',
|
|
1085
|
-
windowResize: function () { try { refreshDesktopModeButton(); } catch (e) {} },
|
|
1086
1066
|
height: 'auto',
|
|
1087
1067
|
contentHeight: 'auto',
|
|
1088
1068
|
aspectRatio: computeAspectRatio(),
|
|
@@ -1691,21 +1671,6 @@ try {
|
|
|
1691
1671
|
|
|
1692
1672
|
calendar.render();
|
|
1693
1673
|
|
|
1694
|
-
// Keep the custom button label stable even if FullCalendar rerenders the toolbar
|
|
1695
|
-
try {
|
|
1696
|
-
const toolbar = container.querySelector('.fc-toolbar');
|
|
1697
|
-
if (toolbar && !toolbar.__oneKiteObserved) {
|
|
1698
|
-
toolbar.__oneKiteObserved = true;
|
|
1699
|
-
const mo = new MutationObserver(() => {
|
|
1700
|
-
try { refreshDesktopModeButton(); } catch (e) {}
|
|
1701
|
-
});
|
|
1702
|
-
mo.observe(toolbar, { childList: true, subtree: true, characterData: true });
|
|
1703
|
-
// Disconnect after a while to avoid permanent overhead
|
|
1704
|
-
setTimeout(() => { try { mo.disconnect(); } catch (e) {} }, 15000);
|
|
1705
|
-
}
|
|
1706
|
-
} catch (e) {}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
1674
|
refreshDesktopModeButton();
|
|
1710
1675
|
|
|
1711
1676
|
|
|
@@ -1800,7 +1765,7 @@ try {
|
|
|
1800
1765
|
// Auto-init on /calendar when ajaxify finishes rendering.
|
|
1801
1766
|
function autoInit(data) {
|
|
1802
1767
|
try {
|
|
1803
|
-
const tpl =
|
|
1768
|
+
const tpl = data && data.template ? data.template.name : (ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '');
|
|
1804
1769
|
if (tpl === 'calendar-onekite') {
|
|
1805
1770
|
init('#onekite-calendar');
|
|
1806
1771
|
}
|
package/lib/utils/mailer.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const meta = require.main.require('./src/meta');
|
|
4
|
-
const emailer = require.main.require('./src/emailer');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Send NodeBB email templates in a way that survives slight core signature differences.
|
|
8
|
-
* - Prefer uid-based sending when a uid is available.
|
|
9
|
-
* - Otherwise use sendToEmail.
|
|
10
|
-
*
|
|
11
|
-
* @param {object} opts
|
|
12
|
-
* @param {string} opts.template
|
|
13
|
-
* @param {number} [opts.uid]
|
|
14
|
-
* @param {string} [opts.toEmail]
|
|
15
|
-
* @param {string} [opts.subject]
|
|
16
|
-
* @param {object} [opts.data]
|
|
17
|
-
*/
|
|
18
|
-
async function sendTemplateEmail(opts) {
|
|
19
|
-
const template = opts && opts.template;
|
|
20
|
-
const uid = opts && Number.isInteger(opts.uid) ? opts.uid : null;
|
|
21
|
-
const toEmail = opts && opts.toEmail ? String(opts.toEmail) : '';
|
|
22
|
-
const subject = opts && opts.subject ? String(opts.subject) : '';
|
|
23
|
-
const data = (opts && opts.data) || {};
|
|
24
|
-
|
|
25
|
-
if (!template) return;
|
|
26
|
-
if (!uid && !toEmail) return;
|
|
27
|
-
|
|
28
|
-
const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
29
|
-
const params = Object.assign({}, data, subject ? { subject } : {});
|
|
30
|
-
|
|
31
|
-
// uid-first
|
|
32
|
-
if (uid && typeof emailer.send === 'function') {
|
|
33
|
-
try {
|
|
34
|
-
// NodeBB: send(template, uid, params)
|
|
35
|
-
await emailer.send(template, uid, params);
|
|
36
|
-
return;
|
|
37
|
-
} catch (err) {
|
|
38
|
-
// fall through to email
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (toEmail && typeof emailer.sendToEmail === 'function') {
|
|
43
|
-
try {
|
|
44
|
-
// NodeBB commonly: sendToEmail(template, email, language, params)
|
|
45
|
-
await emailer.sendToEmail(template, toEmail, language, params);
|
|
46
|
-
return;
|
|
47
|
-
} catch (err) {
|
|
48
|
-
// eslint-disable-next-line no-console
|
|
49
|
-
console.warn('[calendar-onekite] Failed to send email', {
|
|
50
|
-
template,
|
|
51
|
-
toEmail,
|
|
52
|
-
uid,
|
|
53
|
-
err: err && err.message ? err.message : String(err),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
module.exports = { sendTemplateEmail };
|
package/lib/utils/permissions.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const groups = require.main.require('./src/groups');
|
|
4
|
-
const { yearFromTs } = require('./time');
|
|
5
|
-
|
|
6
|
-
function autoCreatorGroupForYear(year) {
|
|
7
|
-
return `onekite-ffvl-${year}`;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async function isPrivileged(uid) {
|
|
11
|
-
try {
|
|
12
|
-
if (await groups.isMember(uid, 'administrators')) return true;
|
|
13
|
-
if (await groups.isMember(uid, 'Global Moderators')) return true;
|
|
14
|
-
} catch (e) {}
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function parseGroupList(s) {
|
|
19
|
-
return String(s || '')
|
|
20
|
-
.split(',')
|
|
21
|
-
.map((x) => x.trim())
|
|
22
|
-
.filter(Boolean);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function canRequest(uid, settings, startTs) {
|
|
26
|
-
const year = yearFromTs(startTs);
|
|
27
|
-
const defaultGroup = autoCreatorGroupForYear(year);
|
|
28
|
-
const extras = parseGroupList(settings && (settings.creatorGroups || settings.allowedGroups));
|
|
29
|
-
const allowed = [defaultGroup, ...extras].filter(Boolean);
|
|
30
|
-
if (!allowed.length) return true;
|
|
31
|
-
for (const g of allowed) {
|
|
32
|
-
if (await groups.isMember(uid, g)) return true;
|
|
33
|
-
}
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function canValidate(uid, settings) {
|
|
38
|
-
if (!uid) return false;
|
|
39
|
-
if (await isPrivileged(uid)) return true;
|
|
40
|
-
const allowed = parseGroupList(settings && settings.validatorGroups);
|
|
41
|
-
for (const g of allowed) {
|
|
42
|
-
if (await groups.isMember(uid, g)) return true;
|
|
43
|
-
}
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function canCreateSpecial(uid, settings) {
|
|
48
|
-
if (!uid) return false;
|
|
49
|
-
if (await isPrivileged(uid)) return true;
|
|
50
|
-
const allowed = parseGroupList(settings && settings.specialCreatorGroups);
|
|
51
|
-
for (const g of allowed) {
|
|
52
|
-
if (await groups.isMember(uid, g)) return true;
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function canDeleteSpecial(uid, settings) {
|
|
58
|
-
if (!uid) return false;
|
|
59
|
-
if (await isPrivileged(uid)) return true;
|
|
60
|
-
const allowed = parseGroupList(settings && (settings.specialDeleterGroups || settings.specialCreatorGroups));
|
|
61
|
-
for (const g of allowed) {
|
|
62
|
-
if (await groups.isMember(uid, g)) return true;
|
|
63
|
-
}
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
module.exports = {
|
|
68
|
-
autoCreatorGroupForYear,
|
|
69
|
-
canRequest,
|
|
70
|
-
canValidate,
|
|
71
|
-
canCreateSpecial,
|
|
72
|
-
canDeleteSpecial,
|
|
73
|
-
};
|
package/lib/utils/time.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function toTs(v) {
|
|
4
|
-
if (v === undefined || v === null || v === '') return NaN;
|
|
5
|
-
// Accept milliseconds timestamps passed as strings or numbers.
|
|
6
|
-
if (typeof v === 'number') return v;
|
|
7
|
-
const s = String(v).trim();
|
|
8
|
-
if (/^[0-9]+$/.test(s)) return parseInt(s, 10);
|
|
9
|
-
const d = new Date(s);
|
|
10
|
-
return d.getTime();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function formatFR(tsOrIso) {
|
|
14
|
-
const d = new Date(
|
|
15
|
-
typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso
|
|
16
|
-
);
|
|
17
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
18
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
19
|
-
const yyyy = d.getFullYear();
|
|
20
|
-
return `${dd}/${mm}/${yyyy}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function yearFromTs(ts) {
|
|
24
|
-
const d = new Date(Number(ts));
|
|
25
|
-
return Number.isFinite(d.getTime()) ? d.getFullYear() : new Date().getFullYear();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
module.exports = {
|
|
29
|
-
toTs,
|
|
30
|
-
formatFR,
|
|
31
|
-
yearFromTs,
|
|
32
|
-
};
|
package/lib/utils/url.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const nconf = require.main.require('nconf');
|
|
4
|
-
|
|
5
|
-
function normalizeBaseUrl(meta) {
|
|
6
|
-
// Prefer meta.config.url, fallback to nconf.get('url')
|
|
7
|
-
let base = (meta && meta.config && (meta.config.url || meta.config['url']))
|
|
8
|
-
? (meta.config.url || meta.config['url'])
|
|
9
|
-
: '';
|
|
10
|
-
if (!base) base = String(nconf.get('url') || '').trim();
|
|
11
|
-
base = String(base || '').trim().replace(/\/$/, '');
|
|
12
|
-
if (base && !/^https?:\/\//i.test(base)) {
|
|
13
|
-
base = `https://${base.replace(/^\/\//, '')}`;
|
|
14
|
-
}
|
|
15
|
-
return base;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalizeCallbackUrl(configured, meta, fallbackPath = '/plugins/calendar-onekite/helloasso') {
|
|
19
|
-
const base = normalizeBaseUrl(meta);
|
|
20
|
-
let url = String(configured || '').trim();
|
|
21
|
-
if (!url) url = base ? `${base}${fallbackPath}` : '';
|
|
22
|
-
if (url && url.startsWith('/') && base) url = `${base}${url}`;
|
|
23
|
-
if (url && !/^https?:\/\//i.test(url)) url = `https://${url.replace(/^\/\//, '')}`;
|
|
24
|
-
return url;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeReturnUrl(meta, path = '/calendar') {
|
|
28
|
-
const base = normalizeBaseUrl(meta);
|
|
29
|
-
return base ? `${base}${path}` : '';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
module.exports = {
|
|
33
|
-
normalizeBaseUrl,
|
|
34
|
-
normalizeCallbackUrl,
|
|
35
|
-
normalizeReturnUrl,
|
|
36
|
-
};
|