create-velox-app 0.6.73 → 0.6.74
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/templates/source/api/utils/auth.ts +35 -0
- package/src/templates/source/root/.claude/skills/veloxts/TROUBLESHOOTING.md +15 -9
- package/src/templates/source/root/CLAUDE.auth.md +66 -11
- package/src/templates/source/root/CLAUDE.default.md +12 -1
- package/src/templates/source/rsc-auth/CLAUDE.md +55 -9
- package/src/templates/source/rsc-auth/src/api/utils/auth.ts +35 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -99,3 +99,38 @@ export const jwt = jwtManager({
|
|
|
99
99
|
issuer: 'velox-app',
|
|
100
100
|
audience: 'velox-app-client',
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Full User Helper
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fetch full user from database when ctx.user doesn't have all fields.
|
|
109
|
+
*
|
|
110
|
+
* `ctx.user` only contains fields returned by `userLoader` (id, email, name, roles).
|
|
111
|
+
* Use this helper when you need additional fields like `organizationId`.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* // In a procedure that needs full user data:
|
|
116
|
+
* .query(async ({ ctx }) => {
|
|
117
|
+
* // ctx.user has: id, email, name, roles
|
|
118
|
+
* // For additional fields, fetch full user:
|
|
119
|
+
* const fullUser = await getFullUser(ctx);
|
|
120
|
+
* return { organizationId: fullUser.organizationId };
|
|
121
|
+
* })
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @param ctx - The procedure context (must have user.id and db)
|
|
125
|
+
* @returns The full user record from database
|
|
126
|
+
* @throws If user is not found (should not happen for authenticated requests)
|
|
127
|
+
*/
|
|
128
|
+
export async function getFullUser<
|
|
129
|
+
TDb extends {
|
|
130
|
+
user: { findUniqueOrThrow: (args: { where: { id: string } }) => Promise<unknown> };
|
|
131
|
+
},
|
|
132
|
+
>(ctx: { user: { id: string }; db: TDb }) {
|
|
133
|
+
return ctx.db.user.findUniqueOrThrow({
|
|
134
|
+
where: { id: ctx.user.id },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -76,18 +76,24 @@ getUser: procedure().input(z.object({ id: z.string() }))
|
|
|
76
76
|
|
|
77
77
|
**Cause**: Procedure not registered in router.
|
|
78
78
|
|
|
79
|
-
**Fix**:
|
|
80
|
-
1. Check `src/procedures/index.ts` exports your procedure
|
|
81
|
-
2. Check `src/index.ts` includes it in collections array
|
|
79
|
+
**Fix**: Add procedure to `createRouter()` in `src/router.ts`:
|
|
82
80
|
|
|
83
81
|
```typescript
|
|
84
|
-
// src/
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
// src/router.ts
|
|
83
|
+
import { createRouter, extractRoutes } from '@veloxts/velox';
|
|
84
|
+
|
|
85
|
+
import { healthProcedures } from './procedures/health.js';
|
|
86
|
+
import { userProcedures } from './procedures/users.js';
|
|
87
|
+
import { postProcedures } from './procedures/posts.js'; // Add import
|
|
88
|
+
|
|
89
|
+
export const { collections, router } = createRouter(
|
|
90
|
+
healthProcedures,
|
|
91
|
+
userProcedures,
|
|
92
|
+
postProcedures // Add here
|
|
93
|
+
);
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const collections = [userProcedures, postProcedures]; // Add here
|
|
95
|
+
export type AppRouter = typeof router;
|
|
96
|
+
export const routes = extractRoutes(collections);
|
|
91
97
|
```
|
|
92
98
|
|
|
93
99
|
### "Input validation failed"
|
|
@@ -67,7 +67,19 @@ export const postProcedures = procedures('posts', {
|
|
|
67
67
|
});
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Then register in `src/
|
|
70
|
+
Then register in `src/router.ts` by importing and adding to `createRouter()`:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// src/router.ts
|
|
74
|
+
import { postProcedures } from './procedures/posts.js';
|
|
75
|
+
|
|
76
|
+
export const { collections, router } = createRouter(
|
|
77
|
+
healthProcedures,
|
|
78
|
+
authProcedures,
|
|
79
|
+
userProcedures,
|
|
80
|
+
postProcedures // Add here
|
|
81
|
+
);
|
|
82
|
+
```
|
|
71
83
|
|
|
72
84
|
## Prisma 7 Configuration
|
|
73
85
|
|
|
@@ -229,19 +241,41 @@ createdAt: z.coerce.date()
|
|
|
229
241
|
updatedAt: z.coerce.date()
|
|
230
242
|
```
|
|
231
243
|
|
|
232
|
-
###
|
|
244
|
+
### Authentication Context (`ctx.user`)
|
|
233
245
|
|
|
234
|
-
|
|
246
|
+
**Important:** `ctx.user` is NOT the raw database user - it only contains fields explicitly returned by `userLoader` in `src/config/auth.ts`.
|
|
235
247
|
|
|
236
|
-
|
|
248
|
+
#### How ctx.user Gets Populated
|
|
237
249
|
|
|
238
|
-
1.
|
|
250
|
+
1. JWT token is validated from cookie/header
|
|
251
|
+
2. User ID is extracted from token payload
|
|
252
|
+
3. `userLoader(userId)` is called to fetch user data
|
|
253
|
+
4. Only the fields returned by `userLoader` are available on `ctx.user`
|
|
254
|
+
|
|
255
|
+
#### Default Fields
|
|
256
|
+
|
|
257
|
+
The default `userLoader` returns:
|
|
258
|
+
```typescript
|
|
259
|
+
{
|
|
260
|
+
id: string;
|
|
261
|
+
email: string;
|
|
262
|
+
name: string;
|
|
263
|
+
roles: string[];
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### Adding Fields to ctx.user
|
|
268
|
+
|
|
269
|
+
To add a field like `organizationId`:
|
|
270
|
+
|
|
271
|
+
1. Update `userLoader` in `src/config/auth.ts`:
|
|
239
272
|
```typescript
|
|
240
273
|
async function userLoader(userId: string) {
|
|
241
274
|
const user = await db.user.findUnique({ where: { id: userId } });
|
|
242
275
|
return {
|
|
243
276
|
id: user.id,
|
|
244
277
|
email: user.email,
|
|
278
|
+
name: user.name,
|
|
245
279
|
roles: parseUserRoles(user.roles),
|
|
246
280
|
organizationId: user.organizationId, // Add new fields here
|
|
247
281
|
};
|
|
@@ -250,15 +284,36 @@ async function userLoader(userId: string) {
|
|
|
250
284
|
|
|
251
285
|
2. Update related schemas (`UserSchema`, `UpdateUserInput`, etc.).
|
|
252
286
|
|
|
287
|
+
#### Common Mistake
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// ❌ WRONG - organizationId is undefined (not in userLoader)
|
|
291
|
+
const orgId = ctx.user.organizationId;
|
|
292
|
+
|
|
293
|
+
// ✅ CORRECT - After adding to userLoader in src/config/auth.ts
|
|
294
|
+
const orgId = ctx.user.organizationId;
|
|
295
|
+
|
|
296
|
+
// ✅ ALTERNATIVE - Use getFullUser helper when you need extra fields
|
|
297
|
+
import { getFullUser } from '@/utils/auth';
|
|
298
|
+
const fullUser = await getFullUser(ctx);
|
|
299
|
+
const orgId = fullUser.organizationId;
|
|
300
|
+
```
|
|
301
|
+
|
|
253
302
|
### Role Configuration
|
|
254
303
|
|
|
255
|
-
Roles are stored as a JSON string array in the database (e.g., `["
|
|
304
|
+
Roles are stored as a JSON string array in the database (e.g., `["user"]`, `["admin"]`).
|
|
305
|
+
|
|
306
|
+
The `parseUserRoles()` function from `@veloxts/auth` safely parses the JSON string:
|
|
307
|
+
```typescript
|
|
308
|
+
// Imported and used in src/config/auth.ts
|
|
309
|
+
import { parseUserRoles } from '@veloxts/auth';
|
|
310
|
+
roles: parseUserRoles(user.roles), // Converts '["user"]' to ['user']
|
|
311
|
+
```
|
|
256
312
|
|
|
257
|
-
When
|
|
258
|
-
- `prisma/schema.prisma` -
|
|
259
|
-
- `src/
|
|
260
|
-
-
|
|
261
|
-
- `src/schemas/auth.ts` - Role validation schemas
|
|
313
|
+
When adding new roles, update:
|
|
314
|
+
- `prisma/schema.prisma` - Default value in the roles field
|
|
315
|
+
- `src/schemas/auth.ts` - Role validation schemas (if using enum validation)
|
|
316
|
+
- Any guards that check for specific roles (e.g., `hasRole('admin')`)
|
|
262
317
|
|
|
263
318
|
### MCP Project Path
|
|
264
319
|
|
|
@@ -66,7 +66,18 @@ export const postProcedures = procedures('posts', {
|
|
|
66
66
|
});
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
Then register in `src/
|
|
69
|
+
Then register in `src/router.ts` by importing and adding to `createRouter()`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// src/router.ts
|
|
73
|
+
import { postProcedures } from './procedures/posts.js';
|
|
74
|
+
|
|
75
|
+
export const { collections, router } = createRouter(
|
|
76
|
+
healthProcedures,
|
|
77
|
+
userProcedures,
|
|
78
|
+
postProcedures // Add here
|
|
79
|
+
);
|
|
80
|
+
```
|
|
70
81
|
|
|
71
82
|
## Prisma 7 Configuration
|
|
72
83
|
|
|
@@ -122,6 +122,44 @@ await logout(); // Clears cookies server-side
|
|
|
122
122
|
- At least one number
|
|
123
123
|
- Not a common password
|
|
124
124
|
|
|
125
|
+
### Authentication Context (`ctx.user`)
|
|
126
|
+
|
|
127
|
+
**Important:** `ctx.user` is NOT the raw database user - it only contains fields explicitly returned by `userLoader` in `src/api/handler.ts`.
|
|
128
|
+
|
|
129
|
+
#### How ctx.user Gets Populated
|
|
130
|
+
|
|
131
|
+
1. JWT token is validated from cookie/header
|
|
132
|
+
2. User ID is extracted from token payload
|
|
133
|
+
3. `userLoader(userId)` is called to fetch user data
|
|
134
|
+
4. Only the fields returned by `userLoader` are available on `ctx.user`
|
|
135
|
+
|
|
136
|
+
#### Default Fields
|
|
137
|
+
|
|
138
|
+
The default `userLoader` returns:
|
|
139
|
+
```typescript
|
|
140
|
+
{
|
|
141
|
+
id: string;
|
|
142
|
+
email: string;
|
|
143
|
+
name: string;
|
|
144
|
+
roles: string[];
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Common Mistake
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// ❌ WRONG - organizationId is undefined (not in userLoader)
|
|
152
|
+
const orgId = ctx.user.organizationId;
|
|
153
|
+
|
|
154
|
+
// ✅ CORRECT - After adding to userLoader in src/api/handler.ts
|
|
155
|
+
const orgId = ctx.user.organizationId;
|
|
156
|
+
|
|
157
|
+
// ✅ ALTERNATIVE - Use getFullUser helper when you need extra fields
|
|
158
|
+
import { getFullUser } from '@/api/utils/auth';
|
|
159
|
+
const fullUser = await getFullUser(ctx);
|
|
160
|
+
const orgId = fullUser.organizationId;
|
|
161
|
+
```
|
|
162
|
+
|
|
125
163
|
## Server Actions with validated()
|
|
126
164
|
|
|
127
165
|
Use the `validated()` helper for secure server actions:
|
|
@@ -302,33 +340,41 @@ updatedAt: z.coerce.date()
|
|
|
302
340
|
|
|
303
341
|
### Extending User Context
|
|
304
342
|
|
|
305
|
-
The `ctx.user` object is populated by `userLoader` in `src/api/
|
|
343
|
+
The `ctx.user` object is populated by `userLoader` in `src/api/handler.ts` (inline in `createAuthConfig()`).
|
|
306
344
|
|
|
307
345
|
To add fields to `ctx.user` (e.g., `organizationId`):
|
|
308
346
|
|
|
309
|
-
1. Update `userLoader`:
|
|
347
|
+
1. Update `userLoader` in `src/api/handler.ts`:
|
|
310
348
|
```typescript
|
|
311
|
-
async
|
|
349
|
+
userLoader: async (userId: string) => {
|
|
312
350
|
const user = await db.user.findUnique({ where: { id: userId } });
|
|
313
351
|
return {
|
|
314
352
|
id: user.id,
|
|
315
353
|
email: user.email,
|
|
354
|
+
name: user.name,
|
|
316
355
|
roles: parseUserRoles(user.roles),
|
|
317
356
|
organizationId: user.organizationId, // Add new fields here
|
|
318
357
|
};
|
|
319
|
-
}
|
|
358
|
+
},
|
|
320
359
|
```
|
|
321
360
|
|
|
322
361
|
2. Update related schemas (`UserSchema`, `UpdateUserInput`, etc.).
|
|
323
362
|
|
|
324
363
|
### Role Configuration
|
|
325
364
|
|
|
326
|
-
Roles are stored as a JSON string array in the database (e.g., `["
|
|
365
|
+
Roles are stored as a JSON string array in the database (e.g., `["user"]`, `["admin"]`).
|
|
366
|
+
|
|
367
|
+
The `parseUserRoles()` function from `@veloxts/auth` safely parses the JSON string:
|
|
368
|
+
```typescript
|
|
369
|
+
// Imported and used in src/api/handler.ts
|
|
370
|
+
import { parseUserRoles } from '@veloxts/auth';
|
|
371
|
+
roles: parseUserRoles(user.roles), // Converts '["user"]' to ['user']
|
|
372
|
+
```
|
|
327
373
|
|
|
328
|
-
When
|
|
329
|
-
- `prisma/schema.prisma` -
|
|
330
|
-
- `src/api/
|
|
331
|
-
-
|
|
374
|
+
When adding new roles, update:
|
|
375
|
+
- `prisma/schema.prisma` - Default value in the roles field
|
|
376
|
+
- `src/api/schemas/auth.ts` - Role validation schemas (if using enum validation)
|
|
377
|
+
- Any guards or server actions that check for specific roles
|
|
332
378
|
|
|
333
379
|
### MCP Project Path
|
|
334
380
|
|
|
@@ -99,3 +99,38 @@ export const jwt = jwtManager({
|
|
|
99
99
|
issuer: 'velox-app',
|
|
100
100
|
audience: 'velox-app-client',
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Full User Helper
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fetch full user from database when ctx.user doesn't have all fields.
|
|
109
|
+
*
|
|
110
|
+
* `ctx.user` only contains fields returned by `userLoader` (id, email, name, roles).
|
|
111
|
+
* Use this helper when you need additional fields like `organizationId`.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* // In a procedure that needs full user data:
|
|
116
|
+
* .query(async ({ ctx }) => {
|
|
117
|
+
* // ctx.user has: id, email, name, roles
|
|
118
|
+
* // For additional fields, fetch full user:
|
|
119
|
+
* const fullUser = await getFullUser(ctx);
|
|
120
|
+
* return { organizationId: fullUser.organizationId };
|
|
121
|
+
* })
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @param ctx - The procedure context (must have user.id and db)
|
|
125
|
+
* @returns The full user record from database
|
|
126
|
+
* @throws If user is not found (should not happen for authenticated requests)
|
|
127
|
+
*/
|
|
128
|
+
export async function getFullUser<
|
|
129
|
+
TDb extends {
|
|
130
|
+
user: { findUniqueOrThrow: (args: { where: { id: string } }) => Promise<unknown> };
|
|
131
|
+
},
|
|
132
|
+
>(ctx: { user: { id: string }; db: TDb }) {
|
|
133
|
+
return ctx.db.user.findUniqueOrThrow({
|
|
134
|
+
where: { id: ctx.user.id },
|
|
135
|
+
});
|
|
136
|
+
}
|