@jcbuisson/express-x 2.1.14 → 2.1.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "src/index.mjs",
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,
@@ -310,7 +297,7 @@ export function expressX(config) {
310
297
  })
311
298
  }
312
299
 
313
- export default class EXError extends Error {
300
+ export class EXError extends Error {
314
301
  constructor(code, message) {
315
302
  super(message)
316
303
  this.code = code
@@ -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.data.expiresAt
346
+ const expiresAt = context.socket?.data?.expiresAt
360
347
  if (expiresAt) {
361
348
  const expiresAtDate = new Date(expiresAt)
362
349
  const now = new Date()
@@ -385,8 +372,9 @@ export const isNotExpired = async (context) => {
385
372
  */
386
373
  export const isAuthenticated = async (context) => {
387
374
  // do nothing if it's not a client call from a ws connexion
388
- if (!context.socket) return
389
- if (!context.socket.data.user) throw new EXError('not-authenticated', 'no user in socket.data')
375
+ if (context.caller !== 'client') return
376
+ if (!context.socket?.data) throw new EXError('not-authenticated', 'no data in socket')
377
+ if (!context.socket.data?.user) throw new EXError('not-authenticated', 'no user in socket.data')
390
378
  }
391
379
 
392
380
  /*
@@ -394,5 +382,7 @@ export const isAuthenticated = async (context) => {
394
382
  */
395
383
  export const extendExpiration = (duration) => async (context) => {
396
384
  const now = new Date()
385
+ if (context.caller !== 'client') return
386
+ if (!context.socket?.data) throw new EXError('not-authenticated', 'no data in socket')
397
387
  context.socket.data.expiresAt = new Date(now.getTime() + duration)
398
388
  }
@@ -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
- }