just-another-http-api 1.0.4 → 1.2.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/.eslintrc +148 -0
- package/LICENSE +1 -1
- package/README.md +401 -151
- package/api.js +236 -125
- package/package.json +15 -9
- package/src/auth.js +140 -0
- package/src/cache.js +69 -0
- package/src/cors.js +9 -0
- package/src/upload.js +146 -0
- package/utils/response.js +48 -0
package/api.js
CHANGED
|
@@ -1,133 +1,262 @@
|
|
|
1
|
-
const
|
|
2
|
-
const restifyErrors = require ( 'restify-errors' );
|
|
3
|
-
const corsPlugin = require ( 'restify-cors-middleware2' );
|
|
1
|
+
const fastify = require ( 'fastify' );
|
|
4
2
|
const recursiveRead = require ( 'recursive-readdir' );
|
|
5
3
|
const packageJson = require ( './package.json' );
|
|
6
4
|
const path = require ( 'path' );
|
|
7
|
-
const multer = require ( 'multer' );
|
|
8
|
-
const storage = multer.memoryStorage ();
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* @param {Function} every An optional agrument that accepts a function ready to receive an object. The function will be called everytime a endpoint is requested (good for analytical usage)
|
|
15
|
-
* @param {RestifyServer} _server Optionally override the restify instance in this API and use your own. Accepts a `restify.createServer()` instance.
|
|
16
|
-
*/
|
|
17
|
-
module.exports = async ( config, every = null, _server = null ) => {
|
|
6
|
+
const caching = require ( './src/cache' );
|
|
7
|
+
const uploads = require ( './src/upload' );
|
|
8
|
+
const auth = require ( './src/auth' );
|
|
9
|
+
const cors = require ( './src/cors' );
|
|
18
10
|
|
|
19
|
-
|
|
11
|
+
let app;
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
module.exports = async ( config, _app = null ) => {
|
|
14
|
+
app = _app || await createServer ( config );
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
const endpoints = await loadEndpoints ( config );
|
|
17
|
+
endpoints.forEach ( endpoint => registerEndpoint ( app, endpoint, config ) );
|
|
18
|
+
|
|
19
|
+
await app.listen ( { port: process.env.PORT || config?.port || 4001 } );
|
|
20
|
+
|
|
21
|
+
return app;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function createServer ( config ) {
|
|
25
|
+
const app = fastify ( {
|
|
26
|
+
logger: config.logs || false,
|
|
27
|
+
name: config.name || packageJson.name,
|
|
28
|
+
} );
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if ( config
|
|
30
|
+
try {
|
|
31
|
+
|
|
32
|
+
if ( config.cors ) {
|
|
33
|
+
app.register ( require ( '@fastify/cors' ), ( instance ) => {
|
|
34
|
+
return ( req, callback ) => {
|
|
35
|
+
const corsOptions = {
|
|
36
|
+
// This is NOT recommended for production as it enables reflection exploits
|
|
37
|
+
origin: true
|
|
38
|
+
};
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
console.log ( 'hbawfjawhv' );
|
|
41
|
+
|
|
42
|
+
// // do not include CORS headers for requests from localhost
|
|
43
|
+
// if ( /^localhost$/m.test ( req.headers.origin ) ) {
|
|
44
|
+
// corsOptions.origin = false;
|
|
45
|
+
// }
|
|
46
|
+
|
|
47
|
+
// callback expects two parameters: error and options
|
|
48
|
+
callback ( null, corsOptions );
|
|
49
|
+
};
|
|
50
|
+
} );
|
|
51
|
+
}
|
|
52
|
+
await uploads.initialiseUploads ( app, config );
|
|
53
|
+
await caching.initialiseCaching ( app, config );
|
|
54
|
+
await auth.initialiseAuth ( app, config );
|
|
55
|
+
|
|
56
|
+
if ( config.middleware && Array.isArray ( config.middleware ) ) {
|
|
57
|
+
for ( const func of config.middleware ) {
|
|
58
|
+
if ( typeof func === 'function' ) {
|
|
59
|
+
await app.register ( func );
|
|
60
|
+
}
|
|
61
|
+
}
|
|
39
62
|
}
|
|
40
63
|
}
|
|
64
|
+
catch ( error ) {
|
|
65
|
+
console.error ( 'Error during server initialization:', error );
|
|
66
|
+
throw error; // Rethrow the error to handle it at a higher level, if necessary
|
|
67
|
+
}
|
|
41
68
|
|
|
42
|
-
|
|
69
|
+
return app;
|
|
70
|
+
}
|
|
43
71
|
|
|
72
|
+
async function loadEndpoints ( config ) {
|
|
44
73
|
const files = await recursiveReadDir ( config?.docRoot || 'routes' );
|
|
45
|
-
|
|
74
|
+
|
|
75
|
+
return files.map ( filePath => ( {
|
|
46
76
|
handlers: require ( path.resolve ( filePath ) ),
|
|
47
77
|
path: handlerPathToApiPath ( filePath, config?.docRoot || 'routes' )
|
|
48
78
|
} ) );
|
|
79
|
+
}
|
|
49
80
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
method === 'post' && upload ? upload.single ( 'file' ) : null
|
|
55
|
-
].filter ( Boolean );
|
|
81
|
+
function registerEndpoint ( app, endpoint, globalConfig ) {
|
|
82
|
+
Object.keys ( endpoint.handlers ).filter ( method => method !== 'config' ).forEach ( method => {
|
|
83
|
+
const handlerConfig = endpoint.handlers.config?.[ method ] || {};
|
|
84
|
+
const requiresAuth = handlerConfig?.requiresAuth !== undefined ? handlerConfig.requiresAuth : !!globalConfig?.auth?.requiresAuth;
|
|
56
85
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if ( error.code ) {
|
|
115
|
-
res.send ( new restifyErrors.makeErrFromCode ( error.code, error.message ) );
|
|
116
|
-
}
|
|
86
|
+
const preHandlers = [];
|
|
87
|
+
if ( requiresAuth ) {
|
|
88
|
+
preHandlers.push ( async ( req, reply ) => {
|
|
89
|
+
req.authConfig = handlerConfig.auth || globalConfig.auth;
|
|
90
|
+
await auth.checkAuth ( req, reply );
|
|
91
|
+
} );
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ( globalConfig?.uploads?.enabled && handlerConfig?.upload?.enabled ) {
|
|
95
|
+
preHandlers.push ( uploads.handleUpload ( handlerConfig, globalConfig ) );
|
|
96
|
+
}
|
|
97
|
+
if ( handlerConfig?.cors !== undefined ) {
|
|
98
|
+
preHandlers.push ( cors.addCustomCors ( handlerConfig, globalConfig ) );
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fastifyMethod = translateLegacyMethods ( method.toLowerCase () );
|
|
102
|
+
const handler = endpoint.handlers[ method ];
|
|
103
|
+
const wrappedHandler = fastifyHandlerWrapper ( handler, endpoint.handlers.config, globalConfig );
|
|
104
|
+
|
|
105
|
+
app[ fastifyMethod ] (
|
|
106
|
+
endpoint.path,
|
|
107
|
+
{
|
|
108
|
+
preHandler: preHandlers
|
|
109
|
+
},
|
|
110
|
+
wrappedHandler
|
|
111
|
+
);
|
|
112
|
+
} );
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function translateLegacyMethods ( method ) {
|
|
116
|
+
switch ( method ) {
|
|
117
|
+
case 'del':
|
|
118
|
+
return 'delete';
|
|
119
|
+
default:
|
|
120
|
+
return method;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function fastifyHandlerWrapper ( handler, config, globalConfig ) {
|
|
125
|
+
return async ( req, reply ) => {
|
|
126
|
+
try {
|
|
127
|
+
let response = await caching.checkRequestCache ( app, req, reply, config, globalConfig );
|
|
128
|
+
|
|
129
|
+
if ( !response ){
|
|
130
|
+
response = await handler ( req );
|
|
131
|
+
await caching.setRequestCache ( app, req, response, config, globalConfig );
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
handleResponse ( reply, response, req.method, req.routeOptions.url );
|
|
135
|
+
}
|
|
136
|
+
catch ( error ) {
|
|
137
|
+
handleError ( reply, error );
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
function handleResponse ( reply, response, method, path ) {
|
|
117
143
|
|
|
118
|
-
|
|
119
|
-
|
|
144
|
+
if ( !response ) {
|
|
145
|
+
reply.code ( 204 ).send ();
|
|
146
|
+
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
120
149
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
if ( typeof response !== 'object' || response === null ) {
|
|
151
|
+
handleNonObjectResponse ( reply, response, method );
|
|
152
|
+
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setResponseHeaders ( reply, response );
|
|
157
|
+
handleSpecialResponseTypes ( reply, response, method, path );
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function setResponseHeaders ( reply, response ) {
|
|
161
|
+
if ( response.headers ) {
|
|
162
|
+
Object.entries ( response.headers ).forEach ( ( [ key, value ] ) => {
|
|
163
|
+
reply.header ( key, value );
|
|
124
164
|
} );
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleSpecialResponseTypes ( reply, response, method, path ) {
|
|
169
|
+
if ( response.redirect ) {
|
|
170
|
+
reply.redirect ( 301, response.redirect.url );
|
|
171
|
+
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if ( response.html ) {
|
|
176
|
+
reply.code ( 200 ).type ( 'text/html' ).send ( response.html );
|
|
177
|
+
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if ( response.text ) {
|
|
182
|
+
reply.code ( 200 ).type ( 'text/plain' ).send ( response.text );
|
|
183
|
+
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if ( response.error ) {
|
|
188
|
+
handleErrorResponse ( reply, response.error );
|
|
189
|
+
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if ( response.file ) {
|
|
194
|
+
reply.send ( response.file );
|
|
195
|
+
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sendGenericResponse ( reply, response, method, path );
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sendGenericResponse ( reply, response, method, path ) {
|
|
203
|
+
|
|
204
|
+
const data = response.json ?? response.body ?? response.response ?? response;
|
|
205
|
+
|
|
206
|
+
if ( data !== undefined ) {
|
|
207
|
+
reply.type ( 'application/json' ).code ( method === 'post' ? 201 : 200 ).send ( data );
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
handleUnknownResponseType ( reply, method, path );
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function handleErrorResponse ( reply, error ) {
|
|
216
|
+
console.error ( error );
|
|
217
|
+
const statusCode = error?.statusCode ?? 500;
|
|
218
|
+
reply.code ( statusCode ).type ( 'application/json' ).send ( { error: error.message } );
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handleUnknownResponseType ( reply, method, path ) {
|
|
222
|
+
reply.type ( 'application/json' ).code ( 500 ).send ( {
|
|
223
|
+
error: `Unrecognized response type for ${method} ${path}`
|
|
125
224
|
} );
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function handleNonObjectResponse ( reply, response, method ) {
|
|
228
|
+
reply.type ( 'text/plain' ).code ( method === 'post' ? 201 : 200 ).send ( response.toString () );
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handleError ( reply, error ) {
|
|
232
|
+
// Set content type for the error response
|
|
233
|
+
reply.header ( 'Content-Type', 'application/json' );
|
|
234
|
+
|
|
235
|
+
if ( error instanceof Error ) {
|
|
236
|
+
// Send internal server error with the error stack
|
|
237
|
+
reply.status ( 500 ).send ( {
|
|
238
|
+
error: 'Internal Server Error',
|
|
239
|
+
message: error.message,
|
|
240
|
+
stack: error.stack
|
|
241
|
+
} );
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Check if error object contains a specific status code
|
|
245
|
+
if ( error.code && typeof error.code === 'number' ) {
|
|
246
|
+
reply.status ( error.code ).send ( {
|
|
247
|
+
error: 'Error',
|
|
248
|
+
message: error.message
|
|
249
|
+
} );
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Send a generic internal server error response
|
|
253
|
+
reply.status ( 500 ).send ( {
|
|
254
|
+
error: 'Internal Server Error',
|
|
255
|
+
message: 'An unknown error occurred'
|
|
256
|
+
} );
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
131
260
|
|
|
132
261
|
const handlerPathToApiPath = ( path, docRoot ) => {
|
|
133
262
|
|
|
@@ -152,28 +281,10 @@ const recursiveReadDir = async ( docRoot ) => {
|
|
|
152
281
|
try {
|
|
153
282
|
const files = await recursiveRead ( docRoot );
|
|
154
283
|
|
|
155
|
-
// Remove all falsy values and reverse the array.
|
|
156
284
|
return files.filter ( filePath => filePath ? !filePath.includes ( 'DS_Store' ) : false ).reverse ();
|
|
157
285
|
}
|
|
158
286
|
catch ( e ){
|
|
159
287
|
console.error ( 'JustAnother: Failed to load your routes directory for generating endpoints.' );
|
|
160
288
|
throw e;
|
|
161
289
|
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const unknownMethodHandler = ( req, res ) => {
|
|
165
|
-
if ( req.method.toLowerCase () === 'options' ) {
|
|
166
|
-
const allowHeaders = [ '*' ];
|
|
167
|
-
|
|
168
|
-
if ( res.methods.indexOf ( 'OPTIONS' ) === -1 ) res.methods.push ( 'OPTIONS' );
|
|
169
|
-
|
|
170
|
-
res.header ( 'Access-Control-Allow-Credentials', true );
|
|
171
|
-
res.header ( 'Access-Control-Allow-Headers', allowHeaders.join ( ', ' ) );
|
|
172
|
-
res.header ( 'Access-Control-Allow-Methods', res.methods.join ( ', ' ) );
|
|
173
|
-
res.header ( 'Access-Control-Allow-Origin', req.headers.origin );
|
|
174
|
-
|
|
175
|
-
return res.send ( 204 );
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return res.send ( new restifyErrors.MethodNotAllowedError ( { code: 405 }, `${ req.method } method is not available on this endpoint` ) );
|
|
179
290
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "just-another-http-api",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "A framework built on top of
|
|
3
|
+
"version": "1.2.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"restapi",
|
|
17
17
|
"restful",
|
|
18
18
|
"rest",
|
|
19
|
-
"
|
|
19
|
+
"fastify",
|
|
20
20
|
"http",
|
|
21
21
|
"server",
|
|
22
22
|
"just another",
|
|
@@ -25,15 +25,21 @@
|
|
|
25
25
|
"author": "Oliver Edgington <oliver@edgington.com> (https://github.com/OllieEdge)",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
28
|
+
"@aws-sdk/lib-storage": "^3.454.0",
|
|
29
|
+
"@fastify/caching": "^8.3.0",
|
|
30
|
+
"@fastify/cors": "^8.4.1",
|
|
31
|
+
"@fastify/jwt": "^7.2.3",
|
|
32
|
+
"@fastify/redis": "^6.1.1",
|
|
33
|
+
"abstract-cache": "^1.0.1",
|
|
34
|
+
"abstract-cache-redis": "^2.0.0",
|
|
35
|
+
"fastify": "^4.24.3",
|
|
36
|
+
"fastify-multer": "^2.0.3",
|
|
37
|
+
"recursive-readdir": "^2.2.3"
|
|
33
38
|
},
|
|
34
39
|
"devDependencies": {
|
|
35
|
-
"chai": "^4.3.
|
|
40
|
+
"chai": "^4.3.10",
|
|
36
41
|
"chai-as-promised": "^7.1.1",
|
|
42
|
+
"eip-cloud-services": "^1.1.0",
|
|
37
43
|
"mocha": "^10.2.0"
|
|
38
44
|
}
|
|
39
45
|
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
let config;
|
|
2
|
+
|
|
3
|
+
exports.initialiseAuth = async ( app, _config ) => {
|
|
4
|
+
config = _config;
|
|
5
|
+
|
|
6
|
+
const authType = config?.auth?.type;
|
|
7
|
+
|
|
8
|
+
switch ( authType ) {
|
|
9
|
+
case 'jwt':
|
|
10
|
+
|
|
11
|
+
checkJwtIsAvailable ( config );
|
|
12
|
+
|
|
13
|
+
app.register ( require ( '@fastify/jwt' ), {
|
|
14
|
+
secret: config.auth.jwtSecret,
|
|
15
|
+
} );
|
|
16
|
+
|
|
17
|
+
app.decorate ( 'authenticate', async function ( request, reply ) {
|
|
18
|
+
try {
|
|
19
|
+
await request.jwtVerify ();
|
|
20
|
+
}
|
|
21
|
+
catch ( err ) {
|
|
22
|
+
if ( err.name === 'JsonWebTokenError' ) {
|
|
23
|
+
reply.status ( 400 ).send ( { error: 'Invalid Token' } );
|
|
24
|
+
}
|
|
25
|
+
else if ( err.name === 'TokenExpiredError' ) {
|
|
26
|
+
reply.status ( 401 ).send ( { error: 'Token Expired' } );
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
reply.send ( err );
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
app.post ( config?.auth?.tokenEndpoint || '/auth/login', async ( req, reply ) => {
|
|
35
|
+
|
|
36
|
+
const jwtToTokenise = await config.auth.jwtLoginHandle ( req.body );
|
|
37
|
+
|
|
38
|
+
if ( !jwtToTokenise ) {
|
|
39
|
+
reply.status ( 401 );
|
|
40
|
+
reply.send ( { error: 'Invalid credentials' } );
|
|
41
|
+
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const accessToken = app.jwt.sign ( { username: jwtToTokenise }, { expiresIn: config.auth.jwtExpiresIn } );
|
|
46
|
+
const refreshToken = config.auth.jwtEnabledRefreshTokens && checkJwtRefreshIsAvailable ( config ) ? app.jwt.sign ( { username: jwtToTokenise }, { expiresIn: config.auth.jwtRefreshExpiresIn } ) : null;
|
|
47
|
+
const expires = config.auth.jwtExpiresIn;
|
|
48
|
+
|
|
49
|
+
await config.auth.jwtStoreRefreshToken ( jwtToTokenise, refreshToken );
|
|
50
|
+
|
|
51
|
+
reply.send ( { user: jwtToTokenise, accessToken, refreshToken, expires } );
|
|
52
|
+
|
|
53
|
+
return;
|
|
54
|
+
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
if ( config.auth.jwtEnabledRefreshTokens && checkJwtRefreshIsAvailable ( config ) ){
|
|
58
|
+
app.post ( config?.auth?.refreshTokenEndpoint || '/auth/refresh', async ( req, reply ) => {
|
|
59
|
+
|
|
60
|
+
const { user, refreshToken } = req.body;
|
|
61
|
+
|
|
62
|
+
const isValid = await config.auth.jwtRetrieveRefreshToken ( user, refreshToken );
|
|
63
|
+
if ( !isValid ) {
|
|
64
|
+
reply.status ( 401 ).send ( { error: 'Invalid refresh token' } );
|
|
65
|
+
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const accessToken = app.jwt.sign ( { username: user }, { expiresIn: config.auth.jwtExpiresIn } );
|
|
70
|
+
const newRefreshToken = config.auth.jwtEnabledRefreshTokens && checkJwtRefreshIsAvailable ( config ) ? app.jwt.sign ( { username: user }, { expiresIn: config.auth.jwtRefreshExpiresIn } ) : null;
|
|
71
|
+
const expires = config.auth.jwtExpiresIn;
|
|
72
|
+
|
|
73
|
+
await config.auth.jwtStoreRefreshToken ( user, newRefreshToken );
|
|
74
|
+
|
|
75
|
+
reply.send ( { user, accessToken, refreshToken: newRefreshToken, expires } );
|
|
76
|
+
|
|
77
|
+
return;
|
|
78
|
+
|
|
79
|
+
} );
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
exports.checkAuth = async ( req, reply ) => {
|
|
90
|
+
const authType = config.auth.type; // authConfig is set in each route
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
switch ( authType ) {
|
|
94
|
+
case 'jwt':
|
|
95
|
+
await req.jwtVerify ();
|
|
96
|
+
break;
|
|
97
|
+
case 'basic':
|
|
98
|
+
// Basic auth logic
|
|
99
|
+
break;
|
|
100
|
+
case 'oauth2':
|
|
101
|
+
// OAuth2 auth logic
|
|
102
|
+
break;
|
|
103
|
+
case 'token':
|
|
104
|
+
// Token-based auth logic
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
throw new Error ( 'Unsupported authentication type' );
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
catch ( err ) {
|
|
113
|
+
reply.send ( err );
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const checkJwtIsAvailable = ( config ) => {
|
|
118
|
+
if ( !config?.auth?.jwtLoginHandle || typeof config.auth.jwtLoginHandle !== 'function' ){
|
|
119
|
+
throw new Error ( '`auth.type` is set to "jwt", but `auth.jwtLoginHandle` is not a function or is not defined.' );
|
|
120
|
+
}
|
|
121
|
+
if ( !config?.auth?.jwtExpiresIn || typeof config.auth.jwtExpiresIn !== 'number' ){
|
|
122
|
+
throw new Error ( '`auth.jwtEnabledRefreshTokens` is set to true, but `auth.jwtExpiresIn` is not a number or is not defined.' );
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const checkJwtRefreshIsAvailable = ( config ) => {
|
|
129
|
+
if ( !config?.auth?.jwtStoreRefreshToken || typeof config.auth.jwtStoreRefreshToken !== 'function' ){
|
|
130
|
+
throw new Error ( '`auth.jwtEnabledRefreshTokens` is set to true, but `auth.jwtStoreRefreshToken` is not a function or is not defined.' );
|
|
131
|
+
}
|
|
132
|
+
if ( !config?.auth?.jwtRetrieveRefreshToken || typeof config.auth.jwtRetrieveRefreshToken !== 'function' ){
|
|
133
|
+
throw new Error ( '`auth.jwtEnabledRefreshTokens` is set to true, but `auth.jwtRetrieveRefreshToken` is not a function or is not defined.' );
|
|
134
|
+
}
|
|
135
|
+
if ( !config?.auth?.jwtRefreshExpiresIn || typeof config.auth.jwtRefreshExpiresIn !== 'number' ){
|
|
136
|
+
throw new Error ( '`auth.jwtEnabledRefreshTokens` is set to true, but `auth.jwtRefreshExpiresIn` is not a number or is not defined.' );
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return true;
|
|
140
|
+
};
|
package/src/cache.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const getCacheKey = ( config, req ) => `${config?.cache?.redisPrefix || ''}:${req.method.toLowerCase ()}:${req.routeOptions.url}:${JSON.stringify ( req.query )}`;
|
|
2
|
+
|
|
3
|
+
exports.initialiseCaching = async ( app, config ) => {
|
|
4
|
+
|
|
5
|
+
if ( config.cache && config.cache.enabled && config.cache.redisClient ) {
|
|
6
|
+
|
|
7
|
+
await app.register ( require ( '@fastify/redis' ), { client: config.cache.redisClient } );
|
|
8
|
+
await app.register ( require ( '@fastify/caching' ), require ( 'abstract-cache' ) ( {
|
|
9
|
+
useAwait: true,
|
|
10
|
+
driver: {
|
|
11
|
+
name: 'abstract-cache-redis',
|
|
12
|
+
options: { client: config.cache.redisClient }
|
|
13
|
+
}
|
|
14
|
+
} ) );
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
exports.checkRequestCache = async ( app, req, reply, handleConfig, globalConfig ) => {
|
|
22
|
+
|
|
23
|
+
if ( globalConfig?.cache?.enabled && handleConfig?.[ req.method.toLowerCase () ]?.cache ) {
|
|
24
|
+
|
|
25
|
+
const cacheKey = getCacheKey ( globalConfig, req );
|
|
26
|
+
const script = `
|
|
27
|
+
local value = redis.call('GET', KEYS[1])
|
|
28
|
+
local ttl = redis.call('TTL', KEYS[1])
|
|
29
|
+
return {value, ttl}
|
|
30
|
+
`;
|
|
31
|
+
const result = await app.redis.eval ( script, 1, cacheKey );
|
|
32
|
+
|
|
33
|
+
if ( result[ 0 ] ) {
|
|
34
|
+
const cachedResponse = JSON.parse ( result[ 0 ] );
|
|
35
|
+
const ttl = result[ 1 ];
|
|
36
|
+
const maxAge = handleConfig?.[ req.method.toLowerCase () ]?.expires || globalConfig?.cache?.expires || 60;
|
|
37
|
+
const ageInSeconds = maxAge - ttl; // Calculate the age based on TTL and maxAge
|
|
38
|
+
|
|
39
|
+
if ( globalConfig?.cache?.addCacheHeaders ){
|
|
40
|
+
cachedResponse.headers ??= {};
|
|
41
|
+
cachedResponse.headers[ 'X-Cache' ] = 'HIT';
|
|
42
|
+
cachedResponse.headers[ 'X-Cache-Age' ] = ageInSeconds;
|
|
43
|
+
cachedResponse.headers[ 'X-Cache-Expires' ] = maxAge;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cachedResponse;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
exports.setRequestCache = async ( app, req, response, handleConfig, globalConfig ) => {
|
|
55
|
+
|
|
56
|
+
if ( globalConfig?.cache?.enabled && handleConfig?.[ req.method.toLowerCase () ]?.cache ){
|
|
57
|
+
const cacheKey = getCacheKey ( globalConfig, req );
|
|
58
|
+
|
|
59
|
+
await app.redis.set ( cacheKey, JSON.stringify ( response ), 'EX', handleConfig?.[ req.method.toLowerCase () ]?.expires || globalConfig?.cache?.expires || 60 );
|
|
60
|
+
|
|
61
|
+
if ( globalConfig?.cache?.addCacheHeaders ){
|
|
62
|
+
response.headers ??= {};
|
|
63
|
+
response.headers[ 'X-Cache' ] = 'MISS';
|
|
64
|
+
response.headers[ 'X-Cache-Age' ] = 0;
|
|
65
|
+
response.headers[ 'X-Cache-Expires' ] = handleConfig?.[ req.method.toLowerCase () ]?.expires || globalConfig?.cache?.expires || 60;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
};
|