@devmoods/express-extras 0.14.0 → 0.15.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/dist/auth.d.ts +8 -7
- package/dist/auth.js +74 -5
- package/dist/context.d.ts +1 -2
- package/dist/listen.d.ts +0 -2
- package/dist/listen.js +7 -5
- package/dist/logger.d.ts +4 -4
- package/dist/logger.js +11 -6
- package/dist/routes.js +1 -1
- package/dist/sentry.d.ts +3 -0
- package/dist/slack.js +3 -1
- package/package.json +6 -6
package/dist/auth.d.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { type Request, type RequestHandler, type Response } from 'express';
|
|
2
2
|
import type { RedisClient } from './redis.js';
|
|
3
3
|
export declare function createAuthToken(payload: any, secret?: string): string;
|
|
4
|
-
interface
|
|
4
|
+
export interface AuthLoginMethods<User = any> {
|
|
5
5
|
emailAndPassword?: (email: string, verifyPassword: (realPassword: string) => Promise<boolean>) => Promise<User>;
|
|
6
6
|
}
|
|
7
|
-
interface AuthOptions<UserModel extends Record<string, any>> {
|
|
8
|
-
getUserByToken
|
|
9
|
-
methods:
|
|
7
|
+
export interface AuthOptions<UserModel extends Record<string, any>> {
|
|
8
|
+
getUserByToken(token: Record<string, any>): Promise<UserModel>;
|
|
9
|
+
methods: AuthLoginMethods<UserModel>;
|
|
10
10
|
redis: RedisClient;
|
|
11
|
-
filterUser
|
|
11
|
+
filterUser(user: UserModel): Partial<UserModel>;
|
|
12
12
|
tokenExpiry?: number;
|
|
13
|
+
onResetRequested?(user: UserModel, temporaryToken: string): void | Promise<void>;
|
|
14
|
+
onPasswordChanged?(user: UserModel, newPassword: string): Promise<void>;
|
|
13
15
|
}
|
|
14
|
-
export declare function createAuth<UserModel extends Record<string, any>>({ getUserByToken, methods: { emailAndPassword }, filterUser, redis, tokenExpiry, }: AuthOptions<UserModel>): {
|
|
16
|
+
export declare function createAuth<UserModel extends Record<string, any>>({ getUserByToken, methods: { emailAndPassword }, filterUser, redis, tokenExpiry, onResetRequested, onPasswordChanged, }: AuthOptions<UserModel>): {
|
|
15
17
|
createRouter: () => import("express-serve-static-core").Router;
|
|
16
18
|
useCurrentUser: () => UserModel | undefined;
|
|
17
19
|
middleware: (authorize?: boolean) => RequestHandler;
|
|
@@ -24,4 +26,3 @@ export declare function parseAuthorizationHeader(req: Request): string | null;
|
|
|
24
26
|
export declare function setAccessTokenCookie(res: Response, token: string | null): void;
|
|
25
27
|
export declare function checkIfPasswordsMatch(realPassword: string, attemptedPassword: string): Promise<boolean>;
|
|
26
28
|
export declare function hashPassword(password: string): Promise<string>;
|
|
27
|
-
export {};
|
package/dist/auth.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import * as bcrypt from 'bcrypt';
|
|
2
2
|
import { Router, } from 'express';
|
|
3
3
|
import jwt from 'jsonwebtoken';
|
|
4
|
+
import * as crypto from 'node:crypto';
|
|
4
5
|
import { v4 } from 'uuid';
|
|
5
6
|
import { setContext, useContext } from './context.js';
|
|
6
|
-
import { Unauthorized } from './errors.js';
|
|
7
|
+
import { BadRequest, NotFound, Unauthorized } from './errors.js';
|
|
7
8
|
import { getLogger } from './logger.js';
|
|
8
9
|
import { route } from './middleware.js';
|
|
9
|
-
import { validateBody } from './validate.js';
|
|
10
|
+
import { validateBody, validateParams, } from './validate.js';
|
|
11
|
+
const logger = getLogger('auth');
|
|
10
12
|
const TOKEN_EXPIRES_IN = 3600 * 48;
|
|
13
|
+
const TEMP_TOKEN_EXIRES_IN = 3600;
|
|
11
14
|
export function createAuthToken(payload, secret = process.env.APP_JWT_SECRET) {
|
|
12
15
|
return jwt.sign(payload, secret, { expiresIn: TOKEN_EXPIRES_IN });
|
|
13
16
|
}
|
|
14
|
-
export function createAuth({ getUserByToken, methods: { emailAndPassword }, filterUser = (x) => x, redis, tokenExpiry = TOKEN_EXPIRES_IN, }) {
|
|
17
|
+
export function createAuth({ getUserByToken, methods: { emailAndPassword }, filterUser = (x) => x, redis, tokenExpiry = TOKEN_EXPIRES_IN, onResetRequested, onPasswordChanged, }) {
|
|
15
18
|
const useCurrentUser = () => useContext('currentUser');
|
|
16
19
|
const createRouter = () => {
|
|
17
20
|
const router = Router();
|
|
@@ -36,7 +39,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
36
39
|
return [201, { token }];
|
|
37
40
|
}
|
|
38
41
|
catch (error) {
|
|
39
|
-
|
|
42
|
+
logger.error(error);
|
|
40
43
|
throw Unauthorized(error.message);
|
|
41
44
|
}
|
|
42
45
|
}));
|
|
@@ -49,6 +52,72 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
49
52
|
setAccessTokenCookie(res, null);
|
|
50
53
|
res.redirect('/');
|
|
51
54
|
}));
|
|
55
|
+
const skipPasswordCheck = async () => true;
|
|
56
|
+
const tokenSchema = {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
token: { type: 'string', minLength: 50, maxLength: 64 },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const emailSchema = {
|
|
63
|
+
type: 'string',
|
|
64
|
+
minLength: 4,
|
|
65
|
+
};
|
|
66
|
+
router.post('/reset/:token', validateParams(tokenSchema), validateBody({
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
email: emailSchema,
|
|
70
|
+
password: { type: 'string' },
|
|
71
|
+
},
|
|
72
|
+
required: ['email', 'password'],
|
|
73
|
+
}), route(async (req) => {
|
|
74
|
+
const { email, password } = req.body;
|
|
75
|
+
const { token } = req.params;
|
|
76
|
+
if (!emailAndPassword) {
|
|
77
|
+
throw new Error('Not applicable for login method');
|
|
78
|
+
}
|
|
79
|
+
const user = await emailAndPassword(email, skipPasswordCheck);
|
|
80
|
+
const tokenEmail = await redis.get(token);
|
|
81
|
+
if (!tokenEmail || email !== tokenEmail) {
|
|
82
|
+
throw BadRequest('invalid email');
|
|
83
|
+
}
|
|
84
|
+
await onPasswordChanged?.(user, await hashPassword(password));
|
|
85
|
+
await redis.del(token);
|
|
86
|
+
logger.info('user changed password', { email: tokenEmail });
|
|
87
|
+
return 200;
|
|
88
|
+
}));
|
|
89
|
+
router.get('/reset/:token', validateParams(tokenSchema), route(async (req) => {
|
|
90
|
+
const token = req.params.token;
|
|
91
|
+
const email = await redis.get(token);
|
|
92
|
+
if (!email) {
|
|
93
|
+
throw NotFound();
|
|
94
|
+
}
|
|
95
|
+
return 200;
|
|
96
|
+
}));
|
|
97
|
+
router.post('/reset', middleware(false), validateBody({
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
email: emailSchema,
|
|
101
|
+
},
|
|
102
|
+
required: ['email'],
|
|
103
|
+
}), route(async (req) => {
|
|
104
|
+
const email = req.body.email;
|
|
105
|
+
if (!emailAndPassword) {
|
|
106
|
+
throw new Error('Not applicable for login method');
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const user = await emailAndPassword(email, skipPasswordCheck);
|
|
110
|
+
const temporaryToken = crypto.randomBytes(32).toString('hex');
|
|
111
|
+
await redis.set(temporaryToken, user.email, {
|
|
112
|
+
EX: TEMP_TOKEN_EXIRES_IN,
|
|
113
|
+
});
|
|
114
|
+
await onResetRequested?.(user, temporaryToken);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
logger.error(error, 'auth reset failed');
|
|
118
|
+
}
|
|
119
|
+
return 202;
|
|
120
|
+
}));
|
|
52
121
|
router.get('/token/refresh', middleware(false), route(async (req, res) => {
|
|
53
122
|
const user = useCurrentUser();
|
|
54
123
|
if (!user) {
|
|
@@ -88,7 +157,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
88
157
|
next();
|
|
89
158
|
}
|
|
90
159
|
catch (error) {
|
|
91
|
-
|
|
160
|
+
logger.error(error);
|
|
92
161
|
next(Unauthorized('Invalid token'));
|
|
93
162
|
return;
|
|
94
163
|
}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Request, type RequestHandler } from 'express';
|
|
2
|
-
interface RequestContextOptions {
|
|
2
|
+
export interface RequestContextOptions {
|
|
3
3
|
getRequestId?: (req: Request) => string | undefined;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
@@ -23,4 +23,3 @@ export declare function useRequestId(): string | undefined;
|
|
|
23
23
|
* @deprecated
|
|
24
24
|
*/
|
|
25
25
|
export declare const getRequestId: typeof useRequestId;
|
|
26
|
-
export {};
|
package/dist/listen.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
2
|
import type * as http from 'node:http';
|
|
3
|
-
import { type Logger } from './logger.js';
|
|
4
3
|
/**
|
|
5
4
|
* Listen on the given port and setup proper signal
|
|
6
5
|
* handlers for terminating the process properly.
|
|
@@ -9,7 +8,6 @@ import { type Logger } from './logger.js';
|
|
|
9
8
|
*/
|
|
10
9
|
export declare function listen(server: http.Server, options: {
|
|
11
10
|
port: number;
|
|
12
|
-
logger: Logger;
|
|
13
11
|
onShutdown?: () => void | Promise<void>;
|
|
14
12
|
healthCheck?: () => Promise<void>;
|
|
15
13
|
}): void;
|
package/dist/listen.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createTerminus } from '@godaddy/terminus';
|
|
2
|
+
import { getLogger } from './logger.js';
|
|
3
|
+
const logger = getLogger();
|
|
2
4
|
const READINESS_PERIOD_MS = parseInt(process.env.DM_READINESS_PERIOD_MS || '', 10) || 0;
|
|
3
5
|
const FORCE_EXIT_TIMEOUT = parseInt(process.env.DM_FORCE_EXIT_TIMEOUT || '', 10) || 1000;
|
|
4
6
|
/**
|
|
@@ -12,7 +14,7 @@ export function listen(server, options) {
|
|
|
12
14
|
timeout: FORCE_EXIT_TIMEOUT,
|
|
13
15
|
signals: ['SIGTERM', 'SIGINT'],
|
|
14
16
|
async onShutdown() {
|
|
15
|
-
|
|
17
|
+
logger.info('app shutting down');
|
|
16
18
|
},
|
|
17
19
|
beforeShutdown() {
|
|
18
20
|
return new Promise((resolve) => {
|
|
@@ -20,11 +22,11 @@ export function listen(server, options) {
|
|
|
20
22
|
});
|
|
21
23
|
},
|
|
22
24
|
async onSignal() {
|
|
23
|
-
|
|
25
|
+
logger.info('app received shutdown signal');
|
|
24
26
|
await options?.onShutdown?.();
|
|
25
27
|
},
|
|
26
28
|
async onSendFailureDuringShutdown() {
|
|
27
|
-
|
|
29
|
+
logger.info('app sent service not available message to client');
|
|
28
30
|
},
|
|
29
31
|
healthChecks: {
|
|
30
32
|
'/health': async ({ state }) => {
|
|
@@ -35,7 +37,7 @@ export function listen(server, options) {
|
|
|
35
37
|
await options?.healthCheck?.();
|
|
36
38
|
}
|
|
37
39
|
catch (error) {
|
|
38
|
-
|
|
40
|
+
logger.error(error, 'health check failed');
|
|
39
41
|
throw error;
|
|
40
42
|
}
|
|
41
43
|
},
|
|
@@ -43,6 +45,6 @@ export function listen(server, options) {
|
|
|
43
45
|
});
|
|
44
46
|
const port = options.port;
|
|
45
47
|
server.listen(port, () => {
|
|
46
|
-
|
|
48
|
+
logger.info('app listening', { port });
|
|
47
49
|
});
|
|
48
50
|
}
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CreateSentry } from './sentry.js';
|
|
2
|
+
export declare type LoggerContext = Record<string, any>;
|
|
2
3
|
export interface LoggerOptions {
|
|
3
4
|
/**
|
|
4
5
|
* Name of the logger
|
|
@@ -7,15 +8,14 @@ export interface LoggerOptions {
|
|
|
7
8
|
/**
|
|
8
9
|
* Context to attach to each log line
|
|
9
10
|
*/
|
|
10
|
-
getContext?: () =>
|
|
11
|
-
[key: string]: any;
|
|
12
|
-
};
|
|
11
|
+
getContext?: () => LoggerContext;
|
|
13
12
|
/**
|
|
14
13
|
* Defaults to `process.env.NODE_ENV === 'test'`
|
|
15
14
|
*/
|
|
16
15
|
silent?: () => boolean;
|
|
17
16
|
}
|
|
18
17
|
export declare function attachSentryClientToLoggers(sentryClient: CreateSentry['client']): void;
|
|
18
|
+
export declare function setGlobalLoggerContext(getContext: () => LoggerContext): void;
|
|
19
19
|
export declare type Logger = ReturnType<typeof createLogger>;
|
|
20
20
|
export declare function createLogger({ name, getContext, silent, }: LoggerOptions): {
|
|
21
21
|
debug(message: string, context?: {
|
|
@@ -30,7 +30,7 @@ export declare function createLogger({ name, getContext, silent, }: LoggerOption
|
|
|
30
30
|
error(error: Error | null, message?: string): void;
|
|
31
31
|
fatal(error: Error | null, message?: string): void;
|
|
32
32
|
};
|
|
33
|
-
export declare function getLogger(name
|
|
33
|
+
export declare function getLogger(name?: string): Logger;
|
|
34
34
|
export declare function getLoggers(): Map<string, {
|
|
35
35
|
debug(message: string, context?: {
|
|
36
36
|
[key: string]: any;
|
package/dist/logger.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import bunyan from 'bunyan';
|
|
2
2
|
import { useRequestId } from './context.js';
|
|
3
|
-
function defaultGetContext() {
|
|
4
|
-
return {
|
|
5
|
-
requestId: useRequestId(),
|
|
6
|
-
};
|
|
7
|
-
}
|
|
8
3
|
const _loggers = new Map();
|
|
9
4
|
let _sentryClient;
|
|
10
5
|
export function attachSentryClientToLoggers(sentryClient) {
|
|
11
6
|
_sentryClient = sentryClient;
|
|
12
7
|
}
|
|
8
|
+
let _globalGetContext;
|
|
9
|
+
export function setGlobalLoggerContext(getContext) {
|
|
10
|
+
_globalGetContext = getContext;
|
|
11
|
+
}
|
|
12
|
+
function defaultGetContext() {
|
|
13
|
+
return {
|
|
14
|
+
requestId: useRequestId(),
|
|
15
|
+
..._globalGetContext?.(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
13
18
|
export function createLogger({ name, getContext, silent = () => process.env.NODE_ENV === 'test', }) {
|
|
14
19
|
if (_loggers.has(name)) {
|
|
15
20
|
throw new Error(`A logger named ${name} already exists`);
|
|
@@ -60,7 +65,7 @@ export function createLogger({ name, getContext, silent = () => process.env.NODE
|
|
|
60
65
|
},
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
|
-
export function getLogger(name) {
|
|
68
|
+
export function getLogger(name = 'default') {
|
|
64
69
|
const logger = _loggers.get(name);
|
|
65
70
|
if (!logger) {
|
|
66
71
|
return createLogger({ name });
|
package/dist/routes.js
CHANGED
|
@@ -10,7 +10,7 @@ function split(value) {
|
|
|
10
10
|
.toString()
|
|
11
11
|
.replace('/?', '')
|
|
12
12
|
.replace('(?=/|$)', '$')
|
|
13
|
-
.match(/^\/\^((?:\\[.*+?^${}()|[\]
|
|
13
|
+
.match(/^\/\^((?:\\[.*+?^${}()|[\]\\/]|[^.*+?^${}()|[\]\\/])*)\$\//);
|
|
14
14
|
return match
|
|
15
15
|
? match[1].replace(/\\(.)/g, '$1')
|
|
16
16
|
: '<complex:' + value.toString() + '>';
|
package/dist/sentry.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export declare function createSentryOptions(app: Application | null, options?: S
|
|
|
29
29
|
autoSessionTracking?: boolean | undefined;
|
|
30
30
|
sendClientReports?: boolean | undefined;
|
|
31
31
|
dist?: string | undefined;
|
|
32
|
+
instrumenter?: import("@sentry/types").Instrumenter | undefined;
|
|
32
33
|
transportOptions?: Partial<import("@sentry/node/types/transports/http.js").NodeTransportOptions> | undefined;
|
|
33
34
|
tracesSampleRate: number;
|
|
34
35
|
initialScope?: import("@sentry/types").CaptureContext | undefined;
|
|
@@ -46,8 +47,10 @@ export declare function createSentryOptions(app: Application | null, options?: S
|
|
|
46
47
|
} | undefined;
|
|
47
48
|
tracesSampler?: ((samplingContext: import("@sentry/types").SamplingContext) => number | boolean) | undefined;
|
|
48
49
|
beforeSend?: ((event: Sentry.Event, hint: Sentry.EventHint) => Sentry.Event | PromiseLike<Sentry.Event | null> | null) | undefined;
|
|
50
|
+
beforeSendTransaction?: ((event: Sentry.Event, hint: Sentry.EventHint) => Sentry.Event | PromiseLike<Sentry.Event | null> | null) | undefined;
|
|
49
51
|
beforeBreadcrumb?: ((breadcrumb: Sentry.Breadcrumb, hint?: Sentry.BreadcrumbHint | undefined) => Sentry.Breadcrumb | null) | undefined;
|
|
50
52
|
serverName?: string | undefined;
|
|
51
53
|
tracePropagationTargets?: import("@sentry/types").TracePropagationTargets | undefined;
|
|
54
|
+
shouldCreateSpanForRequest?(url: string): boolean;
|
|
52
55
|
onFatalError?(error: Error): void;
|
|
53
56
|
};
|
package/dist/slack.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createFetch } from '@devmoods/fetch';
|
|
2
2
|
import { getLogger } from './logger.js';
|
|
3
|
+
const logger = getLogger();
|
|
3
4
|
/**
|
|
4
5
|
* Send messages to Slack using Incoming Webhooks
|
|
5
6
|
*
|
|
@@ -24,6 +25,7 @@ export function createSlack({ webhookUrl = process.env.SLACK_WEBHOOK_URL, dryRun
|
|
|
24
25
|
*/
|
|
25
26
|
async function postMessage(message, blocks) {
|
|
26
27
|
if (dryRun) {
|
|
28
|
+
logger.info('slack post message (dry run)', { message });
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
try {
|
|
@@ -40,7 +42,7 @@ export function createSlack({ webhookUrl = process.env.SLACK_WEBHOOK_URL, dryRun
|
|
|
40
42
|
return response.jsonData;
|
|
41
43
|
}
|
|
42
44
|
catch (error) {
|
|
43
|
-
|
|
45
|
+
logger.error(error);
|
|
44
46
|
throw error;
|
|
45
47
|
}
|
|
46
48
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@types/jest": "29.2.2",
|
|
39
39
|
"@types/node": "18.11.9",
|
|
40
40
|
"@types/supertest": "2.0.12",
|
|
41
|
-
"@typescript-eslint/eslint-plugin": "^5.42.
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
|
42
42
|
"eslint": "^8.27.0",
|
|
43
43
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
|
44
44
|
"eslint-plugin-prettier": "^4.2.1",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"import-sort-cli": "6.0.0",
|
|
48
48
|
"import-sort-style-module": "6.0.0",
|
|
49
49
|
"isomorphic-fetch": "3.0.0",
|
|
50
|
-
"jest": "29.
|
|
50
|
+
"jest": "29.3.1",
|
|
51
51
|
"prettier": "2.7.1",
|
|
52
52
|
"supertest": "6.3.1",
|
|
53
53
|
"ts-jest": "29.0.3",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@devmoods/fetch": "^3.3.0",
|
|
59
59
|
"@godaddy/terminus": "^4.11.2",
|
|
60
|
-
"@sentry/node": "^7.
|
|
61
|
-
"@sentry/tracing": "^7.
|
|
60
|
+
"@sentry/node": "^7.18.0",
|
|
61
|
+
"@sentry/tracing": "^7.18.0",
|
|
62
62
|
"@types/bcrypt": "^5.0.0",
|
|
63
63
|
"@types/bunyan": "^1.8.8",
|
|
64
64
|
"@types/cls-hooked": "^4.3.3",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"cls-hooked": "^4.2.2",
|
|
75
75
|
"compose-middleware": "^5.0.1",
|
|
76
76
|
"cookie-parser": "^1.4.6",
|
|
77
|
-
"dotenv
|
|
77
|
+
"dotenv": "^16.0.3",
|
|
78
78
|
"express": "^4.18.2",
|
|
79
79
|
"helmet": "^6.0.0",
|
|
80
80
|
"jsonwebtoken": "^8.5.1",
|