just-another-http-api 1.0.5 → 1.2.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/.eslintrc +148 -0
- package/LICENSE +1 -1
- package/README.md +401 -151
- package/api.js +220 -126
- 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,134 +1,246 @@
|
|
|
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
|
-
version: packageJson.version
|
|
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 } );
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
} );
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
try {
|
|
31
|
+
|
|
32
|
+
if ( config.cors ) {
|
|
33
|
+
app.register ( require ( '@fastify/cors' ), config.cors );
|
|
34
|
+
}
|
|
35
|
+
await uploads.initialiseUploads ( app, config );
|
|
36
|
+
await caching.initialiseCaching ( app, config );
|
|
37
|
+
await auth.initialiseAuth ( app, config );
|
|
38
|
+
|
|
39
|
+
if ( config.middleware && Array.isArray ( config.middleware ) ) {
|
|
40
|
+
for ( const func of config.middleware ) {
|
|
41
|
+
if ( typeof func === 'function' ) {
|
|
42
|
+
await app.register ( func );
|
|
43
|
+
}
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
}
|
|
47
|
+
catch ( error ) {
|
|
48
|
+
console.error ( 'Error during server initialization:', error );
|
|
49
|
+
throw error; // Rethrow the error to handle it at a higher level, if necessary
|
|
50
|
+
}
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
return app;
|
|
53
|
+
}
|
|
43
54
|
|
|
55
|
+
async function loadEndpoints ( config ) {
|
|
44
56
|
const files = await recursiveReadDir ( config?.docRoot || 'routes' );
|
|
45
|
-
|
|
57
|
+
|
|
58
|
+
return files.map ( filePath => ( {
|
|
46
59
|
handlers: require ( path.resolve ( filePath ) ),
|
|
47
60
|
path: handlerPathToApiPath ( filePath, config?.docRoot || 'routes' )
|
|
48
61
|
} ) );
|
|
62
|
+
}
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
method === 'post' && upload ? upload.single ( 'file' ) : null
|
|
55
|
-
].filter ( Boolean );
|
|
56
|
-
|
|
57
|
-
server[ method ] ( ...endpointArgs, async ( req, res ) => {
|
|
58
|
-
try {
|
|
59
|
-
const response = await endpoint.handlers[ method ] ( req );
|
|
60
|
-
|
|
61
|
-
if ( every ) {
|
|
62
|
-
every ( { path: endpoint.path, method, req } );
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// If optional headers have been provided in the response add them here.
|
|
66
|
-
if ( response.hasOwnProperty ( 'headers' ) ){
|
|
67
|
-
res.set ( response.headers );
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if ( response.hasOwnProperty ( 'redirect' ) ){
|
|
71
|
-
res.redirect ( response.redirect.code, response.redirect.url, () => null );
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if ( response.hasOwnProperty ( 'cache' ) ){
|
|
75
|
-
res.cache ( response.cache.type, response.cache.options );
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// If response.html is set, we want to send the HTML back as a raw string and set the content type.
|
|
79
|
-
if ( response.hasOwnProperty ( 'html' ) ){
|
|
80
|
-
res.sendRaw ( 200, response.html, { 'Content-Type': 'text/html' } );
|
|
81
|
-
} //
|
|
82
|
-
else if ( response.hasOwnProperty ( 'json' ) || response.hasOwnProperty ( 'body' ) || response.hasOwnProperty ( 'response' ) || typeof response === 'string' ){
|
|
83
|
-
data = response?.json || response?.body || response?.response || response;
|
|
84
|
-
res.setHeader ( 'Content-Type', 'application/json' );
|
|
85
|
-
res.send ( method === 'post' ? 201 : 200, data );
|
|
86
|
-
}
|
|
87
|
-
else if ( response.hasOwnProperty ( 'error' ) ){
|
|
88
|
-
console.error ( response.error );
|
|
89
|
-
res.setHeader ( 'Content-Type', 'application/json' );
|
|
90
|
-
res.send ( new restifyErrors.makeErrFromCode ( response?.error?.statusCode, response?.error?.message ) );
|
|
91
|
-
}
|
|
92
|
-
else if ( response.hasOwnProperty ( 'file' ) ){
|
|
93
|
-
res.sendRaw ( response.file );
|
|
94
|
-
}
|
|
95
|
-
else if ( typeof response === 'object' ){
|
|
96
|
-
res.send ( response ); //Try and send whatever it is
|
|
97
|
-
}
|
|
98
|
-
else if ( !response ){
|
|
99
|
-
res.send ( 204 );
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
res.setHeader ( 'Content-Type', 'application/json' );
|
|
103
|
-
res.send ( new restifyErrors.makeErrFromCode ( 500, `Just Another Http API did not understand the response provided for request: ${ method } to ${ endpoint.path }. Check your return value.` ) );
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
catch ( error ){
|
|
109
|
-
res.setHeader ( 'Content-Type', 'application/json' );
|
|
110
|
-
if ( error instanceof Error ) {
|
|
111
|
-
res.send ( new restifyErrors.InternalServerError ( { code: 500 }, error.stack.replace ( /\n/g, ' ' ) ) );
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
if ( error.code ) {
|
|
115
|
-
res.send ( new restifyErrors.makeErrFromCode ( error.code, error.message ) );
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
res.send ( new restifyErrors.InternalServerError ( { code: 500 }, JSON.stringify ( error, null, 2 ) ) );
|
|
119
|
-
}
|
|
64
|
+
function registerEndpoint ( app, endpoint, globalConfig ) {
|
|
65
|
+
Object.keys ( endpoint.handlers ).filter ( method => method !== 'config' ).forEach ( method => {
|
|
66
|
+
const handlerConfig = endpoint.handlers.config?.[ method ] || {};
|
|
67
|
+
const requiresAuth = handlerConfig?.requiresAuth !== undefined ? handlerConfig.requiresAuth : !!globalConfig?.auth?.requiresAuth;
|
|
120
68
|
|
|
121
|
-
|
|
122
|
-
|
|
69
|
+
const preHandlers = [];
|
|
70
|
+
if ( requiresAuth ) {
|
|
71
|
+
preHandlers.push ( async ( req, reply ) => {
|
|
72
|
+
req.authConfig = handlerConfig.auth || globalConfig.auth;
|
|
73
|
+
await auth.checkAuth ( req, reply );
|
|
123
74
|
} );
|
|
124
|
-
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if ( globalConfig?.uploads?.enabled && handlerConfig?.upload?.enabled ) {
|
|
78
|
+
preHandlers.push ( uploads.handleUpload ( handlerConfig, globalConfig ) );
|
|
79
|
+
}
|
|
80
|
+
if ( handlerConfig?.cors !== undefined ) {
|
|
81
|
+
preHandlers.push ( cors.addCustomCors ( handlerConfig, globalConfig ) );
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fastifyMethod = translateLegacyMethods ( method.toLowerCase () );
|
|
85
|
+
const handler = endpoint.handlers[ method ];
|
|
86
|
+
const wrappedHandler = fastifyHandlerWrapper ( handler, endpoint.handlers.config, globalConfig );
|
|
87
|
+
|
|
88
|
+
app[ fastifyMethod ] (
|
|
89
|
+
endpoint.path,
|
|
90
|
+
{
|
|
91
|
+
preHandler: preHandlers
|
|
92
|
+
},
|
|
93
|
+
wrappedHandler
|
|
94
|
+
);
|
|
125
95
|
} );
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function translateLegacyMethods ( method ) {
|
|
99
|
+
switch ( method ) {
|
|
100
|
+
case 'del':
|
|
101
|
+
return 'delete';
|
|
102
|
+
default:
|
|
103
|
+
return method;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fastifyHandlerWrapper ( handler, config, globalConfig ) {
|
|
108
|
+
return async ( req, reply ) => {
|
|
109
|
+
try {
|
|
110
|
+
let response = await caching.checkRequestCache ( app, req, reply, config, globalConfig );
|
|
111
|
+
|
|
112
|
+
if ( !response ){
|
|
113
|
+
response = await handler ( req );
|
|
114
|
+
await caching.setRequestCache ( app, req, response, config, globalConfig );
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
handleResponse ( reply, response, req.method, req.routeOptions.url );
|
|
118
|
+
}
|
|
119
|
+
catch ( error ) {
|
|
120
|
+
handleError ( reply, error );
|
|
121
|
+
}
|
|
122
|
+
};
|
|
130
123
|
};
|
|
131
124
|
|
|
125
|
+
function handleResponse ( reply, response, method, path ) {
|
|
126
|
+
|
|
127
|
+
if ( !response ) {
|
|
128
|
+
reply.code ( 204 ).send ();
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ( typeof response !== 'object' || response === null ) {
|
|
134
|
+
handleNonObjectResponse ( reply, response, method );
|
|
135
|
+
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setResponseHeaders ( reply, response );
|
|
140
|
+
handleSpecialResponseTypes ( reply, response, method, path );
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function setResponseHeaders ( reply, response ) {
|
|
144
|
+
if ( response.headers ) {
|
|
145
|
+
Object.entries ( response.headers ).forEach ( ( [ key, value ] ) => {
|
|
146
|
+
reply.header ( key, value );
|
|
147
|
+
} );
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handleSpecialResponseTypes ( reply, response, method, path ) {
|
|
152
|
+
if ( response.redirect ) {
|
|
153
|
+
reply.redirect ( 301, response.redirect.url );
|
|
154
|
+
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if ( response.html ) {
|
|
159
|
+
reply.code ( 200 ).type ( 'text/html' ).send ( response.html );
|
|
160
|
+
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ( response.text ) {
|
|
165
|
+
reply.code ( 200 ).type ( 'text/plain' ).send ( response.text );
|
|
166
|
+
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if ( response.error ) {
|
|
171
|
+
handleErrorResponse ( reply, response.error );
|
|
172
|
+
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if ( response.file ) {
|
|
177
|
+
reply.send ( response.file );
|
|
178
|
+
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
sendGenericResponse ( reply, response, method, path );
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sendGenericResponse ( reply, response, method, path ) {
|
|
186
|
+
|
|
187
|
+
const data = response.json ?? response.body ?? response.response ?? response;
|
|
188
|
+
|
|
189
|
+
if ( data !== undefined ) {
|
|
190
|
+
reply.type ( 'application/json' ).code ( method === 'post' ? 201 : 200 ).send ( data );
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
handleUnknownResponseType ( reply, method, path );
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function handleErrorResponse ( reply, error ) {
|
|
199
|
+
console.error ( error );
|
|
200
|
+
const statusCode = error?.statusCode ?? 500;
|
|
201
|
+
reply.code ( statusCode ).type ( 'application/json' ).send ( { error: error.message } );
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function handleUnknownResponseType ( reply, method, path ) {
|
|
205
|
+
reply.type ( 'application/json' ).code ( 500 ).send ( {
|
|
206
|
+
error: `Unrecognized response type for ${method} ${path}`
|
|
207
|
+
} );
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function handleNonObjectResponse ( reply, response, method ) {
|
|
211
|
+
reply.type ( 'text/plain' ).code ( method === 'post' ? 201 : 200 ).send ( response.toString () );
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handleError ( reply, error ) {
|
|
215
|
+
// Set content type for the error response
|
|
216
|
+
reply.header ( 'Content-Type', 'application/json' );
|
|
217
|
+
|
|
218
|
+
if ( error instanceof Error ) {
|
|
219
|
+
// Send internal server error with the error stack
|
|
220
|
+
reply.status ( 500 ).send ( {
|
|
221
|
+
error: 'Internal Server Error',
|
|
222
|
+
message: error.message,
|
|
223
|
+
stack: error.stack
|
|
224
|
+
} );
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Check if error object contains a specific status code
|
|
228
|
+
if ( error.code && typeof error.code === 'number' ) {
|
|
229
|
+
reply.status ( error.code ).send ( {
|
|
230
|
+
error: 'Error',
|
|
231
|
+
message: error.message
|
|
232
|
+
} );
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Send a generic internal server error response
|
|
236
|
+
reply.status ( 500 ).send ( {
|
|
237
|
+
error: 'Internal Server Error',
|
|
238
|
+
message: 'An unknown error occurred'
|
|
239
|
+
} );
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
132
244
|
const handlerPathToApiPath = ( path, docRoot ) => {
|
|
133
245
|
|
|
134
246
|
const targetPath = path.replace ( docRoot.replace ( /.\//igm, '' ), '' ).split ( '/' );
|
|
@@ -152,28 +264,10 @@ const recursiveReadDir = async ( docRoot ) => {
|
|
|
152
264
|
try {
|
|
153
265
|
const files = await recursiveRead ( docRoot );
|
|
154
266
|
|
|
155
|
-
// Remove all falsy values and reverse the array.
|
|
156
267
|
return files.filter ( filePath => filePath ? !filePath.includes ( 'DS_Store' ) : false ).reverse ();
|
|
157
268
|
}
|
|
158
269
|
catch ( e ){
|
|
159
270
|
console.error ( 'JustAnother: Failed to load your routes directory for generating endpoints.' );
|
|
160
271
|
throw e;
|
|
161
272
|
}
|
|
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
273
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "just-another-http-api",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A framework built on top of
|
|
3
|
+
"version": "1.2.1",
|
|
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
|
+
};
|