nodebb-plugin-calendar-onekite 11.1.5 → 11.1.6
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/helloasso.js +145 -37
- package/package.json +1 -1
package/lib/helloasso.js
CHANGED
|
@@ -1,62 +1,170 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* HelloAsso API helper (NodeBB v4 compatible, no external deps).
|
|
5
|
+
* Uses native https to avoid optional dependencies.
|
|
6
|
+
*/
|
|
5
7
|
|
|
6
|
-
const
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const { URL } = require('url');
|
|
7
10
|
const Settings = require('./settings');
|
|
8
11
|
|
|
9
12
|
function baseUrl(env) {
|
|
10
13
|
return env === 'production' ? 'https://api.helloasso.com' : 'https://api.helloasso-sandbox.com';
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
function httpsRequest(method, urlString, headers = {}, body = undefined) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const url = new URL(urlString);
|
|
19
|
+
|
|
20
|
+
const opts = {
|
|
21
|
+
method,
|
|
22
|
+
protocol: url.protocol,
|
|
23
|
+
hostname: url.hostname,
|
|
24
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
25
|
+
path: url.pathname + (url.search || ''),
|
|
26
|
+
headers: headers || {},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const req = https.request(opts, (res) => {
|
|
30
|
+
let data = '';
|
|
31
|
+
res.setEncoding('utf8');
|
|
32
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
33
|
+
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data }));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
req.on('error', reject);
|
|
37
|
+
|
|
38
|
+
if (body !== undefined && body !== null) {
|
|
39
|
+
req.write(body);
|
|
40
|
+
}
|
|
41
|
+
req.end();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
13
45
|
async function getAccessToken() {
|
|
14
46
|
const s = await Settings.get();
|
|
15
|
-
if (!s.helloassoClientId || !s.helloassoClientSecret)
|
|
16
|
-
|
|
47
|
+
if (!s.helloassoClientId || !s.helloassoClientSecret) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tokenUrl = `${baseUrl(s.helloassoEnv)}/oauth2/token`;
|
|
52
|
+
const form = new URLSearchParams({
|
|
53
|
+
grant_type: 'client_credentials',
|
|
54
|
+
client_id: String(s.helloassoClientId),
|
|
55
|
+
client_secret: String(s.helloassoClientSecret),
|
|
56
|
+
}).toString();
|
|
57
|
+
|
|
58
|
+
const res = await httpsRequest('POST', tokenUrl, {
|
|
59
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
60
|
+
'Content-Length': Buffer.byteLength(form),
|
|
61
|
+
}, form);
|
|
62
|
+
|
|
63
|
+
if (res.status < 200 || res.status >= 300) {
|
|
64
|
+
// Keep plugin stable: return null token and let caller show a helpful message
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
17
68
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
form: {
|
|
22
|
-
grant_type: 'client_credentials',
|
|
23
|
-
client_id: s.helloassoClientId,
|
|
24
|
-
client_secret: s.helloassoClientSecret,
|
|
25
|
-
},
|
|
26
|
-
json: true,
|
|
27
|
-
});
|
|
28
|
-
return resp && resp.access_token;
|
|
29
|
-
} catch (e) {
|
|
69
|
+
const json = JSON.parse(res.body || '{}');
|
|
70
|
+
return json.access_token || null;
|
|
71
|
+
} catch {
|
|
30
72
|
return null;
|
|
31
73
|
}
|
|
32
74
|
}
|
|
33
75
|
|
|
34
|
-
async function
|
|
35
|
-
// This is a simplified implementation; returns null if token missing.
|
|
76
|
+
async function apiJson(method, path, { query, body } = {}) {
|
|
36
77
|
const s = await Settings.get();
|
|
37
78
|
const token = await getAccessToken();
|
|
38
|
-
if (!token
|
|
79
|
+
if (!token) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const url = new URL(`${baseUrl(s.helloassoEnv)}${path}`);
|
|
84
|
+
if (query && typeof query === 'object') {
|
|
85
|
+
Object.entries(query).forEach(([k, v]) => {
|
|
86
|
+
if (v !== undefined && v !== null && v !== '') {
|
|
87
|
+
url.searchParams.set(k, String(v));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
39
91
|
|
|
40
|
-
const
|
|
92
|
+
const payload = body ? JSON.stringify(body) : undefined;
|
|
93
|
+
const headers = {
|
|
94
|
+
Authorization: `Bearer ${token}`,
|
|
95
|
+
Accept: 'application/json',
|
|
96
|
+
};
|
|
97
|
+
if (payload) {
|
|
98
|
+
headers['Content-Type'] = 'application/json';
|
|
99
|
+
headers['Content-Length'] = Buffer.byteLength(payload);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const res = await httpsRequest(method, url.toString(), headers, payload);
|
|
41
103
|
|
|
104
|
+
if (res.status < 200 || res.status >= 300) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
42
107
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
108
|
+
return JSON.parse(res.body || '{}');
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function listItems() {
|
|
115
|
+
const s = await Settings.get();
|
|
116
|
+
if (!s.helloassoOrganizationSlug || !s.helloassoFormType || !s.helloassoFormSlug) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Endpoint used by HelloAsso to list form items
|
|
121
|
+
const data = await apiJson('GET', `/v5/organizations/${encodeURIComponent(s.helloassoOrganizationSlug)}/${encodeURIComponent(s.helloassoFormType)}/${encodeURIComponent(s.helloassoFormSlug)}/items`);
|
|
122
|
+
if (!data) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Normalize (HelloAsso returns {items:[...]} in many responses; be defensive)
|
|
127
|
+
const items = Array.isArray(data.items) ? data.items : (Array.isArray(data.data) ? data.data : []);
|
|
128
|
+
return items.map((it) => ({
|
|
129
|
+
id: it.id ?? it.itemId ?? it.slug ?? it.name,
|
|
130
|
+
name: it.name ?? it.label ?? 'Item',
|
|
131
|
+
price: it.amount ?? it.price ?? it.unitPrice ?? 0,
|
|
132
|
+
raw: it,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function createCheckoutIntent({ itemId, quantity = 1, payer = {} }) {
|
|
137
|
+
const s = await Settings.get();
|
|
138
|
+
if (!s.helloassoOrganizationSlug) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Minimal payload; depending on your HelloAsso form, you might need to add more fields.
|
|
143
|
+
const payload = {
|
|
144
|
+
totalAmount: undefined, // let HelloAsso compute if supported
|
|
145
|
+
initialAmount: undefined,
|
|
146
|
+
items: [{
|
|
147
|
+
itemId,
|
|
148
|
+
quantity,
|
|
149
|
+
}],
|
|
150
|
+
payer: {
|
|
151
|
+
firstName: payer.firstName || '',
|
|
152
|
+
lastName: payer.lastName || '',
|
|
153
|
+
email: payer.email || '',
|
|
154
|
+
},
|
|
155
|
+
backUrl: s.helloassoBackUrl || '',
|
|
156
|
+
errorUrl: s.helloassoErrorUrl || '',
|
|
157
|
+
returnUrl: s.helloassoReturnUrl || '',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const data = await apiJson('POST', `/v5/organizations/${encodeURIComponent(s.helloassoOrganizationSlug)}/checkout-intents`, { body: payload });
|
|
161
|
+
if (!data) {
|
|
58
162
|
return null;
|
|
59
163
|
}
|
|
164
|
+
return data.redirectUrl || data.checkoutUrl || data.url || null;
|
|
60
165
|
}
|
|
61
166
|
|
|
62
|
-
module.exports = {
|
|
167
|
+
module.exports = {
|
|
168
|
+
listItems,
|
|
169
|
+
createCheckoutIntent,
|
|
170
|
+
};
|