nodebb-plugin-calendar-onekite 1.4.7 → 2.0.0

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/.drone.yml ADDED
@@ -0,0 +1,62 @@
1
+ kind: pipeline
2
+ type: docker
3
+ name: deploy-nodebb-plugin
4
+
5
+ trigger:
6
+ branch:
7
+ - master
8
+ event:
9
+ - push
10
+
11
+ steps:
12
+ - name: sanity-check
13
+ image: node:20-alpine
14
+ commands:
15
+ - node -v
16
+ - test -f plugin.json
17
+ - test -f library.js
18
+ - test -d static
19
+ - test -d templates
20
+
21
+ - name: deploy-files
22
+ image: appleboy/drone-scp
23
+ settings:
24
+ host:
25
+ from_secret: SSH_HOST
26
+ username:
27
+ from_secret: SSH_USER
28
+ port:
29
+ from_secret: SSH_PORT
30
+ key:
31
+ from_secret: SSH_KEY
32
+ source:
33
+ - ./
34
+ target:
35
+ from_secret: PLUGIN_TARGET_DIR
36
+ strip_components: 0
37
+ overwrite: true
38
+
39
+ - name: install-and-build
40
+ image: appleboy/drone-ssh
41
+ environment:
42
+ PLUGIN_TARGET_DIR:
43
+ from_secret: PLUGIN_TARGET_DIR
44
+ settings:
45
+ host:
46
+ from_secret: SSH_HOST
47
+ username:
48
+ from_secret: SSH_USER
49
+ port:
50
+ from_secret: SSH_PORT
51
+ key:
52
+ from_secret: SSH_KEY
53
+ script:
54
+ - 'echo "Plugin: $PLUGIN_TARGET_DIR"'
55
+ - 'test -d "$PLUGIN_TARGET_DIR"'
56
+
57
+ - 'cd "$PLUGIN_TARGET_DIR"'
58
+ - 'if [ -f package.json ]; then npm ci --omit=dev; else echo "No package.json, skipping npm ci"; fi'
59
+
60
+ #- 'cd "$NODEBB_PATH"'
61
+ #- './nodebb build'
62
+ #- './nodebb restart || true'
package/README.md CHANGED
@@ -0,0 +1,21 @@
1
+ # nodebb-plugin-calendar-onekite (no-jQuery)
2
+
3
+ ## Features
4
+ - FullCalendar frontend (Month/Week/Day/List) with drag&drop + resize (editable for authorized groups)
5
+ - Event CRUD, booking (multi-day), admin validation workflow, HelloAsso payment intent + webhook
6
+ - Admin pages (ACP) without jQuery; robust under ajaxify via MutationObserver
7
+ - Widget: calendarUpcoming
8
+
9
+ ## Important: FullCalendar assets
10
+ This package loads FullCalendar from jsDelivr CDN in templates/calendar.tpl.
11
+ If you need offline / no-CDN, tell me and I will vendor the files under static/vendor/fullcalendar.
12
+
13
+ ## Install
14
+ - Copy into your NodeBB plugins folder
15
+ - Activate plugin in ACP
16
+ - Rebuild: ./nodebb build
17
+
18
+ ## Routes
19
+ - Pages: /calendar, /calendar/my-reservations
20
+ - Admin: /admin/plugins/calendar-onekite, /admin/calendar/planning
21
+ - API: available under /api/... and /api/v3/... (NodeBB v4 client helper uses /api/v3)
package/helloasso.js CHANGED
@@ -1,81 +1,129 @@
1
- 'use strict';
2
-
3
- const fetch = require('node-fetch');
4
-
5
- const HELLOASSO_CLIENT_ID = process.env.HELLOASSO_CLIENT_ID;
6
- const HELLOASSO_CLIENT_SECRET = process.env.HELLOASSO_CLIENT_SECRET;
7
- const HELLOASSO_ORG_SLUG = process.env.HELLOASSO_ORG_SLUG;
8
- const HELLOASSO_FORM_SLUG = process.env.HELLOASSO_FORM_SLUG;
9
- const HELLOASSO_API_BASE = 'https://api.helloasso.com';
10
-
11
- async function getHelloAssoAccessToken() {
12
- if (!HELLOASSO_CLIENT_ID || !HELLOASSO_CLIENT_SECRET) {
13
- throw new Error('[HelloAsso] HELLOASSO_CLIENT_ID/SECRET non définis');
14
- }
15
-
16
- const res = await fetch(`${HELLOASSO_API_BASE}/oauth2/token`, {
17
- method: 'POST',
18
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
19
- body: new URLSearchParams({
20
- grant_type: 'client_credentials',
21
- client_id: HELLOASSO_CLIENT_ID,
22
- client_secret: HELLOASSO_CLIENT_SECRET
23
- })
24
- });
25
-
26
- if (!res.ok) {
27
- const txt = await res.text();
28
- throw new Error('[HelloAsso] échec OAuth2: ' + txt);
29
- }
30
- const data = await res.json();
31
- return data.access_token;
32
- }
33
-
34
- async function createHelloAssoCheckoutIntent({ eid, rid, uid, itemId, quantity, amount }) {
35
- if (!HELLOASSO_ORG_SLUG || !HELLOASSO_FORM_SLUG) {
36
- console.warn('[calendar-onekite] HelloAsso non configuré, retournera une URL générique.');
37
- return 'https://www.helloasso.com/';
38
- }
39
-
40
- const token = await getHelloAssoAccessToken();
41
-
42
- const body = {
43
- items: [{
44
- name: `Réservation ${itemId} (événement ${eid})`,
45
- quantity,
46
- amount: Math.round(amount * 100)
47
- }],
48
- customFields: {
49
- eid,
50
- rid,
51
- uid,
52
- itemId,
53
- quantity
54
- },
55
- returnUrl: 'https://tonforum.fr/calendar/payment/ok',
56
- cancelUrl: 'https://tonforum.fr/calendar/payment/cancel'
57
- };
58
-
59
- const url = `${HELLOASSO_API_BASE}/v5/organizations/${HELLOASSO_ORG_SLUG}/forms/${HELLOASSO_FORM_SLUG}/checkout-intents`;
60
-
61
- const res = await fetch(url, {
62
- method: 'POST',
63
- headers: {
64
- Authorization: 'Bearer ' + token,
65
- 'Content-Type': 'application/json'
66
- },
67
- body: JSON.stringify(body)
68
- });
69
-
70
- if (!res.ok) {
71
- const txt = await res.text();
72
- throw new Error('[HelloAsso] erreur CheckoutIntent: ' + txt);
73
- }
74
-
75
- const data = await res.json();
76
- return data.redirectUrl || data.paymentUrl || 'https://www.helloasso.com/';
77
- }
78
-
79
- module.exports = {
80
- createHelloAssoCheckoutIntent,
81
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * HelloAsso integration helper.
5
+ *
6
+ * This file contains a minimal implementation to create a checkout "intent" URL.
7
+ * You MUST configure the following settings in the plugin ACP (or environment variables):
8
+ * - helloassoOrganizationSlug
9
+ * - helloassoFormSlug (checkout form)
10
+ * - helloassoClientId
11
+ * - helloassoClientSecret
12
+ *
13
+ * Notes:
14
+ * - HelloAsso uses OAuth2 client_credentials for API access.
15
+ * - Depending on your HelloAsso setup, endpoints may differ (sandbox/production).
16
+ * - This module is designed to be easy to adapt.
17
+ */
18
+
19
+ const meta = require.main.require('./src/meta');
20
+ const Settings = meta.settings;
21
+
22
+ let cachedToken = null;
23
+ let cachedTokenExp = 0;
24
+
25
+ async function getSettings() {
26
+ const s = (await Settings.get('calendar-onekite')) || {};
27
+ return {
28
+ apiBase: s.helloassoApiBase || 'https://api.helloasso.com',
29
+ organizationSlug: s.helloassoOrganizationSlug || '',
30
+ formSlug: s.helloassoFormSlug || '',
31
+ clientId: s.helloassoClientId || '',
32
+ clientSecret: s.helloassoClientSecret || '',
33
+ returnUrl: s.helloassoReturnUrl || '',
34
+ };
35
+ }
36
+
37
+ async function fetchJson(url, opts) {
38
+ const res = await fetch(url, opts);
39
+ const text = await res.text();
40
+ let data = null;
41
+ try { data = text ? JSON.parse(text) : null; } catch {}
42
+ if (!res.ok) {
43
+ const msg = data?.message || data?.error || text || `HTTP ${res.status}`;
44
+ throw new Error(msg);
45
+ }
46
+ return data;
47
+ }
48
+
49
+ async function getAccessToken() {
50
+ const s = await getSettings();
51
+ if (!s.clientId || !s.clientSecret) {
52
+ throw new Error('HelloAsso: clientId/clientSecret non configurés');
53
+ }
54
+
55
+ const now = Math.floor(Date.now() / 1000);
56
+ if (cachedToken && cachedTokenExp && now < cachedTokenExp - 30) {
57
+ return cachedToken;
58
+ }
59
+
60
+ const tokenUrl = `${s.apiBase}/oauth2/token`;
61
+ const body = new URLSearchParams();
62
+ body.set('grant_type', 'client_credentials');
63
+ body.set('client_id', s.clientId);
64
+ body.set('client_secret', s.clientSecret);
65
+
66
+ const data = await fetchJson(tokenUrl, {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
69
+ body,
70
+ });
71
+
72
+ cachedToken = data.access_token;
73
+ cachedTokenExp = now + Number(data.expires_in || 3600);
74
+ return cachedToken;
75
+ }
76
+
77
+ /**
78
+ * Create a checkout intent (returns a URL).
79
+ * We store rid/eid/uid in "metadata"/customFields so the webhook can reconcile.
80
+ *
81
+ * This implementation targets the "checkout intent" endpoint. If your HelloAsso
82
+ * account uses a different flow, adapt this function accordingly.
83
+ */
84
+ async function createHelloAssoCheckoutIntent({ eid, rid, uid, itemId, quantity, amount }) {
85
+ const s = await getSettings();
86
+ if (!s.organizationSlug || !s.formSlug) {
87
+ throw new Error('HelloAsso: organizationSlug/formSlug non configurés');
88
+ }
89
+
90
+ const token = await getAccessToken();
91
+
92
+ // Endpoint may vary. This is the common v5 checkout intent endpoint.
93
+ const url = `${s.apiBase}/v5/organizations/${encodeURIComponent(s.organizationSlug)}/forms/Checkout/${encodeURIComponent(s.formSlug)}/checkout-intents`;
94
+
95
+ const payload = {
96
+ totalAmount: Math.round(Number(amount || 0) * 100), // cents
97
+ initialAmount: Math.round(Number(amount || 0) * 100),
98
+ // Return URL after payment
99
+ returnUrl: s.returnUrl || undefined,
100
+ // Store custom fields for reconciliation (webhook)
101
+ metadata: {
102
+ eid: String(eid),
103
+ rid: String(rid),
104
+ uid: String(uid),
105
+ itemId: String(itemId),
106
+ quantity: String(quantity),
107
+ },
108
+ };
109
+
110
+ const data = await fetchJson(url, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Authorization': `Bearer ${token}`,
114
+ 'Content-Type': 'application/json',
115
+ },
116
+ body: JSON.stringify(payload),
117
+ });
118
+
119
+ // HelloAsso returns a checkoutUrl / redirectUrl depending on endpoint
120
+ const checkoutUrl = data?.redirectUrl || data?.checkoutUrl || data?.url;
121
+ if (!checkoutUrl) {
122
+ throw new Error('HelloAsso: checkout URL introuvable dans la réponse');
123
+ }
124
+ return checkoutUrl;
125
+ }
126
+
127
+ module.exports = {
128
+ createHelloAssoCheckoutIntent,
129
+ };