@devmoods/express-extras 0.41.0 → 0.42.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 -2
- package/dist/auth.js +27 -10
- package/dist/csrf.d.ts +1 -0
- package/dist/csrf.js +9 -2
- package/dist/middleware.d.ts +1 -1
- package/dist/postgres.d.ts +2 -1
- package/dist/postgres.js +13 -2
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +4 -1
- package/dist/validate.d.ts +5 -5
- package/package.json +8 -8
package/dist/auth.d.ts
CHANGED
|
@@ -30,7 +30,10 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
|
30
30
|
redis: AuthOptions<UserModel>['redis'];
|
|
31
31
|
setCurrentUser: (user: UserModel) => void;
|
|
32
32
|
createTemporaryToken(user: UserModel): Promise<string>;
|
|
33
|
-
middleware: (
|
|
33
|
+
middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
|
|
34
|
+
login: (res: Response, user: UserModel) => Promise<{
|
|
35
|
+
token: string;
|
|
36
|
+
}>;
|
|
34
37
|
}
|
|
35
38
|
export type AnyUser = Record<string, any>;
|
|
36
39
|
export interface AuthProvider<UserModel extends AnyUser> {
|
|
@@ -45,9 +48,12 @@ export interface AuthOptions<UserModel extends AnyUser> {
|
|
|
45
48
|
export interface CreateAuth<UserModel extends AnyUser> {
|
|
46
49
|
createRouter: () => Router;
|
|
47
50
|
useCurrentUser: () => UserModel | undefined;
|
|
48
|
-
middleware: (
|
|
51
|
+
middleware: (options: AuthMiddlewareOptions<UserModel>) => RequestHandler;
|
|
49
52
|
createTemporaryToken(user: UserModel): Promise<string>;
|
|
50
53
|
}
|
|
54
|
+
export type AuthMiddlewareOptions<UserModel extends AnyUser> = false | {
|
|
55
|
+
isAuthorized?: (user: UserModel) => Promise<boolean> | boolean;
|
|
56
|
+
};
|
|
51
57
|
export declare function createAuth<UserModel extends AnyUser>({ getUserByToken, filterUser, redis, providers, }: AuthOptions<UserModel>): CreateAuth<UserModel>;
|
|
52
58
|
/**
|
|
53
59
|
* Extract a Bearer token from the HTTP Authorization header
|
package/dist/auth.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Router, } from 'express';
|
|
|
4
4
|
import jwt from 'jsonwebtoken';
|
|
5
5
|
import { v4 } from 'uuid';
|
|
6
6
|
import { setContext, useContext } from './context.js';
|
|
7
|
-
import { BadRequest, NotFound, Unauthorized } from './errors.js';
|
|
7
|
+
import { ApiError, BadRequest, Forbidden, NotFound, Unauthorized, } from './errors.js';
|
|
8
8
|
import { getLogger } from './logger.js';
|
|
9
9
|
import { route } from './middleware.js';
|
|
10
10
|
import {} from './redis.js';
|
|
@@ -57,10 +57,7 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
57
57
|
}), route(async ({ body: { email, password } }, res) => {
|
|
58
58
|
try {
|
|
59
59
|
const user = await options.login(email, (realPassword) => checkIfPasswordsMatch(realPassword, password));
|
|
60
|
-
const token =
|
|
61
|
-
await context.redis.set(token, '1', { EX: TOKEN_EXPIRES_IN });
|
|
62
|
-
setAccessTokenCookie(res, token);
|
|
63
|
-
context.setCurrentUser(user);
|
|
60
|
+
const { token } = await context.login(res, user);
|
|
64
61
|
logger.info('access token created');
|
|
65
62
|
return [201, { token }];
|
|
66
63
|
}
|
|
@@ -146,12 +143,20 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
146
143
|
export function createAuth({ getUserByToken, filterUser, redis, providers, }) {
|
|
147
144
|
const useCurrentUser = () => useContext('currentUser');
|
|
148
145
|
const setCurrentUser = (user) => setContext('currentUser', user);
|
|
146
|
+
const login = async (res, user) => {
|
|
147
|
+
const token = createAuthToken(filterUser(user));
|
|
148
|
+
await redis.set(token, '1', { EX: TOKEN_EXPIRES_IN });
|
|
149
|
+
setAccessTokenCookie(res, token);
|
|
150
|
+
setCurrentUser(user);
|
|
151
|
+
return { token };
|
|
152
|
+
};
|
|
149
153
|
const providerContext = {
|
|
150
154
|
filterUser,
|
|
151
155
|
redis,
|
|
152
156
|
setCurrentUser,
|
|
153
157
|
createTemporaryToken,
|
|
154
158
|
middleware,
|
|
159
|
+
login,
|
|
155
160
|
};
|
|
156
161
|
const createRouter = () => {
|
|
157
162
|
const router = Router();
|
|
@@ -201,19 +206,24 @@ export function createAuth({ getUserByToken, filterUser, redis, providers, }) {
|
|
|
201
206
|
}
|
|
202
207
|
return router;
|
|
203
208
|
};
|
|
204
|
-
function middleware(
|
|
209
|
+
function middleware(options = {}) {
|
|
210
|
+
let authenticate = true;
|
|
211
|
+
if (typeof options === 'boolean') {
|
|
212
|
+
authenticate = options;
|
|
213
|
+
options = {};
|
|
214
|
+
}
|
|
205
215
|
return async (req, res, next) => {
|
|
206
216
|
try {
|
|
207
217
|
const token = extractAccessTokenFromRequest(req);
|
|
208
218
|
if (!token) {
|
|
209
|
-
if (!
|
|
219
|
+
if (!authenticate) {
|
|
210
220
|
return next();
|
|
211
221
|
}
|
|
212
222
|
throw new InvalidToken('token missing');
|
|
213
223
|
}
|
|
214
224
|
const valid = await redis.get(token);
|
|
215
225
|
if (!valid) {
|
|
216
|
-
if (!
|
|
226
|
+
if (!authenticate) {
|
|
217
227
|
logger.warn('request with invalid access token');
|
|
218
228
|
return next();
|
|
219
229
|
}
|
|
@@ -221,13 +231,20 @@ export function createAuth({ getUserByToken, filterUser, redis, providers, }) {
|
|
|
221
231
|
}
|
|
222
232
|
const decoded = decodeAuthToken(token);
|
|
223
233
|
const user = await getUserByToken(decoded);
|
|
234
|
+
if (options.isAuthorized && !(await options.isAuthorized(user))) {
|
|
235
|
+
throw Forbidden('Permission denied');
|
|
236
|
+
}
|
|
224
237
|
setCurrentUser(user);
|
|
225
238
|
next();
|
|
226
239
|
}
|
|
227
240
|
catch (error) {
|
|
228
241
|
logger.error(error);
|
|
229
|
-
|
|
230
|
-
|
|
242
|
+
if (error instanceof ApiError) {
|
|
243
|
+
next(error);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
next(Unauthorized('Invalid token'));
|
|
247
|
+
}
|
|
231
248
|
}
|
|
232
249
|
};
|
|
233
250
|
}
|
package/dist/csrf.d.ts
CHANGED
package/dist/csrf.js
CHANGED
|
@@ -9,6 +9,13 @@ const defaultCookieName = () => {
|
|
|
9
9
|
? '__Host-devmoods.x-csrf-token'
|
|
10
10
|
: 'devmoods.x-csrf-token';
|
|
11
11
|
};
|
|
12
|
+
export const getCsrfSecret = () => {
|
|
13
|
+
const secret = readEnv('CSRF_SECRET', '');
|
|
14
|
+
if (!secret) {
|
|
15
|
+
throw new Error('CSRF_SECRET is not set');
|
|
16
|
+
}
|
|
17
|
+
return secret;
|
|
18
|
+
};
|
|
12
19
|
export function csrf({ excempt = false, sessionIdentifier, cookieName, } = {}) {
|
|
13
20
|
return (req, res, next) => {
|
|
14
21
|
const existingOptions = useCsrfOptions();
|
|
@@ -34,7 +41,7 @@ function useCsrfOptions() {
|
|
|
34
41
|
});
|
|
35
42
|
}
|
|
36
43
|
const generateTokenAndHash = (req, { overwrite = false } = {}) => {
|
|
37
|
-
const secret =
|
|
44
|
+
const secret = getCsrfSecret();
|
|
38
45
|
const { sessionIdentifier, cookieName } = useCsrfOptions();
|
|
39
46
|
const csrfCookie = req.cookies[cookieName];
|
|
40
47
|
if (typeof csrfCookie === 'string' && !overwrite) {
|
|
@@ -84,7 +91,7 @@ const validateRequest = (req) => {
|
|
|
84
91
|
}
|
|
85
92
|
const [csrfToken, csrfTokenHash] = csrfCookie.split('|');
|
|
86
93
|
const csrfTokenFromRequest = req.headers['x-csrf-token'];
|
|
87
|
-
const secret =
|
|
94
|
+
const secret = getCsrfSecret();
|
|
88
95
|
if (!csrfTokenFromRequest) {
|
|
89
96
|
throw Forbidden('Missing CSRF token');
|
|
90
97
|
}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export declare function after(): RequestHandler;
|
|
|
38
38
|
type ResponseHeaders = Record<string, string>;
|
|
39
39
|
type AnyResponseBody = Record<string, any> | string;
|
|
40
40
|
type HttpResponseCode = number;
|
|
41
|
-
type ResponseWrapper<TResponseBody extends AnyResponseBody> = TResponseBody | HttpResponseCode | [HttpResponseCode, TResponseBody] | [HttpResponseCode, TResponseBody, ResponseHeaders];
|
|
41
|
+
type ResponseWrapper<TResponseBody extends AnyResponseBody> = TResponseBody | TResponseBody[] | HttpResponseCode | [HttpResponseCode, TResponseBody] | [HttpResponseCode, TResponseBody, ResponseHeaders];
|
|
42
42
|
/**
|
|
43
43
|
* Alias of `route()`
|
|
44
44
|
*/
|
package/dist/postgres.d.ts
CHANGED
|
@@ -17,12 +17,13 @@ export declare class IntegrityError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
type Value = null | string | number | Date;
|
|
19
19
|
export interface ConnectionOptions {
|
|
20
|
-
onLogStatement?: (statement: SqlStatement | string, values
|
|
20
|
+
onLogStatement?: (statement: SqlStatement | string, values: unknown[] | undefined, duration: number) => void;
|
|
21
21
|
}
|
|
22
22
|
export declare class Connection {
|
|
23
23
|
private options;
|
|
24
24
|
client: PoolClient;
|
|
25
25
|
constructor(client: PoolClient, options: ConnectionOptions);
|
|
26
|
+
private onLogStatement;
|
|
26
27
|
query<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<QueryResult<T>>;
|
|
27
28
|
all<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<T[]>;
|
|
28
29
|
insert<T extends QueryResultRow>(text: string | SqlStatement, values?: Value[]): Promise<QueryResult<T>>;
|
package/dist/postgres.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pg, {} from 'pg';
|
|
2
2
|
import { format } from 'sql-formatter';
|
|
3
|
-
import { readEnv } from './utils.js';
|
|
3
|
+
import { readEnv, startTimer } from './utils.js';
|
|
4
4
|
export const escapePostgresIdentifier = pg.escapeIdentifier;
|
|
5
5
|
export const escapePostgresLiteral = pg.escapeLiteral;
|
|
6
6
|
export const formatSql = (statement) => {
|
|
@@ -35,9 +35,17 @@ export class Connection {
|
|
|
35
35
|
this.options = options;
|
|
36
36
|
this.client = client;
|
|
37
37
|
}
|
|
38
|
+
onLogStatement(text, values, duration) {
|
|
39
|
+
if (this.options.onLogStatement) {
|
|
40
|
+
return this.options.onLogStatement(text, values, duration);
|
|
41
|
+
}
|
|
42
|
+
if (readEnv.boolean('LOG_SQL', false)) {
|
|
43
|
+
console.log(`\n${formatSql(text)}\n\nValues: ${JSON.stringify(values)}\nDuration: ${duration}ms\n`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
38
46
|
async query(text, values) {
|
|
47
|
+
const timer = startTimer();
|
|
39
48
|
try {
|
|
40
|
-
this.options.onLogStatement?.(text, values);
|
|
41
49
|
return await this.client.query(text, values);
|
|
42
50
|
}
|
|
43
51
|
catch (error) {
|
|
@@ -46,6 +54,9 @@ export class Connection {
|
|
|
46
54
|
}
|
|
47
55
|
throw error;
|
|
48
56
|
}
|
|
57
|
+
finally {
|
|
58
|
+
this.onLogStatement?.(text, values, timer.getDuration());
|
|
59
|
+
}
|
|
49
60
|
}
|
|
50
61
|
async all(text, values) {
|
|
51
62
|
const result = await this.query(text, values);
|
package/dist/utils.d.ts
CHANGED
package/dist/utils.js
CHANGED
|
@@ -4,8 +4,11 @@ import {} from 'node:events';
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
export function startTimer() {
|
|
7
|
-
|
|
7
|
+
let start = process.hrtime();
|
|
8
8
|
return {
|
|
9
|
+
reset() {
|
|
10
|
+
start = process.hrtime();
|
|
11
|
+
},
|
|
9
12
|
getDuration() {
|
|
10
13
|
const elapsed = process.hrtime(start);
|
|
11
14
|
return (elapsed[0] * 1e9 + elapsed[1]) / 1e9;
|
package/dist/validate.d.ts
CHANGED
|
@@ -39,12 +39,12 @@ export type ValidationResult = {
|
|
|
39
39
|
[key: string]: any;
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
-
export declare function compileValidateSchema(schema: ValidationSchema, options?: Options): (data:
|
|
43
|
-
isValid:
|
|
44
|
-
errors: null;
|
|
42
|
+
export declare function compileValidateSchema(schema: ValidationSchema, options?: Options): (data: unknown) => {
|
|
43
|
+
readonly isValid: true;
|
|
44
|
+
readonly errors: null;
|
|
45
45
|
} | {
|
|
46
|
-
isValid:
|
|
47
|
-
errors: {
|
|
46
|
+
readonly isValid: false;
|
|
47
|
+
readonly errors: {
|
|
48
48
|
[key: string]: any[];
|
|
49
49
|
};
|
|
50
50
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"@types/koa-compose": "^3.2.8",
|
|
23
23
|
"@types/node": "22.10.1",
|
|
24
24
|
"@types/supertest": "^6.0.2",
|
|
25
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
26
|
-
"@typescript-eslint/parser": "^8.
|
|
27
|
-
"@vitest/coverage-v8": "^2.1.
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
|
26
|
+
"@typescript-eslint/parser": "^8.17.0",
|
|
27
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
28
28
|
"cookiejar": "^2.1.4",
|
|
29
29
|
"eslint": "^8.57.0",
|
|
30
30
|
"eslint-plugin-import": "^2.31.0",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
"ts-node": "10.9.2",
|
|
36
36
|
"tsx": "^4.19.2",
|
|
37
37
|
"typescript": "5.7.2",
|
|
38
|
-
"vitest": "^2.1.
|
|
38
|
+
"vitest": "^2.1.8",
|
|
39
39
|
"vitest-fail-on-console": "^0.7.1"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@devmoods/fetch": "^4.0.1",
|
|
43
|
-
"@sentry/node": "^8.
|
|
44
|
-
"@sentry/profiling-node": "^8.
|
|
43
|
+
"@sentry/node": "^8.42.0",
|
|
44
|
+
"@sentry/profiling-node": "^8.42.0",
|
|
45
45
|
"@types/bcrypt": "^5.0.2",
|
|
46
46
|
"@types/body-parser": "^1.19.5",
|
|
47
47
|
"@types/cookie-parser": "^1.4.8",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"commander": "^12.1.0",
|
|
59
59
|
"compose-middleware": "^5.0.1",
|
|
60
60
|
"cookie-parser": "^1.4.7",
|
|
61
|
-
"dotenv": "^16.4.
|
|
61
|
+
"dotenv": "^16.4.6",
|
|
62
62
|
"express": "^4.21.1",
|
|
63
63
|
"helmet": "^8.0.0",
|
|
64
64
|
"jsonwebtoken": "^9.0.2",
|