create-velox-app 0.6.31 → 0.6.52

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/GUIDE.md +230 -0
  3. package/dist/cli.js +1 -0
  4. package/dist/index.js +14 -4
  5. package/dist/templates/auth.js +10 -0
  6. package/dist/templates/index.js +30 -1
  7. package/dist/templates/placeholders.js +0 -3
  8. package/dist/templates/rsc-auth.d.ts +12 -0
  9. package/dist/templates/rsc-auth.js +208 -0
  10. package/dist/templates/rsc.js +40 -1
  11. package/dist/templates/shared/css-generator.d.ts +26 -0
  12. package/dist/templates/shared/css-generator.js +553 -0
  13. package/dist/templates/shared/index.d.ts +3 -0
  14. package/dist/templates/shared/index.js +3 -0
  15. package/dist/templates/shared/rsc-styles.d.ts +54 -0
  16. package/dist/templates/shared/rsc-styles.js +68 -0
  17. package/dist/templates/shared/theme.d.ts +133 -0
  18. package/dist/templates/shared/theme.js +141 -0
  19. package/dist/templates/spa.js +10 -0
  20. package/dist/templates/trpc.js +10 -0
  21. package/dist/templates/types.d.ts +2 -1
  22. package/dist/templates/types.js +6 -0
  23. package/package.json +6 -3
  24. package/src/templates/source/api/config/database.ts +13 -32
  25. package/src/templates/source/api/docker-compose.yml +21 -0
  26. package/src/templates/source/root/CLAUDE.auth.md +6 -0
  27. package/src/templates/source/root/CLAUDE.default.md +6 -0
  28. package/src/templates/source/rsc/CLAUDE.md +56 -2
  29. package/src/templates/source/rsc/app/actions/posts.ts +1 -1
  30. package/src/templates/source/rsc/app/actions/users.ts +111 -20
  31. package/src/templates/source/rsc/app/layouts/dashboard.tsx +21 -16
  32. package/src/templates/source/rsc/app/layouts/marketing.tsx +34 -0
  33. package/src/templates/source/rsc/app/layouts/minimal-content.tsx +21 -0
  34. package/src/templates/source/rsc/app/layouts/minimal.tsx +86 -5
  35. package/src/templates/source/rsc/app/layouts/root.tsx +148 -44
  36. package/src/templates/source/rsc/docker-compose.yml +21 -0
  37. package/src/templates/source/rsc/package.json +3 -3
  38. package/src/templates/source/rsc/src/api/database.ts +13 -32
  39. package/src/templates/source/rsc/src/api/handler.ts +1 -1
  40. package/src/templates/source/rsc/src/entry.client.tsx +65 -18
  41. package/src/templates/source/rsc-auth/CLAUDE.md +230 -0
  42. package/src/templates/source/rsc-auth/app/actions/auth.ts +112 -0
  43. package/src/templates/source/rsc-auth/app/actions/users.ts +289 -0
  44. package/src/templates/source/rsc-auth/app/layouts/dashboard.tsx +132 -0
  45. package/src/templates/source/rsc-auth/app/layouts/marketing.tsx +59 -0
  46. package/src/templates/source/rsc-auth/app/layouts/minimal-content.tsx +21 -0
  47. package/src/templates/source/rsc-auth/app/layouts/minimal.tsx +111 -0
  48. package/src/templates/source/rsc-auth/app/layouts/root.tsx +355 -0
  49. package/src/templates/source/rsc-auth/app/pages/_not-found.tsx +15 -0
  50. package/src/templates/source/rsc-auth/app/pages/auth/login.tsx +198 -0
  51. package/src/templates/source/rsc-auth/app/pages/auth/register.tsx +225 -0
  52. package/src/templates/source/rsc-auth/app/pages/dashboard/index.tsx +267 -0
  53. package/src/templates/source/rsc-auth/app/pages/index.tsx +83 -0
  54. package/src/templates/source/rsc-auth/app/pages/users.tsx +47 -0
  55. package/src/templates/source/rsc-auth/app.config.ts +12 -0
  56. package/src/templates/source/rsc-auth/docker-compose.yml +21 -0
  57. package/src/templates/source/rsc-auth/env.example +11 -0
  58. package/src/templates/source/rsc-auth/gitignore +34 -0
  59. package/src/templates/source/rsc-auth/package.json +44 -0
  60. package/src/templates/source/rsc-auth/prisma/schema.prisma +23 -0
  61. package/src/templates/source/rsc-auth/prisma.config.ts +22 -0
  62. package/src/templates/source/rsc-auth/public/favicon.svg +4 -0
  63. package/src/templates/source/rsc-auth/src/api/database.ts +129 -0
  64. package/src/templates/source/rsc-auth/src/api/handler.ts +85 -0
  65. package/src/templates/source/rsc-auth/src/api/procedures/auth.ts +262 -0
  66. package/src/templates/source/rsc-auth/src/api/procedures/health.ts +48 -0
  67. package/src/templates/source/rsc-auth/src/api/procedures/users.ts +87 -0
  68. package/src/templates/source/rsc-auth/src/api/schemas/auth.ts +79 -0
  69. package/src/templates/source/rsc-auth/src/api/schemas/user.ts +38 -0
  70. package/src/templates/source/rsc-auth/src/api/utils/auth.ts +157 -0
  71. package/src/templates/source/rsc-auth/src/entry.client.tsx +63 -0
  72. package/src/templates/source/rsc-auth/src/entry.server.tsx +262 -0
  73. package/src/templates/source/rsc-auth/tsconfig.json +24 -0
  74. package/src/templates/source/shared/scripts/check-client-imports.sh +75 -0
@@ -0,0 +1,230 @@
1
+ # CLAUDE.md
2
+
3
+ This is a VeloxTS full-stack application using React Server Components with JWT Authentication.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ __PROJECT_NAME__/
9
+ ├── app/ # Application layer (RSC)
10
+ │ ├── pages/ # File-based routing (RSC pages)
11
+ │ │ ├── index.tsx # Home page (/)
12
+ │ │ ├── users.tsx # Users page (/users)
13
+ │ │ ├── auth/ # Auth pages
14
+ │ │ │ ├── login.tsx # Login page
15
+ │ │ │ └── register.tsx # Registration page
16
+ │ │ └── dashboard/ # Protected pages
17
+ │ │ └── index.tsx # Dashboard (requires auth)
18
+ │ ├── layouts/ # Layout components
19
+ │ │ └── root.tsx # Root layout
20
+ │ └── actions/ # Server actions
21
+ │ ├── users.ts # User actions with validated()
22
+ │ └── auth.ts # Auth actions
23
+ ├── src/ # Source layer
24
+ │ ├── api/ # API layer (Fastify embedded in Vinxi)
25
+ │ │ ├── handler.ts # API handler with auth plugin
26
+ │ │ ├── database.ts # Prisma client
27
+ │ │ ├── procedures/ # API procedure definitions
28
+ │ │ │ ├── auth.ts # Authentication procedures
29
+ │ │ │ ├── health.ts # Health check
30
+ │ │ │ └── users.ts # User CRUD
31
+ │ │ ├── schemas/ # Zod validation schemas
32
+ │ │ └── utils/ # Utilities
33
+ │ │ └── auth.ts # JWT helpers, token store
34
+ │ ├── entry.client.tsx # Client hydration
35
+ │ └── entry.server.tsx # Server rendering
36
+ ├── prisma/
37
+ │ └── schema.prisma # Database schema (with password field)
38
+ ├── app.config.ts # Vinxi configuration
39
+ └── package.json
40
+ ```
41
+
42
+ ## Authentication
43
+
44
+ ### JWT-Based Authentication with httpOnly Cookies
45
+ - Access tokens (15 min expiry) stored in httpOnly cookies
46
+ - Refresh tokens (7 day expiry) stored in httpOnly cookies
47
+ - Secure password hashing (bcrypt)
48
+ - Rate limiting on auth endpoints
49
+ - Token revocation support
50
+ - Tokens are NOT accessible to JavaScript (XSS protection)
51
+
52
+ ### Auth Architecture
53
+
54
+ This template uses the **Procedure Bridge Pattern** for authentication:
55
+
56
+ 1. **Server Actions** (`app/actions/auth.ts`) call auth procedures directly
57
+ 2. **Tokens are stored in httpOnly cookies** by server actions (not localStorage)
58
+ 3. **API requests use cookies** via `credentials: 'include'`
59
+
60
+ ```typescript
61
+ // app/actions/auth.ts - Procedure Bridge Pattern
62
+ 'use server';
63
+ import { authAction } from '@veloxts/web/server';
64
+ import { authProcedures } from '@/api/procedures/auth';
65
+ import { db } from '@/api/database';
66
+
67
+ // Login: executes procedure directly, stores tokens in httpOnly cookies
68
+ export const login = authAction.fromTokenProcedure(
69
+ authProcedures.procedures.createSession,
70
+ { parseFormData: true, contextExtensions: { db }, skipGuards: true }
71
+ );
72
+
73
+ // Register: same pattern
74
+ export const register = authAction.fromTokenProcedure(
75
+ authProcedures.procedures.createAccount,
76
+ { parseFormData: true, contextExtensions: { db }, skipGuards: true }
77
+ );
78
+
79
+ // Logout: clears auth cookies
80
+ export const logout = authAction.fromLogoutProcedure(
81
+ authProcedures.procedures.deleteSession,
82
+ { contextExtensions: { db }, skipGuards: true }
83
+ );
84
+ ```
85
+
86
+ ### Why Procedure Bridge vs HTTP Fetch?
87
+
88
+ | Aspect | Procedure Bridge | HTTP Fetch |
89
+ |--------|-----------------|------------|
90
+ | Network | Direct in-process | HTTP round-trip |
91
+ | Type Safety | Full inference | Lost at boundary |
92
+ | Cookie Access | Yes (server action) | No (client-side) |
93
+ | Guards/Middleware | Reused | Bypassed |
94
+ | URL Hardcoding | None | Required |
95
+
96
+ ### Auth Endpoints (REST API)
97
+ ```
98
+ POST /api/auth/register - Create new account
99
+ POST /api/auth/login - Authenticate, get tokens
100
+ POST /api/auth/refresh - Refresh access token
101
+ POST /api/auth/logout - Revoke current token
102
+ GET /api/auth/me - Get current user (protected)
103
+ ```
104
+
105
+ ### Client-Side Auth
106
+
107
+ ```typescript
108
+ // Dashboard - uses cookies automatically
109
+ const response = await fetch('/api/auth/me', {
110
+ credentials: 'include', // Sends httpOnly cookies
111
+ });
112
+
113
+ // Logout - uses server action
114
+ import { logout } from '@/app/actions/auth';
115
+ await logout(); // Clears cookies server-side
116
+ ```
117
+
118
+ ### Password Requirements
119
+ - Minimum 12 characters
120
+ - At least one uppercase letter
121
+ - At least one lowercase letter
122
+ - At least one number
123
+ - Not a common password
124
+
125
+ ## Server Actions with validated()
126
+
127
+ Use the `validated()` helper for secure server actions:
128
+
129
+ ```typescript
130
+ // app/actions/users.ts
131
+ 'use server';
132
+ import { validated, validatedMutation, validatedQuery } from '@veloxts/web/server';
133
+ import { z } from 'zod';
134
+
135
+ // Public query (no auth required)
136
+ export const searchUsers = validatedQuery(
137
+ z.object({ query: z.string().optional() }),
138
+ async (input) => {
139
+ return db.user.findMany({ where: { name: { contains: input.query } } });
140
+ }
141
+ );
142
+
143
+ // Authenticated mutation (ctx.user available)
144
+ export const updateProfile = validatedMutation(
145
+ z.object({ name: z.string() }),
146
+ async (input, ctx) => {
147
+ return db.user.update({ where: { id: ctx.user.id }, data: input });
148
+ }
149
+ );
150
+
151
+ // Custom security options with rate limiting
152
+ export const createUser = validated(
153
+ CreateUserSchema,
154
+ async (input) => { /* ... */ },
155
+ {
156
+ rateLimit: { maxRequests: 10, windowMs: 60_000 },
157
+ maxInputSize: 10 * 1024,
158
+ }
159
+ );
160
+
161
+ // Role-based authorization
162
+ export const adminDeleteUser = validated(
163
+ DeleteUserSchema,
164
+ async (input) => { /* ... */ },
165
+ {
166
+ requireAuth: true,
167
+ requireRoles: ['admin'],
168
+ }
169
+ );
170
+ ```
171
+
172
+ ### Security Features
173
+ - **Input validation** - Zod schema validation
174
+ - **Input sanitization** - Prototype pollution prevention
175
+ - **Input size limits** - DoS protection (default 1MB)
176
+ - **Rate limiting** - Sliding window per IP
177
+ - **Authentication** - Optional via `requireAuth: true`
178
+ - **Authorization** - Role-based via `requireRoles`
179
+
180
+ ## Commands
181
+
182
+ ```bash
183
+ # Development
184
+ __RUN_CMD__ dev # Start dev server
185
+
186
+ # Database
187
+ __RUN_CMD__ db:generate # Generate Prisma client
188
+ __RUN_CMD__ db:push # Push schema to database
189
+ __RUN_CMD__ db:migrate # Run migrations
190
+ __RUN_CMD__ db:studio # Open Prisma Studio
191
+
192
+ # Production
193
+ __RUN_CMD__ build # Build for production
194
+ __RUN_CMD__ start # Start production server
195
+ ```
196
+
197
+ ## Development
198
+
199
+ 1. Run `__RUN_CMD__ db:push` to set up the database
200
+ 2. Run `__RUN_CMD__ dev` to start the development server
201
+ 3. Open http://localhost:__API_PORT__ in your browser
202
+ 4. Register an account at /auth/register
203
+ 5. Access protected pages at /dashboard
204
+
205
+ ## Environment Variables
206
+
207
+ Required for production:
208
+ ```bash
209
+ # Generate with: openssl rand -base64 64
210
+ JWT_SECRET="..."
211
+ JWT_REFRESH_SECRET="..."
212
+ ```
213
+
214
+ ## API Endpoints
215
+
216
+ ### Public
217
+ - `GET /api/health` - Health check
218
+ - `GET /api/users` - List users
219
+
220
+ ### Authentication
221
+ - `POST /api/auth/register` - Create account (rate limited)
222
+ - `POST /api/auth/login` - Get tokens (rate limited)
223
+ - `POST /api/auth/refresh` - Refresh token
224
+ - `POST /api/auth/logout` - Logout (protected)
225
+ - `GET /api/auth/me` - Current user (protected)
226
+
227
+ ### Protected (require Bearer token)
228
+ - `POST /api/users` - Create user
229
+ - `PUT /api/users/:id` - Update user
230
+ - `DELETE /api/users/:id` - Delete user
@@ -0,0 +1,112 @@
1
+ 'use server';
2
+
3
+ /**
4
+ * Authentication Server Actions - Procedure Bridge Pattern
5
+ *
6
+ * Uses authAction helpers to execute auth procedures directly,
7
+ * storing tokens in httpOnly cookies for security.
8
+ *
9
+ * This is the recommended pattern for authentication in VeloxTS RSC apps:
10
+ * - Tokens are stored in httpOnly cookies (not accessible to JavaScript)
11
+ * - The procedure's validation, guards, and business logic are reused
12
+ * - No HTTP round-trip overhead (direct in-process execution)
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * // In a client component
17
+ * const result = await login({ email: 'user@example.com', password: '...' });
18
+ *
19
+ * if (result.success) {
20
+ * // Tokens are stored in cookies automatically
21
+ * redirect('/dashboard');
22
+ * } else {
23
+ * setError(result.error.message);
24
+ * }
25
+ * ```
26
+ */
27
+
28
+ import { authAction, validated } from '@veloxts/web/server';
29
+ import { z } from 'zod';
30
+
31
+ import { db } from '@/api/database';
32
+ import { authProcedures } from '@/api/procedures/auth';
33
+
34
+ // ============================================================================
35
+ // Auth Actions (Procedure Bridge Pattern)
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Login action - validates credentials, stores tokens in httpOnly cookies
40
+ *
41
+ * Uses the procedure bridge pattern to:
42
+ * 1. Execute the createSession procedure directly (no HTTP)
43
+ * 2. Store tokens in httpOnly cookies via onSuccess callback
44
+ * 3. Return a sanitized response (tokens stripped for security)
45
+ *
46
+ * Rate limited via the procedure's middleware (5 attempts per 15 minutes).
47
+ */
48
+ export const login = authAction.fromTokenProcedure(authProcedures.procedures.createSession, {
49
+ parseFormData: true,
50
+ contextExtensions: { db },
51
+ skipGuards: true, // Login has no guards, only rate limit middleware
52
+ });
53
+
54
+ /**
55
+ * Register action - creates new account, stores tokens in httpOnly cookies
56
+ *
57
+ * Uses the procedure bridge pattern to:
58
+ * 1. Execute the createAccount procedure directly
59
+ * 2. Store tokens in httpOnly cookies
60
+ * 3. Return sanitized response
61
+ *
62
+ * Rate limited via the procedure's middleware (3 attempts per hour).
63
+ */
64
+ export const register = authAction.fromTokenProcedure(authProcedures.procedures.createAccount, {
65
+ parseFormData: true,
66
+ contextExtensions: { db },
67
+ skipGuards: true, // Register has no guards, only rate limit middleware
68
+ });
69
+
70
+ /**
71
+ * Logout action - clears auth cookies
72
+ *
73
+ * For logout, we clear cookies client-side since the deleteSession procedure
74
+ * requires authenticated context which is complex to set up in server actions.
75
+ * The token will naturally expire.
76
+ *
77
+ * For production apps needing token revocation, call the API endpoint directly
78
+ * from the client or implement a custom logout procedure without guards.
79
+ */
80
+ export const logout = authAction.fromLogoutProcedure(authProcedures.procedures.deleteSession, {
81
+ contextExtensions: { db },
82
+ skipGuards: true, // Skip auth guard - we'll clear cookies regardless
83
+ });
84
+
85
+ // ============================================================================
86
+ // Standalone Actions (No Procedure Required)
87
+ // ============================================================================
88
+
89
+ /**
90
+ * Check if email is available for registration
91
+ *
92
+ * This is a simple database lookup that doesn't need procedure bridge.
93
+ * Rate limited to prevent email enumeration attacks.
94
+ */
95
+ export const checkEmailAvailable = validated(
96
+ z.object({ email: z.string().email() }),
97
+ async (input) => {
98
+ const existing = await db.user.findUnique({
99
+ where: { email: input.email.toLowerCase().trim() },
100
+ select: { id: true },
101
+ });
102
+
103
+ // Always return the same timing to prevent enumeration
104
+ return { available: !existing };
105
+ },
106
+ {
107
+ rateLimit: {
108
+ maxRequests: 10,
109
+ windowMs: 60_000, // 10 checks per minute
110
+ },
111
+ }
112
+ );
@@ -0,0 +1,289 @@
1
+ 'use server';
2
+
3
+ /**
4
+ * User Server Actions with Authentication
5
+ *
6
+ * Type-safe server actions with built-in security using VeloxTS validated() helper.
7
+ * These demonstrate authenticated actions with role-based authorization.
8
+ *
9
+ * Security features included:
10
+ * - Input validation via Zod schemas
11
+ * - Input size limits (DoS protection)
12
+ * - Input sanitization (prototype pollution prevention)
13
+ * - Rate limiting (sliding window)
14
+ * - Authentication checks
15
+ * - Role-based authorization
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * // In a client component
20
+ * const result = await updateProfile({ name: 'John' });
21
+ *
22
+ * if (result.success) {
23
+ * console.log('Updated:', result.data);
24
+ * } else {
25
+ * if (result.error.code === 'AUTHENTICATION_REQUIRED') {
26
+ * redirect('/auth/login');
27
+ * }
28
+ * console.log('Error:', result.error.message);
29
+ * }
30
+ * ```
31
+ */
32
+
33
+ import { validated, validatedMutation, validatedQuery } from '@veloxts/web/server';
34
+ import { z } from 'zod';
35
+
36
+ import { db } from '@/api/database';
37
+
38
+ // ============================================================================
39
+ // Schemas
40
+ // ============================================================================
41
+
42
+ /**
43
+ * Schema for updating own profile (authenticated users)
44
+ */
45
+ const UpdateProfileSchema = z.object({
46
+ name: z.string().min(1, 'Name is required').max(100, 'Name is too long').optional(),
47
+ email: z.string().email('Invalid email address').optional(),
48
+ });
49
+
50
+ /**
51
+ * Schema for admin user creation
52
+ */
53
+ const AdminCreateUserSchema = z.object({
54
+ name: z.string().min(1, 'Name is required').max(100, 'Name is too long'),
55
+ email: z.string().email('Invalid email address'),
56
+ roles: z.array(z.string()).default(['user']),
57
+ });
58
+
59
+ /**
60
+ * Schema for admin user deletion
61
+ */
62
+ const AdminDeleteUserSchema = z.object({
63
+ id: z.string().min(1, 'User ID is required'),
64
+ });
65
+
66
+ /**
67
+ * Schema for searching users
68
+ */
69
+ const SearchUsersSchema = z.object({
70
+ query: z.string().max(100).optional(),
71
+ limit: z.number().min(1).max(100).default(10),
72
+ });
73
+
74
+ // ============================================================================
75
+ // Public Actions (no authentication required)
76
+ // ============================================================================
77
+
78
+ /**
79
+ * Search users - public query action
80
+ *
81
+ * Uses validatedQuery() which allows unauthenticated access by default.
82
+ * Only returns public user information (no sensitive fields).
83
+ */
84
+ export const searchUsers = validatedQuery(SearchUsersSchema, async (input) => {
85
+ const users = await db.user.findMany({
86
+ where: input.query
87
+ ? {
88
+ OR: [{ name: { contains: input.query } }, { email: { contains: input.query } }],
89
+ }
90
+ : undefined,
91
+ take: input.limit,
92
+ orderBy: { createdAt: 'desc' },
93
+ select: {
94
+ id: true,
95
+ name: true,
96
+ email: true,
97
+ createdAt: true,
98
+ // Exclude password, roles, etc.
99
+ },
100
+ });
101
+
102
+ return users;
103
+ });
104
+
105
+ // ============================================================================
106
+ // Authenticated Actions (require login)
107
+ // ============================================================================
108
+
109
+ /**
110
+ * Get current user's profile
111
+ *
112
+ * Uses validatedMutation() which requires authentication by default.
113
+ * Returns the authenticated user's full profile.
114
+ */
115
+ export const getProfile = validatedMutation(
116
+ z.object({}), // No input needed
117
+ async (_input, ctx) => {
118
+ // ctx.user is available because validatedMutation requires auth
119
+ const user = await db.user.findUnique({
120
+ where: { id: ctx.user.id },
121
+ select: {
122
+ id: true,
123
+ name: true,
124
+ email: true,
125
+ roles: true,
126
+ createdAt: true,
127
+ updatedAt: true,
128
+ },
129
+ });
130
+
131
+ if (!user) {
132
+ throw new Error('User not found');
133
+ }
134
+
135
+ return user;
136
+ }
137
+ );
138
+
139
+ /**
140
+ * Update current user's profile
141
+ *
142
+ * Users can only update their own profile.
143
+ * Includes rate limiting to prevent abuse.
144
+ */
145
+ export const updateProfile = validated(
146
+ UpdateProfileSchema,
147
+ async (input, ctx) => {
148
+ const user = await db.user.update({
149
+ where: { id: ctx.user.id },
150
+ data: {
151
+ ...(input.name && { name: input.name.trim() }),
152
+ ...(input.email && { email: input.email.toLowerCase().trim() }),
153
+ },
154
+ select: {
155
+ id: true,
156
+ name: true,
157
+ email: true,
158
+ updatedAt: true,
159
+ },
160
+ });
161
+
162
+ return user;
163
+ },
164
+ {
165
+ requireAuth: true,
166
+ rateLimit: {
167
+ maxRequests: 10,
168
+ windowMs: 60_000, // 10 updates per minute
169
+ },
170
+ }
171
+ );
172
+
173
+ // ============================================================================
174
+ // Admin Actions (require admin role)
175
+ // ============================================================================
176
+
177
+ /**
178
+ * Admin: Create a new user
179
+ *
180
+ * Only administrators can create users directly.
181
+ * Regular users must go through the registration flow.
182
+ */
183
+ export const adminCreateUser = validated(
184
+ AdminCreateUserSchema,
185
+ async (input) => {
186
+ const user = await db.user.create({
187
+ data: {
188
+ name: input.name.trim(),
189
+ email: input.email.toLowerCase().trim(),
190
+ roles: JSON.stringify(input.roles),
191
+ // Note: No password - admin-created users need to set password via reset flow
192
+ },
193
+ select: {
194
+ id: true,
195
+ name: true,
196
+ email: true,
197
+ roles: true,
198
+ createdAt: true,
199
+ },
200
+ });
201
+
202
+ return user;
203
+ },
204
+ {
205
+ requireAuth: true,
206
+ requireRoles: ['admin'],
207
+ rateLimit: {
208
+ maxRequests: 20,
209
+ windowMs: 60_000,
210
+ },
211
+ }
212
+ );
213
+
214
+ /**
215
+ * Admin: Delete a user
216
+ *
217
+ * Only administrators can delete users.
218
+ * Cannot delete own account (safety measure).
219
+ */
220
+ export const adminDeleteUser = validated(
221
+ AdminDeleteUserSchema,
222
+ async (input, ctx) => {
223
+ // Prevent self-deletion
224
+ if (input.id === ctx.user.id) {
225
+ throw new Error('Cannot delete your own account');
226
+ }
227
+
228
+ await db.user.delete({
229
+ where: { id: input.id },
230
+ });
231
+
232
+ return { deleted: true, id: input.id };
233
+ },
234
+ {
235
+ requireAuth: true,
236
+ requireRoles: ['admin'],
237
+ rateLimit: {
238
+ maxRequests: 10,
239
+ windowMs: 60_000,
240
+ },
241
+ }
242
+ );
243
+
244
+ /**
245
+ * Admin: List all users with full details
246
+ *
247
+ * Returns all user data including roles.
248
+ * Only accessible to administrators.
249
+ */
250
+ export const adminListUsers = validated(
251
+ z.object({
252
+ page: z.number().min(1).default(1),
253
+ limit: z.number().min(1).max(100).default(20),
254
+ }),
255
+ async (input) => {
256
+ const skip = (input.page - 1) * input.limit;
257
+
258
+ const [users, total] = await Promise.all([
259
+ db.user.findMany({
260
+ skip,
261
+ take: input.limit,
262
+ orderBy: { createdAt: 'desc' },
263
+ select: {
264
+ id: true,
265
+ name: true,
266
+ email: true,
267
+ roles: true,
268
+ createdAt: true,
269
+ updatedAt: true,
270
+ },
271
+ }),
272
+ db.user.count(),
273
+ ]);
274
+
275
+ return {
276
+ users,
277
+ pagination: {
278
+ page: input.page,
279
+ limit: input.limit,
280
+ total,
281
+ pages: Math.ceil(total / input.limit),
282
+ },
283
+ };
284
+ },
285
+ {
286
+ requireAuth: true,
287
+ requireRoles: ['admin'],
288
+ }
289
+ );