@devmoods/express-extras 0.41.0 → 0.42.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 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: (authorize?: boolean) => RequestHandler;
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: (authorize?: boolean) => RequestHandler;
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 = createAuthToken(context.filterUser(user));
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(authorize = true) {
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 (!authorize) {
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 (!authorize) {
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
- next(Unauthorized('Invalid token'));
230
- return;
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
@@ -1,4 +1,5 @@
1
1
  import { type RequestHandler, type Request, type Response } from 'express';
2
+ export declare const getCsrfSecret: () => string;
2
3
  export interface CsrfMiddlewareOptions {
3
4
  excempt?: boolean;
4
5
  cookieName?: string;
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 = readEnv('CSRF_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 = readEnv('CSRF_SECRET', '');
94
+ const secret = getCsrfSecret();
88
95
  if (!csrfTokenFromRequest) {
89
96
  throw Forbidden('Missing CSRF token');
90
97
  }
@@ -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
  */
@@ -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?: unknown[]) => void;
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
@@ -4,6 +4,7 @@ import { type EventEmitter } from 'node:events';
4
4
  *
5
5
  */
6
6
  export declare function startTimer(): {
7
+ reset(): void;
7
8
  getDuration(): number;
8
9
  };
9
10
  export declare function readEnv(key: string): string | undefined;
package/dist/utils.js CHANGED
@@ -4,8 +4,11 @@ import {} from 'node:events';
4
4
  *
5
5
  */
6
6
  export function startTimer() {
7
- const start = process.hrtime();
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;
@@ -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: any) => {
43
- isValid: boolean;
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: boolean;
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.41.0",
3
+ "version": "0.42.1",
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.16.0",
26
- "@typescript-eslint/parser": "^8.16.0",
27
- "@vitest/coverage-v8": "^2.1.6",
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.6",
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.41.0",
44
- "@sentry/profiling-node": "^8.41.0",
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.5",
61
+ "dotenv": "^16.4.6",
62
62
  "express": "^4.21.1",
63
63
  "helmet": "^8.0.0",
64
64
  "jsonwebtoken": "^9.0.2",