just-another-http-api 1.3.0 → 1.4.1

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/api.js CHANGED
@@ -8,6 +8,8 @@ const uploads = require ( './src/upload' );
8
8
  const auth = require ( './src/auth' );
9
9
  const cors = require ( './src/cors' );
10
10
 
11
+ const preventDuplicateRequests = require('./utils/preventDuplicateRequest');
12
+
11
13
  let app;
12
14
 
13
15
  module.exports = async ( config, _app = null ) => {
@@ -87,6 +89,9 @@ function registerEndpoint ( app, endpoint, globalConfig ) {
87
89
  if ( handlerConfig?.cors !== undefined ) {
88
90
  preHandlers.push ( cors.addCustomCors ( handlerConfig, globalConfig ) );
89
91
  }
92
+ if ( method.toLowerCase() === 'post' && handlerConfig?.preventDuplicate ) {
93
+ preHandlers.push( preventDuplicateRequests );
94
+ }
90
95
 
91
96
  const fastifyMethod = translateLegacyMethods ( method.toLowerCase () );
92
97
  const handler = endpoint.handlers[ method ];
@@ -97,6 +102,7 @@ function registerEndpoint ( app, endpoint, globalConfig ) {
97
102
  {
98
103
  preHandler: preHandlers,
99
104
  websocket: handlerConfig?.websocket || false,
105
+ bodyLimit: handlerConfig?.bodyLimit || undefined,
100
106
  },
101
107
  wrappedHandler
102
108
  );
@@ -203,8 +209,7 @@ function handleSpecialResponseTypes ( reply, response, method, path ) {
203
209
  }
204
210
 
205
211
  function sendGenericResponse ( reply, response, method, path ) {
206
-
207
- const data = response.json ?? response.body ?? response.response ?? response;
212
+ const data = response.json !== undefined ? response.json : response.body ?? response.response ?? response;
208
213
 
209
214
  if ( data !== undefined ) {
210
215
  reply.type ( 'application/json' ).code ( method === 'post' ? 201 : 200 ).send ( data );
@@ -0,0 +1,51 @@
1
+ module.exports = [ {
2
+ languageOptions: {
3
+ ecmaVersion: 2021,
4
+ sourceType: 'module',
5
+ parserOptions: {
6
+ ecmaFeatures: {
7
+ jsx: true
8
+ }
9
+ }
10
+ },
11
+ rules: {
12
+ 'max-len': 'off',
13
+ 'semi': [ 'error', 'always' ],
14
+ 'no-underscore-dangle': 'off',
15
+ 'class-methods-use-this': 'off',
16
+ 'no-param-reassign': 'off',
17
+ 'no-restricted-syntax': 'warn',
18
+ 'no-continue': 'warn',
19
+ 'consistent-return': 'warn',
20
+ 'guard-for-in': 'warn',
21
+ 'import/no-commonjs': 'off',
22
+ 'import/no-dynamic-require': 'off',
23
+ 'new-cap': 'warn',
24
+ 'no-nested-ternary': 'off',
25
+ 'prefer-const': 'error',
26
+ 'newline-before-return': [ 'error', 'always' ],
27
+ 'quotes': [ 'error', 'single' ],
28
+ 'no-multiple-empty-lines': [ 'error', { max: 1 } ],
29
+ 'space-before-function-paren': [ 'error', 'always' ],
30
+ 'func-call-spacing': [ 'error', 'always' ],
31
+ 'block-spacing': [ 'error', 'always' ],
32
+ 'space-in-parens': [ 'error', 'always' ],
33
+ 'object-curly-spacing': [ 'error', 'always' ],
34
+ 'no-spaced-func': 'off',
35
+ 'space-unary-ops': [ 'error', { 'words': true, 'nonwords': false, 'overrides': { 'new': false, '++': false } } ],
36
+ 'arrow-spacing': [ 'error', { 'before': true, 'after': true } ],
37
+ 'indent': [ 'error', 4, { 'SwitchCase': 1 } ],
38
+ 'key-spacing': [ 'error', { 'beforeColon': false, 'afterColon': true } ],
39
+ 'computed-property-spacing': [ 'error', 'always' ],
40
+ 'array-bracket-spacing': [ 'error', 'never' ],
41
+ 'comma-spacing': [ 'error', { 'before': false, 'after': true } ],
42
+ 'brace-style': [ 'error', 'stroustrup' ],
43
+ 'space-infix-ops': 'error',
44
+ 'keyword-spacing': [ 'error', { overrides: {
45
+ if: { after: true },
46
+ for: { after: true },
47
+ while: { after: true }
48
+ } } ],
49
+ 'array-bracket-spacing': [ 'error', 'always' ],
50
+ },
51
+ } ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "just-another-http-api",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "A framework built on top of fastify aimed at removing the need for any network or server configuration. ",
5
5
  "homepage": "https://github.com/OllieEdge/just-another-http-api#readme",
6
6
  "repository": {
@@ -25,23 +25,24 @@
25
25
  "author": "Oliver Edgington <oliver@edgington.com> (https://github.com/OllieEdge)",
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "@aws-sdk/lib-storage": "^3.454.0",
28
+ "@aws-sdk/lib-storage": "^3.569.0",
29
29
  "@fastify/caching": "^8.3.0",
30
30
  "@fastify/cookie": "^9.4.0",
31
- "@fastify/cors": "^8.4.1",
31
+ "@fastify/cors": "^9.0.1",
32
32
  "@fastify/jwt": "^7.2.3",
33
- "@fastify/redis": "^6.1.1",
34
- "@fastify/websocket": "^8.2.0",
33
+ "@fastify/redis": "^6.2.0",
34
+ "@fastify/websocket": "^10.0.1",
35
35
  "abstract-cache": "^1.0.1",
36
36
  "abstract-cache-redis": "^2.0.0",
37
- "fastify": "^4.24.3",
37
+ "eip-cloud-services": "^1.1.9",
38
+ "fastify": "^4.27.0",
38
39
  "fastify-multer": "^2.0.3",
39
40
  "recursive-readdir": "^2.2.3"
40
41
  },
41
42
  "devDependencies": {
42
43
  "chai": "^4.3.10",
43
44
  "chai-as-promised": "^7.1.1",
44
- "eip-cloud-services": "^1.1.0",
45
- "mocha": "^10.2.0"
45
+ "eslint": "^9.2.0",
46
+ "mocha": "^10.4.0"
46
47
  }
47
48
  }
@@ -11,6 +11,7 @@ class WebsocketGroup {
11
11
  this.connections = new Map ();
12
12
  this.messageReceivedHandler = messageReceivedHandler || ( async () => {} );
13
13
  this.connectionClosedHandler = connectionClosedHandler;
14
+ this.pingConnections = setInterval ( this.#pingConnections.bind ( this ), 30000 );
14
15
  }
15
16
 
16
17
  async initialize () {
@@ -31,17 +32,30 @@ class WebsocketGroup {
31
32
 
32
33
  #broadcastMessageToClients ( message ) {
33
34
  this.connections.forEach ( conn => {
34
- conn.socket.send ( typeof message === 'string' ? message : JSON.stringify ( message ) );
35
+ conn.send ( typeof message === 'string' ? message : JSON.stringify ( message ) );
35
36
  } );
36
37
  }
37
38
 
38
39
  #handleIndividualMessage ( individualMessage ) {
39
40
  const { connectionId, message } = JSON.parse ( individualMessage );
40
41
  if(this.connections.has(connectionId)) {
41
- this.connections.get(connectionId).socket.send(message);
42
+ this.connections.get(connectionId).send(message);
42
43
  }
43
44
  }
44
45
 
46
+ #pingConnections () {
47
+ this.connections.forEach ( ( connection, connectionId ) => {
48
+ if(!connection.isAlive) {
49
+ this.connections.delete ( connectionId );
50
+ this.#clean(connectionId);
51
+ }
52
+ else{
53
+ connection.isAlive = false;
54
+ connection.ping ();
55
+ }
56
+ } );
57
+ }
58
+
45
59
  getConnections () {
46
60
  return this.connections;
47
61
  }
@@ -55,9 +69,10 @@ class WebsocketGroup {
55
69
  }
56
70
 
57
71
  addNewConnection ( connection, connectionId = crypto.randomUUID () ) {
72
+ connection.isAlive = true;
58
73
  this.connections.set ( connectionId, connection );
59
74
 
60
- connection.socket.on ( 'message', async message => {
75
+ connection.on ( 'message', async message => {
61
76
  const userMessage = {
62
77
  groupName: this.groupName,
63
78
  connectionId,
@@ -66,12 +81,16 @@ class WebsocketGroup {
66
81
  await redis.publish ( `${this.groupName}_messageReceived`, JSON.stringify ( userMessage ) );
67
82
  } );
68
83
 
69
- connection.socket.on ( 'close', () => {
84
+ connection.on ( 'close', () => {
70
85
  this.connections.delete ( connectionId );
71
86
  this.#clean(connectionId);
72
87
  } );
88
+
89
+ connection.on ( 'pong', () => {
90
+ connection.isAlive = true;
91
+ } );
73
92
 
74
- connection.socket.on ( 'error', error => {
93
+ connection.on ( 'error', error => {
75
94
  console.error ( 'WebSocket error:', error );
76
95
  this.connections.delete ( connectionId );
77
96
  this.#clean(connectionId);
@@ -92,6 +111,7 @@ class WebsocketGroup {
92
111
  }
93
112
 
94
113
  if ( this.connections.size === 0 ) {
114
+ clearInterval ( this.pingConnections );
95
115
  this.connections.clear ();
96
116
  await redis.unsubscribe ( `${this.groupName}_individualMessage` );
97
117
  await redis.unsubscribe ( `${this.groupName}_broadcast` );
@@ -0,0 +1,27 @@
1
+ const crypto = require('crypto');
2
+ const { redis } = require('eip-cloud-services');
3
+
4
+ const TIMEFRAME_TO_PREVENT_DUPLICATE_REQUEST_SECONDS = 5
5
+
6
+ const hash = (item) => {
7
+ return crypto.createHash('sha256').update(item).digest('hex');
8
+ };
9
+
10
+ module.exports = async ( req ) => {
11
+ const clientIP = req.ip;
12
+ const requestBodyHash = hash(JSON.stringify(req.body));
13
+ const finalhash = hash(requestBodyHash);
14
+ const cacheKey = `hashedRequests:${clientIP}:${finalhash}`;
15
+
16
+ try {
17
+ const existingRequest = await redis.get(cacheKey);
18
+ if (existingRequest) {
19
+ const error = new Error('Too many requests');
20
+ error.code = 429;
21
+ throw error;
22
+ }
23
+ await redis.set(cacheKey, '1', TIMEFRAME_TO_PREVENT_DUPLICATE_REQUEST_SECONDS);
24
+ } catch (err) {
25
+ throw err;
26
+ }
27
+ };
package/.eslintrc DELETED
@@ -1,148 +0,0 @@
1
- {
2
- "parserOptions": {
3
- "ecmaVersion": 12,
4
- "sourceType": "module",
5
- "ecmaFeatures": {
6
- "jsx": true
7
- }
8
- },
9
- "env": {
10
- "commonjs": true,
11
- "node": true
12
- },
13
- "globals": {
14
- "__lib": "readonly",
15
- "__routes": "readonly",
16
- "__base": "readonly",
17
- "__utils": "readonly"
18
- },
19
- "rules": {
20
- "max-len": [
21
- "off",
22
- 120
23
- ],
24
- "semi": [
25
- "error",
26
- "always"
27
- ],
28
- "no-underscore-dangle": 0,
29
- "class-methods-use-this": 0,
30
- "no-param-reassign": 0,
31
- "no-restricted-syntax": 1,
32
- "no-continue": 1,
33
- "consistent-return": 1,
34
- "guard-for-in": 1,
35
- "import/no-commonjs": 0,
36
- "import/no-dynamic-require": 0,
37
- "new-cap": 1,
38
- "no-nested-ternary": 0,
39
- "prefer-const": "error",
40
- "newline-before-return": "error",
41
- "quotes": [
42
- "error",
43
- "single"
44
- ],
45
- "no-multiple-empty-lines": [
46
- "error",
47
- {
48
- "max": 1
49
- }
50
- ],
51
- "space-before-function-paren": [
52
- "error",
53
- "always"
54
- ],
55
- "func-call-spacing": [
56
- "error",
57
- "always"
58
- ],
59
- "block-spacing": [
60
- "error",
61
- "always"
62
- ],
63
- "space-in-parens": [
64
- "error",
65
- "always"
66
- ],
67
- "object-curly-spacing": [
68
- "error",
69
- "always"
70
- ],
71
- "no-spaced-func": 0,
72
- "space-unary-ops": [
73
- "error",
74
- {
75
- "words": true,
76
- "nonwords": false,
77
- "overrides": {
78
- "new": false,
79
- "++": false
80
- }
81
- }
82
- ],
83
- "arrow-spacing": [
84
- "error",
85
- {
86
- "before": true,
87
- "after": true
88
- }
89
- ],
90
- "indent": [
91
- "error",
92
- 4,
93
- {
94
- "SwitchCase": 1
95
- }
96
- ],
97
- "key-spacing": [
98
- "error",
99
- {
100
- "beforeColon": false,
101
- "afterColon": true
102
- }
103
- ],
104
- "computed-property-spacing": [
105
- "error",
106
- "always"
107
- ],
108
- "array-bracket-spacing": [
109
- "error",
110
- "always"
111
- ],
112
- "comma-spacing": [
113
- "error",
114
- {
115
- "before": false,
116
- "after": true
117
- }
118
- ],
119
- "brace-style": [
120
- "error",
121
- "stroustrup"
122
- ],
123
- "space-infix-ops": [
124
- "error",
125
- {
126
- "int32Hint": true
127
- }
128
- ],
129
- "keyword-spacing": [
130
- "error",
131
- {
132
- "overrides": {
133
- "if": {
134
- "after": true
135
- },
136
- "for": {
137
- "after": true
138
- },
139
- "while": {
140
- "after": true
141
- }
142
- }
143
- }
144
- ],
145
- "no-octal-escape": 0,
146
- "no-octal": 0
147
- }
148
- }