@devmoods/express-extras 0.20.0 → 0.21.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/dist/auth.d.ts +8 -0
- package/dist/auth.js +20 -5
- package/dist/email.js +1 -1
- package/dist/listen.d.ts +1 -1
- package/dist/listen.js +6 -1
- package/dist/logger.d.ts +10 -22
- package/dist/logger.js +47 -21
- package/dist/routes.d.ts +2 -2
- package/dist/routes.js +9 -40
- package/package.json +14 -14
package/dist/auth.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ export declare function createAuth<UserModel extends Record<string, any>>({ getU
|
|
|
24
24
|
* of `req`.
|
|
25
25
|
*/
|
|
26
26
|
export declare function parseAuthorizationHeader(req: Request): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Extract an access token from the `Request`-object.
|
|
29
|
+
*
|
|
30
|
+
* Checks multiple places and uses the first match. Priority:
|
|
31
|
+
* 1. Request header `Authorization: `
|
|
32
|
+
* 2. Cookie named `access_token`
|
|
33
|
+
*/
|
|
34
|
+
export declare function extractAccessTokenFromRequest(req: Request): string | null;
|
|
27
35
|
export declare function setAccessTokenCookie(res: Response, token: string | null): void;
|
|
28
36
|
export declare function checkIfPasswordsMatch(realPassword: string, attemptedPassword: string): Promise<boolean>;
|
|
29
37
|
export declare function hashPassword(password: string): Promise<string>;
|
package/dist/auth.js
CHANGED
|
@@ -16,6 +16,7 @@ export function createAuthToken(payload, secret = process.env.APP_JWT_SECRET) {
|
|
|
16
16
|
}
|
|
17
17
|
export function createAuth({ getUserByToken, methods: { emailAndPassword }, filterUser = (x) => x, redis, tokenExpiry = TOKEN_EXPIRES_IN, onResetRequested, onPasswordChanged, }) {
|
|
18
18
|
const useCurrentUser = () => useContext('currentUser');
|
|
19
|
+
const setCurrentUser = (user) => setContext('currentUser', user);
|
|
19
20
|
const createRouter = () => {
|
|
20
21
|
const router = Router();
|
|
21
22
|
router.get('/me', middleware(), route(() => {
|
|
@@ -36,6 +37,8 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
36
37
|
const token = createAuthToken(filterUser(user));
|
|
37
38
|
await redis.set(token, '1', { EX: tokenExpiry });
|
|
38
39
|
setAccessTokenCookie(res, token);
|
|
40
|
+
setCurrentUser(user);
|
|
41
|
+
logger.info('access token created');
|
|
39
42
|
return [201, { token }];
|
|
40
43
|
}
|
|
41
44
|
catch (error) {
|
|
@@ -75,12 +78,14 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
75
78
|
const { token } = req.params;
|
|
76
79
|
const email = await redis.get(token);
|
|
77
80
|
if (!email) {
|
|
81
|
+
logger.info('invalid reset token', { token });
|
|
78
82
|
throw BadRequest('invalid reset token');
|
|
79
83
|
}
|
|
80
84
|
const user = await emailAndPassword(email, skipPasswordCheck);
|
|
85
|
+
setCurrentUser(user);
|
|
81
86
|
await onPasswordChanged?.(user, await hashPassword(password));
|
|
82
87
|
await redis.del(token);
|
|
83
|
-
logger.info('user changed password'
|
|
88
|
+
logger.info('user changed password');
|
|
84
89
|
return 200;
|
|
85
90
|
}));
|
|
86
91
|
}
|
|
@@ -88,6 +93,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
88
93
|
const token = req.params.token;
|
|
89
94
|
const email = await redis.get(token);
|
|
90
95
|
if (!email) {
|
|
96
|
+
logger.info('invalid reset token', { token });
|
|
91
97
|
throw NotFound();
|
|
92
98
|
}
|
|
93
99
|
return 200;
|
|
@@ -110,6 +116,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
110
116
|
EX: TEMP_TOKEN_EXIRES_IN,
|
|
111
117
|
});
|
|
112
118
|
await onResetRequested?.(user, temporaryToken);
|
|
119
|
+
logger.info('password reset request', { email });
|
|
113
120
|
}
|
|
114
121
|
catch (error) {
|
|
115
122
|
logger.error(error, 'auth reset failed');
|
|
@@ -118,7 +125,6 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
118
125
|
}));
|
|
119
126
|
router.get('/token/refresh', middleware(false), route(async (req, res) => {
|
|
120
127
|
const user = useCurrentUser();
|
|
121
|
-
logger.info('refreshed token', { user });
|
|
122
128
|
if (!user) {
|
|
123
129
|
throw Unauthorized();
|
|
124
130
|
}
|
|
@@ -126,6 +132,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
126
132
|
const token = createAuthToken(rest);
|
|
127
133
|
await redis.set(token, '1', { EX: tokenExpiry });
|
|
128
134
|
setAccessTokenCookie(res, token);
|
|
135
|
+
logger.info('refreshed token', { user });
|
|
129
136
|
return { token };
|
|
130
137
|
}));
|
|
131
138
|
return router;
|
|
@@ -143,6 +150,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
143
150
|
const valid = await redis.get(token);
|
|
144
151
|
if (!valid) {
|
|
145
152
|
if (!authorize) {
|
|
153
|
+
logger.warn('request with invalid access token');
|
|
146
154
|
return next();
|
|
147
155
|
}
|
|
148
156
|
throw new InvalidToken('token expired');
|
|
@@ -152,7 +160,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
152
160
|
throw new Error('Failed to decode token');
|
|
153
161
|
}
|
|
154
162
|
const user = await getUserByToken({ ...decoded });
|
|
155
|
-
|
|
163
|
+
setCurrentUser(user);
|
|
156
164
|
next();
|
|
157
165
|
}
|
|
158
166
|
catch (error) {
|
|
@@ -191,7 +199,14 @@ export function parseAuthorizationHeader(req) {
|
|
|
191
199
|
}
|
|
192
200
|
return token;
|
|
193
201
|
}
|
|
194
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Extract an access token from the `Request`-object.
|
|
204
|
+
*
|
|
205
|
+
* Checks multiple places and uses the first match. Priority:
|
|
206
|
+
* 1. Request header `Authorization: `
|
|
207
|
+
* 2. Cookie named `access_token`
|
|
208
|
+
*/
|
|
209
|
+
export function extractAccessTokenFromRequest(req) {
|
|
195
210
|
const authorizationHeader = parseAuthorizationHeader(req);
|
|
196
211
|
if (authorizationHeader) {
|
|
197
212
|
return authorizationHeader;
|
|
@@ -227,7 +242,7 @@ export async function checkIfPasswordsMatch(realPassword, attemptedPassword) {
|
|
|
227
242
|
}
|
|
228
243
|
const validPassword = await bcrypt.compare(attemptedPassword, realPassword);
|
|
229
244
|
if (!validPassword) {
|
|
230
|
-
throw new Error(`Passwords
|
|
245
|
+
throw new Error(`Passwords do not match`);
|
|
231
246
|
}
|
|
232
247
|
return true;
|
|
233
248
|
}
|
package/dist/email.js
CHANGED
|
@@ -15,7 +15,7 @@ export function createEmail({ host, port, user, pass, senderName, senderAddress,
|
|
|
15
15
|
text: message,
|
|
16
16
|
...rest,
|
|
17
17
|
});
|
|
18
|
-
getLogger('
|
|
18
|
+
getLogger('email').info('email-sent', { email: result });
|
|
19
19
|
};
|
|
20
20
|
return { send, transporter };
|
|
21
21
|
}
|
package/dist/listen.d.ts
CHANGED
package/dist/listen.js
CHANGED
|
@@ -34,13 +34,18 @@ export function listen(server, options) {
|
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
37
|
-
await options?.healthCheck?.();
|
|
37
|
+
const meta = await options?.healthCheck?.();
|
|
38
|
+
return {
|
|
39
|
+
meta,
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
};
|
|
38
42
|
}
|
|
39
43
|
catch (error) {
|
|
40
44
|
logger.error(error, 'health check failed');
|
|
41
45
|
throw error;
|
|
42
46
|
}
|
|
43
47
|
},
|
|
48
|
+
verbatim: true,
|
|
44
49
|
},
|
|
45
50
|
});
|
|
46
51
|
const port = options.port;
|
package/dist/logger.d.ts
CHANGED
|
@@ -18,29 +18,17 @@ export declare function attachSentryClientToLoggers(sentryClient: CreateSentry['
|
|
|
18
18
|
export declare function setGlobalLoggerContext(getContext: () => LoggerContext): void;
|
|
19
19
|
export type Logger = ReturnType<typeof createLogger>;
|
|
20
20
|
export declare function createLogger({ name, getContext, silent, }: LoggerOptions): {
|
|
21
|
-
debug(message: string, context?:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}): void;
|
|
27
|
-
warn(message: string, context?: {
|
|
28
|
-
[key: string]: any;
|
|
29
|
-
}): void;
|
|
30
|
-
error(error: Error | null, message?: string): void;
|
|
31
|
-
fatal(error: Error | null, message?: string): void;
|
|
21
|
+
debug(message: string, context?: LoggerContext): void;
|
|
22
|
+
info(message: string, context?: LoggerContext): void;
|
|
23
|
+
warn(message: string, context?: LoggerContext): void;
|
|
24
|
+
error(error: Error | null, message?: string, context?: LoggerContext): void;
|
|
25
|
+
fatal(error: Error | null, message?: string, context?: LoggerContext): void;
|
|
32
26
|
};
|
|
33
27
|
export declare function getLogger(name?: string): Logger;
|
|
34
28
|
export declare function getLoggers(): Map<string, {
|
|
35
|
-
debug(message: string, context?:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}): void;
|
|
41
|
-
warn(message: string, context?: {
|
|
42
|
-
[key: string]: any;
|
|
43
|
-
}): void;
|
|
44
|
-
error(error: Error | null, message?: string | undefined): void;
|
|
45
|
-
fatal(error: Error | null, message?: string | undefined): void;
|
|
29
|
+
debug(message: string, context?: LoggerContext): void;
|
|
30
|
+
info(message: string, context?: LoggerContext): void;
|
|
31
|
+
warn(message: string, context?: LoggerContext): void;
|
|
32
|
+
error(error: Error | null, message?: string | undefined, context?: LoggerContext): void;
|
|
33
|
+
fatal(error: Error | null, message?: string | undefined, context?: LoggerContext): void;
|
|
46
34
|
}>;
|
package/dist/logger.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as os from 'node:os';
|
|
2
2
|
import { useRequestId } from './context.js';
|
|
3
|
+
const LEVELS = {
|
|
4
|
+
TRACE: 10,
|
|
5
|
+
DEBUG: 20,
|
|
6
|
+
INFO: 30,
|
|
7
|
+
WARN: 40,
|
|
8
|
+
ERROR: 50,
|
|
9
|
+
FATAL: 60,
|
|
10
|
+
};
|
|
11
|
+
const LEVELS_TO_NAME = Object.fromEntries(Object.entries(LEVELS).map(([name, level]) => [level, name.toLowerCase()]));
|
|
3
12
|
const _loggers = new Map();
|
|
4
13
|
let _sentryClient;
|
|
5
14
|
export function attachSentryClientToLoggers(sentryClient) {
|
|
@@ -19,14 +28,9 @@ export function createLogger({ name, getContext, silent = () => process.env.NODE
|
|
|
19
28
|
if (_loggers.has(name)) {
|
|
20
29
|
throw new Error(`A logger named ${name} already exists`);
|
|
21
30
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
_loggers.set(name, logger);
|
|
27
|
-
if (silent()) {
|
|
28
|
-
logger.level(bunyan.FATAL + 1);
|
|
29
|
-
}
|
|
31
|
+
const hostname = os.hostname();
|
|
32
|
+
const pid = process.pid;
|
|
33
|
+
const logLevel = silent() ? LEVELS.FATAL + 1 : LEVELS.INFO;
|
|
30
34
|
function getFinalContext(context) {
|
|
31
35
|
return {
|
|
32
36
|
...defaultGetContext(),
|
|
@@ -34,35 +38,57 @@ export function createLogger({ name, getContext, silent = () => process.env.NODE
|
|
|
34
38
|
...context,
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
|
-
|
|
41
|
+
function log(level, message, context) {
|
|
42
|
+
const payload = JSON.stringify({
|
|
43
|
+
level: LEVELS_TO_NAME[level],
|
|
44
|
+
hostname,
|
|
45
|
+
pid,
|
|
46
|
+
message,
|
|
47
|
+
...context,
|
|
48
|
+
time: new Date().toISOString(),
|
|
49
|
+
v: 1,
|
|
50
|
+
});
|
|
51
|
+
if (level >= logLevel) {
|
|
52
|
+
console.log(payload);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const api = {
|
|
38
56
|
debug(message, context = {}) {
|
|
39
|
-
return
|
|
57
|
+
return log(LEVELS.DEBUG, message, getFinalContext(context));
|
|
40
58
|
},
|
|
41
59
|
info(message, context = {}) {
|
|
42
|
-
return
|
|
60
|
+
return log(LEVELS.INFO, message, getFinalContext(context));
|
|
43
61
|
},
|
|
44
62
|
warn(message, context = {}) {
|
|
45
|
-
return
|
|
63
|
+
return log(LEVELS.WARN, message, getFinalContext(context));
|
|
46
64
|
},
|
|
47
|
-
error(error, message) {
|
|
48
|
-
const
|
|
65
|
+
error(error, message, context = {}) {
|
|
66
|
+
const finalContext = getFinalContext(context);
|
|
49
67
|
if (_sentryClient != null) {
|
|
50
68
|
_sentryClient.captureException(error, {
|
|
51
|
-
tags:
|
|
69
|
+
tags: finalContext,
|
|
52
70
|
});
|
|
53
71
|
}
|
|
54
|
-
return
|
|
72
|
+
return log(LEVELS.ERROR, message || error?.message || '', {
|
|
73
|
+
...finalContext,
|
|
74
|
+
err: error?.stack,
|
|
75
|
+
});
|
|
55
76
|
},
|
|
56
|
-
fatal(error, message) {
|
|
57
|
-
const
|
|
77
|
+
fatal(error, message, context = {}) {
|
|
78
|
+
const finalContext = getFinalContext(context);
|
|
58
79
|
if (_sentryClient != null) {
|
|
59
80
|
_sentryClient.captureException(error, {
|
|
60
|
-
tags:
|
|
81
|
+
tags: finalContext,
|
|
61
82
|
});
|
|
62
83
|
}
|
|
63
|
-
return
|
|
84
|
+
return log(LEVELS.FATAL, message || error?.message || '', {
|
|
85
|
+
...finalContext,
|
|
86
|
+
err: error?.stack,
|
|
87
|
+
});
|
|
64
88
|
},
|
|
65
89
|
};
|
|
90
|
+
_loggers.set(name, api);
|
|
91
|
+
return api;
|
|
66
92
|
}
|
|
67
93
|
export function getLogger(name = 'default') {
|
|
68
94
|
const logger = _loggers.get(name);
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
1
|
+
import type { RequestHandler, Express } from 'express';
|
|
2
|
+
export declare function createDocsRouter(app: Express, extras?: Record<string, any>): RequestHandler;
|
package/dist/routes.js
CHANGED
|
@@ -1,41 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.toString()
|
|
11
|
-
.replace('/?', '')
|
|
12
|
-
.replace('(?=/|$)', '$')
|
|
13
|
-
.match(/^\/\^((?:\\[.*+?^${}()|[\]\\/]|[^.*+?^${}()|[\]\\/])*)\$\//);
|
|
14
|
-
return match
|
|
15
|
-
? match[1].replace(/\\(.)/g, '$1')
|
|
16
|
-
: '<complex:' + value.toString() + '>';
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function getRoutesOfLayer(path, layer) {
|
|
20
|
-
if (layer.method) {
|
|
21
|
-
return [{ method: layer.method.toUpperCase(), path }];
|
|
22
|
-
}
|
|
23
|
-
else if (layer.route) {
|
|
24
|
-
return getRoutesOfLayer(path + split(layer.route.path), layer.route.stack[0]);
|
|
25
|
-
}
|
|
26
|
-
else if (layer.name === 'router' && layer.handle.stack) {
|
|
27
|
-
let routes = [];
|
|
28
|
-
layer.handle.stack.forEach(function (stackItem) {
|
|
29
|
-
routes = routes.concat(getRoutesOfLayer(path + split(layer.regexp), stackItem));
|
|
30
|
-
});
|
|
31
|
-
return routes;
|
|
32
|
-
}
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
export function listRoutes(app) {
|
|
36
|
-
const routes = [];
|
|
37
|
-
for (const layer of app._router.stack) {
|
|
38
|
-
routes.push(...getRoutesOfLayer('', layer));
|
|
39
|
-
}
|
|
40
|
-
return routes;
|
|
1
|
+
import { parseExpressApp } from 'express-route-parser';
|
|
2
|
+
import { route } from './middleware.js';
|
|
3
|
+
export function createDocsRouter(app, extras = {}) {
|
|
4
|
+
return route(() => {
|
|
5
|
+
return {
|
|
6
|
+
...extras,
|
|
7
|
+
routes: parseExpressApp(app),
|
|
8
|
+
};
|
|
9
|
+
});
|
|
41
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -8,6 +8,17 @@
|
|
|
8
8
|
"bin": {
|
|
9
9
|
"migrate": "bin/migrate"
|
|
10
10
|
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "ts-node --esm example/index.ts",
|
|
13
|
+
"clean": "rm -rf dist",
|
|
14
|
+
"lint": "eslint src",
|
|
15
|
+
"test": "NODE_ENV=test vitest",
|
|
16
|
+
"build": "pnpm run clean && tsc --project tsconfig.build.json",
|
|
17
|
+
"prepublishOnly": "pnpm run clean && pnpm run build",
|
|
18
|
+
"prettier": "prettier --write '**/*.{js,css,md,ts,tsx}'",
|
|
19
|
+
"import-sort": "import-sort --write '**/*.{js,jsx,ts,tsx}'",
|
|
20
|
+
"format": "pnpm run import-sort && pnpm run prettier"
|
|
21
|
+
},
|
|
11
22
|
"author": {
|
|
12
23
|
"name": "hanse",
|
|
13
24
|
"email": "hanse@koren.im"
|
|
@@ -51,7 +62,6 @@
|
|
|
51
62
|
"@sentry/node": "^7.60.1",
|
|
52
63
|
"@types/bcrypt": "^5.0.0",
|
|
53
64
|
"@types/body-parser": "^1.19.2",
|
|
54
|
-
"@types/bunyan": "^1.8.8",
|
|
55
65
|
"@types/cookie-parser": "^1.4.3",
|
|
56
66
|
"@types/express": "^4.17.17",
|
|
57
67
|
"@types/jsonwebtoken": "^9.0.2",
|
|
@@ -61,11 +71,11 @@
|
|
|
61
71
|
"ajv": "^8.12.0",
|
|
62
72
|
"bcrypt": "^5.1.0",
|
|
63
73
|
"body-parser": "^1.20.2",
|
|
64
|
-
"bunyan": "^1.8.15",
|
|
65
74
|
"compose-middleware": "^5.0.1",
|
|
66
75
|
"cookie-parser": "^1.4.6",
|
|
67
76
|
"dotenv": "^16.3.1",
|
|
68
77
|
"express": "^4.18.2",
|
|
78
|
+
"express-route-parser": "^1.0.5",
|
|
69
79
|
"helmet": "^7.0.0",
|
|
70
80
|
"jsonwebtoken": "^9.0.1",
|
|
71
81
|
"meow": "^12.0.1",
|
|
@@ -90,15 +100,5 @@
|
|
|
90
100
|
"@typescript-eslint/no-shadow": "off",
|
|
91
101
|
"react-hooks/rules-of-hooks": "off"
|
|
92
102
|
}
|
|
93
|
-
},
|
|
94
|
-
"scripts": {
|
|
95
|
-
"start": "ts-node --esm example/index.ts",
|
|
96
|
-
"clean": "rm -rf dist",
|
|
97
|
-
"lint": "eslint src",
|
|
98
|
-
"test": "NODE_ENV=test vitest",
|
|
99
|
-
"build": "pnpm run clean && tsc --project tsconfig.build.json",
|
|
100
|
-
"prettier": "prettier --write '**/*.{js,css,md,ts,tsx}'",
|
|
101
|
-
"import-sort": "import-sort --write '**/*.{js,jsx,ts,tsx}'",
|
|
102
|
-
"format": "pnpm run import-sort && pnpm run prettier"
|
|
103
103
|
}
|
|
104
|
-
}
|
|
104
|
+
}
|