@devmoods/express-extras 0.23.0 → 0.24.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/README.md +1 -6
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +11 -15
- package/dist/faktory.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/listen.js +3 -2
- package/dist/postgres.d.ts +2 -1
- package/dist/postgres.js +3 -2
- package/dist/redis.js +2 -1
- package/dist/sentry.js +6 -2
- package/dist/setupTests.d.ts +1 -0
- package/dist/setupTests.js +9 -0
- package/dist/slack.js +2 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +14 -0
- package/dist/validate.d.ts +1 -1
- package/dist/validate.js +4 -3
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -44,12 +44,7 @@ FORCE_EXIT_TIMEOUT=
|
|
|
44
44
|
|
|
45
45
|
APP_JWT_SECRET=123456
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
PGUSER=my_database
|
|
49
|
-
PGPASSWORD=
|
|
50
|
-
PGDATABASE=my_database_dev
|
|
51
|
-
PGPORT=5432
|
|
52
|
-
|
|
47
|
+
POSTGRES_URL=postgresql://user:pass@localhost:5432/my_database
|
|
53
48
|
REDIS_URL=
|
|
54
49
|
|
|
55
50
|
SLACK_WEBHOOK_URL=
|
package/dist/auth.d.ts
CHANGED
|
@@ -12,14 +12,14 @@ export interface AuthOptions<UserModel extends Record<string, any>> {
|
|
|
12
12
|
tokenExpiry?: number;
|
|
13
13
|
onPasswordResetRequest?(user: UserModel, temporaryToken: string): void | Promise<void>;
|
|
14
14
|
onPasswordChanged?(user: UserModel, newPassword: string): Promise<void>;
|
|
15
|
-
onEmailVerificationRequest?(user: UserModel,
|
|
15
|
+
onEmailVerificationRequest?(user: UserModel, temporaryToken: string): Promise<void>;
|
|
16
16
|
onEmailVerified?(user: UserModel): Promise<void>;
|
|
17
17
|
}
|
|
18
18
|
export interface CreateAuth<UserModel extends Record<string, any>> {
|
|
19
19
|
createRouter: () => Router;
|
|
20
20
|
useCurrentUser: () => UserModel | undefined;
|
|
21
21
|
middleware: (authorize?: boolean) => RequestHandler;
|
|
22
|
-
|
|
22
|
+
createTemporaryToken(user: UserModel): Promise<string>;
|
|
23
23
|
}
|
|
24
24
|
export declare function createAuth<UserModel extends Record<string, any>>({ getUserByToken, methods: { emailAndPassword }, filterUser, redis, tokenExpiry, onPasswordResetRequest, onPasswordChanged, onEmailVerified, onEmailVerificationRequest, }: AuthOptions<UserModel>): CreateAuth<UserModel>;
|
|
25
25
|
/**
|
package/dist/auth.js
CHANGED
|
@@ -8,10 +8,12 @@ import { BadRequest, NotFound, Unauthorized } from './errors.js';
|
|
|
8
8
|
import { getLogger } from './logger.js';
|
|
9
9
|
import { route } from './middleware.js';
|
|
10
10
|
import { validateBody, validateParams, } from './validate.js';
|
|
11
|
+
import { readEnv } from './utils.js';
|
|
11
12
|
const logger = getLogger('auth');
|
|
12
|
-
const TOKEN_EXPIRES_IN = 3600 * 48;
|
|
13
|
-
const TEMP_TOKEN_EXIRES_IN = 3600;
|
|
14
|
-
|
|
13
|
+
const TOKEN_EXPIRES_IN = readEnv.number('AUTH_TOKEN_EXPIRES', 3600 * 48);
|
|
14
|
+
const TEMP_TOKEN_EXIRES_IN = readEnv.number('AUTH_TEMP_TOKEN_EXPIRES', 3600);
|
|
15
|
+
const AUTH_SECRET = readEnv('AUTH_JWT_SECRET', '');
|
|
16
|
+
export function createAuthToken(payload, secret = AUTH_SECRET) {
|
|
15
17
|
return jwt.sign(payload, secret, { expiresIn: TOKEN_EXPIRES_IN });
|
|
16
18
|
}
|
|
17
19
|
export function createAuth({ getUserByToken, methods: { emailAndPassword }, filterUser = (x) => x, redis, tokenExpiry = TOKEN_EXPIRES_IN, onPasswordResetRequest, onPasswordChanged, onEmailVerified, onEmailVerificationRequest, }) {
|
|
@@ -92,11 +94,8 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
92
94
|
throw new Error('Not applicable for login method');
|
|
93
95
|
}
|
|
94
96
|
const user = await emailAndPassword(email, skipPasswordCheck);
|
|
95
|
-
const
|
|
96
|
-
await
|
|
97
|
-
EX: TEMP_TOKEN_EXIRES_IN,
|
|
98
|
-
});
|
|
99
|
-
await onEmailVerificationRequest?.(user, verificationToken);
|
|
97
|
+
const temporaryToken = await createTemporaryToken(user);
|
|
98
|
+
await onEmailVerificationRequest?.(user, temporaryToken);
|
|
100
99
|
return 202;
|
|
101
100
|
}));
|
|
102
101
|
router.post('/verify-email', validateBody({
|
|
@@ -155,10 +154,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
155
154
|
}
|
|
156
155
|
try {
|
|
157
156
|
const user = await emailAndPassword(email, skipPasswordCheck);
|
|
158
|
-
const temporaryToken =
|
|
159
|
-
await redis.set(temporaryToken, user.email, {
|
|
160
|
-
EX: TEMP_TOKEN_EXIRES_IN,
|
|
161
|
-
});
|
|
157
|
+
const temporaryToken = await createTemporaryToken(user);
|
|
162
158
|
await onPasswordResetRequest?.(user, temporaryToken);
|
|
163
159
|
logger.info('password reset request', { email });
|
|
164
160
|
}
|
|
@@ -199,7 +195,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
199
195
|
}
|
|
200
196
|
throw new InvalidToken('token expired');
|
|
201
197
|
}
|
|
202
|
-
const decoded = jwt.verify(token.trim(),
|
|
198
|
+
const decoded = jwt.verify(token.trim(), AUTH_SECRET);
|
|
203
199
|
if (typeof decoded === 'string') {
|
|
204
200
|
throw new Error('Failed to decode token');
|
|
205
201
|
}
|
|
@@ -214,7 +210,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
214
210
|
}
|
|
215
211
|
};
|
|
216
212
|
}
|
|
217
|
-
async function
|
|
213
|
+
async function createTemporaryToken(user) {
|
|
218
214
|
try {
|
|
219
215
|
const temporaryToken = crypto.randomBytes(32).toString('hex');
|
|
220
216
|
await redis.set(temporaryToken, user.email, {
|
|
@@ -231,7 +227,7 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
231
227
|
createRouter,
|
|
232
228
|
useCurrentUser,
|
|
233
229
|
middleware,
|
|
234
|
-
|
|
230
|
+
createTemporaryToken,
|
|
235
231
|
};
|
|
236
232
|
}
|
|
237
233
|
/**
|
package/dist/faktory.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare function createJobs({ queueName }: JobsOptions): {
|
|
|
11
11
|
};
|
|
12
12
|
queueName: string;
|
|
13
13
|
};
|
|
14
|
-
export type
|
|
14
|
+
export type Job = ReturnType<ReturnType<typeof createJobs>['job']>;
|
|
15
15
|
export interface FaktoryWorkerOptions {
|
|
16
16
|
jobs: unknown[];
|
|
17
17
|
queueName: string;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -11,5 +11,5 @@ export * from './redis.js';
|
|
|
11
11
|
export * from './routes.js';
|
|
12
12
|
export * from './sentry.js';
|
|
13
13
|
export * from './slack.js';
|
|
14
|
+
export * from './utils.js';
|
|
14
15
|
export * from './validate.js';
|
|
15
|
-
export const envPort = (defaultPort = 3000) => parseInt(process.env.PORT, 10) || defaultPort;
|
package/dist/listen.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createTerminus } from '@godaddy/terminus';
|
|
2
2
|
import { getLogger } from './logger.js';
|
|
3
|
+
import { readEnv } from './utils.js';
|
|
3
4
|
const logger = getLogger('http');
|
|
4
|
-
const READINESS_PERIOD_MS =
|
|
5
|
-
const FORCE_EXIT_TIMEOUT =
|
|
5
|
+
const READINESS_PERIOD_MS = readEnv.number('READINESS_PERIOD_MS', 0);
|
|
6
|
+
const FORCE_EXIT_TIMEOUT = readEnv.number('FORCE_EXIT_TIMEOUT', 5000);
|
|
6
7
|
/**
|
|
7
8
|
* Listen on the given port and setup proper signal
|
|
8
9
|
* handlers for terminating the process properly.
|
package/dist/postgres.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import pg, { type PoolClient, type QueryResult, type QueryResultRow } from 'pg';
|
|
|
2
2
|
import { SQL, type SQLStatement } from 'sql-template-strings';
|
|
3
3
|
export interface PostgresOptions extends pg.PoolConfig {
|
|
4
4
|
onError?: (error: Error, client: any) => void;
|
|
5
|
+
url?: pg.PoolConfig['connectionString'];
|
|
5
6
|
}
|
|
6
7
|
export declare class DoesNotExistError extends Error {
|
|
7
8
|
constructor(message: string);
|
|
@@ -31,7 +32,7 @@ export declare class Transaction extends Connection {
|
|
|
31
32
|
*
|
|
32
33
|
* @param config
|
|
33
34
|
*/
|
|
34
|
-
export declare function createPostgres({ onError, ...config }?: PostgresOptions): {
|
|
35
|
+
export declare function createPostgres({ onError, url, ...config }?: PostgresOptions): {
|
|
35
36
|
insert: <T extends pg.QueryResultRow>(text: string | SQLStatement, values?: Value[] | undefined) => Promise<pg.QueryResult<T>>;
|
|
36
37
|
query: <T_1 extends pg.QueryResultRow>(text: string | SQLStatement, values?: Value[] | undefined) => Promise<pg.QueryResult<T_1>>;
|
|
37
38
|
first: <T_2>(text: string | SQLStatement, values?: Value[] | undefined) => Promise<T_2 | null>;
|
package/dist/postgres.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
2
|
import { SQL } from 'sql-template-strings';
|
|
3
|
+
import { readEnv } from './utils.js';
|
|
3
4
|
export class DoesNotExistError extends Error {
|
|
4
5
|
constructor(message) {
|
|
5
6
|
super(message);
|
|
@@ -67,8 +68,8 @@ export class Transaction extends Connection {
|
|
|
67
68
|
*
|
|
68
69
|
* @param config
|
|
69
70
|
*/
|
|
70
|
-
export function createPostgres({ onError = () => { }, ...config } = {}) {
|
|
71
|
-
const pool = new pg.Pool(config);
|
|
71
|
+
export function createPostgres({ onError = () => { }, url = readEnv('POSTGRES_URL'), ...config } = {}) {
|
|
72
|
+
const pool = new pg.Pool({ connectionString: url, ...config });
|
|
72
73
|
pool.on('error', onError);
|
|
73
74
|
async function withConnection(callback) {
|
|
74
75
|
const client = await pool.connect();
|
package/dist/redis.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createClient } from 'redis';
|
|
2
|
-
|
|
2
|
+
import { readEnv } from './utils.js';
|
|
3
|
+
export function createRedis({ url = readEnv('REDIS_URL', ''), onError, }) {
|
|
3
4
|
const client = createClient({ url });
|
|
4
5
|
client.on('error', onError);
|
|
5
6
|
return client;
|
package/dist/sentry.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import * as Sentry from '@sentry/node';
|
|
2
|
+
import { attachSentryClientToLoggers } from './logger.js';
|
|
3
|
+
import { readEnv } from './utils.js';
|
|
2
4
|
/**
|
|
3
5
|
* Create a pre-configured Sentry client
|
|
4
6
|
*/
|
|
5
7
|
export function createSentry(options = {}) {
|
|
8
|
+
const client = Sentry;
|
|
6
9
|
return {
|
|
7
|
-
client
|
|
10
|
+
client,
|
|
8
11
|
init(app, getOptions = (defaultOptions) => defaultOptions) {
|
|
12
|
+
attachSentryClientToLoggers(client);
|
|
9
13
|
Sentry.init(getOptions(createSentryOptions(app, options)));
|
|
10
14
|
},
|
|
11
15
|
};
|
|
@@ -15,7 +19,7 @@ export function createSentry(options = {}) {
|
|
|
15
19
|
*/
|
|
16
20
|
export function createSentryOptions(app, options = {}) {
|
|
17
21
|
return {
|
|
18
|
-
dsn:
|
|
22
|
+
dsn: readEnv('SENTRY_DSN'),
|
|
19
23
|
integrations: [
|
|
20
24
|
new Sentry.Integrations.Http({ tracing: true }),
|
|
21
25
|
app && new Sentry.Integrations.Express({ app }),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import createFetchMock from 'vitest-fetch-mock';
|
|
5
|
+
import { vi } from 'vitest';
|
|
6
|
+
const fetchMocker = createFetchMock(vi);
|
|
7
|
+
fetchMocker.enableMocks();
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
globalThis.crypto = crypto;
|
package/dist/slack.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createFetch } from '@devmoods/fetch';
|
|
2
2
|
import { getLogger } from './logger.js';
|
|
3
|
+
import { readEnv } from './utils.js';
|
|
3
4
|
const logger = getLogger('slack');
|
|
4
5
|
/**
|
|
5
6
|
* Send messages to Slack using Incoming Webhooks
|
|
@@ -14,7 +15,7 @@ const logger = getLogger('slack');
|
|
|
14
15
|
*
|
|
15
16
|
* See https://api.slack.com/messaging/webhooks
|
|
16
17
|
*/
|
|
17
|
-
export function createSlack({ webhookUrl =
|
|
18
|
+
export function createSlack({ webhookUrl = readEnv('SLACK_WEBHOOK_URL', ''), dryRun = process.env.NODE_ENV !== 'production', } = {}) {
|
|
18
19
|
const fetch = createFetch();
|
|
19
20
|
/**
|
|
20
21
|
* Post a message to Slack
|
package/dist/utils.d.ts
CHANGED
|
@@ -5,3 +5,13 @@
|
|
|
5
5
|
export declare function startTimer(): {
|
|
6
6
|
getDuration(): number;
|
|
7
7
|
};
|
|
8
|
+
export declare function readEnv(key: string): string | undefined;
|
|
9
|
+
export declare function readEnv(key: string, defaultValue: string): string;
|
|
10
|
+
export declare namespace readEnv {
|
|
11
|
+
var number: typeof readEnvNumber;
|
|
12
|
+
var boolean: typeof readEnvBoolean;
|
|
13
|
+
}
|
|
14
|
+
declare function readEnvNumber(key: string, defaultValue: number): number;
|
|
15
|
+
declare function readEnvBoolean(key: string, defaultValue: boolean): boolean;
|
|
16
|
+
export declare const envPort: (defaultPort?: number) => number;
|
|
17
|
+
export {};
|
package/dist/utils.js
CHANGED
|
@@ -11,3 +11,17 @@ export function startTimer() {
|
|
|
11
11
|
},
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
export function readEnv(key, defaultValue) {
|
|
15
|
+
const value = process.env[key];
|
|
16
|
+
return value ?? defaultValue;
|
|
17
|
+
}
|
|
18
|
+
readEnv.number = readEnvNumber;
|
|
19
|
+
readEnv.boolean = readEnvBoolean;
|
|
20
|
+
function readEnvNumber(key, defaultValue) {
|
|
21
|
+
const str = readEnv(key, `${defaultValue}`);
|
|
22
|
+
return parseFloat(str);
|
|
23
|
+
}
|
|
24
|
+
function readEnvBoolean(key, defaultValue) {
|
|
25
|
+
return ['true', '1', 't'].includes(readEnv(key, `${defaultValue}`).toLowerCase());
|
|
26
|
+
}
|
|
27
|
+
export const envPort = (defaultPort = 3000) => readEnv.number('PORT', defaultPort);
|
package/dist/validate.d.ts
CHANGED
package/dist/validate.js
CHANGED
|
@@ -45,7 +45,7 @@ export function compileValidateSchema(schema, options = {}) {
|
|
|
45
45
|
errors: null,
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
const errors = transformAjvErrors(validate.errors)
|
|
48
|
+
const errors = transformAjvErrors(validate.errors);
|
|
49
49
|
validate.errors = null;
|
|
50
50
|
return {
|
|
51
51
|
isValid: false,
|
|
@@ -65,7 +65,7 @@ export function validateSchema(data, schema, options = {}) {
|
|
|
65
65
|
errors: null,
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
-
const errors = transformAjvErrors(ajv.errors)
|
|
68
|
+
const errors = transformAjvErrors(ajv.errors);
|
|
69
69
|
ajv.errors = null;
|
|
70
70
|
return {
|
|
71
71
|
isValid: false,
|
|
@@ -73,8 +73,9 @@ export function validateSchema(data, schema, options = {}) {
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
function transformAjvErrors(errors) {
|
|
76
|
+
/* c8 ignore next 3 */
|
|
76
77
|
if (errors == null) {
|
|
77
|
-
return
|
|
78
|
+
return {};
|
|
78
79
|
}
|
|
79
80
|
return errors
|
|
80
81
|
.map((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
"@devmoods/eslint-config": "^1.2.0",
|
|
27
27
|
"@sentry/types": "^7.61.1",
|
|
28
28
|
"@types/express-serve-static-core": "^4.17.35",
|
|
29
|
-
"@types/node": "20.4.
|
|
29
|
+
"@types/node": "20.4.8",
|
|
30
30
|
"@types/supertest": "^2.0.12",
|
|
31
|
-
"@typescript-eslint/eslint-plugin": "^6.
|
|
32
|
-
"@typescript-eslint/parser": "^6.
|
|
31
|
+
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
|
32
|
+
"@typescript-eslint/parser": "^6.3.0",
|
|
33
|
+
"@vitest/coverage-v8": "^0.34.1",
|
|
33
34
|
"@vitest/expect": "^0.34.1",
|
|
34
35
|
"chalk": "^5.3.0",
|
|
35
36
|
"eslint": "^8.46.0",
|
|
@@ -44,7 +45,8 @@
|
|
|
44
45
|
"supertest": "6.3.3",
|
|
45
46
|
"ts-node": "10.9.1",
|
|
46
47
|
"typescript": "5.1.6",
|
|
47
|
-
"vitest": "^0.34.1"
|
|
48
|
+
"vitest": "^0.34.1",
|
|
49
|
+
"vitest-fetch-mock": "^0.2.2"
|
|
48
50
|
},
|
|
49
51
|
"dependencies": {
|
|
50
52
|
"@devmoods/fetch": "^3.3.0",
|
|
@@ -101,7 +103,7 @@
|
|
|
101
103
|
"start:worker": "ts-node --esm example/worker.ts",
|
|
102
104
|
"clean": "rm -rf dist",
|
|
103
105
|
"lint": "eslint src",
|
|
104
|
-
"test": "NODE_ENV=test vitest",
|
|
106
|
+
"test": "NODE_ENV=test DOTENV_CONFIG_PATH=.env.test vitest",
|
|
105
107
|
"build": "pnpm run clean && tsc --project tsconfig.build.json",
|
|
106
108
|
"prettier": "prettier --write '**/*.{js,css,md,ts,tsx}'",
|
|
107
109
|
"import-sort": "import-sort --write '**/*.{js,jsx,ts,tsx}'",
|