@twasik4/pocket-service 1.0.0 → 1.0.2
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 +56 -0
- package/dist/index.d.ts +700 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/worker-thread.d.ts +2 -0
- package/dist/worker-thread.js +2 -0
- package/dist/worker-thread.js.map +1 -0
- package/package.json +3 -1
- package/src/workers/auth/strategies.ts +304 -0
- package/src/workers/express-types.ts +164 -36
- package/src/workers/index.ts +1 -0
- package/src/workers/service.ts +50 -3
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Request } from "express";
|
|
2
|
+
import { createRemoteJWKSet, JWTPayload, jwtVerify } from "jose";
|
|
3
|
+
import { PeerCertificate, TLSSocket } from "tls";
|
|
4
|
+
import {
|
|
5
|
+
AuthenticatedUser,
|
|
6
|
+
AuthStrategy,
|
|
7
|
+
RouteDefinition,
|
|
8
|
+
} from "../express-types";
|
|
9
|
+
|
|
10
|
+
type JwtClaimValue = string | string[] | number | boolean | null | undefined;
|
|
11
|
+
|
|
12
|
+
export type JwtStrategyClaims = {
|
|
13
|
+
sub?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type JwtTokenVerifier = (
|
|
18
|
+
token: string,
|
|
19
|
+
req: Request,
|
|
20
|
+
route: RouteDefinition<any, any, boolean>,
|
|
21
|
+
) => JwtStrategyClaims | null | Promise<JwtStrategyClaims | null>;
|
|
22
|
+
|
|
23
|
+
export type CreateJwtAuthStrategyOptions = {
|
|
24
|
+
name?: string;
|
|
25
|
+
headerName?: string;
|
|
26
|
+
tokenPrefix?: string;
|
|
27
|
+
verifyToken: JwtTokenVerifier;
|
|
28
|
+
mapPayloadToUser?: (
|
|
29
|
+
claims: JwtStrategyClaims,
|
|
30
|
+
req: Request,
|
|
31
|
+
route: RouteDefinition<any, any, boolean>,
|
|
32
|
+
) => AuthenticatedUser | null | Promise<AuthenticatedUser | null>;
|
|
33
|
+
canHandle?: AuthStrategy["canHandle"];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type CreateJoseJwtVerifierOptions = {
|
|
37
|
+
jwksUri?: string;
|
|
38
|
+
secret?: string | Uint8Array;
|
|
39
|
+
issuer?: string | string[];
|
|
40
|
+
audience?: string | string[];
|
|
41
|
+
algorithms?: string[];
|
|
42
|
+
clockTolerance?: string | number;
|
|
43
|
+
requiredClaims?: string[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type CreateMtlsAuthStrategyOptions = {
|
|
47
|
+
name?: string;
|
|
48
|
+
requireAuthorized?: boolean;
|
|
49
|
+
mapCertificateToUser?: (
|
|
50
|
+
certificate: PeerCertificate,
|
|
51
|
+
req: Request,
|
|
52
|
+
route: RouteDefinition<any, any, boolean>,
|
|
53
|
+
) => AuthenticatedUser | null | Promise<AuthenticatedUser | null>;
|
|
54
|
+
canHandle?: AuthStrategy["canHandle"];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function readAuthTokenFromHeader(
|
|
58
|
+
req: Request,
|
|
59
|
+
headerName: string,
|
|
60
|
+
tokenPrefix: string,
|
|
61
|
+
): string | null {
|
|
62
|
+
const header = req.headers[headerName.toLowerCase()];
|
|
63
|
+
|
|
64
|
+
if (typeof header !== "string") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const value = header.trim();
|
|
69
|
+
if (!value) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!tokenPrefix) {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const prefix = `${tokenPrefix} `;
|
|
78
|
+
if (!value.startsWith(prefix)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return value.slice(prefix.length).trim() || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isUserValue(
|
|
86
|
+
value: unknown,
|
|
87
|
+
): value is Exclude<JwtClaimValue, null | undefined> {
|
|
88
|
+
return (
|
|
89
|
+
typeof value === "string" ||
|
|
90
|
+
typeof value === "number" ||
|
|
91
|
+
typeof value === "boolean" ||
|
|
92
|
+
(Array.isArray(value) && value.every((v) => typeof v === "string"))
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function claimsToAuthenticatedUser(
|
|
97
|
+
claims: JwtStrategyClaims,
|
|
98
|
+
): AuthenticatedUser | null {
|
|
99
|
+
if (typeof claims.sub !== "string" || claims.sub.length === 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const user: AuthenticatedUser = { userId: claims.sub };
|
|
104
|
+
for (const [key, value] of Object.entries(claims)) {
|
|
105
|
+
if (key === "sub") {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isUserValue(value)) {
|
|
110
|
+
user[key] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return user;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function jwtPayloadToStrategyClaims(payload: JWTPayload): JwtStrategyClaims {
|
|
118
|
+
const claims: JwtStrategyClaims = {};
|
|
119
|
+
|
|
120
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
121
|
+
claims[key] = value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof payload.sub === "string") {
|
|
125
|
+
claims.sub = payload.sub;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return claims;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasRequiredClaims(payload: JWTPayload, requiredClaims: string[]) {
|
|
132
|
+
return requiredClaims.every((claim) => payload[claim] !== undefined);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function createJoseJwtVerifier(
|
|
136
|
+
options: CreateJoseJwtVerifierOptions,
|
|
137
|
+
): JwtTokenVerifier {
|
|
138
|
+
const requiredClaims = options.requiredClaims ?? ["sub"];
|
|
139
|
+
const jwtVerifyOptions = {
|
|
140
|
+
issuer: options.issuer,
|
|
141
|
+
audience: options.audience,
|
|
142
|
+
algorithms: options.algorithms,
|
|
143
|
+
clockTolerance: options.clockTolerance,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (options.jwksUri) {
|
|
147
|
+
const jwksKeySource = createRemoteJWKSet(new URL(options.jwksUri));
|
|
148
|
+
|
|
149
|
+
return async (token) => {
|
|
150
|
+
try {
|
|
151
|
+
const { payload } = await jwtVerify(
|
|
152
|
+
token,
|
|
153
|
+
jwksKeySource,
|
|
154
|
+
jwtVerifyOptions,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (!hasRequiredClaims(payload, requiredClaims)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return jwtPayloadToStrategyClaims(payload);
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const secretKey =
|
|
169
|
+
typeof options.secret === "string"
|
|
170
|
+
? new TextEncoder().encode(options.secret)
|
|
171
|
+
: options.secret;
|
|
172
|
+
|
|
173
|
+
if (!(secretKey instanceof Uint8Array)) {
|
|
174
|
+
throw new Error("createJoseJwtVerifier requires either jwksUri or secret.");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return async (token) => {
|
|
178
|
+
try {
|
|
179
|
+
const { payload } = await jwtVerify(token, secretKey, jwtVerifyOptions);
|
|
180
|
+
|
|
181
|
+
if (!hasRequiredClaims(payload, requiredClaims)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return jwtPayloadToStrategyClaims(payload);
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseSubjectAltNames(subjectAltName?: string): string[] {
|
|
193
|
+
if (!subjectAltName) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return subjectAltName
|
|
198
|
+
.split(",")
|
|
199
|
+
.map((entry) => entry.trim())
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveMtlsIdentity(certificate: PeerCertificate): string | null {
|
|
204
|
+
const sans = parseSubjectAltNames(certificate.subjectaltname);
|
|
205
|
+
const uriSan = sans.find((value) => value.startsWith("URI:"));
|
|
206
|
+
if (uriSan) {
|
|
207
|
+
return uriSan.slice(4).trim() || null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const dnsSan = sans.find((value) => value.startsWith("DNS:"));
|
|
211
|
+
if (dnsSan) {
|
|
212
|
+
return dnsSan.slice(4).trim() || null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const subjectCn = certificate.subject?.CN;
|
|
216
|
+
if (typeof subjectCn === "string" && subjectCn.length > 0) {
|
|
217
|
+
return subjectCn;
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(subjectCn) && subjectCn.length > 0) {
|
|
220
|
+
const first = subjectCn[0];
|
|
221
|
+
if (typeof first === "string" && first.length > 0) {
|
|
222
|
+
return first;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (certificate.fingerprint256) {
|
|
227
|
+
return certificate.fingerprint256;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function createJwtAuthStrategy(
|
|
234
|
+
options: CreateJwtAuthStrategyOptions,
|
|
235
|
+
): AuthStrategy {
|
|
236
|
+
const headerName = options.headerName ?? "authorization";
|
|
237
|
+
const tokenPrefix = options.tokenPrefix ?? "Bearer";
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
name: options.name ?? "jwt",
|
|
241
|
+
canHandle: options.canHandle,
|
|
242
|
+
authenticate: async (req, route) => {
|
|
243
|
+
const token = readAuthTokenFromHeader(req, headerName, tokenPrefix);
|
|
244
|
+
if (!token) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const claims = await options.verifyToken(token, req, route);
|
|
249
|
+
if (!claims) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (options.mapPayloadToUser) {
|
|
254
|
+
return options.mapPayloadToUser(claims, req, route);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return claimsToAuthenticatedUser(claims);
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function createMtlsAuthStrategy(
|
|
263
|
+
options: CreateMtlsAuthStrategyOptions = {},
|
|
264
|
+
): AuthStrategy {
|
|
265
|
+
const requireAuthorized = options.requireAuthorized ?? true;
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
name: options.name ?? "mtls",
|
|
269
|
+
canHandle: options.canHandle,
|
|
270
|
+
authenticate: async (req, route) => {
|
|
271
|
+
const socket = req.socket as TLSSocket;
|
|
272
|
+
if (typeof socket.getPeerCertificate !== "function") {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (requireAuthorized && socket.authorized !== true) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const certificate = socket.getPeerCertificate(true);
|
|
281
|
+
if (!certificate || Object.keys(certificate).length === 0) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (options.mapCertificateToUser) {
|
|
286
|
+
return options.mapCertificateToUser(certificate, req, route);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const userId = resolveMtlsIdentity(certificate);
|
|
290
|
+
if (!userId) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
userId,
|
|
296
|
+
certSubjectCn: certificate.subject?.CN ?? "",
|
|
297
|
+
certIssuerCn: certificate.issuer?.CN ?? "",
|
|
298
|
+
certFingerprint256: certificate.fingerprint256 ?? "",
|
|
299
|
+
certSerialNumber: certificate.serialNumber ?? "",
|
|
300
|
+
mtlsAuthorized: socket.authorized,
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -22,6 +22,74 @@ export type AuthenticatedUser = {
|
|
|
22
22
|
[key: string]: string | string[] | number | boolean;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
export type AuthResolver = (
|
|
26
|
+
req: Request,
|
|
27
|
+
route: RouteDefinition<any, any, boolean>,
|
|
28
|
+
) => AuthenticatedUser | null | Promise<AuthenticatedUser | null>;
|
|
29
|
+
|
|
30
|
+
export type AuthStrategy = {
|
|
31
|
+
name: string;
|
|
32
|
+
canHandle?: (
|
|
33
|
+
req: Request,
|
|
34
|
+
route: RouteDefinition<any, any, boolean>,
|
|
35
|
+
) => boolean | Promise<boolean>;
|
|
36
|
+
authenticate: AuthResolver;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function defaultHeaderAuthResolver(
|
|
40
|
+
req: Request,
|
|
41
|
+
): AuthenticatedUser | null {
|
|
42
|
+
const userIdHeader = req.headers["x-user-id"];
|
|
43
|
+
const userMetaHeader = req.headers["x-user-meta"];
|
|
44
|
+
|
|
45
|
+
if (typeof userMetaHeader === "string") {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(userMetaHeader) as Partial<AuthenticatedUser>;
|
|
48
|
+
|
|
49
|
+
if (typeof parsed.userId === "string") {
|
|
50
|
+
return parsed as AuthenticatedUser;
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof userIdHeader !== "string") {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
userId: userIdHeader,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createAuthResolver(
|
|
67
|
+
strategies: AuthStrategy[],
|
|
68
|
+
opts: { fallbackResolver?: AuthResolver } = {},
|
|
69
|
+
): AuthResolver {
|
|
70
|
+
return async (req, route) => {
|
|
71
|
+
for (const strategy of strategies) {
|
|
72
|
+
if (strategy.canHandle) {
|
|
73
|
+
const canHandle = await strategy.canHandle(req, route);
|
|
74
|
+
if (!canHandle) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const user = await strategy.authenticate(req, route);
|
|
80
|
+
if (user) {
|
|
81
|
+
return user;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (opts.fallbackResolver) {
|
|
86
|
+
return opts.fallbackResolver(req, route);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
25
93
|
export type TypedRequest<
|
|
26
94
|
TBody = unknown,
|
|
27
95
|
TParams = unknown,
|
|
@@ -76,6 +144,12 @@ export function getRegisteredRoutes() {
|
|
|
76
144
|
return registeredRoutes;
|
|
77
145
|
}
|
|
78
146
|
|
|
147
|
+
type CreateMetaRouterOptions = {
|
|
148
|
+
authResolver?: AuthResolver;
|
|
149
|
+
onUnauthorized?: (req: Request, res: Response) => void;
|
|
150
|
+
onAuthError?: (err: unknown, req: Request, res: Response) => void;
|
|
151
|
+
};
|
|
152
|
+
|
|
79
153
|
/**
|
|
80
154
|
* Utility function to create an Express router with enhanced type safety and built-in support for request validation and authentication. This function allows you to define routes with associated Zod schemas for validating request bodies and URL parameters, as well as specifying whether authentication is required for each route. The returned object includes the configured Express router, a list of registered routes for introspection, and a helper function to add new routes with the specified configurations.
|
|
81
155
|
*/
|
|
@@ -135,34 +209,87 @@ export function createMetaRouter(): {
|
|
|
135
209
|
TRequireAuth
|
|
136
210
|
>,
|
|
137
211
|
) => void;
|
|
212
|
+
} {
|
|
213
|
+
return createMetaRouterWithAuth();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createMetaRouterWithAuth(options?: CreateMetaRouterOptions): {
|
|
217
|
+
/**
|
|
218
|
+
* The Express router instance that can be used to register routes and middleware. This router is configured to work with the enhanced route definitions provided by the `addRoute` function, which supports request validation and authentication requirements.
|
|
219
|
+
*/
|
|
220
|
+
router: Router;
|
|
221
|
+
/**
|
|
222
|
+
* List of registered routes with their configurations, which can be used for introspection, documentation generation, or analytics purposes. Each entry includes the HTTP method, full path, authentication requirement, validation schemas, and any additional metadata provided during route registration.
|
|
223
|
+
*/
|
|
224
|
+
routes: RouteDefinition<any, any, boolean>[];
|
|
225
|
+
/**
|
|
226
|
+
* Adds a new route to the router with the specified configuration and request handler.
|
|
227
|
+
*
|
|
228
|
+
* - If `requireAuth` is set to true, the route will automatically include middleware to check for an authenticated user and return a 401 error if the user is not authenticated.
|
|
229
|
+
* - If `bodyValidator` or `paramsValidator` are provided, the route will include middleware to validate the request body and URL parameters against the provided Zod schemas, returning a 400 error if validation fails.
|
|
230
|
+
*
|
|
231
|
+
* The route definition is also stored in an internal list for introspection purposes.
|
|
232
|
+
*
|
|
233
|
+
* @param route The route definition, including method, path, validation schemas, and authentication requirements.
|
|
234
|
+
* @param handler The request handler function for the route.
|
|
235
|
+
* @returns void
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* addRoute(
|
|
239
|
+
* {
|
|
240
|
+
* fullPath: "/me",
|
|
241
|
+
* method: "GET",
|
|
242
|
+
* requireAuth: true,
|
|
243
|
+
* meta: {
|
|
244
|
+
* description: "Get current authenticated user info",
|
|
245
|
+
* },
|
|
246
|
+
* },
|
|
247
|
+
* async (req, res) => {
|
|
248
|
+
* try {
|
|
249
|
+
* ...fetch user info from database using req.user.userId...
|
|
250
|
+
* res.status(200).json(...user info...);
|
|
251
|
+
* } catch (err) {
|
|
252
|
+
* console.error("Failed to fetch user info:", err);
|
|
253
|
+
* res
|
|
254
|
+
* .status(500)
|
|
255
|
+
* .json({ isSuccess: false, message: "Internal server error" });
|
|
256
|
+
* }
|
|
257
|
+
* },
|
|
258
|
+
* );
|
|
259
|
+
*/
|
|
260
|
+
addRoute: <
|
|
261
|
+
TBody extends ZodSchema | undefined = undefined,
|
|
262
|
+
TParams extends ZodSchema | undefined = undefined,
|
|
263
|
+
TRequireAuth extends boolean = false,
|
|
264
|
+
>(
|
|
265
|
+
route: RouteDefinition<TBody, TParams, TRequireAuth>,
|
|
266
|
+
/** Request handler for the route */ handler: TypedRequestHandler<
|
|
267
|
+
TBody,
|
|
268
|
+
TParams,
|
|
269
|
+
TRequireAuth
|
|
270
|
+
>,
|
|
271
|
+
) => void;
|
|
138
272
|
} {
|
|
139
273
|
const router = Router();
|
|
140
274
|
const routes: RouteDefinition<any, any, boolean>[] = [];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
userId: userIdHeader,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
275
|
+
const authResolver: AuthResolver =
|
|
276
|
+
options?.authResolver ?? ((req) => defaultHeaderAuthResolver(req));
|
|
277
|
+
const onUnauthorized =
|
|
278
|
+
options?.onUnauthorized ??
|
|
279
|
+
((_: Request, res: Response) => {
|
|
280
|
+
res.status(401).json({
|
|
281
|
+
isSuccess: false,
|
|
282
|
+
message: "Unauthorized",
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
const onAuthError =
|
|
286
|
+
options?.onAuthError ??
|
|
287
|
+
((_: unknown, __: Request, res: Response) => {
|
|
288
|
+
res.status(401).json({
|
|
289
|
+
isSuccess: false,
|
|
290
|
+
message: "Unauthorized",
|
|
291
|
+
});
|
|
292
|
+
});
|
|
166
293
|
|
|
167
294
|
/**
|
|
168
295
|
* Adds a new route to the router with the specified configuration and request handler.
|
|
@@ -222,19 +349,20 @@ export function createMetaRouter(): {
|
|
|
222
349
|
paramsValidator,
|
|
223
350
|
});
|
|
224
351
|
if (route.requireAuth) {
|
|
225
|
-
middlewareStack.push((req, res, next) => {
|
|
226
|
-
|
|
352
|
+
middlewareStack.push(async (req, res, next) => {
|
|
353
|
+
try {
|
|
354
|
+
const user = await authResolver(req, route);
|
|
227
355
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
356
|
+
if (!user) {
|
|
357
|
+
onUnauthorized(req, res);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
235
360
|
|
|
236
|
-
|
|
237
|
-
|
|
361
|
+
(req as TypedRequest<TBody, TParams, true>).user = user;
|
|
362
|
+
next();
|
|
363
|
+
} catch (err) {
|
|
364
|
+
onAuthError(err, req, res);
|
|
365
|
+
}
|
|
238
366
|
});
|
|
239
367
|
}
|
|
240
368
|
if (bodyValidator && route.method !== "GET") {
|
package/src/workers/index.ts
CHANGED
package/src/workers/service.ts
CHANGED
|
@@ -4,7 +4,15 @@ import path from "path";
|
|
|
4
4
|
import { createClient, RedisClientType } from "redis";
|
|
5
5
|
import { Logger } from "winston";
|
|
6
6
|
import { Worker } from "worker_threads";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AuthResolver,
|
|
9
|
+
AuthStrategy,
|
|
10
|
+
createAuthResolver,
|
|
11
|
+
createMetaRouter,
|
|
12
|
+
createMetaRouterWithAuth,
|
|
13
|
+
defaultHeaderAuthResolver,
|
|
14
|
+
RouteDefinition,
|
|
15
|
+
} from "./express-types";
|
|
8
16
|
import { ClickhouseConfig, createTypedClickhouse } from "./db/clickhouse";
|
|
9
17
|
import {
|
|
10
18
|
createMongo,
|
|
@@ -39,12 +47,27 @@ export class Service<TCollections extends MongoCollectionsConfig = {}> {
|
|
|
39
47
|
private serviceStreamSubscriptions: string[] = [];
|
|
40
48
|
private _status: "starting" | "running" | "stopped" | "stopping" = "starting";
|
|
41
49
|
private readonly HEARTBEAT_INTERVAL = 1000 * 5;
|
|
50
|
+
private authStrategies: AuthStrategy[] = [];
|
|
51
|
+
private authResolverOverride?: AuthResolver;
|
|
52
|
+
private useDefaultHeaderAuthFallback = true;
|
|
42
53
|
private expressOptions: ExpressOptions = {
|
|
43
54
|
corsWhitelist: ["*"],
|
|
44
55
|
asJson: true,
|
|
45
56
|
customMiddleware: [],
|
|
46
57
|
};
|
|
47
58
|
|
|
59
|
+
private getAuthResolver(): AuthResolver {
|
|
60
|
+
if (this.authResolverOverride) {
|
|
61
|
+
return this.authResolverOverride;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return createAuthResolver(this.authStrategies, {
|
|
65
|
+
fallbackResolver: this.useDefaultHeaderAuthFallback
|
|
66
|
+
? (req) => defaultHeaderAuthResolver(req)
|
|
67
|
+
: undefined,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
get streamKey() {
|
|
49
72
|
return this.simpleName + ":events";
|
|
50
73
|
}
|
|
@@ -428,7 +451,9 @@ export class Service<TCollections extends MongoCollectionsConfig = {}> {
|
|
|
428
451
|
|
|
429
452
|
private initBaseRoutes() {
|
|
430
453
|
this.log.info("Registering base routes");
|
|
431
|
-
const { router, routes, addRoute } =
|
|
454
|
+
const { router, routes, addRoute } = createMetaRouterWithAuth({
|
|
455
|
+
authResolver: this.getAuthResolver(),
|
|
456
|
+
});
|
|
432
457
|
|
|
433
458
|
addRoute(
|
|
434
459
|
{
|
|
@@ -487,7 +512,9 @@ export class Service<TCollections extends MongoCollectionsConfig = {}> {
|
|
|
487
512
|
* Default worker routes under /workers
|
|
488
513
|
*/
|
|
489
514
|
private initWorkerRoutes(): void {
|
|
490
|
-
const { router, routes, addRoute } =
|
|
515
|
+
const { router, routes, addRoute } = createMetaRouterWithAuth({
|
|
516
|
+
authResolver: this.getAuthResolver(),
|
|
517
|
+
});
|
|
491
518
|
|
|
492
519
|
addRoute(
|
|
493
520
|
{
|
|
@@ -830,6 +857,26 @@ export class Service<TCollections extends MongoCollectionsConfig = {}> {
|
|
|
830
857
|
return this;
|
|
831
858
|
}
|
|
832
859
|
|
|
860
|
+
withAuthStrategy(strategy: AuthStrategy) {
|
|
861
|
+
this.authStrategies.push(strategy);
|
|
862
|
+
return this;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
withAuthStrategies(strategies: AuthStrategy[]) {
|
|
866
|
+
this.authStrategies.push(...strategies);
|
|
867
|
+
return this;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
withAuthResolver(resolver: AuthResolver) {
|
|
871
|
+
this.authResolverOverride = resolver;
|
|
872
|
+
return this;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
withoutDefaultHeaderAuthFallback() {
|
|
876
|
+
this.useDefaultHeaderAuthFallback = false;
|
|
877
|
+
return this;
|
|
878
|
+
}
|
|
879
|
+
|
|
833
880
|
private internalAddRouter(
|
|
834
881
|
base: string,
|
|
835
882
|
router: Router,
|