create-velox-app 0.6.73 → 0.6.75
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 +12 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +39 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -6
- 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
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# create-velox-app
|
|
2
2
|
|
|
3
|
+
## 0.6.75
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat(create): add --pm flag to skip package manager prompt
|
|
8
|
+
|
|
9
|
+
## 0.6.74
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- docs: fix erroneous file paths and improve auth context documentation
|
|
14
|
+
|
|
3
15
|
## 0.6.73
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
* Handles command-line arguments and initiates the scaffolding process.
|
|
7
7
|
*/
|
|
8
8
|
import type { DatabaseType, TemplateType } from './templates/index.js';
|
|
9
|
+
/** Valid package manager types */
|
|
10
|
+
export type PackageManagerType = 'npm' | 'pnpm' | 'yarn';
|
|
9
11
|
/** @internal Exported for testing */
|
|
10
12
|
export interface ParsedArgs {
|
|
11
13
|
projectName?: string;
|
|
12
14
|
template?: TemplateType;
|
|
13
15
|
database?: DatabaseType;
|
|
16
|
+
packageManager?: PackageManagerType;
|
|
14
17
|
help: boolean;
|
|
15
18
|
version: boolean;
|
|
16
19
|
}
|
package/dist/cli.js
CHANGED
|
@@ -39,6 +39,8 @@ Options:
|
|
|
39
39
|
Available: ${getTemplateNames()}
|
|
40
40
|
-d, --database <name> Database to use (default: "sqlite")
|
|
41
41
|
Available: ${getDatabaseNames()}
|
|
42
|
+
--pm <manager> Package manager to use (npm, pnpm, yarn)
|
|
43
|
+
Skips interactive prompt if specified
|
|
42
44
|
-h, --help Show this help message
|
|
43
45
|
-v, --version Show version number
|
|
44
46
|
|
|
@@ -60,8 +62,11 @@ Examples:
|
|
|
60
62
|
npx create-velox-app my-app --auth # Auth template (shortcut)
|
|
61
63
|
npx create-velox-app my-app --rsc -d postgresql # RSC with PostgreSQL
|
|
62
64
|
npx create-velox-app my-app --template=spa # SPA + API template
|
|
65
|
+
npx create-velox-app my-app --pm npm # Non-interactive (npm)
|
|
63
66
|
npx create-velox-app # Prompt for all options
|
|
64
67
|
`;
|
|
68
|
+
/** Valid package managers */
|
|
69
|
+
const VALID_PACKAGE_MANAGERS = new Set(['npm', 'pnpm', 'yarn']);
|
|
65
70
|
/** Template shorthand flags (--auth, --spa, etc.) */
|
|
66
71
|
const TEMPLATE_FLAGS = new Set([
|
|
67
72
|
'--spa',
|
|
@@ -207,6 +212,39 @@ export function parseArgs(args) {
|
|
|
207
212
|
}
|
|
208
213
|
continue;
|
|
209
214
|
}
|
|
215
|
+
// Handle --pm=<value>
|
|
216
|
+
if (arg.startsWith('--pm=')) {
|
|
217
|
+
const value = arg.split('=')[1];
|
|
218
|
+
if (!value) {
|
|
219
|
+
console.error('Error: --pm requires a value. Available: npm, pnpm, yarn');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
if (VALID_PACKAGE_MANAGERS.has(value)) {
|
|
223
|
+
result.packageManager = value;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.error(`Invalid package manager: ${value}. Available: npm, pnpm, yarn`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Handle --pm <value>
|
|
232
|
+
if (arg === '--pm') {
|
|
233
|
+
const value = args[i + 1];
|
|
234
|
+
if (!value || value.startsWith('-')) {
|
|
235
|
+
console.error('Error: --pm requires a value. Available: npm, pnpm, yarn');
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
if (VALID_PACKAGE_MANAGERS.has(value)) {
|
|
239
|
+
result.packageManager = value;
|
|
240
|
+
i++; // Skip next arg
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
console.error(`Invalid package manager: ${value}. Available: npm, pnpm, yarn`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
210
248
|
// Non-flag argument
|
|
211
249
|
if (!arg.startsWith('-')) {
|
|
212
250
|
if (!result.projectName) {
|
|
@@ -245,7 +283,7 @@ async function main() {
|
|
|
245
283
|
process.exit(0);
|
|
246
284
|
}
|
|
247
285
|
// Run scaffolder
|
|
248
|
-
await createVeloxApp(parsed.projectName, parsed.template, parsed.database);
|
|
286
|
+
await createVeloxApp(parsed.projectName, parsed.template, parsed.database, parsed.packageManager);
|
|
249
287
|
}
|
|
250
288
|
catch (error) {
|
|
251
289
|
// Handle unexpected errors with actionable guidance
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare function isPathSafe(baseDir: string, targetPath: string): boolean
|
|
|
19
19
|
/**
|
|
20
20
|
* Main scaffolding function that creates a new VeloxTS project
|
|
21
21
|
*/
|
|
22
|
-
export declare function createVeloxApp(initialProjectName?: string, initialTemplate?: TemplateType, initialDatabase?: DatabaseType): Promise<void>;
|
|
22
|
+
export declare function createVeloxApp(initialProjectName?: string, initialTemplate?: TemplateType, initialDatabase?: DatabaseType, initialPackageManager?: 'npm' | 'pnpm' | 'yarn'): Promise<void>;
|
|
23
23
|
/**
|
|
24
24
|
* Detect which package manager is being used
|
|
25
25
|
* @internal Exported for testing
|
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ export function isPathSafe(baseDir, targetPath) {
|
|
|
59
59
|
/**
|
|
60
60
|
* Main scaffolding function that creates a new VeloxTS project
|
|
61
61
|
*/
|
|
62
|
-
export async function createVeloxApp(initialProjectName, initialTemplate, initialDatabase) {
|
|
62
|
+
export async function createVeloxApp(initialProjectName, initialTemplate, initialDatabase, initialPackageManager) {
|
|
63
63
|
// Print welcome banner
|
|
64
64
|
console.log('');
|
|
65
65
|
p.intro(pc.cyan(pc.bold('create-velox-app')));
|
|
@@ -67,7 +67,7 @@ export async function createVeloxApp(initialProjectName, initialTemplate, initia
|
|
|
67
67
|
let projectCreated = false;
|
|
68
68
|
try {
|
|
69
69
|
// Collect project configuration
|
|
70
|
-
const config = await promptProjectConfig(initialProjectName, initialTemplate, initialDatabase);
|
|
70
|
+
const config = await promptProjectConfig(initialProjectName, initialTemplate, initialDatabase, initialPackageManager);
|
|
71
71
|
projectDirectory = config.directory;
|
|
72
72
|
// Show configuration summary
|
|
73
73
|
p.log.info(pc.dim('Configuration:'));
|
|
@@ -127,7 +127,7 @@ export async function createVeloxApp(initialProjectName, initialTemplate, initia
|
|
|
127
127
|
/**
|
|
128
128
|
* Prompt user for project configuration
|
|
129
129
|
*/
|
|
130
|
-
async function promptProjectConfig(initialName, initialTemplate, initialDatabase) {
|
|
130
|
+
async function promptProjectConfig(initialName, initialTemplate, initialDatabase, initialPackageManager) {
|
|
131
131
|
// Project name
|
|
132
132
|
const name = initialName
|
|
133
133
|
? initialName
|
|
@@ -199,10 +199,11 @@ async function promptProjectConfig(initialName, initialTemplate, initialDatabase
|
|
|
199
199
|
throw new Error(`Database "${database}" is not yet available. Please choose SQLite for now.`);
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
-
// Package manager selection (prompt unless running in CI/non-interactive mode)
|
|
203
|
-
let packageManager = detectPackageManager();
|
|
202
|
+
// Package manager selection (prompt unless provided via CLI or running in CI/non-interactive mode)
|
|
203
|
+
let packageManager = initialPackageManager ?? detectPackageManager();
|
|
204
|
+
// Only show package manager prompt if not provided via CLI
|
|
204
205
|
// SKIP_INSTALL is set in smoke tests to skip interactive prompts
|
|
205
|
-
if (process.env.SKIP_INSTALL !== 'true') {
|
|
206
|
+
if (!initialPackageManager && process.env.SKIP_INSTALL !== 'true') {
|
|
206
207
|
const selectedPackageManager = await p.select({
|
|
207
208
|
message: 'Choose a package manager',
|
|
208
209
|
initialValue: packageManager,
|
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
|
+
}
|