create-velox-app 0.4.3 → 0.4.5

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 (174) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/index.js +21 -14
  3. package/dist/index.js.map +1 -1
  4. package/dist/templates/auth.d.ts +6 -3
  5. package/dist/templates/auth.d.ts.map +1 -1
  6. package/dist/templates/auth.js +56 -1112
  7. package/dist/templates/auth.js.map +1 -1
  8. package/dist/templates/compiler.d.ts +68 -0
  9. package/dist/templates/compiler.d.ts.map +1 -0
  10. package/dist/templates/compiler.js +149 -0
  11. package/dist/templates/compiler.js.map +1 -0
  12. package/dist/templates/default.d.ts +5 -2
  13. package/dist/templates/default.d.ts.map +1 -1
  14. package/dist/templates/default.js +51 -509
  15. package/dist/templates/default.js.map +1 -1
  16. package/dist/templates/index.d.ts.map +1 -1
  17. package/dist/templates/index.js +19 -10
  18. package/dist/templates/index.js.map +1 -1
  19. package/dist/templates/placeholders.d.ts +70 -0
  20. package/dist/templates/placeholders.d.ts.map +1 -0
  21. package/dist/templates/placeholders.js +145 -0
  22. package/dist/templates/placeholders.js.map +1 -0
  23. package/dist/templates/shared/index.d.ts +9 -0
  24. package/dist/templates/shared/index.d.ts.map +1 -0
  25. package/dist/templates/shared/index.js +9 -0
  26. package/dist/templates/shared/index.js.map +1 -0
  27. package/dist/templates/shared/root.d.ts +14 -0
  28. package/dist/templates/shared/root.d.ts.map +1 -0
  29. package/dist/templates/shared/root.js +43 -0
  30. package/dist/templates/shared/root.js.map +1 -0
  31. package/dist/templates/shared/web-base.d.ts +18 -0
  32. package/dist/templates/shared/web-base.d.ts.map +1 -0
  33. package/dist/templates/shared/web-base.js +63 -0
  34. package/dist/templates/shared/web-base.js.map +1 -0
  35. package/dist/templates/shared/web-styles.d.ts +10 -0
  36. package/dist/templates/shared/web-styles.d.ts.map +1 -0
  37. package/dist/templates/shared/web-styles.js +26 -0
  38. package/dist/templates/shared/web-styles.js.map +1 -0
  39. package/dist/templates/shared.d.ts +2 -13
  40. package/dist/templates/shared.d.ts.map +1 -1
  41. package/dist/templates/shared.js +2 -289
  42. package/dist/templates/shared.js.map +1 -1
  43. package/dist/templates/source/api/config/app.d.ts +13 -0
  44. package/dist/templates/source/api/config/app.d.ts.map +1 -0
  45. package/dist/templates/source/api/config/app.js +14 -0
  46. package/dist/templates/source/api/config/app.js.map +1 -0
  47. package/dist/templates/source/api/config/auth.d.ts +34 -0
  48. package/dist/templates/source/api/config/auth.d.ts.map +1 -0
  49. package/dist/templates/source/api/config/auth.js +165 -0
  50. package/dist/templates/source/api/config/auth.js.map +1 -0
  51. package/dist/templates/source/api/config/index.auth.d.ts +6 -0
  52. package/dist/templates/source/api/config/index.auth.d.ts.map +1 -0
  53. package/dist/templates/source/api/config/index.auth.js +6 -0
  54. package/dist/templates/source/api/config/index.auth.js.map +1 -0
  55. package/dist/templates/source/api/config/index.default.d.ts +5 -0
  56. package/dist/templates/source/api/config/index.default.d.ts.map +1 -0
  57. package/dist/templates/source/api/config/index.default.js +5 -0
  58. package/dist/templates/source/api/config/index.default.js.map +1 -0
  59. package/dist/templates/source/api/database/index.d.ts +9 -0
  60. package/dist/templates/source/api/database/index.d.ts.map +1 -0
  61. package/dist/templates/source/api/database/index.js +18 -0
  62. package/dist/templates/source/api/database/index.js.map +1 -0
  63. package/dist/templates/source/api/index.auth.d.ts +5 -0
  64. package/dist/templates/source/api/index.auth.d.ts.map +1 -0
  65. package/dist/templates/source/api/index.auth.js +59 -0
  66. package/dist/templates/source/api/index.auth.js.map +1 -0
  67. package/dist/templates/source/api/index.default.d.ts +5 -0
  68. package/dist/templates/source/api/index.default.d.ts.map +1 -0
  69. package/dist/templates/source/api/index.default.js +56 -0
  70. package/dist/templates/source/api/index.default.js.map +1 -0
  71. package/dist/templates/source/api/prisma.config.d.ts +9 -0
  72. package/dist/templates/source/api/prisma.config.d.ts.map +1 -0
  73. package/dist/templates/source/api/prisma.config.js +15 -0
  74. package/dist/templates/source/api/prisma.config.js.map +1 -0
  75. package/dist/templates/source/api/procedures/auth.d.ts +14 -0
  76. package/dist/templates/source/api/procedures/auth.d.ts.map +1 -0
  77. package/dist/templates/source/api/procedures/auth.js +221 -0
  78. package/dist/templates/source/api/procedures/auth.js.map +1 -0
  79. package/dist/templates/source/api/procedures/health.d.ts +5 -0
  80. package/dist/templates/source/api/procedures/health.d.ts.map +1 -0
  81. package/dist/templates/source/api/procedures/health.js +21 -0
  82. package/dist/templates/source/api/procedures/health.js.map +1 -0
  83. package/dist/templates/source/api/procedures/index.auth.d.ts +7 -0
  84. package/dist/templates/source/api/procedures/index.auth.d.ts.map +1 -0
  85. package/dist/templates/source/api/procedures/index.auth.js +7 -0
  86. package/dist/templates/source/api/procedures/index.auth.js.map +1 -0
  87. package/dist/templates/source/api/procedures/index.default.d.ts +6 -0
  88. package/dist/templates/source/api/procedures/index.default.d.ts.map +1 -0
  89. package/dist/templates/source/api/procedures/index.default.js +6 -0
  90. package/dist/templates/source/api/procedures/index.default.js.map +1 -0
  91. package/dist/templates/source/api/procedures/users.auth.d.ts +7 -0
  92. package/dist/templates/source/api/procedures/users.auth.d.ts.map +1 -0
  93. package/dist/templates/source/api/procedures/users.auth.js +111 -0
  94. package/dist/templates/source/api/procedures/users.auth.js.map +1 -0
  95. package/dist/templates/source/api/procedures/users.default.d.ts +5 -0
  96. package/dist/templates/source/api/procedures/users.default.d.ts.map +1 -0
  97. package/dist/templates/source/api/procedures/users.default.js +86 -0
  98. package/dist/templates/source/api/procedures/users.default.js.map +1 -0
  99. package/dist/templates/source/api/schemas/index.d.ts +5 -0
  100. package/dist/templates/source/api/schemas/index.d.ts.map +1 -0
  101. package/dist/templates/source/api/schemas/index.js +5 -0
  102. package/dist/templates/source/api/schemas/index.js.map +1 -0
  103. package/dist/templates/source/api/schemas/user.d.ts +11 -0
  104. package/dist/templates/source/api/schemas/user.d.ts.map +1 -0
  105. package/dist/templates/source/api/schemas/user.js +20 -0
  106. package/dist/templates/source/api/schemas/user.js.map +1 -0
  107. package/dist/templates/source/api/tsup.config.d.ts +3 -0
  108. package/dist/templates/source/api/tsup.config.d.ts.map +1 -0
  109. package/dist/templates/source/api/tsup.config.js +10 -0
  110. package/dist/templates/source/api/tsup.config.js.map +1 -0
  111. package/dist/templates/source/web/main.d.ts +9 -0
  112. package/dist/templates/source/web/main.d.ts.map +1 -0
  113. package/dist/templates/source/web/main.js +27 -0
  114. package/dist/templates/source/web/main.js.map +1 -0
  115. package/dist/templates/source/web/routes/__root.d.ts +2 -0
  116. package/dist/templates/source/web/routes/__root.d.ts.map +1 -0
  117. package/dist/templates/source/web/routes/__root.js +28 -0
  118. package/dist/templates/source/web/routes/__root.js.map +1 -0
  119. package/dist/templates/source/web/routes/about.d.ts +2 -0
  120. package/dist/templates/source/web/routes/about.d.ts.map +1 -0
  121. package/dist/templates/source/web/routes/about.js +33 -0
  122. package/dist/templates/source/web/routes/about.js.map +1 -0
  123. package/dist/templates/source/web/routes/index.auth.d.ts +2 -0
  124. package/dist/templates/source/web/routes/index.auth.d.ts.map +1 -0
  125. package/dist/templates/source/web/routes/index.auth.js +159 -0
  126. package/dist/templates/source/web/routes/index.auth.js.map +1 -0
  127. package/dist/templates/source/web/routes/index.default.d.ts +2 -0
  128. package/dist/templates/source/web/routes/index.default.d.ts.map +1 -0
  129. package/dist/templates/source/web/routes/index.default.js +60 -0
  130. package/dist/templates/source/web/routes/index.default.js.map +1 -0
  131. package/dist/templates/source/web/vite.config.d.ts +3 -0
  132. package/dist/templates/source/web/vite.config.d.ts.map +1 -0
  133. package/dist/templates/source/web/vite.config.js +22 -0
  134. package/dist/templates/source/web/vite.config.js.map +1 -0
  135. package/package.json +11 -9
  136. package/src/templates/source/api/config/app.ts +13 -0
  137. package/src/templates/source/api/config/auth.ts +202 -0
  138. package/src/templates/source/api/config/database.ts +22 -0
  139. package/src/templates/source/api/env.auth +22 -0
  140. package/src/templates/source/api/env.default +13 -0
  141. package/src/templates/source/api/index.auth.ts +30 -0
  142. package/src/templates/source/api/index.default.ts +27 -0
  143. package/src/templates/source/api/package.auth.json +40 -0
  144. package/src/templates/source/api/package.default.json +38 -0
  145. package/src/templates/source/api/prisma/schema.auth.prisma +30 -0
  146. package/src/templates/source/api/prisma/schema.default.prisma +28 -0
  147. package/src/templates/source/api/prisma.config.ts +15 -0
  148. package/src/templates/source/api/procedures/auth.ts +285 -0
  149. package/src/templates/source/api/procedures/health.ts +24 -0
  150. package/src/templates/source/api/procedures/users.auth.ts +170 -0
  151. package/src/templates/source/api/procedures/users.default.ts +119 -0
  152. package/src/templates/source/api/schemas/user.ts +29 -0
  153. package/src/templates/source/api/tsconfig.json +12 -0
  154. package/src/templates/source/api/tsup.config.ts +10 -0
  155. package/src/templates/source/root/CLAUDE.auth.md +148 -0
  156. package/src/templates/source/root/CLAUDE.default.md +128 -0
  157. package/src/templates/source/root/README.md +72 -0
  158. package/src/templates/source/root/gitignore +37 -0
  159. package/src/templates/source/root/package.json +17 -0
  160. package/src/templates/source/root/pnpm-workspace.yaml +2 -0
  161. package/src/templates/source/root/tsconfig.json +19 -0
  162. package/src/templates/source/web/App.module.css +282 -0
  163. package/src/templates/source/web/favicon.svg +12 -0
  164. package/src/templates/source/web/index.html +13 -0
  165. package/src/templates/source/web/main.tsx +38 -0
  166. package/src/templates/source/web/package.json +26 -0
  167. package/src/templates/source/web/routes/__root.tsx +31 -0
  168. package/src/templates/source/web/routes/about.tsx +36 -0
  169. package/src/templates/source/web/routes/index.auth.tsx +230 -0
  170. package/src/templates/source/web/routes/index.default.tsx +79 -0
  171. package/src/templates/source/web/styles/global.css +90 -0
  172. package/src/templates/source/web/tsconfig.json +24 -0
  173. package/src/templates/source/web/vite.config.ts +22 -0
  174. package/LICENSE +0 -21
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Auth Procedures
3
+ *
4
+ * Authentication procedures for user registration, login, and token management.
5
+ *
6
+ * REST Endpoints:
7
+ * - POST /auth/register - Create new account
8
+ * - POST /auth/login - Authenticate and get tokens
9
+ * - POST /auth/refresh - Refresh access token
10
+ * - POST /auth/logout - Revoke current token
11
+ * - GET /auth/me - Get current user (protected)
12
+ */
13
+
14
+ import {
15
+ AuthError,
16
+ authenticated,
17
+ createAuthRateLimiter,
18
+ hashPassword,
19
+ jwtManager,
20
+ verifyPassword,
21
+ defineProcedures,
22
+ procedure,
23
+ z,
24
+ } from '@veloxts/velox';
25
+
26
+ import { authConfig, parseUserRoles, tokenStore } from '../config/auth.js';
27
+ import { prisma } from '../config/database.js';
28
+
29
+ // ============================================================================
30
+ // Rate Limiter
31
+ // ============================================================================
32
+
33
+ const rateLimiter = createAuthRateLimiter({
34
+ login: {
35
+ maxAttempts: 5,
36
+ windowMs: 15 * 60 * 1000,
37
+ lockoutDurationMs: 15 * 60 * 1000,
38
+ progressiveBackoff: true,
39
+ },
40
+ register: {
41
+ maxAttempts: 3,
42
+ windowMs: 60 * 60 * 1000,
43
+ lockoutDurationMs: 60 * 60 * 1000,
44
+ },
45
+ refresh: {
46
+ maxAttempts: 10,
47
+ windowMs: 60 * 1000,
48
+ lockoutDurationMs: 60 * 1000,
49
+ },
50
+ });
51
+
52
+ // ============================================================================
53
+ // Password Blacklist
54
+ // ============================================================================
55
+
56
+ const COMMON_PASSWORDS = new Set([
57
+ 'password', 'password123', '12345678', '123456789',
58
+ 'qwerty123', 'letmein', 'welcome', 'admin123',
59
+ ]);
60
+
61
+ // ============================================================================
62
+ // Schemas
63
+ // ============================================================================
64
+
65
+ const PasswordSchema = z
66
+ .string()
67
+ .min(12, 'Password must be at least 12 characters')
68
+ .max(128, 'Password must not exceed 128 characters')
69
+ .refine((pwd) => /[a-z]/.test(pwd), 'Password must contain at least one lowercase letter')
70
+ .refine((pwd) => /[A-Z]/.test(pwd), 'Password must contain at least one uppercase letter')
71
+ .refine((pwd) => /[0-9]/.test(pwd), 'Password must contain at least one number')
72
+ .refine(
73
+ (pwd) => !COMMON_PASSWORDS.has(pwd.toLowerCase()),
74
+ 'Password is too common. Please choose a stronger password.'
75
+ );
76
+
77
+ const EmailSchema = z
78
+ .string()
79
+ .email('Invalid email address')
80
+ .transform((email) => email.toLowerCase().trim());
81
+
82
+ const RegisterInput = z.object({
83
+ name: z.string().min(2).max(100).trim(),
84
+ email: EmailSchema,
85
+ password: PasswordSchema,
86
+ });
87
+
88
+ const LoginInput = z.object({
89
+ email: EmailSchema,
90
+ password: z.string().min(1),
91
+ });
92
+
93
+ const RefreshInput = z.object({
94
+ refreshToken: z.string(),
95
+ });
96
+
97
+ const TokenResponse = z.object({
98
+ accessToken: z.string(),
99
+ refreshToken: z.string(),
100
+ expiresIn: z.number(),
101
+ tokenType: z.literal('Bearer'),
102
+ });
103
+
104
+ const UserResponse = z.object({
105
+ id: z.string(),
106
+ name: z.string(),
107
+ email: z.string(),
108
+ roles: z.array(z.string()),
109
+ });
110
+
111
+ const LogoutResponse = z.object({
112
+ success: z.boolean(),
113
+ message: z.string(),
114
+ });
115
+
116
+ // ============================================================================
117
+ // JWT Manager
118
+ // ============================================================================
119
+
120
+ const jwt = jwtManager(authConfig.jwt);
121
+
122
+ // Dummy hash for timing attack prevention
123
+ const DUMMY_HASH = '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.uy7dPSSXB5G6Uy';
124
+
125
+ // ============================================================================
126
+ // Auth Procedures
127
+ // ============================================================================
128
+
129
+ export const authProcedures = defineProcedures('auth', {
130
+ register: procedure()
131
+ .rest({ method: 'POST', path: '/auth/register' })
132
+ .use(rateLimiter.register())
133
+ .input(RegisterInput)
134
+ .output(TokenResponse)
135
+ .mutation(async ({ input }) => {
136
+ const normalizedEmail = input.email.toLowerCase().trim();
137
+
138
+ const existing = await prisma.user.findUnique({
139
+ where: { email: normalizedEmail },
140
+ });
141
+
142
+ if (existing) {
143
+ throw new AuthError(
144
+ 'Registration failed. If this email is not already registered, please try again.',
145
+ 400,
146
+ 'REGISTRATION_FAILED'
147
+ );
148
+ }
149
+
150
+ const hashedPassword = await hashPassword(input.password);
151
+
152
+ const user = await prisma.user.create({
153
+ data: {
154
+ name: input.name.trim(),
155
+ email: normalizedEmail,
156
+ password: hashedPassword,
157
+ roles: JSON.stringify(['user']),
158
+ },
159
+ });
160
+
161
+ return jwt.createTokenPair({
162
+ id: user.id,
163
+ email: user.email,
164
+ roles: ['user'],
165
+ });
166
+ }),
167
+
168
+ login: procedure()
169
+ .rest({ method: 'POST', path: '/auth/login' })
170
+ .use(
171
+ rateLimiter.login((ctx) => {
172
+ const input = ctx.input as { email?: string } | undefined;
173
+ return input?.email?.toLowerCase() ?? '';
174
+ })
175
+ )
176
+ .input(LoginInput)
177
+ .output(TokenResponse)
178
+ .mutation(async ({ input }) => {
179
+ const normalizedEmail = input.email.toLowerCase().trim();
180
+
181
+ const user = await prisma.user.findUnique({
182
+ where: { email: normalizedEmail },
183
+ });
184
+
185
+ const hashToVerify = user?.password || DUMMY_HASH;
186
+ const isValid = await verifyPassword(input.password, hashToVerify);
187
+
188
+ if (!user || !user.password || !isValid) {
189
+ throw new AuthError('Invalid email or password', 401, 'INVALID_CREDENTIALS');
190
+ }
191
+
192
+ const roles = parseUserRoles(user.roles);
193
+
194
+ return jwt.createTokenPair({
195
+ id: user.id,
196
+ email: user.email,
197
+ roles,
198
+ });
199
+ }),
200
+
201
+ refresh: procedure()
202
+ .rest({ method: 'POST', path: '/auth/refresh' })
203
+ .use(rateLimiter.refresh())
204
+ .input(RefreshInput)
205
+ .output(TokenResponse)
206
+ .mutation(async ({ input }) => {
207
+ try {
208
+ const payload = jwt.verifyToken(input.refreshToken);
209
+
210
+ if (payload.type !== 'refresh') {
211
+ throw new AuthError('Invalid token type', 401, 'INVALID_TOKEN_TYPE');
212
+ }
213
+
214
+ if (payload.jti && tokenStore.isRevoked(payload.jti)) {
215
+ throw new AuthError('Token has been revoked', 401, 'TOKEN_REVOKED');
216
+ }
217
+
218
+ if (payload.jti) {
219
+ const previousUserId = tokenStore.isRefreshTokenUsed(payload.jti);
220
+ if (previousUserId) {
221
+ tokenStore.revokeAllUserTokens(previousUserId);
222
+ throw new AuthError(
223
+ 'Security alert: Refresh token reuse detected.',
224
+ 401,
225
+ 'TOKEN_REUSE_DETECTED'
226
+ );
227
+ }
228
+ tokenStore.markRefreshTokenUsed(payload.jti, payload.sub);
229
+ }
230
+
231
+ const user = await prisma.user.findUnique({
232
+ where: { id: payload.sub },
233
+ });
234
+
235
+ if (!user) {
236
+ throw new AuthError('User not found', 401, 'USER_NOT_FOUND');
237
+ }
238
+
239
+ return jwt.createTokenPair({
240
+ id: user.id,
241
+ email: user.email,
242
+ roles: parseUserRoles(user.roles),
243
+ });
244
+ } catch (error) {
245
+ if (error instanceof AuthError) throw error;
246
+ throw new AuthError('Invalid refresh token', 401, 'INVALID_REFRESH_TOKEN');
247
+ }
248
+ }),
249
+
250
+ logout: procedure()
251
+ .rest({ method: 'POST', path: '/auth/logout' })
252
+ .guard(authenticated)
253
+ .output(LogoutResponse)
254
+ .mutation(async ({ ctx }) => {
255
+ const tokenId = ctx.auth?.token?.jti;
256
+
257
+ if (tokenId) {
258
+ tokenStore.revoke(tokenId, 15 * 60 * 1000);
259
+ }
260
+
261
+ return {
262
+ success: true,
263
+ message: 'Successfully logged out',
264
+ };
265
+ }),
266
+
267
+ getMe: procedure()
268
+ .rest({ method: 'GET', path: '/auth/me' })
269
+ .guard(authenticated)
270
+ .output(UserResponse)
271
+ .query(async ({ ctx }) => {
272
+ const user = ctx.user;
273
+
274
+ if (!user) {
275
+ throw new AuthError('Not authenticated', 401, 'NOT_AUTHENTICATED');
276
+ }
277
+
278
+ return {
279
+ id: user.id,
280
+ name: (user.name as string) || '',
281
+ email: user.email,
282
+ roles: Array.isArray(user.roles) ? user.roles : ['user'],
283
+ };
284
+ }),
285
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Health Check Procedures
3
+ */
4
+
5
+ import { VELOX_VERSION, defineProcedures, procedure, z } from '@veloxts/velox';
6
+
7
+ export const healthProcedures = defineProcedures('health', {
8
+ getHealth: procedure()
9
+ .rest({ method: 'GET', path: '/health' })
10
+ .output(
11
+ z.object({
12
+ status: z.literal('ok'),
13
+ version: z.string(),
14
+ timestamp: z.string().datetime(),
15
+ uptime: z.number(),
16
+ })
17
+ )
18
+ .query(async () => ({
19
+ status: 'ok' as const,
20
+ version: VELOX_VERSION,
21
+ timestamp: new Date().toISOString(),
22
+ uptime: process.uptime(),
23
+ })),
24
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * User Procedures
3
+ *
4
+ * CRUD procedures for user management with authentication guards.
5
+ */
6
+
7
+ import {
8
+ AuthError,
9
+ authenticated,
10
+ hasRole,
11
+ defineProcedures,
12
+ GuardError,
13
+ procedure,
14
+ paginationInputSchema,
15
+ z,
16
+ } from '@veloxts/velox';
17
+
18
+ import {
19
+ CreateUserInput,
20
+ UpdateUserInput,
21
+ type User,
22
+ UserSchema,
23
+ } from '../schemas/user.js';
24
+
25
+ // ============================================================================
26
+ // Database Types
27
+ // ============================================================================
28
+
29
+ interface DbUser {
30
+ id: string;
31
+ name: string;
32
+ email: string;
33
+ createdAt: Date;
34
+ updatedAt: Date;
35
+ }
36
+
37
+ interface DbClient {
38
+ user: {
39
+ findUnique: (args: { where: { id: string } }) => Promise<DbUser | null>;
40
+ findMany: (args?: { skip?: number; take?: number }) => Promise<DbUser[]>;
41
+ create: (args: { data: { name: string; email: string } }) => Promise<DbUser>;
42
+ update: (args: { where: { id: string }; data: { name?: string; email?: string } }) => Promise<DbUser>;
43
+ delete: (args: { where: { id: string } }) => Promise<DbUser>;
44
+ count: () => Promise<number>;
45
+ };
46
+ }
47
+
48
+ function getDb(ctx: { db: unknown }): DbClient {
49
+ return ctx.db as DbClient;
50
+ }
51
+
52
+ function toUserResponse(dbUser: DbUser): User {
53
+ return {
54
+ id: dbUser.id,
55
+ name: dbUser.name,
56
+ email: dbUser.email,
57
+ createdAt: dbUser.createdAt instanceof Date ? dbUser.createdAt.toISOString() : dbUser.createdAt,
58
+ updatedAt: dbUser.updatedAt instanceof Date ? dbUser.updatedAt.toISOString() : dbUser.updatedAt,
59
+ };
60
+ }
61
+
62
+ // ============================================================================
63
+ // User Procedures
64
+ // ============================================================================
65
+
66
+ export const userProcedures = defineProcedures('users', {
67
+ getUser: procedure()
68
+ .input(z.object({ id: z.string().uuid() }))
69
+ .output(UserSchema.nullable())
70
+ .query(async ({ input, ctx }) => {
71
+ const db = getDb(ctx);
72
+ const user = await db.user.findUnique({ where: { id: input.id } });
73
+ return user ? toUserResponse(user) : null;
74
+ }),
75
+
76
+ listUsers: procedure()
77
+ .input(paginationInputSchema.optional())
78
+ .output(
79
+ z.object({
80
+ data: z.array(UserSchema),
81
+ meta: z.object({
82
+ page: z.number(),
83
+ limit: z.number(),
84
+ total: z.number(),
85
+ }),
86
+ })
87
+ )
88
+ .query(async ({ input, ctx }) => {
89
+ const db = getDb(ctx);
90
+ const page = input?.page ?? 1;
91
+ const limit = input?.limit ?? 10;
92
+ const skip = (page - 1) * limit;
93
+
94
+ const [dbUsers, total] = await Promise.all([
95
+ db.user.findMany({ skip, take: limit }),
96
+ db.user.count(),
97
+ ]);
98
+
99
+ return {
100
+ data: dbUsers.map(toUserResponse),
101
+ meta: { page, limit, total },
102
+ };
103
+ }),
104
+
105
+ createUser: procedure()
106
+ .guard(authenticated)
107
+ .input(CreateUserInput)
108
+ .output(UserSchema)
109
+ .mutation(async ({ input, ctx }) => {
110
+ const db = getDb(ctx);
111
+ const user = await db.user.create({ data: input });
112
+ return toUserResponse(user);
113
+ }),
114
+
115
+ updateUser: procedure()
116
+ .guard(authenticated)
117
+ .input(z.object({ id: z.string().uuid() }).merge(UpdateUserInput))
118
+ .output(UserSchema)
119
+ .mutation(async ({ input, ctx }) => {
120
+ const db = getDb(ctx);
121
+ const { id, ...data } = input;
122
+
123
+ if (!ctx.user) {
124
+ throw new AuthError('Authentication required', 401, 'NOT_AUTHENTICATED');
125
+ }
126
+
127
+ const isOwner = ctx.user.id === id;
128
+ const isAdmin = Array.isArray(ctx.user.roles) && ctx.user.roles.includes('admin');
129
+
130
+ if (!isOwner && !isAdmin) {
131
+ throw new GuardError('ownership', 'You can only update your own profile', 403);
132
+ }
133
+
134
+ const updated = await db.user.update({ where: { id }, data });
135
+ return toUserResponse(updated);
136
+ }),
137
+
138
+ patchUser: procedure()
139
+ .guard(authenticated)
140
+ .input(z.object({ id: z.string().uuid() }).merge(UpdateUserInput))
141
+ .output(UserSchema)
142
+ .mutation(async ({ input, ctx }) => {
143
+ const db = getDb(ctx);
144
+ const { id, ...data } = input;
145
+
146
+ if (!ctx.user) {
147
+ throw new AuthError('Authentication required', 401, 'NOT_AUTHENTICATED');
148
+ }
149
+
150
+ const isOwner = ctx.user.id === id;
151
+ const isAdmin = Array.isArray(ctx.user.roles) && ctx.user.roles.includes('admin');
152
+
153
+ if (!isOwner && !isAdmin) {
154
+ throw new GuardError('ownership', 'You can only update your own profile', 403);
155
+ }
156
+
157
+ const updated = await db.user.update({ where: { id }, data });
158
+ return toUserResponse(updated);
159
+ }),
160
+
161
+ deleteUser: procedure()
162
+ .guard(hasRole('admin'))
163
+ .input(z.object({ id: z.string().uuid() }))
164
+ .output(z.object({ success: z.boolean() }))
165
+ .mutation(async ({ input, ctx }) => {
166
+ const db = getDb(ctx);
167
+ await db.user.delete({ where: { id: input.id } });
168
+ return { success: true };
169
+ }),
170
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * User Procedures
3
+ */
4
+
5
+ import { defineProcedures, procedure, paginationInputSchema, z } from '@veloxts/velox';
6
+
7
+ import { CreateUserInput, UpdateUserInput, UserSchema } from '../schemas/user.js';
8
+
9
+ // Database types
10
+ interface DbUser {
11
+ id: string;
12
+ name: string;
13
+ email: string;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ }
17
+
18
+ interface DbClient {
19
+ user: {
20
+ findUnique: (args: { where: { id: string } }) => Promise<DbUser | null>;
21
+ findMany: (args?: { skip?: number; take?: number }) => Promise<DbUser[]>;
22
+ create: (args: { data: { name: string; email: string } }) => Promise<DbUser>;
23
+ update: (args: { where: { id: string }; data: { name?: string; email?: string } }) => Promise<DbUser>;
24
+ delete: (args: { where: { id: string } }) => Promise<DbUser>;
25
+ count: () => Promise<number>;
26
+ };
27
+ }
28
+
29
+ function getDb(ctx: { db: unknown }): DbClient {
30
+ return ctx.db as DbClient;
31
+ }
32
+
33
+ function toUserResponse(dbUser: DbUser) {
34
+ return {
35
+ id: dbUser.id,
36
+ name: dbUser.name,
37
+ email: dbUser.email,
38
+ createdAt: dbUser.createdAt.toISOString(),
39
+ updatedAt: dbUser.updatedAt.toISOString(),
40
+ };
41
+ }
42
+
43
+ export const userProcedures = defineProcedures('users', {
44
+ getUser: procedure()
45
+ .input(z.object({ id: z.string().uuid() }))
46
+ .output(UserSchema.nullable())
47
+ .query(async ({ input, ctx }) => {
48
+ const db = getDb(ctx);
49
+ const user = await db.user.findUnique({ where: { id: input.id } });
50
+ return user ? toUserResponse(user) : null;
51
+ }),
52
+
53
+ listUsers: procedure()
54
+ .input(paginationInputSchema.optional())
55
+ .output(
56
+ z.object({
57
+ data: z.array(UserSchema),
58
+ meta: z.object({
59
+ page: z.number(),
60
+ limit: z.number(),
61
+ total: z.number(),
62
+ }),
63
+ })
64
+ )
65
+ .query(async ({ input, ctx }) => {
66
+ const db = getDb(ctx);
67
+ const page = input?.page ?? 1;
68
+ const limit = input?.limit ?? 10;
69
+ const skip = (page - 1) * limit;
70
+
71
+ const [dbUsers, total] = await Promise.all([
72
+ db.user.findMany({ skip, take: limit }),
73
+ db.user.count(),
74
+ ]);
75
+
76
+ return {
77
+ data: dbUsers.map(toUserResponse),
78
+ meta: { page, limit, total },
79
+ };
80
+ }),
81
+
82
+ createUser: procedure()
83
+ .input(CreateUserInput)
84
+ .output(UserSchema)
85
+ .mutation(async ({ input, ctx }) => {
86
+ const db = getDb(ctx);
87
+ const user = await db.user.create({ data: input });
88
+ return toUserResponse(user);
89
+ }),
90
+
91
+ updateUser: procedure()
92
+ .input(z.object({ id: z.string().uuid() }).merge(UpdateUserInput))
93
+ .output(UserSchema)
94
+ .mutation(async ({ input, ctx }) => {
95
+ const db = getDb(ctx);
96
+ const { id, ...data } = input;
97
+ const user = await db.user.update({ where: { id }, data });
98
+ return toUserResponse(user);
99
+ }),
100
+
101
+ patchUser: procedure()
102
+ .input(z.object({ id: z.string().uuid() }).merge(UpdateUserInput))
103
+ .output(UserSchema)
104
+ .mutation(async ({ input, ctx }) => {
105
+ const db = getDb(ctx);
106
+ const { id, ...data } = input;
107
+ const user = await db.user.update({ where: { id }, data });
108
+ return toUserResponse(user);
109
+ }),
110
+
111
+ deleteUser: procedure()
112
+ .input(z.object({ id: z.string().uuid() }))
113
+ .output(z.object({ success: z.boolean() }))
114
+ .mutation(async ({ input, ctx }) => {
115
+ const db = getDb(ctx);
116
+ await db.user.delete({ where: { id: input.id } });
117
+ return { success: true };
118
+ }),
119
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * User Schemas
3
+ */
4
+
5
+ import { createIdSchema, emailSchema, z } from '@veloxts/velox';
6
+
7
+ export const UserSchema = z.object({
8
+ id: createIdSchema('uuid'),
9
+ name: z.string().min(1).max(100),
10
+ email: emailSchema,
11
+ createdAt: z.coerce.date().transform((d) => d.toISOString()),
12
+ updatedAt: z.coerce.date().transform((d) => d.toISOString()),
13
+ });
14
+
15
+ export type User = z.infer<typeof UserSchema>;
16
+
17
+ export const CreateUserInput = z.object({
18
+ name: z.string().min(1).max(100),
19
+ email: emailSchema,
20
+ });
21
+
22
+ export type CreateUserData = z.infer<typeof CreateUserInput>;
23
+
24
+ export const UpdateUserInput = z.object({
25
+ name: z.string().min(1).max(100).optional(),
26
+ email: emailSchema.optional(),
27
+ });
28
+
29
+ export type UpdateUserData = z.infer<typeof UpdateUserInput>;
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "../../tsconfig.json",
4
+ "compilerOptions": {
5
+ "rootDir": "./src",
6
+ "outDir": "./dist",
7
+ "declaration": false,
8
+ "declarationMap": false
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
12
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ target: 'node18',
7
+ clean: true,
8
+ dts: false,
9
+ sourcemap: true,
10
+ });