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 +62 -0
- package/README.md +21 -0
- package/helloasso.js +129 -81
- package/library.js +290 -454
- package/package.json +18 -18
- package/plugin.json +33 -21
- package/static/css/calendar-onekite.css +4 -0
- package/static/js/admin.js +184 -172
- package/static/js/calendar-my-reservations.js +55 -0
- package/static/js/calendar.js +429 -42
- package/templates/admin/calendar-planning.tpl +25 -30
- package/templates/admin/plugins/calendar-onekite.tpl +61 -34
- package/templates/calendar-my-reservations.tpl +29 -45
- package/templates/calendar.tpl +100 -91
- package/templates/emails/calendar-payment-confirmed.tpl +1 -12
- package/templates/emails/calendar-reservation-approved.tpl +1 -13
- package/templates/emails/calendar-reservation-created.tpl +1 -13
- package/templates/widgets/calendar-upcoming.tpl +14 -22
- package/static/style.css +0 -47
- package/templates/admin/plugins/calendar-planning.tpl +0 -30
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
};
|