@jaypie/mcp 0.1.0 → 0.1.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/dist/index.js +0 -0
- package/package.json +9 -5
- package/prompts/Branch_Management.md +34 -0
- package/prompts/Development_Process.md +67 -0
- package/prompts/Jaypie_Agent_Rules.md +110 -0
- package/prompts/Jaypie_Auth0_Express_Mongoose.md +736 -0
- package/prompts/Jaypie_Browser_and_Frontend_Web_Packages.md +18 -0
- package/prompts/Jaypie_CDK_Constructs_and_Patterns.md +156 -0
- package/prompts/Jaypie_CICD_with_GitHub_Actions.md +151 -0
- package/prompts/Jaypie_Commander_CLI_Package.md +166 -0
- package/prompts/Jaypie_Core_Errors_and_Logging.md +39 -0
- package/prompts/Jaypie_Eslint_NPM_Package.md +78 -0
- package/prompts/Jaypie_Ideal_Project_Structure.md +78 -0
- package/prompts/Jaypie_Init_Express_on_Lambda.md +87 -0
- package/prompts/Jaypie_Init_Jaypie_CDK_Package.md +35 -0
- package/prompts/Jaypie_Init_Lambda_Package.md +245 -0
- package/prompts/Jaypie_Init_Monorepo_Project.md +44 -0
- package/prompts/Jaypie_Init_Project_Subpackage.md +70 -0
- package/prompts/Jaypie_Legacy_Patterns.md +11 -0
- package/prompts/Jaypie_Llm_Calls.md +113 -0
- package/prompts/Jaypie_Llm_Tools.md +124 -0
- package/prompts/Jaypie_Mocks_and_Testkit.md +137 -0
- package/prompts/Jaypie_Mongoose_Models_Package.md +231 -0
- package/prompts/Jaypie_Mongoose_with_Express_CRUD.md +1000 -0
- package/prompts/Jaypie_Scrub.md +177 -0
- package/prompts/Write_Efficient_Prompt_Guides.md +48 -0
- package/prompts/Write_and_Maintain_Engaging_Readme.md +67 -0
- package/prompts/templates/cdk-subpackage/bin/cdk.ts +11 -0
- package/prompts/templates/cdk-subpackage/cdk.json +19 -0
- package/prompts/templates/cdk-subpackage/lib/cdk-app.ts +41 -0
- package/prompts/templates/cdk-subpackage/lib/cdk-infrastructure.ts +15 -0
- package/prompts/templates/express-subpackage/index.ts +8 -0
- package/prompts/templates/express-subpackage/src/app.ts +18 -0
- package/prompts/templates/express-subpackage/src/handler.config.ts +44 -0
- package/prompts/templates/express-subpackage/src/routes/resource/__tests__/resourceGet.route.spec.ts +29 -0
- package/prompts/templates/express-subpackage/src/routes/resource/resourceGet.route.ts +22 -0
- package/prompts/templates/express-subpackage/src/routes/resource.router.ts +11 -0
- package/prompts/templates/express-subpackage/src/types/express.ts +9 -0
- package/prompts/templates/project-monorepo/.vscode/settings.json +72 -0
- package/prompts/templates/project-monorepo/eslint.config.mjs +1 -0
- package/prompts/templates/project-monorepo/package.json +20 -0
- package/prompts/templates/project-monorepo/tsconfig.base.json +18 -0
- package/prompts/templates/project-monorepo/tsconfig.json +6 -0
- package/prompts/templates/project-monorepo/vitest.workspace.js +3 -0
- package/prompts/templates/project-subpackage/package.json +16 -0
- package/prompts/templates/project-subpackage/tsconfig.json +11 -0
- package/prompts/templates/project-subpackage/vite.config.ts +21 -0
- package/prompts/templates/project-subpackage/vitest.config.ts +7 -0
- package/prompts/templates/project-subpackage/vitest.setup.ts +6 -0
- package/LICENSE.txt +0 -21
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Auth0 authentication and authorization patterns for Jaypie Express with Mongoose
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Jaypie Auth0 Express Mongoose
|
|
6
|
+
|
|
7
|
+
Complete authentication and authorization patterns for Jaypie Express applications using Auth0 JWT tokens with Mongoose user management in TypeScript.
|
|
8
|
+
|
|
9
|
+
## Pattern
|
|
10
|
+
|
|
11
|
+
- TypeScript with ES modules (`"type": "module"`)
|
|
12
|
+
- Auth0 JWT token validation with fallback to project secrets
|
|
13
|
+
- User context resolution from MongoDB with aggregated permissions
|
|
14
|
+
- Group-based authorization with portfolio access control
|
|
15
|
+
- Role-based access control (RBAC) patterns
|
|
16
|
+
- Secure user data handling with proper error responses
|
|
17
|
+
- Type-safe interfaces for all authentication and authorization patterns
|
|
18
|
+
|
|
19
|
+
## Dependencies
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"express-oauth2-jwt-bearer": "^1.6.0",
|
|
25
|
+
"@yourorg/models": "^1.0.0",
|
|
26
|
+
"jaypie": "^1.1.0",
|
|
27
|
+
"express": "^4.19.0",
|
|
28
|
+
"mongoose": "^8.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/express": "^4.17.0",
|
|
32
|
+
"@types/mongoose": "^8.0.0",
|
|
33
|
+
"typescript": "^5.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## TypeScript Configuration
|
|
39
|
+
|
|
40
|
+
**tsconfig.json**
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"compilerOptions": {
|
|
44
|
+
"target": "ES2022",
|
|
45
|
+
"module": "ESNext",
|
|
46
|
+
"moduleResolution": "node",
|
|
47
|
+
"declaration": true,
|
|
48
|
+
"outDir": "./dist",
|
|
49
|
+
"strict": true,
|
|
50
|
+
"esModuleInterop": true,
|
|
51
|
+
"skipLibCheck": true,
|
|
52
|
+
"forceConsistentCasingInFileNames": true,
|
|
53
|
+
"resolveJsonModule": true,
|
|
54
|
+
"allowSyntheticDefaultImports": true
|
|
55
|
+
},
|
|
56
|
+
"include": ["src/**/*", "index.ts"],
|
|
57
|
+
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Environment Variables
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
AUTH0_DOMAIN=your-auth0-domain.auth0.com
|
|
65
|
+
AUTH0_AUDIENCE=your-api-identifier
|
|
66
|
+
PROJECT_SECRET=your-project-secret
|
|
67
|
+
PROJECT_USER_XID=system-user-id
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Core Authentication
|
|
71
|
+
|
|
72
|
+
**src/util/validateAuth.ts** - JWT and project secret validation
|
|
73
|
+
```typescript
|
|
74
|
+
import { getHeaderFrom, HTTP, UnauthorizedError } from "jaypie";
|
|
75
|
+
import { auth } from "express-oauth2-jwt-bearer";
|
|
76
|
+
import type { Request, Response, NextFunction } from "express";
|
|
77
|
+
|
|
78
|
+
interface AuthRequest extends Request {
|
|
79
|
+
auth?: {
|
|
80
|
+
payload: {
|
|
81
|
+
sub: string;
|
|
82
|
+
[key: string]: any;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const validateAuth = async (req: AuthRequest): Promise<boolean> => {
|
|
88
|
+
const clientProjectSecret = getHeaderFrom(HTTP.HEADER.PROJECT.SECRET, req);
|
|
89
|
+
|
|
90
|
+
// Project secret authentication (for service-to-service calls)
|
|
91
|
+
if (clientProjectSecret && process.env.PROJECT_SECRET &&
|
|
92
|
+
clientProjectSecret === process.env.PROJECT_SECRET) {
|
|
93
|
+
req.auth = { payload: { sub: process.env.PROJECT_USER_XID || "system" } };
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Auth0 JWT token validation
|
|
98
|
+
const validateAuthToken = auth({
|
|
99
|
+
audience: process.env.AUTH0_AUDIENCE,
|
|
100
|
+
issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await validateAuthToken(req, {} as Response, (error?: Error) => {
|
|
104
|
+
if (error) throw new UnauthorizedError();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export default validateAuth;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Alternative: Promise-based validation**
|
|
114
|
+
```typescript
|
|
115
|
+
import { UnauthorizedError } from "jaypie";
|
|
116
|
+
import { auth } from "express-oauth2-jwt-bearer";
|
|
117
|
+
import type { Request, Response, NextFunction } from "express";
|
|
118
|
+
|
|
119
|
+
const jwtCheck = auth({
|
|
120
|
+
audience: process.env.AUTH0_AUDIENCE,
|
|
121
|
+
issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export default async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
jwtCheck(req, res, (err) => {
|
|
127
|
+
if (err) {
|
|
128
|
+
reject(new UnauthorizedError("Invalid token"));
|
|
129
|
+
} else {
|
|
130
|
+
resolve();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## User Context Resolution
|
|
138
|
+
|
|
139
|
+
**src/util/userLocal.ts** - MongoDB user lookup with permissions
|
|
140
|
+
```typescript
|
|
141
|
+
import { ConfigurationError, UnauthorizedError } from "jaypie";
|
|
142
|
+
import Model from "@yourorg/models";
|
|
143
|
+
import type { Request } from "express";
|
|
144
|
+
|
|
145
|
+
interface AuthRequest extends Request {
|
|
146
|
+
auth?: {
|
|
147
|
+
payload: {
|
|
148
|
+
sub: string;
|
|
149
|
+
[key: string]: any;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface UserWithPortfolios {
|
|
155
|
+
_id: string;
|
|
156
|
+
xid: string;
|
|
157
|
+
groups: string[];
|
|
158
|
+
portfolios: string[];
|
|
159
|
+
roles?: string[];
|
|
160
|
+
[key: string]: any;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const userLocal = async (req: AuthRequest = {} as AuthRequest): Promise<UserWithPortfolios> => {
|
|
164
|
+
if (!req?.auth?.payload?.sub) {
|
|
165
|
+
throw new ConfigurationError();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const xid = req.auth.payload.sub;
|
|
169
|
+
const users = await Model.User.aggregate([
|
|
170
|
+
{ $match: { xid, deletedAt: { $exists: false } } },
|
|
171
|
+
{
|
|
172
|
+
$lookup: {
|
|
173
|
+
from: "groups",
|
|
174
|
+
let: { groupIds: "$groups" },
|
|
175
|
+
pipeline: [
|
|
176
|
+
{ $match: { $expr: { $in: ["$_id", "$$groupIds"] } } },
|
|
177
|
+
{ $project: { portfolios: 1, _id: 1 } },
|
|
178
|
+
],
|
|
179
|
+
as: "groups_docs",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
$addFields: {
|
|
184
|
+
portfolios: {
|
|
185
|
+
$reduce: {
|
|
186
|
+
input: "$groups_docs.portfolios",
|
|
187
|
+
initialValue: [],
|
|
188
|
+
in: { $setUnion: ["$$value", "$$this"] },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{ $project: { groups_docs: 0 } },
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
if (!users?.length) throw new UnauthorizedError();
|
|
197
|
+
|
|
198
|
+
const user = new Model.User(users[0]);
|
|
199
|
+
user.portfolios = users[0].portfolios.map(String);
|
|
200
|
+
return user;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export default userLocal;
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Simplified user context (without aggregation)**
|
|
207
|
+
```typescript
|
|
208
|
+
import { UnauthorizedError } from "jaypie";
|
|
209
|
+
import Model from "@yourorg/models";
|
|
210
|
+
import type { Request } from "express";
|
|
211
|
+
|
|
212
|
+
interface AuthRequest extends Request {
|
|
213
|
+
auth?: {
|
|
214
|
+
payload: {
|
|
215
|
+
sub: string;
|
|
216
|
+
email?: string;
|
|
217
|
+
name?: string;
|
|
218
|
+
[key: string]: any;
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface UserContext {
|
|
224
|
+
sub: string;
|
|
225
|
+
email?: string;
|
|
226
|
+
name?: string;
|
|
227
|
+
groups: string[];
|
|
228
|
+
portfolios: string[];
|
|
229
|
+
roles: string[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default async (req: AuthRequest): Promise<UserContext> => {
|
|
233
|
+
const user = req.auth?.payload;
|
|
234
|
+
if (!user) {
|
|
235
|
+
throw new UnauthorizedError("User not authenticated");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Look up user in database if needed
|
|
239
|
+
const dbUser = await Model.User.findOne({
|
|
240
|
+
xid: user.sub,
|
|
241
|
+
deletedAt: { $exists: false }
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!dbUser) {
|
|
245
|
+
throw new UnauthorizedError("User not found");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
sub: user.sub,
|
|
250
|
+
email: user.email,
|
|
251
|
+
name: user.name,
|
|
252
|
+
groups: dbUser.groups || [],
|
|
253
|
+
portfolios: dbUser.portfolios || [],
|
|
254
|
+
roles: dbUser.roles || [],
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Authorization Patterns
|
|
260
|
+
|
|
261
|
+
**src/util/authUserHasGroups.ts** - Group membership validation
|
|
262
|
+
```typescript
|
|
263
|
+
import { ForbiddenError } from "jaypie";
|
|
264
|
+
|
|
265
|
+
interface AuthUserHasGroupsOptions {
|
|
266
|
+
requireGroups?: boolean;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface User {
|
|
270
|
+
groups?: string[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export default (user: User, options: AuthUserHasGroupsOptions = {}): boolean => {
|
|
274
|
+
const { requireGroups = true } = options;
|
|
275
|
+
|
|
276
|
+
if (requireGroups && (!user.groups || user.groups.length === 0)) {
|
|
277
|
+
throw new ForbiddenError("User must belong to at least one group");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return true;
|
|
281
|
+
};
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**src/util/authUserHasAllGroups.ts** - Specific group requirements
|
|
285
|
+
```typescript
|
|
286
|
+
import { ForbiddenError } from "jaypie";
|
|
287
|
+
|
|
288
|
+
interface AuthUserHasAllGroupsOptions {
|
|
289
|
+
groupUuids: string[];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface User {
|
|
293
|
+
groups?: string[];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export default async (user: User, { groupUuids }: AuthUserHasAllGroupsOptions): Promise<boolean> => {
|
|
297
|
+
const userGroups = user.groups || [];
|
|
298
|
+
const hasAllGroups = groupUuids.every(groupUuid =>
|
|
299
|
+
userGroups.includes(groupUuid)
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if (!hasAllGroups) {
|
|
303
|
+
throw new ForbiddenError("User lacks required group permissions");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return true;
|
|
307
|
+
};
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**src/util/authUserHasAnyGroups.ts** - Alternative group validation
|
|
311
|
+
```typescript
|
|
312
|
+
import { ForbiddenError } from "jaypie";
|
|
313
|
+
|
|
314
|
+
interface AuthUserHasAnyGroupsOptions {
|
|
315
|
+
groupUuids: string[];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
interface User {
|
|
319
|
+
groups?: string[];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default async (user: User, { groupUuids }: AuthUserHasAnyGroupsOptions): Promise<boolean> => {
|
|
323
|
+
const userGroups = user.groups || [];
|
|
324
|
+
const hasAnyGroup = groupUuids.some(groupUuid =>
|
|
325
|
+
userGroups.includes(groupUuid)
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (!hasAnyGroup) {
|
|
329
|
+
throw new ForbiddenError("User lacks required group permissions");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return true;
|
|
333
|
+
};
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**src/util/authUserHasPortfolios.ts** - Portfolio access control
|
|
337
|
+
```typescript
|
|
338
|
+
import { ForbiddenError } from "jaypie";
|
|
339
|
+
|
|
340
|
+
interface User {
|
|
341
|
+
portfolios?: string[];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export default (user: User, portfolioIds: string | string[]): boolean => {
|
|
345
|
+
const portfolioIdsArray = Array.isArray(portfolioIds) ? portfolioIds : [portfolioIds];
|
|
346
|
+
|
|
347
|
+
const userPortfolios = user.portfolios || [];
|
|
348
|
+
const hasAccess = portfolioIdsArray.every(portfolioId =>
|
|
349
|
+
userPortfolios.includes(portfolioId)
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
if (!hasAccess) {
|
|
353
|
+
throw new ForbiddenError("User lacks portfolio access");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return true;
|
|
357
|
+
};
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**src/util/authUserHasRoles.ts** - Role-based access control
|
|
361
|
+
```typescript
|
|
362
|
+
import { ForbiddenError } from "jaypie";
|
|
363
|
+
|
|
364
|
+
interface User {
|
|
365
|
+
roles?: string[];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export default (user: User, requiredRoles: string | string[]): boolean => {
|
|
369
|
+
const requiredRolesArray = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
|
|
370
|
+
|
|
371
|
+
const userRoles = user.roles || [];
|
|
372
|
+
const hasRequiredRole = requiredRolesArray.some(role =>
|
|
373
|
+
userRoles.includes(role)
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (!hasRequiredRole) {
|
|
377
|
+
throw new ForbiddenError("User lacks required role permissions");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return true;
|
|
381
|
+
};
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## System Context
|
|
385
|
+
|
|
386
|
+
**src/util/systemContextLocal.ts** - System metadata
|
|
387
|
+
```typescript
|
|
388
|
+
import type { Request } from "express";
|
|
389
|
+
|
|
390
|
+
interface SystemContext {
|
|
391
|
+
timestamp: string;
|
|
392
|
+
requestId: string;
|
|
393
|
+
userAgent?: string;
|
|
394
|
+
ip?: string;
|
|
395
|
+
environment?: string;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export default async (req: Request): Promise<SystemContext> => {
|
|
399
|
+
return {
|
|
400
|
+
timestamp: new Date().toISOString(),
|
|
401
|
+
requestId: req.headers["x-request-id"] as string || req.id,
|
|
402
|
+
userAgent: req.headers["user-agent"],
|
|
403
|
+
ip: req.ip || req.connection?.remoteAddress,
|
|
404
|
+
environment: process.env.NODE_ENV,
|
|
405
|
+
};
|
|
406
|
+
};
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Usage in Handler Config
|
|
410
|
+
|
|
411
|
+
**src/handler.config.ts** - Complete configuration
|
|
412
|
+
```typescript
|
|
413
|
+
import { force } from "jaypie";
|
|
414
|
+
import Model from "@yourorg/models";
|
|
415
|
+
import systemContextLocal from "./util/systemContextLocal.js";
|
|
416
|
+
import userLocal from "./util/userLocal.js";
|
|
417
|
+
import validateAuth from "./util/validateAuth.js";
|
|
418
|
+
|
|
419
|
+
interface HandlerConfigOptions {
|
|
420
|
+
locals?: Record<string, any>;
|
|
421
|
+
setup?: Array<() => void | Promise<void>>;
|
|
422
|
+
teardown?: Array<() => void | Promise<void>>;
|
|
423
|
+
validate?: Array<() => boolean | Promise<boolean>>;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
interface HandlerConfig {
|
|
427
|
+
name: string;
|
|
428
|
+
locals: Record<string, any>;
|
|
429
|
+
setup: Array<() => void | Promise<void>>;
|
|
430
|
+
teardown: Array<() => void | Promise<void>>;
|
|
431
|
+
validate: Array<() => boolean | Promise<boolean>>;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const handlerConfig = (
|
|
435
|
+
nameOrConfig: string | (HandlerConfig & { name: string }),
|
|
436
|
+
options: HandlerConfigOptions = {}
|
|
437
|
+
): HandlerConfig => {
|
|
438
|
+
let name: string;
|
|
439
|
+
let locals: Record<string, any>;
|
|
440
|
+
let setup: Array<() => void | Promise<void>>;
|
|
441
|
+
let teardown: Array<() => void | Promise<void>>;
|
|
442
|
+
let validate: Array<() => boolean | Promise<boolean>>;
|
|
443
|
+
|
|
444
|
+
if (typeof nameOrConfig === "object") {
|
|
445
|
+
({ name, locals = {}, setup = [], teardown = [], validate = [] } = nameOrConfig);
|
|
446
|
+
} else {
|
|
447
|
+
name = nameOrConfig;
|
|
448
|
+
({ locals = {}, setup = [], teardown = [], validate = [] } = options);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
name,
|
|
453
|
+
locals: {
|
|
454
|
+
...force.object(locals),
|
|
455
|
+
systemContext: systemContextLocal,
|
|
456
|
+
user: userLocal
|
|
457
|
+
},
|
|
458
|
+
setup: [Model.connect, ...force.array(setup)],
|
|
459
|
+
teardown: [...force.array(teardown), Model.disconnect],
|
|
460
|
+
validate: [validateAuth, ...force.array(validate)],
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
export default handlerConfig;
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Route Authorization Examples
|
|
468
|
+
|
|
469
|
+
**Admin-only route**
|
|
470
|
+
```typescript
|
|
471
|
+
import { expressHandler } from "jaypie";
|
|
472
|
+
import handlerConfig from "../handler.config.js";
|
|
473
|
+
import authUserHasRoles from "../util/authUserHasRoles.js";
|
|
474
|
+
import type { Request } from "express";
|
|
475
|
+
|
|
476
|
+
interface AdminRouteRequest extends Request {
|
|
477
|
+
locals: {
|
|
478
|
+
user: {
|
|
479
|
+
roles: string[];
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
interface AdminRouteResponse {
|
|
485
|
+
message: string;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export default expressHandler(
|
|
489
|
+
handlerConfig({
|
|
490
|
+
name: "adminRoute",
|
|
491
|
+
validate: (req: AdminRouteRequest) => authUserHasRoles(req.locals.user, ["admin"]),
|
|
492
|
+
}),
|
|
493
|
+
async (req: AdminRouteRequest): Promise<AdminRouteResponse> => {
|
|
494
|
+
return { message: "Admin access granted" };
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Group-specific route**
|
|
500
|
+
```typescript
|
|
501
|
+
import { expressHandler } from "jaypie";
|
|
502
|
+
import handlerConfig from "../handler.config.js";
|
|
503
|
+
import authUserHasAllGroups from "../util/authUserHasAllGroups.js";
|
|
504
|
+
import type { Request } from "express";
|
|
505
|
+
|
|
506
|
+
interface GroupRouteRequest extends Request {
|
|
507
|
+
locals: {
|
|
508
|
+
user: {
|
|
509
|
+
groups: string[];
|
|
510
|
+
};
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
interface GroupRouteResponse {
|
|
515
|
+
message: string;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export default expressHandler(
|
|
519
|
+
handlerConfig({
|
|
520
|
+
name: "groupRoute",
|
|
521
|
+
validate: async (req: GroupRouteRequest) => {
|
|
522
|
+
const requiredGroups = ["managers", "developers"];
|
|
523
|
+
await authUserHasAllGroups(req.locals.user, { groupUuids: requiredGroups });
|
|
524
|
+
},
|
|
525
|
+
}),
|
|
526
|
+
async (req: GroupRouteRequest): Promise<GroupRouteResponse> => {
|
|
527
|
+
return { message: "Group access granted" };
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Portfolio-scoped route**
|
|
533
|
+
```typescript
|
|
534
|
+
import { expressHandler } from "jaypie";
|
|
535
|
+
import handlerConfig from "../handler.config.js";
|
|
536
|
+
import authUserHasPortfolios from "../util/authUserHasPortfolios.js";
|
|
537
|
+
import type { Request } from "express";
|
|
538
|
+
|
|
539
|
+
interface PortfolioRouteRequest extends Request {
|
|
540
|
+
params: {
|
|
541
|
+
portfolioId: string;
|
|
542
|
+
};
|
|
543
|
+
locals: {
|
|
544
|
+
user: {
|
|
545
|
+
portfolios: string[];
|
|
546
|
+
};
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
interface PortfolioRouteResponse {
|
|
551
|
+
message: string;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export default expressHandler(
|
|
555
|
+
handlerConfig({
|
|
556
|
+
name: "portfolioRoute",
|
|
557
|
+
validate: (req: PortfolioRouteRequest) => {
|
|
558
|
+
const portfolioId = req.params.portfolioId;
|
|
559
|
+
authUserHasPortfolios(req.locals.user, portfolioId);
|
|
560
|
+
},
|
|
561
|
+
}),
|
|
562
|
+
async (req: PortfolioRouteRequest): Promise<PortfolioRouteResponse> => {
|
|
563
|
+
return { message: "Portfolio access granted" };
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## Testing Authentication
|
|
569
|
+
|
|
570
|
+
**Mock setup**
|
|
571
|
+
```js
|
|
572
|
+
import { expect, vi } from "vitest";
|
|
573
|
+
|
|
574
|
+
// Mock validateAuth
|
|
575
|
+
vi.mock("./src/util/validateAuth.js", () => ({
|
|
576
|
+
default: vi.fn().mockResolvedValue(true),
|
|
577
|
+
}));
|
|
578
|
+
|
|
579
|
+
// Mock userLocal
|
|
580
|
+
vi.mock("./src/util/userLocal.js", () => ({
|
|
581
|
+
default: vi.fn().mockResolvedValue({
|
|
582
|
+
sub: "test-user-id",
|
|
583
|
+
email: "test@example.com",
|
|
584
|
+
groups: ["test-group-id"],
|
|
585
|
+
portfolios: ["test-portfolio-id"],
|
|
586
|
+
roles: ["user"],
|
|
587
|
+
}),
|
|
588
|
+
}));
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Test authenticated route**
|
|
592
|
+
```typescript
|
|
593
|
+
import { describe, expect, it } from "vitest";
|
|
594
|
+
import userLocal from "../util/userLocal.js";
|
|
595
|
+
import authenticatedRoute from "../routes/authenticated.route.js";
|
|
596
|
+
|
|
597
|
+
describe("Authenticated Route", () => {
|
|
598
|
+
it("returns data for authenticated user", async () => {
|
|
599
|
+
const mockUser = {
|
|
600
|
+
sub: "user123",
|
|
601
|
+
groups: ["group1"],
|
|
602
|
+
portfolios: ["portfolio1"],
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
userLocal.mockResolvedValue(mockUser);
|
|
606
|
+
|
|
607
|
+
const mockRequest = {
|
|
608
|
+
locals: { user: mockUser },
|
|
609
|
+
} as any;
|
|
610
|
+
|
|
611
|
+
const response = await authenticatedRoute(mockRequest);
|
|
612
|
+
|
|
613
|
+
expect(response).toEqual({
|
|
614
|
+
message: "Access granted",
|
|
615
|
+
user: mockUser,
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("throws when user lacks required groups", async () => {
|
|
620
|
+
const mockUser = {
|
|
621
|
+
sub: "user123",
|
|
622
|
+
groups: [],
|
|
623
|
+
portfolios: [],
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const mockRequest = {
|
|
627
|
+
locals: { user: mockUser },
|
|
628
|
+
} as any;
|
|
629
|
+
|
|
630
|
+
await expect(() => authenticatedRoute(mockRequest)).toThrowForbiddenError();
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
## Security Best Practices
|
|
636
|
+
|
|
637
|
+
### Token Validation
|
|
638
|
+
- Always validate JWT tokens server-side
|
|
639
|
+
- Use proper audience and issuer validation
|
|
640
|
+
- Handle token expiration gracefully
|
|
641
|
+
- Implement token refresh patterns
|
|
642
|
+
|
|
643
|
+
### Error Handling
|
|
644
|
+
- Never expose sensitive user data in error messages
|
|
645
|
+
- Use generic error messages for authorization failures
|
|
646
|
+
- Log security events for monitoring
|
|
647
|
+
- Implement rate limiting for authentication endpoints
|
|
648
|
+
|
|
649
|
+
### User Data Protection
|
|
650
|
+
- Only expose necessary user data to client
|
|
651
|
+
- Hash/encrypt sensitive user information
|
|
652
|
+
- Implement proper session management
|
|
653
|
+
- Use secure HTTP headers
|
|
654
|
+
|
|
655
|
+
## Common Patterns
|
|
656
|
+
|
|
657
|
+
### Multi-tenant Authorization
|
|
658
|
+
```typescript
|
|
659
|
+
interface User {
|
|
660
|
+
tenants?: string[];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const authUserHasTenantAccess = (user: User, tenantId: string): boolean => {
|
|
664
|
+
const userTenants = user.tenants || [];
|
|
665
|
+
if (!userTenants.includes(tenantId)) {
|
|
666
|
+
throw new ForbiddenError("User lacks tenant access");
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
};
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Resource-level Permissions
|
|
673
|
+
```typescript
|
|
674
|
+
interface User {
|
|
675
|
+
groups: string[];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
interface Resource {
|
|
679
|
+
groups: string[];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const authUserCanAccessResource = async (user: User, resourceId: string): Promise<boolean> => {
|
|
683
|
+
const resource = await Model.Resource.findById(resourceId) as Resource;
|
|
684
|
+
if (!resource) {
|
|
685
|
+
throw new NotFoundError("Resource not found");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const hasAccess = resource.groups.some(group =>
|
|
689
|
+
user.groups.includes(group)
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
if (!hasAccess) {
|
|
693
|
+
throw new ForbiddenError("User lacks resource access");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return true;
|
|
697
|
+
};
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Conditional Authorization
|
|
701
|
+
```typescript
|
|
702
|
+
interface User {
|
|
703
|
+
sub: string;
|
|
704
|
+
roles: string[];
|
|
705
|
+
groups: string[];
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
interface Resource {
|
|
709
|
+
ownerId: string;
|
|
710
|
+
groups: string[];
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const authUserCanModifyResource = (user: User, resource: Resource): boolean => {
|
|
714
|
+
// Resource owner can always modify
|
|
715
|
+
if (resource.ownerId === user.sub) {
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Admins can modify any resource
|
|
720
|
+
if (user.roles.includes("admin")) {
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Managers can modify resources in their groups
|
|
725
|
+
if (user.roles.includes("manager")) {
|
|
726
|
+
const hasGroupAccess = resource.groups.some(group =>
|
|
727
|
+
user.groups.includes(group)
|
|
728
|
+
);
|
|
729
|
+
if (hasGroupAccess) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
throw new ForbiddenError("User cannot modify this resource");
|
|
735
|
+
};
|
|
736
|
+
```
|