@devmoods/express-extras 0.62.0 → 0.63.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/types.d.ts +2 -1
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +32 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.js +4 -0
- package/dist/faktory.js +4 -0
- package/dist/logger.js +2 -1
- package/dist/middleware.d.ts +10 -1
- package/dist/middleware.js +45 -8
- package/dist/postgres.js +1 -1
- package/dist/validate.js +3 -0
- package/package.json +4 -4
package/dist/auth/types.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
|
48
48
|
export interface AuthProvider<UserModel extends AnyUser> {
|
|
49
49
|
setupRoutes(router: TypedRouter, context: AuthProviderContext<UserModel>): void;
|
|
50
50
|
}
|
|
51
|
+
export type AuthIsAllowedFn<UserModel extends AnyUser> = (user: UserModel) => Promise<boolean> | boolean;
|
|
51
52
|
export type AuthMiddlewareOptions<UserModel extends AnyUser> = false | {
|
|
52
|
-
isAuthorized?:
|
|
53
|
+
isAuthorized?: AuthIsAllowedFn<UserModel>;
|
|
53
54
|
};
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RequestHandler, type Response, type Request, type Router } from 'express';
|
|
2
|
-
import { TokenStorage, type AnyUser, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken } from './auth/types.js';
|
|
2
|
+
import { TokenStorage, type AnyUser, type AuthIsAllowedFn, type AuthMiddlewareOptions, type AuthOptions, type DecodedTemporaryToken } from './auth/types.js';
|
|
3
3
|
import { type RedisClient } from './redis.js';
|
|
4
4
|
export * from './auth/types.js';
|
|
5
5
|
export * from './auth/emailAndPassword.js';
|
|
@@ -11,6 +11,7 @@ export interface CreateAuth<UserModel extends AnyUser> {
|
|
|
11
11
|
useCurrentUser: () => UserModel | undefined;
|
|
12
12
|
middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
|
|
13
13
|
before: () => RequestHandler;
|
|
14
|
+
allow: (fn?: AuthIsAllowedFn<UserModel>) => RequestHandler;
|
|
14
15
|
isAuthorized: (fn?: (user: UserModel) => boolean) => RequestHandler;
|
|
15
16
|
createTemporaryToken(user: UserModel): Promise<string>;
|
|
16
17
|
verifyTemporaryToken(token: string, user: UserModel | null): Promise<string>;
|
package/dist/auth.js
CHANGED
|
@@ -130,7 +130,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
130
130
|
authenticate = options;
|
|
131
131
|
options = {};
|
|
132
132
|
}
|
|
133
|
-
return async (req, res, next)
|
|
133
|
+
return async function authenticationMiddleware(req, res, next) {
|
|
134
134
|
try {
|
|
135
135
|
const token = extractAccessTokenFromRequest(req);
|
|
136
136
|
if (!token) {
|
|
@@ -192,6 +192,36 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
192
192
|
}
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
+
function allow(isAuthorized = () => true) {
|
|
196
|
+
return async function authorizationMiddleware(req, res, next) {
|
|
197
|
+
try {
|
|
198
|
+
const user = useCurrentUser();
|
|
199
|
+
if (!user) {
|
|
200
|
+
throw new InvalidToken('User not logged in');
|
|
201
|
+
}
|
|
202
|
+
if (isAuthorized && !(await isAuthorized(user))) {
|
|
203
|
+
throw new PermissionDenied('Permission denied');
|
|
204
|
+
}
|
|
205
|
+
next();
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
logger.error(error);
|
|
209
|
+
if (error instanceof PermissionDenied) {
|
|
210
|
+
if (req.accepts('html')) {
|
|
211
|
+
return res.redirect(loginUrl);
|
|
212
|
+
}
|
|
213
|
+
return next(Forbidden('Permission denied'));
|
|
214
|
+
}
|
|
215
|
+
if (error instanceof InvalidToken) {
|
|
216
|
+
if (req.accepts('html')) {
|
|
217
|
+
return res.redirect(loginUrl);
|
|
218
|
+
}
|
|
219
|
+
return next(Unauthorized(`Invalid token: ${error.message}`));
|
|
220
|
+
}
|
|
221
|
+
next(error);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
195
225
|
async function createOtp(user) {
|
|
196
226
|
const otp = crypto.randomInt(100000, 999999).toString();
|
|
197
227
|
await tokenStorage.set(otp, String(user.id), config.value.AUTH_OTP_EXPIRES_IN);
|
|
@@ -265,6 +295,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
265
295
|
return storedUid;
|
|
266
296
|
}
|
|
267
297
|
return {
|
|
298
|
+
allow,
|
|
268
299
|
before: () => middleware(false),
|
|
269
300
|
isAuthorized: (fn) => middleware({ isAuthorized: fn || (() => true) }),
|
|
270
301
|
createRouter: () => createAuthRouter().getRouter(),
|
package/dist/context.d.ts
CHANGED
|
@@ -20,3 +20,5 @@ export declare function setContext<T>(key: string, value: T): void;
|
|
|
20
20
|
* Obtain the requestId from the current async request context.
|
|
21
21
|
*/
|
|
22
22
|
export declare function useRequestId(): string | undefined;
|
|
23
|
+
export declare const ANONYMOUS_SESSION_KEY = "anonymous-session";
|
|
24
|
+
export declare function useAnonymousSession(): string | undefined;
|
package/dist/context.js
CHANGED
|
@@ -42,3 +42,7 @@ export function setContext(key, value) {
|
|
|
42
42
|
export function useRequestId() {
|
|
43
43
|
return useContext(REQUEST_ID);
|
|
44
44
|
}
|
|
45
|
+
export const ANONYMOUS_SESSION_KEY = 'anonymous-session';
|
|
46
|
+
export function useAnonymousSession() {
|
|
47
|
+
return useContext(ANONYMOUS_SESSION_KEY);
|
|
48
|
+
}
|
package/dist/faktory.js
CHANGED
|
@@ -82,6 +82,10 @@ export async function createFaktoryWorker({ queueName, jobs, faktory: createFact
|
|
|
82
82
|
faktory.use(async (ctx, next) => {
|
|
83
83
|
const jobId = ctx.job.jid;
|
|
84
84
|
const timer = startTimer();
|
|
85
|
+
logger.info('job started', {
|
|
86
|
+
jobName: ctx.job.jobtype,
|
|
87
|
+
jobId,
|
|
88
|
+
});
|
|
85
89
|
await context.run(new Map(), async () => {
|
|
86
90
|
const store = context.getStore();
|
|
87
91
|
if (store) {
|
package/dist/logger.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as os from 'node:os';
|
|
2
2
|
import * as Sentry from '@sentry/node';
|
|
3
3
|
import { config } from './config.js';
|
|
4
|
-
import { useRequestId } from './context.js';
|
|
4
|
+
import { useAnonymousSession, useRequestId } from './context.js';
|
|
5
5
|
const LEVELS = {
|
|
6
6
|
TRACE: 10,
|
|
7
7
|
DEBUG: 20,
|
|
@@ -21,6 +21,7 @@ function defaultGetContext() {
|
|
|
21
21
|
return {
|
|
22
22
|
app: nameArgIndex > -1 ? process.argv.at(nameArgIndex + 1) : undefined,
|
|
23
23
|
requestId: useRequestId(),
|
|
24
|
+
sessionId: useAnonymousSession(),
|
|
24
25
|
..._globalGetContext?.(),
|
|
25
26
|
};
|
|
26
27
|
}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ServerResponse, type IncomingMessage } from 'node:http';
|
|
2
2
|
import { compose } from 'compose-middleware';
|
|
3
3
|
import { type NextFunction, type RequestHandler, type Request, type Response, json as jsonParser, urlencoded as urlencodedParser } from 'express';
|
|
4
|
+
import { type createAuth } from './auth.js';
|
|
4
5
|
import { type CsrfMiddlewareOptions } from './csrf.js';
|
|
5
6
|
export { urlencodedParser, jsonParser };
|
|
6
7
|
export { asyncMiddleware as route };
|
|
@@ -43,12 +44,16 @@ export interface BeforeMiddlewareOptions {
|
|
|
43
44
|
* Custom CSP directives
|
|
44
45
|
*/
|
|
45
46
|
cspDirectives?: (defaults: BeforeMiddlewareCSPDirectives) => BeforeMiddlewareCSPDirectives;
|
|
47
|
+
/**
|
|
48
|
+
* Auth middleware needs to be available early before request logging happens.
|
|
49
|
+
*/
|
|
50
|
+
auth?: ReturnType<typeof createAuth>;
|
|
46
51
|
}
|
|
47
52
|
export declare function useCspNonce(): string;
|
|
48
53
|
/**
|
|
49
54
|
* Register middleware that must come before the app routes.
|
|
50
55
|
*/
|
|
51
|
-
export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, }?: BeforeMiddlewareOptions): import("compose-middleware").RequestHandler<any, any, void>;
|
|
56
|
+
export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf: useCsrf, urlencoded: useUrlencoded, allowUnsafeInlineScripts, cspDirectives, auth, }?: BeforeMiddlewareOptions): import("compose-middleware").RequestHandler<any, any, void>;
|
|
52
57
|
/**
|
|
53
58
|
* Middleware that sends a 404
|
|
54
59
|
*
|
|
@@ -56,6 +61,10 @@ export declare function before({ cookies, helmet: useHelmet, json: useJson, csrf
|
|
|
56
61
|
* unknown routes.
|
|
57
62
|
*/
|
|
58
63
|
export declare function notFound(): RequestHandler;
|
|
64
|
+
/**
|
|
65
|
+
* Set a session cookie for the request
|
|
66
|
+
*/
|
|
67
|
+
export declare function setAnonymousCookie(): RequestHandler;
|
|
59
68
|
/**
|
|
60
69
|
* Register middlewares that must come after the app routes.
|
|
61
70
|
*/
|
package/dist/middleware.js
CHANGED
|
@@ -4,7 +4,9 @@ import { compose } from 'compose-middleware';
|
|
|
4
4
|
import cookieParser from 'cookie-parser';
|
|
5
5
|
import { json as jsonParser, urlencoded as urlencodedParser, } from 'express';
|
|
6
6
|
import helmet from 'helmet';
|
|
7
|
-
import {
|
|
7
|
+
import {} from './auth.js';
|
|
8
|
+
import { config } from './config.js';
|
|
9
|
+
import { ANONYMOUS_SESSION_KEY, setContext, useContext, withRequestContext, } from './context.js';
|
|
8
10
|
import { csrf, doubleCsrfProtection, } from './csrf.js';
|
|
9
11
|
import { NotFound } from './errors.js';
|
|
10
12
|
import { getLogger } from './logger.js';
|
|
@@ -20,14 +22,17 @@ export function useCspNonce() {
|
|
|
20
22
|
/**
|
|
21
23
|
* Register middleware that must come before the app routes.
|
|
22
24
|
*/
|
|
23
|
-
export function before({ cookies =
|
|
24
|
-
|
|
25
|
+
export function before({ cookies = true, helmet: useHelmet = true, json: useJson = true, csrf: useCsrf = false, urlencoded: useUrlencoded = false, allowUnsafeInlineScripts = false, cspDirectives = (x) => x, auth, } = {}) {
|
|
26
|
+
const middleware = compose([
|
|
25
27
|
withRequestContext(),
|
|
28
|
+
(cookies || useCsrf) && cookieParser(),
|
|
29
|
+
cookies && setAnonymousCookie(),
|
|
26
30
|
typeof useCsrf === 'object' && useCsrf != null
|
|
27
31
|
? csrf(useCsrf)
|
|
28
32
|
: useCsrf
|
|
29
33
|
? csrf()
|
|
30
34
|
: undefined,
|
|
35
|
+
auth && auth.before(),
|
|
31
36
|
logRequestDuration(),
|
|
32
37
|
setCspNonce(),
|
|
33
38
|
useHelmet &&
|
|
@@ -63,9 +68,12 @@ export function before({ cookies = false, helmet: useHelmet = true, json: useJso
|
|
|
63
68
|
}),
|
|
64
69
|
useJson && jsonParser(),
|
|
65
70
|
useUrlencoded && urlencodedParser({ extended: true }),
|
|
66
|
-
(cookies || useCsrf) && cookieParser(),
|
|
67
71
|
analyticsProxy(),
|
|
68
72
|
].filter(Boolean));
|
|
73
|
+
Object.defineProperty(middleware, 'name', {
|
|
74
|
+
value: before.name,
|
|
75
|
+
});
|
|
76
|
+
return middleware;
|
|
69
77
|
}
|
|
70
78
|
function logRequestDuration() {
|
|
71
79
|
const FILTERED_PATHS = [
|
|
@@ -77,7 +85,7 @@ function logRequestDuration() {
|
|
|
77
85
|
'/@id',
|
|
78
86
|
'/analytics.js',
|
|
79
87
|
];
|
|
80
|
-
return (req, res, next)
|
|
88
|
+
return function logRequestDurationMiddleware(req, res, next) {
|
|
81
89
|
if (FILTERED_PATHS.some((p) => req.path.startsWith(p))) {
|
|
82
90
|
return next();
|
|
83
91
|
}
|
|
@@ -86,7 +94,13 @@ function logRequestDuration() {
|
|
|
86
94
|
method: req.method,
|
|
87
95
|
url: req.originalUrl,
|
|
88
96
|
path: req.path,
|
|
97
|
+
query: req.query,
|
|
89
98
|
ip: req.ip,
|
|
99
|
+
userAgent: req.headers['user-agent'] ?? 'unknown',
|
|
100
|
+
accept: req.headers['accept'] ?? 'unknown',
|
|
101
|
+
host: req.headers['host'] ?? 'unknown',
|
|
102
|
+
referer: req.headers['referer'] ?? 'unknown',
|
|
103
|
+
origin: req.headers['origin'] ?? 'unknown',
|
|
90
104
|
});
|
|
91
105
|
res.on('finish', () => {
|
|
92
106
|
const duration = timer.getDuration();
|
|
@@ -108,7 +122,7 @@ function logRequestDuration() {
|
|
|
108
122
|
* unknown routes.
|
|
109
123
|
*/
|
|
110
124
|
export function notFound() {
|
|
111
|
-
return (req, res, next)
|
|
125
|
+
return function notFoundMiddleware(req, res, next) {
|
|
112
126
|
next(NotFound(`Could not ${req.method} ${req.path}`));
|
|
113
127
|
};
|
|
114
128
|
}
|
|
@@ -116,18 +130,41 @@ export function notFound() {
|
|
|
116
130
|
* Set a CSP nonce for the request
|
|
117
131
|
*/
|
|
118
132
|
function setCspNonce() {
|
|
119
|
-
return (req, res, next)
|
|
133
|
+
return function setCspNonceMiddleware(req, res, next) {
|
|
120
134
|
setContext(CSP_NONCE_KEY, crypto.randomBytes(16).toString('base64'));
|
|
121
135
|
next();
|
|
122
136
|
};
|
|
123
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Set a session cookie for the request
|
|
140
|
+
*/
|
|
141
|
+
export function setAnonymousCookie() {
|
|
142
|
+
return function setAnonymousCookieMiddleware(req, res, next) {
|
|
143
|
+
const cookieName = 'aid';
|
|
144
|
+
let sessionId = req.cookies?.[cookieName];
|
|
145
|
+
// Generate new sessionId if none exists
|
|
146
|
+
if (!sessionId) {
|
|
147
|
+
sessionId = crypto.randomUUID();
|
|
148
|
+
// Set the cookie
|
|
149
|
+
res.cookie(cookieName, sessionId, {
|
|
150
|
+
maxAge: 365 * 24 * 60 * 60 * 1000,
|
|
151
|
+
httpOnly: true,
|
|
152
|
+
secure: process.env.NODE_ENV === 'production',
|
|
153
|
+
sameSite: config.value.AUTH_COOKIE_SAME_SITE,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// Store sessionId in request context
|
|
157
|
+
setContext(ANONYMOUS_SESSION_KEY, sessionId);
|
|
158
|
+
next();
|
|
159
|
+
};
|
|
160
|
+
}
|
|
124
161
|
/**
|
|
125
162
|
* In production, we could directly proxy the url
|
|
126
163
|
* to an analytics service from nginx, but in dev
|
|
127
164
|
* we serve a dummy script to avoid 404.
|
|
128
165
|
*/
|
|
129
166
|
function analyticsProxy() {
|
|
130
|
-
return (req, res, next)
|
|
167
|
+
return function analyticsProxyMiddleware(req, res, next) {
|
|
131
168
|
if (req.originalUrl === '/analytics.js') {
|
|
132
169
|
res.setHeader('Content-type', 'text/javascript');
|
|
133
170
|
res.status(200).end('// no-op analytics proxy');
|
package/dist/postgres.js
CHANGED
|
@@ -244,7 +244,7 @@ export function createPostgres({ onError, url = config.value.POSTGRES_URL, ...po
|
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
246
|
export function withTransaction(postgres) {
|
|
247
|
-
return async (req, res, next)
|
|
247
|
+
return async function withTransactionMiddleware(req, res, next) {
|
|
248
248
|
try {
|
|
249
249
|
await postgres.transaction(async () => {
|
|
250
250
|
await new Promise((resolve, reject) => {
|
package/dist/validate.js
CHANGED
|
@@ -80,6 +80,9 @@ export function validateStandardSchema(schemas) {
|
|
|
80
80
|
metadata.responseSchema = toJsonSchema(schemas.response);
|
|
81
81
|
}
|
|
82
82
|
middleware.metadata = metadata;
|
|
83
|
+
Object.defineProperty(middleware, 'name', {
|
|
84
|
+
value: validateStandardSchema.name,
|
|
85
|
+
});
|
|
83
86
|
return middleware;
|
|
84
87
|
}
|
|
85
88
|
function toJsonSchema(standardSchema) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.1",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"ts-node": "10.9.2",
|
|
60
60
|
"tsx": "^4.20.5",
|
|
61
61
|
"typescript": "5.9.2",
|
|
62
|
-
"vite": "^7.1.
|
|
62
|
+
"vite": "^7.1.5",
|
|
63
63
|
"vitest": "^3.2.4",
|
|
64
64
|
"vitest-fail-on-console": "^0.10.1"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@devmoods/fetch": "^4.2.0",
|
|
68
|
-
"@hey-api/openapi-ts": "^0.82.
|
|
68
|
+
"@hey-api/openapi-ts": "^0.82.5",
|
|
69
69
|
"@sentry/node": "^10.10.0",
|
|
70
70
|
"@sentry/profiling-node": "^10.10.0",
|
|
71
71
|
"@standard-schema/spec": "^1.0.0",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"ajv": "^8.17.1",
|
|
82
82
|
"argon2": "^0.44.0",
|
|
83
83
|
"bcrypt": "^6.0.0",
|
|
84
|
-
"chalk": "^5.6.
|
|
84
|
+
"chalk": "^5.6.2",
|
|
85
85
|
"commander": "^14.0.0",
|
|
86
86
|
"compose-middleware": "^5.0.1",
|
|
87
87
|
"cookie-parser": "^1.4.7",
|