nodebb-plugin-calendar-onekite 11.1.33 → 11.1.34
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 +222 -108
- package/lib/api.js +132 -184
- package/lib/controllers.js +3 -2
- package/lib/db.js +24 -33
- package/lib/helloasso.js +179 -68
- package/lib/scheduler.js +34 -14
- package/library.js +15 -8
- package/package.json +5 -3
- package/plugin.json +11 -14
- package/public/admin.js +206 -95
- package/public/client.js +19 -42
- package/templates/admin/plugins/calendar-onekite.tpl +78 -55
- package/templates/calendar-onekite.tpl +6 -6
- package/lib/utils.js +0 -25
- package/templates/emails/calendar-onekite-approved.tpl +0 -1
- package/templates/emails/calendar-onekite-pending.tpl +0 -1
- package/templates/emails/calendar-onekite-refused.tpl +0 -1
package/lib/admin.js
CHANGED
|
@@ -1,135 +1,249 @@
|
|
|
1
|
-
|
|
2
1
|
'use strict';
|
|
3
2
|
|
|
4
3
|
const meta = require.main.require('./src/meta');
|
|
5
|
-
const
|
|
4
|
+
const user = require.main.require('./src/user');
|
|
5
|
+
const emailer = require.main.require('./src/emailer');
|
|
6
|
+
|
|
7
|
+
const dbLayer = require('./db');
|
|
6
8
|
const helloasso = require('./helloasso');
|
|
7
|
-
const reservationDb = require('./db');
|
|
8
|
-
const { csvToList, toDateOnlyISO } = require('./utils');
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const s = await meta.settings.get('calendar-onekite');
|
|
12
|
-
if (s && s.helloassoClientSecret) {
|
|
13
|
-
s.helloassoClientSecret = '***';
|
|
14
|
-
}
|
|
15
|
-
res.json({ ok:true, settings: s || {} });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function saveSettings(req, res) {
|
|
19
|
-
const payload = req.body || {};
|
|
20
|
-
const normalizeCsv = (v) => String(v || '').split(',').map(s => s.trim()).filter(Boolean).join(',');
|
|
21
|
-
|
|
22
|
-
const current = await meta.settings.get('calendar-onekite') || {};
|
|
23
|
-
const toSave = {
|
|
24
|
-
helloassoEnv: payload.helloassoEnv || current.helloassoEnv || 'sandbox',
|
|
25
|
-
helloassoClientId: payload.helloassoClientId ?? current.helloassoClientId ?? '',
|
|
26
|
-
helloassoOrganizationSlug: payload.helloassoOrganizationSlug ?? current.helloassoOrganizationSlug ?? '',
|
|
27
|
-
helloassoFormType: payload.helloassoFormType || current.helloassoFormType || 'shop',
|
|
28
|
-
helloassoFormSlug: payload.helloassoFormSlug ?? current.helloassoFormSlug ?? '',
|
|
29
|
-
|
|
30
|
-
creatorGroups: normalizeCsv(payload.creatorGroups ?? current.creatorGroups),
|
|
31
|
-
validatorGroups: normalizeCsv(payload.validatorGroups ?? current.validatorGroups),
|
|
32
|
-
notifyGroups: normalizeCsv(payload.notifyGroups ?? current.notifyGroups),
|
|
33
|
-
|
|
34
|
-
holdMinutes: Math.max(1, parseInt(payload.holdMinutes ?? current.holdMinutes, 10) || 5),
|
|
35
|
-
};
|
|
10
|
+
const ADMIN_PRIV = 'admin:settings';
|
|
36
11
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
12
|
+
const admin = {};
|
|
13
|
+
|
|
14
|
+
admin.renderAdmin = async function (req, res) {
|
|
15
|
+
res.render('admin/plugins/calendar-onekite', {
|
|
16
|
+
title: 'Calendar OneKite',
|
|
17
|
+
});
|
|
18
|
+
};
|
|
43
19
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
20
|
+
admin.getSettings = async function (req, res) {
|
|
21
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
22
|
+
res.json(settings || {});
|
|
23
|
+
};
|
|
47
24
|
|
|
48
|
-
async function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
25
|
+
admin.saveSettings = async function (req, res) {
|
|
26
|
+
await meta.settings.set('calendar-onekite', req.body || {});
|
|
27
|
+
res.json({ ok: true });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
admin.listPending = async function (req, res) {
|
|
31
|
+
const ids = await dbLayer.listAllReservationIds(5000);
|
|
52
32
|
const pending = [];
|
|
53
33
|
for (const rid of ids) {
|
|
54
|
-
const r = await
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
34
|
+
const r = await dbLayer.getReservation(rid);
|
|
35
|
+
if (r && r.status === 'pending') {
|
|
36
|
+
pending.push(r);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
pending.sort((a, b) => parseInt(a.start, 10) - parseInt(b.start, 10));
|
|
40
|
+
res.json(pending);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
admin.approveReservation = async function (req, res) {
|
|
44
|
+
const rid = req.params.rid;
|
|
45
|
+
const r = await dbLayer.getReservation(rid);
|
|
46
|
+
if (!r) return res.status(404).json({ error: 'not-found' });
|
|
47
|
+
|
|
48
|
+
r.status = 'approved';
|
|
49
|
+
|
|
50
|
+
// Create HelloAsso payment link if configured
|
|
51
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
52
|
+
const env = settings.helloassoEnv || 'prod';
|
|
53
|
+
const token = await helloasso.getAccessToken({
|
|
54
|
+
env,
|
|
55
|
+
clientId: settings.helloassoClientId,
|
|
56
|
+
clientSecret: settings.helloassoClientSecret,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let paymentUrl = null;
|
|
60
|
+
if (token) {
|
|
61
|
+
const requester = await user.getUserData(r.uid);
|
|
62
|
+
// Determine amount from HelloAsso items (pricing comes from HelloAsso)
|
|
63
|
+
let totalAmount = 0;
|
|
64
|
+
try {
|
|
65
|
+
const items = await helloasso.listItems({
|
|
66
|
+
env,
|
|
67
|
+
token,
|
|
68
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
69
|
+
formType: settings.helloassoFormType,
|
|
70
|
+
formSlug: settings.helloassoFormSlug,
|
|
71
|
+
});
|
|
72
|
+
const normalized = (items || []).map((it) => ({
|
|
73
|
+
id: String(it.id || it.itemId || it.reference || it.name),
|
|
74
|
+
price: it.price || it.amount || it.unitPrice || 0,
|
|
75
|
+
})).filter(it => it.id);
|
|
76
|
+
const match = normalized.find(it => it.id === String(r.itemId));
|
|
77
|
+
totalAmount = match ? parseInt(match.price, 10) || 0 : 0;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
totalAmount = 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!totalAmount) {
|
|
83
|
+
return res.status(400).json({ error: 'item-price-not-found' });
|
|
84
|
+
}
|
|
85
|
+
paymentUrl = await helloasso.createCheckoutIntent({
|
|
86
|
+
env,
|
|
87
|
+
token,
|
|
88
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
89
|
+
formType: settings.helloassoFormType,
|
|
90
|
+
formSlug: settings.helloassoFormSlug,
|
|
91
|
+
totalAmount,
|
|
92
|
+
payerEmail: requester && requester.email,
|
|
65
93
|
});
|
|
66
94
|
}
|
|
67
|
-
res.json({ ok:true, pending });
|
|
68
|
-
}
|
|
69
95
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
96
|
+
if (paymentUrl) {
|
|
97
|
+
r.paymentUrl = paymentUrl;
|
|
98
|
+
}
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
const api = require('./api');
|
|
77
|
-
return api.refuseReservation(req, res);
|
|
78
|
-
}
|
|
100
|
+
await dbLayer.saveReservation(r);
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
// Email requester
|
|
103
|
+
try {
|
|
104
|
+
const requester = await user.getUserData(r.uid);
|
|
105
|
+
if (requester && requester.email) {
|
|
106
|
+
await emailer.send('calendar-onekite_approved', requester.email, {
|
|
107
|
+
username: requester.username,
|
|
108
|
+
itemName: r.itemName,
|
|
109
|
+
start: new Date(parseInt(r.start, 10)).toISOString(),
|
|
110
|
+
end: new Date(parseInt(r.end, 10)).toISOString(),
|
|
111
|
+
paymentUrl: paymentUrl || '',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {}
|
|
115
|
+
|
|
116
|
+
res.json({ ok: true, paymentUrl: paymentUrl || null });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
admin.refuseReservation = async function (req, res) {
|
|
120
|
+
const rid = req.params.rid;
|
|
121
|
+
const r = await dbLayer.getReservation(rid);
|
|
122
|
+
if (!r) return res.status(404).json({ error: 'not-found' });
|
|
123
|
+
|
|
124
|
+
r.status = 'refused';
|
|
125
|
+
await dbLayer.saveReservation(r);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const requester = await user.getUserData(r.uid);
|
|
129
|
+
if (requester && requester.email) {
|
|
130
|
+
await emailer.send('calendar-onekite_refused', requester.email, {
|
|
131
|
+
username: requester.username,
|
|
132
|
+
itemName: r.itemName,
|
|
133
|
+
start: new Date(parseInt(r.start, 10)).toISOString(),
|
|
134
|
+
end: new Date(parseInt(r.end, 10)).toISOString(),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {}
|
|
138
|
+
|
|
139
|
+
res.json({ ok: true });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
admin.purgeByYear = async function (req, res) {
|
|
143
|
+
const year = (req.body && req.body.year ? String(req.body.year) : '').trim();
|
|
82
144
|
if (!/^\d{4}$/.test(year)) {
|
|
83
|
-
return res.status(400).json({
|
|
145
|
+
return res.status(400).json({ error: 'invalid-year' });
|
|
84
146
|
}
|
|
85
147
|
const y = parseInt(year, 10);
|
|
86
|
-
const
|
|
87
|
-
const
|
|
148
|
+
const startTs = new Date(Date.UTC(y, 0, 1)).getTime();
|
|
149
|
+
const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
|
|
88
150
|
|
|
89
|
-
const ids = await
|
|
151
|
+
const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 100000);
|
|
152
|
+
let count = 0;
|
|
90
153
|
for (const rid of ids) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const st = Number(r.startTs);
|
|
94
|
-
if (st >= start && st < end) await reservationDb.deleteReservation(rid);
|
|
154
|
+
await dbLayer.removeReservation(rid);
|
|
155
|
+
count++;
|
|
95
156
|
}
|
|
96
|
-
res.json({ ok:true,
|
|
97
|
-
}
|
|
157
|
+
res.json({ ok: true, removed: count });
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Debug endpoint to validate HelloAsso connectivity and item loading
|
|
161
|
+
admin.debugHelloAsso = async function (req, res) {
|
|
162
|
+
const settings = await meta.settings.get('calendar-onekite');
|
|
163
|
+
const env = (settings && settings.helloassoEnv) || 'prod';
|
|
164
|
+
|
|
165
|
+
// Never expose secrets in debug output
|
|
166
|
+
const safeSettings = {
|
|
167
|
+
helloassoEnv: env,
|
|
168
|
+
helloassoClientId: settings && settings.helloassoClientId ? String(settings.helloassoClientId) : '',
|
|
169
|
+
helloassoClientSecret: settings && settings.helloassoClientSecret ? '***' : '',
|
|
170
|
+
helloassoOrganizationSlug: settings && settings.helloassoOrganizationSlug ? String(settings.helloassoOrganizationSlug) : '',
|
|
171
|
+
helloassoFormType: settings && settings.helloassoFormType ? String(settings.helloassoFormType) : '',
|
|
172
|
+
helloassoFormSlug: settings && settings.helloassoFormSlug ? String(settings.helloassoFormSlug) : '',
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const out = {
|
|
176
|
+
ok: true,
|
|
177
|
+
settings: safeSettings,
|
|
178
|
+
token: { ok: false },
|
|
179
|
+
// Catalog = what you actually want for a shop (available products/material)
|
|
180
|
+
catalog: { ok: false, count: 0, sample: [], keys: [] },
|
|
181
|
+
// Sold items = items present in orders (can be 0 if no sales yet)
|
|
182
|
+
soldItems: { ok: false, count: 0, sample: [] },
|
|
183
|
+
};
|
|
98
184
|
|
|
99
|
-
async function debugHelloAsso(req, res) {
|
|
100
|
-
const s = await meta.settings.get('calendar-onekite') || {};
|
|
101
|
-
const safe = { ...s, helloassoClientSecret: s.helloassoClientSecret ? '***' : '' };
|
|
102
185
|
try {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
settings: safe,
|
|
108
|
-
token: { ok: tokenOk },
|
|
109
|
-
items: { ok:true, count: (cat.items||[]).length, sample: (cat.items||[]).slice(0,10) }
|
|
186
|
+
const token = await helloasso.getAccessToken({
|
|
187
|
+
env,
|
|
188
|
+
clientId: settings.helloassoClientId,
|
|
189
|
+
clientSecret: settings.helloassoClientSecret,
|
|
110
190
|
});
|
|
191
|
+
if (!token) {
|
|
192
|
+
out.token = { ok: false, error: 'token-null' };
|
|
193
|
+
return res.json(out);
|
|
194
|
+
}
|
|
195
|
+
out.token = { ok: true };
|
|
196
|
+
|
|
197
|
+
// Catalog items (via /public)
|
|
198
|
+
try {
|
|
199
|
+
const { publicForm, items } = await helloasso.listCatalogItems({
|
|
200
|
+
env,
|
|
201
|
+
token,
|
|
202
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
203
|
+
formType: settings.helloassoFormType,
|
|
204
|
+
formSlug: settings.helloassoFormSlug,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const arr = Array.isArray(items) ? items : [];
|
|
208
|
+
out.catalog.ok = true;
|
|
209
|
+
out.catalog.count = arr.length;
|
|
210
|
+
out.catalog.keys = publicForm && typeof publicForm === 'object' ? Object.keys(publicForm) : [];
|
|
211
|
+
out.catalog.sample = arr.slice(0, 10).map((it) => ({
|
|
212
|
+
id: it.id,
|
|
213
|
+
name: it.name,
|
|
214
|
+
price: it.price ?? null,
|
|
215
|
+
}));
|
|
216
|
+
} catch (e) {
|
|
217
|
+
out.catalog = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [], keys: [] };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Sold items
|
|
221
|
+
try {
|
|
222
|
+
const items = await helloasso.listItems({
|
|
223
|
+
env,
|
|
224
|
+
token,
|
|
225
|
+
organizationSlug: settings.helloassoOrganizationSlug,
|
|
226
|
+
formType: settings.helloassoFormType,
|
|
227
|
+
formSlug: settings.helloassoFormSlug,
|
|
228
|
+
});
|
|
229
|
+
const arr = Array.isArray(items) ? items : [];
|
|
230
|
+
out.soldItems.ok = true;
|
|
231
|
+
out.soldItems.count = arr.length;
|
|
232
|
+
out.soldItems.sample = arr.slice(0, 10).map((it) => ({
|
|
233
|
+
id: it.id || it.itemId || it.reference || it.name,
|
|
234
|
+
name: it.name || it.label || it.itemName,
|
|
235
|
+
price: it.price || it.amount || it.unitPrice || null,
|
|
236
|
+
}));
|
|
237
|
+
} catch (e) {
|
|
238
|
+
out.soldItems = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return res.json(out);
|
|
111
242
|
} catch (e) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
token: { ok:false },
|
|
116
|
-
items: { ok:false, count: 0, sample: [] },
|
|
117
|
-
error: String(e && e.message || e),
|
|
118
|
-
});
|
|
243
|
+
out.ok = false;
|
|
244
|
+
out.token = { ok: false, error: String(e && e.message ? e.message : e) };
|
|
245
|
+
return res.json(out);
|
|
119
246
|
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function renderAdmin(req, res) {
|
|
123
|
-
res.render('admin/plugins/calendar-onekite', {});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
module.exports = {
|
|
127
|
-
renderAdmin,
|
|
128
|
-
getSettings,
|
|
129
|
-
saveSettings,
|
|
130
|
-
listPending,
|
|
131
|
-
approveReservation,
|
|
132
|
-
refuseReservation,
|
|
133
|
-
purgeByYear,
|
|
134
|
-
debugHelloAsso,
|
|
135
247
|
};
|
|
248
|
+
|
|
249
|
+
module.exports = admin;
|