@jcbuisson/express-x 2.1.15 → 2.1.17
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/package.json +1 -1
- package/src/index.mjs +25 -21
- package/src/common-hooks.mjs +0 -81
- package/src/server.mjs +0 -300
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
// import { expressX } from './server.mjs'
|
|
3
|
-
// import { addTimestamp, hashPassword, protect, isAuthenticated, isNotExpired, extendExpiration } from './common-hooks.mjs'
|
|
4
|
-
|
|
5
|
-
// export {
|
|
6
|
-
// expressX,
|
|
7
|
-
|
|
8
|
-
// addTimestamp,
|
|
9
|
-
// hashPassword,
|
|
10
|
-
// protect,
|
|
11
|
-
// isAuthenticated,
|
|
12
|
-
// isNotExpired,
|
|
13
|
-
// extendExpiration,
|
|
14
|
-
// }
|
|
15
|
-
|
|
16
|
-
|
|
17
1
|
import express from "express"
|
|
18
2
|
import { createServer } from "http"
|
|
19
3
|
import { Server } from "socket.io"
|
|
@@ -124,6 +108,9 @@ export function expressX(config) {
|
|
|
124
108
|
|
|
125
109
|
try {
|
|
126
110
|
// call method with context
|
|
111
|
+
if (name === 'highlighted_part') {
|
|
112
|
+
console.log('')
|
|
113
|
+
}
|
|
127
114
|
const result = await serviceMethod(context, ...args)
|
|
128
115
|
|
|
129
116
|
const trimmedResult = result ? JSON.stringify(result).slice(0, 300) : ''
|
|
@@ -133,7 +120,7 @@ export function expressX(config) {
|
|
|
133
120
|
result,
|
|
134
121
|
})
|
|
135
122
|
} catch(err) {
|
|
136
|
-
console.log('!!!!!!error', err.code, err.message)
|
|
123
|
+
console.log('!!!!!!error', 'name', name, 'action', action, 'args', args, 'err.code', err.code, 'err.message', err.message)
|
|
137
124
|
app.log('verbose', err.stack)
|
|
138
125
|
socket.emit('client-response', {
|
|
139
126
|
uid,
|
|
@@ -356,7 +343,7 @@ export function protect(field) {
|
|
|
356
343
|
export const isNotExpired = async (context) => {
|
|
357
344
|
// do nothing if it's not a client call from a ws connexion
|
|
358
345
|
if (!context.socket) return
|
|
359
|
-
const expiresAt = context.socket
|
|
346
|
+
const expiresAt = context.socket?.data?.expiresAt
|
|
360
347
|
if (expiresAt) {
|
|
361
348
|
const expiresAtDate = new Date(expiresAt)
|
|
362
349
|
const now = new Date()
|
|
@@ -371,11 +358,13 @@ export const isNotExpired = async (context) => {
|
|
|
371
358
|
context.socket.leave(room)
|
|
372
359
|
}
|
|
373
360
|
// send an event to the client (typical client handling: logout)
|
|
374
|
-
context.socket.emit('
|
|
361
|
+
context.socket.emit('not-authenticated')
|
|
375
362
|
// throw exception
|
|
376
363
|
throw new EXError('not-authenticated', "Session expired")
|
|
377
364
|
}
|
|
378
365
|
} else {
|
|
366
|
+
// send an event to the client (typical client handling: logout)
|
|
367
|
+
context.socket.emit('not-authenticated')
|
|
379
368
|
throw new EXError('not-authenticated', "No expiresAt in socket.data")
|
|
380
369
|
}
|
|
381
370
|
}
|
|
@@ -385,8 +374,17 @@ export const isNotExpired = async (context) => {
|
|
|
385
374
|
*/
|
|
386
375
|
export const isAuthenticated = async (context) => {
|
|
387
376
|
// do nothing if it's not a client call from a ws connexion
|
|
388
|
-
if (
|
|
389
|
-
if (!context.socket
|
|
377
|
+
if (context.caller !== 'client') return
|
|
378
|
+
if (!context.socket?.data) {
|
|
379
|
+
// send an event to the client (typical client handling: logout)
|
|
380
|
+
context.socket.emit('not-authenticated')
|
|
381
|
+
throw new EXError('not-authenticated', 'no data in socket')
|
|
382
|
+
}
|
|
383
|
+
if (!context.socket.data?.user) {
|
|
384
|
+
// send an event to the client (typical client handling: logout)
|
|
385
|
+
context.socket.emit('not-authenticated')
|
|
386
|
+
throw new EXError('not-authenticated', 'no user in socket.data')
|
|
387
|
+
}
|
|
390
388
|
}
|
|
391
389
|
|
|
392
390
|
/*
|
|
@@ -394,5 +392,11 @@ export const isAuthenticated = async (context) => {
|
|
|
394
392
|
*/
|
|
395
393
|
export const extendExpiration = (duration) => async (context) => {
|
|
396
394
|
const now = new Date()
|
|
395
|
+
if (context.caller !== 'client') return
|
|
396
|
+
if (!context.socket?.data) {
|
|
397
|
+
// send an event to the client (typical client handling: logout)
|
|
398
|
+
context.socket.emit('not-authenticated')
|
|
399
|
+
throw new EXError('not-authenticated', 'no data in socket')
|
|
400
|
+
}
|
|
397
401
|
context.socket.data.expiresAt = new Date(now.getTime() + duration)
|
|
398
402
|
}
|
package/src/common-hooks.mjs
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import bcrypt from 'bcryptjs'
|
|
3
|
-
|
|
4
|
-
import { EXError } from './server.mjs'
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
* Add a timestamp property of name `field` with current time as value
|
|
8
|
-
*/
|
|
9
|
-
export const addTimestamp = (field) => async (context) => {
|
|
10
|
-
context.result[field] = (new Date()).toISOString()
|
|
11
|
-
return context
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/*
|
|
15
|
-
* Hash password of the property `field`
|
|
16
|
-
*/
|
|
17
|
-
export const hashPassword = (passwordField) => async (context) => {
|
|
18
|
-
const user = context.result
|
|
19
|
-
user[passwordField] = await bcrypt.hash(user[passwordField], 5)
|
|
20
|
-
return context
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/*
|
|
24
|
-
* Remove `field` from `context.result`
|
|
25
|
-
*/
|
|
26
|
-
export function protect(field) {
|
|
27
|
-
return async (context) => {
|
|
28
|
-
if (Array.isArray(context.result)) {
|
|
29
|
-
for (const value of context.result) {
|
|
30
|
-
delete value[field]
|
|
31
|
-
}
|
|
32
|
-
} else {
|
|
33
|
-
delete context.result[field]
|
|
34
|
-
}
|
|
35
|
-
return (context)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const isNotExpired = async (context) => {
|
|
40
|
-
// do nothing if it's not a client call from a ws connexion
|
|
41
|
-
if (!context.socket) return
|
|
42
|
-
const expiresAt = context.socket.data.expiresAt
|
|
43
|
-
if (expiresAt) {
|
|
44
|
-
const expiresAtDate = new Date(expiresAt)
|
|
45
|
-
const now = new Date()
|
|
46
|
-
if (now > expiresAtDate) {
|
|
47
|
-
// expiration date is met
|
|
48
|
-
// clear socket.data
|
|
49
|
-
context.socket.data = {}
|
|
50
|
-
// leave all rooms except socket#id
|
|
51
|
-
const rooms = new Set(context.socket.rooms)
|
|
52
|
-
for (const room of rooms) {
|
|
53
|
-
if (room === context.socket.id) continue
|
|
54
|
-
context.socket.leave(room)
|
|
55
|
-
}
|
|
56
|
-
// send an event to the client (typical client handling: logout)
|
|
57
|
-
context.socket.emit('expired')
|
|
58
|
-
// throw exception
|
|
59
|
-
throw new EXError('not-authenticated', "Session expired")
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
throw new EXError('not-authenticated', "No expiresAt in socket.data")
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/*
|
|
67
|
-
* Throw an error for a client service method call when socket.data does not contain user
|
|
68
|
-
*/
|
|
69
|
-
export const isAuthenticated = async (context) => {
|
|
70
|
-
// do nothing if it's not a client call from a ws connexion
|
|
71
|
-
if (!context.socket) return
|
|
72
|
-
if (!context.socket.data.user) throw new EXError('not-authenticated', 'no user in socket.data')
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/*
|
|
76
|
-
* Extend value of socket.data.expiresAt of `duration` milliseconds
|
|
77
|
-
*/
|
|
78
|
-
export const extendExpiration = (duration) => async (context) => {
|
|
79
|
-
const now = new Date()
|
|
80
|
-
context.socket.data.expiresAt = new Date(now.getTime() + duration)
|
|
81
|
-
}
|
package/src/server.mjs
DELETED
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import express from "express"
|
|
2
|
-
import { createServer } from "http"
|
|
3
|
-
import { Server } from "socket.io"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// UTILISER L'ACKNOWLEDGEMENT : https://socket.io/docs/v4/#acknowledgements
|
|
7
|
-
|
|
8
|
-
export default class EXError extends Error {
|
|
9
|
-
constructor(code, message) {
|
|
10
|
-
super(message)
|
|
11
|
-
this.code = code
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function expressX(config) {
|
|
16
|
-
|
|
17
|
-
const services = {}
|
|
18
|
-
const socketConnectListeners = []
|
|
19
|
-
const socketDisconnectingListeners = []
|
|
20
|
-
const socketDisconnectListeners = []
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function addConnectListener(func) {
|
|
24
|
-
socketConnectListeners.push(func)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function addDisconnectingListener(func) {
|
|
28
|
-
socketDisconnectingListeners.push(func)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function addDisconnectListener(func) {
|
|
32
|
-
socketDisconnectListeners.push(func)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const app = express()
|
|
36
|
-
const httpServer = createServer(app)
|
|
37
|
-
|
|
38
|
-
// so that config can be accessed anywhere with app.get('config')
|
|
39
|
-
app.set('config', config)
|
|
40
|
-
|
|
41
|
-
const io = new Server(httpServer, {
|
|
42
|
-
path: config?.WS_PATH || '/socket.io/',
|
|
43
|
-
connectionStateRecovery: {
|
|
44
|
-
// the backup duration of the sessions and the packets
|
|
45
|
-
maxDisconnectionDuration: 2 * 60 * 1000,
|
|
46
|
-
// whether to skip middlewares upon successful recovery
|
|
47
|
-
skipMiddlewares: true,
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
// so that io server is accessible to hooks & services
|
|
52
|
-
app.set('io', io)
|
|
53
|
-
|
|
54
|
-
// logging function - a winston logger must be configured first
|
|
55
|
-
app.log = (severity, message) => {
|
|
56
|
-
const logger = app.get('logger')
|
|
57
|
-
if (logger) logger.log(severity, message); else console.log(`[${severity}]`, message)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
io.on('connection', async function(socket) {
|
|
61
|
-
if (socket.recovered) {
|
|
62
|
-
// recovery was successful: socket.id, socket.rooms and socket.data were restored
|
|
63
|
-
// (network/Wifi disconnections)
|
|
64
|
-
console.log('reconnection!!!', socket.id, socket.data)
|
|
65
|
-
} else {
|
|
66
|
-
// new or unrecoverable connection
|
|
67
|
-
// (page open, page refresh/reload)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
app.log('verbose', `Client connected ${socket.id}`)
|
|
71
|
-
|
|
72
|
-
// // emit 'connection' event for app (expressjs extends EventEmitter)
|
|
73
|
-
// app.emit('connection', socket)
|
|
74
|
-
|
|
75
|
-
socketConnectListeners.forEach(listener => listener(socket))
|
|
76
|
-
|
|
77
|
-
// // send 'connected' event to client
|
|
78
|
-
// socket.emit('connected', socket.id)
|
|
79
|
-
|
|
80
|
-
socket.on('disconnecting', (reason) => {
|
|
81
|
-
app.log('verbose', `Client disconnecting ${socket.id}, ${reason}`)
|
|
82
|
-
socketDisconnectingListeners.forEach(listener => listener(socket, reason))
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
socket.on('disconnect', (reason) => {
|
|
86
|
-
app.log('verbose', `Client disconnect ${socket.id}, ${reason}`)
|
|
87
|
-
socketDisconnectListeners.forEach(listener => listener(socket, reason))
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
/*
|
|
91
|
-
* Handle websocket client request
|
|
92
|
-
* Emit in return a 'client-response' message
|
|
93
|
-
*/
|
|
94
|
-
socket.on('client-request', async ({ uid, name, action, args }) => {
|
|
95
|
-
const trimmedArgs = args ? JSON.stringify(args).slice(0, 300) : ''
|
|
96
|
-
app.log('verbose', `client-request ${uid} ${name} ${action} ${trimmedArgs}`)
|
|
97
|
-
if (name in services) {
|
|
98
|
-
const service = services[name]
|
|
99
|
-
try {
|
|
100
|
-
const serviceMethod = service['__' + action]
|
|
101
|
-
if (serviceMethod) {
|
|
102
|
-
const context = {
|
|
103
|
-
app,
|
|
104
|
-
caller: 'client',
|
|
105
|
-
transport: 'ws',
|
|
106
|
-
socket,
|
|
107
|
-
// connectionId,
|
|
108
|
-
serviceName: name,
|
|
109
|
-
methodName: action,
|
|
110
|
-
args,
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
// call method with context
|
|
115
|
-
const result = await serviceMethod(context, ...args)
|
|
116
|
-
|
|
117
|
-
const trimmedResult = result ? JSON.stringify(result).slice(0, 300) : ''
|
|
118
|
-
app.log('verbose', `client-response ${uid} ${trimmedResult}`)
|
|
119
|
-
socket.emit('client-response', {
|
|
120
|
-
uid,
|
|
121
|
-
result,
|
|
122
|
-
})
|
|
123
|
-
} catch(err) {
|
|
124
|
-
console.log('!!!!!!error', err.code, err.message)
|
|
125
|
-
app.log('verbose', err.stack)
|
|
126
|
-
socket.emit('client-response', {
|
|
127
|
-
uid,
|
|
128
|
-
error: {
|
|
129
|
-
code: err.code || 'unknown-error',
|
|
130
|
-
message: err.message,
|
|
131
|
-
stack: err.stack,
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
} else {
|
|
136
|
-
socket.emit('client-response', {
|
|
137
|
-
uid,
|
|
138
|
-
error: {
|
|
139
|
-
code: 'missing-method',
|
|
140
|
-
message: `there is no method named '${action}' for service '${name}'`,
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
} catch(err) {
|
|
145
|
-
console.log('err', err)
|
|
146
|
-
app.log('verbose', err.stack)
|
|
147
|
-
socket.emit('client-response', {
|
|
148
|
-
uid,
|
|
149
|
-
error: {
|
|
150
|
-
code: err.code || 'unknown-error',
|
|
151
|
-
message: err.message,
|
|
152
|
-
stack: err.stack,
|
|
153
|
-
}
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
socket.emit('client-response', {
|
|
158
|
-
uid,
|
|
159
|
-
error: {
|
|
160
|
-
code: 'missing-service',
|
|
161
|
-
message: `there is no service named '${name}'`,
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
/*
|
|
170
|
-
* create a service `name` with given `methods`
|
|
171
|
-
*/
|
|
172
|
-
function createService(name, methods) {
|
|
173
|
-
const service = {}
|
|
174
|
-
|
|
175
|
-
for (const methodName in methods) {
|
|
176
|
-
const method = methods[methodName]
|
|
177
|
-
if (! method instanceof Function) continue
|
|
178
|
-
|
|
179
|
-
// `context` is the context of execution (transport type, connection, app)
|
|
180
|
-
// `args` is the list of arguments of the method
|
|
181
|
-
service['__' + methodName] = async (context, ...args) => {
|
|
182
|
-
// put args into context
|
|
183
|
-
context.args = args
|
|
184
|
-
|
|
185
|
-
// if a hook or the method throws an error, it will be caught by `socket.on('client-request'`
|
|
186
|
-
// and the client will get a rejected promise
|
|
187
|
-
|
|
188
|
-
// call 'before' hooks, possibly modifying `context`
|
|
189
|
-
const beforeMethodHooks = service?._hooks?.before && service._hooks.before[methodName] || []
|
|
190
|
-
const beforeAllHooks = service?._hooks?.before?.all || []
|
|
191
|
-
for (const hook of [...beforeAllHooks, ...beforeMethodHooks]) {
|
|
192
|
-
await hook(context)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// call method
|
|
196
|
-
const result = await method(...args)
|
|
197
|
-
// put result into context
|
|
198
|
-
context.result = result
|
|
199
|
-
|
|
200
|
-
// call 'after' hooks, possibly modifying `context`
|
|
201
|
-
const afterMethodHooks = service?._hooks?.after && service._hooks.after[methodName] || []
|
|
202
|
-
const afterAllHooks = service?._hooks?.after?.all || []
|
|
203
|
-
for (const hook of [...afterMethodHooks, ...afterAllHooks]) {
|
|
204
|
-
await hook(context)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// publish 'service-event' event associated to service/method
|
|
208
|
-
if (service.publishFunction) {
|
|
209
|
-
// collect channel names to socket is member of
|
|
210
|
-
const channelNames = await service.publishFunction(context)
|
|
211
|
-
app.log('verbose', `publish channels ${name} ${methodName} ${channelNames}`)
|
|
212
|
-
// send event on all these channels
|
|
213
|
-
if (channelNames.length > 0) {
|
|
214
|
-
let sender = io.to(channelNames[0])
|
|
215
|
-
for (let i = 1; i < channelNames.length; i++) {
|
|
216
|
-
sender = sender.to(channelNames[i])
|
|
217
|
-
}
|
|
218
|
-
sender.emit('service-event', {
|
|
219
|
-
name,
|
|
220
|
-
action: methodName,
|
|
221
|
-
result,
|
|
222
|
-
})
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return context.result
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// hooked version of method to be used server-side
|
|
230
|
-
service[methodName] = (...args) => {
|
|
231
|
-
const context = {
|
|
232
|
-
app,
|
|
233
|
-
caller: 'server',
|
|
234
|
-
serviceName: service._name,
|
|
235
|
-
methodName,
|
|
236
|
-
args,
|
|
237
|
-
}
|
|
238
|
-
const hookedMethod = service['__' + methodName]
|
|
239
|
-
return hookedMethod(context, ...args)
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// attach pub/sub publish callback
|
|
244
|
-
service.publish = async (func) => {
|
|
245
|
-
service.publishFunction = func
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
// attach hooks
|
|
249
|
-
service.hooks = (hooks) => {
|
|
250
|
-
service._hooks = hooks
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// cache service in `services`
|
|
254
|
-
services[name] = service
|
|
255
|
-
return service
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function configure(callback) {
|
|
259
|
-
callback(app)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// `app.service(name)` starts here!
|
|
263
|
-
function service(name) {
|
|
264
|
-
// get service from `services` cache
|
|
265
|
-
if (name in services) return services[name]
|
|
266
|
-
app.log('error', `there is no service named '${name}'`, 'missing-service')
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function joinChannel(channelName, socket) {
|
|
270
|
-
app.log('verbose', `joining ${channelName}, ${JSON.stringify(socket.data)}`)
|
|
271
|
-
socket.join(channelName)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function leaveChannel(channelName, socket) {
|
|
275
|
-
app.log('verbose', `leaving ${channelName}, ${JSON.stringify(socket.data)}`)
|
|
276
|
-
socket.leave(channelName)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// There is a need for events sent outside any service method call, for example on unsolicited-by-frontend backend state change
|
|
280
|
-
async function sendAppEvent(channelName, type, value) {
|
|
281
|
-
console.log('sendAppEvent', channelName, type, value)
|
|
282
|
-
io.to(channelName).emit('app-event', { type, value })
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// enhance `app` with objects and methods
|
|
286
|
-
return Object.assign(app, {
|
|
287
|
-
io,
|
|
288
|
-
httpServer,
|
|
289
|
-
createService,
|
|
290
|
-
service,
|
|
291
|
-
configure,
|
|
292
|
-
joinChannel,
|
|
293
|
-
leaveChannel,
|
|
294
|
-
sendAppEvent,
|
|
295
|
-
addConnectListener,
|
|
296
|
-
addDisconnectingListener,
|
|
297
|
-
addDisconnectListener,
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
}
|