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.
- package/dist/cli.js +0 -0
- package/dist/index.js +21 -14
- package/dist/index.js.map +1 -1
- package/dist/templates/auth.d.ts +6 -3
- package/dist/templates/auth.d.ts.map +1 -1
- package/dist/templates/auth.js +56 -1112
- package/dist/templates/auth.js.map +1 -1
- package/dist/templates/compiler.d.ts +68 -0
- package/dist/templates/compiler.d.ts.map +1 -0
- package/dist/templates/compiler.js +149 -0
- package/dist/templates/compiler.js.map +1 -0
- package/dist/templates/default.d.ts +5 -2
- package/dist/templates/default.d.ts.map +1 -1
- package/dist/templates/default.js +51 -509
- package/dist/templates/default.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +19 -10
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/placeholders.d.ts +70 -0
- package/dist/templates/placeholders.d.ts.map +1 -0
- package/dist/templates/placeholders.js +145 -0
- package/dist/templates/placeholders.js.map +1 -0
- package/dist/templates/shared/index.d.ts +9 -0
- package/dist/templates/shared/index.d.ts.map +1 -0
- package/dist/templates/shared/index.js +9 -0
- package/dist/templates/shared/index.js.map +1 -0
- package/dist/templates/shared/root.d.ts +14 -0
- package/dist/templates/shared/root.d.ts.map +1 -0
- package/dist/templates/shared/root.js +43 -0
- package/dist/templates/shared/root.js.map +1 -0
- package/dist/templates/shared/web-base.d.ts +18 -0
- package/dist/templates/shared/web-base.d.ts.map +1 -0
- package/dist/templates/shared/web-base.js +63 -0
- package/dist/templates/shared/web-base.js.map +1 -0
- package/dist/templates/shared/web-styles.d.ts +10 -0
- package/dist/templates/shared/web-styles.d.ts.map +1 -0
- package/dist/templates/shared/web-styles.js +26 -0
- package/dist/templates/shared/web-styles.js.map +1 -0
- package/dist/templates/shared.d.ts +2 -13
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +2 -289
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/source/api/config/app.d.ts +13 -0
- package/dist/templates/source/api/config/app.d.ts.map +1 -0
- package/dist/templates/source/api/config/app.js +14 -0
- package/dist/templates/source/api/config/app.js.map +1 -0
- package/dist/templates/source/api/config/auth.d.ts +34 -0
- package/dist/templates/source/api/config/auth.d.ts.map +1 -0
- package/dist/templates/source/api/config/auth.js +165 -0
- package/dist/templates/source/api/config/auth.js.map +1 -0
- package/dist/templates/source/api/config/index.auth.d.ts +6 -0
- package/dist/templates/source/api/config/index.auth.d.ts.map +1 -0
- package/dist/templates/source/api/config/index.auth.js +6 -0
- package/dist/templates/source/api/config/index.auth.js.map +1 -0
- package/dist/templates/source/api/config/index.default.d.ts +5 -0
- package/dist/templates/source/api/config/index.default.d.ts.map +1 -0
- package/dist/templates/source/api/config/index.default.js +5 -0
- package/dist/templates/source/api/config/index.default.js.map +1 -0
- package/dist/templates/source/api/database/index.d.ts +9 -0
- package/dist/templates/source/api/database/index.d.ts.map +1 -0
- package/dist/templates/source/api/database/index.js +18 -0
- package/dist/templates/source/api/database/index.js.map +1 -0
- package/dist/templates/source/api/index.auth.d.ts +5 -0
- package/dist/templates/source/api/index.auth.d.ts.map +1 -0
- package/dist/templates/source/api/index.auth.js +59 -0
- package/dist/templates/source/api/index.auth.js.map +1 -0
- package/dist/templates/source/api/index.default.d.ts +5 -0
- package/dist/templates/source/api/index.default.d.ts.map +1 -0
- package/dist/templates/source/api/index.default.js +56 -0
- package/dist/templates/source/api/index.default.js.map +1 -0
- package/dist/templates/source/api/prisma.config.d.ts +9 -0
- package/dist/templates/source/api/prisma.config.d.ts.map +1 -0
- package/dist/templates/source/api/prisma.config.js +15 -0
- package/dist/templates/source/api/prisma.config.js.map +1 -0
- package/dist/templates/source/api/procedures/auth.d.ts +14 -0
- package/dist/templates/source/api/procedures/auth.d.ts.map +1 -0
- package/dist/templates/source/api/procedures/auth.js +221 -0
- package/dist/templates/source/api/procedures/auth.js.map +1 -0
- package/dist/templates/source/api/procedures/health.d.ts +5 -0
- package/dist/templates/source/api/procedures/health.d.ts.map +1 -0
- package/dist/templates/source/api/procedures/health.js +21 -0
- package/dist/templates/source/api/procedures/health.js.map +1 -0
- package/dist/templates/source/api/procedures/index.auth.d.ts +7 -0
- package/dist/templates/source/api/procedures/index.auth.d.ts.map +1 -0
- package/dist/templates/source/api/procedures/index.auth.js +7 -0
- package/dist/templates/source/api/procedures/index.auth.js.map +1 -0
- package/dist/templates/source/api/procedures/index.default.d.ts +6 -0
- package/dist/templates/source/api/procedures/index.default.d.ts.map +1 -0
- package/dist/templates/source/api/procedures/index.default.js +6 -0
- package/dist/templates/source/api/procedures/index.default.js.map +1 -0
- package/dist/templates/source/api/procedures/users.auth.d.ts +7 -0
- package/dist/templates/source/api/procedures/users.auth.d.ts.map +1 -0
- package/dist/templates/source/api/procedures/users.auth.js +111 -0
- package/dist/templates/source/api/procedures/users.auth.js.map +1 -0
- package/dist/templates/source/api/procedures/users.default.d.ts +5 -0
- package/dist/templates/source/api/procedures/users.default.d.ts.map +1 -0
- package/dist/templates/source/api/procedures/users.default.js +86 -0
- package/dist/templates/source/api/procedures/users.default.js.map +1 -0
- package/dist/templates/source/api/schemas/index.d.ts +5 -0
- package/dist/templates/source/api/schemas/index.d.ts.map +1 -0
- package/dist/templates/source/api/schemas/index.js +5 -0
- package/dist/templates/source/api/schemas/index.js.map +1 -0
- package/dist/templates/source/api/schemas/user.d.ts +11 -0
- package/dist/templates/source/api/schemas/user.d.ts.map +1 -0
- package/dist/templates/source/api/schemas/user.js +20 -0
- package/dist/templates/source/api/schemas/user.js.map +1 -0
- package/dist/templates/source/api/tsup.config.d.ts +3 -0
- package/dist/templates/source/api/tsup.config.d.ts.map +1 -0
- package/dist/templates/source/api/tsup.config.js +10 -0
- package/dist/templates/source/api/tsup.config.js.map +1 -0
- package/dist/templates/source/web/main.d.ts +9 -0
- package/dist/templates/source/web/main.d.ts.map +1 -0
- package/dist/templates/source/web/main.js +27 -0
- package/dist/templates/source/web/main.js.map +1 -0
- package/dist/templates/source/web/routes/__root.d.ts +2 -0
- package/dist/templates/source/web/routes/__root.d.ts.map +1 -0
- package/dist/templates/source/web/routes/__root.js +28 -0
- package/dist/templates/source/web/routes/__root.js.map +1 -0
- package/dist/templates/source/web/routes/about.d.ts +2 -0
- package/dist/templates/source/web/routes/about.d.ts.map +1 -0
- package/dist/templates/source/web/routes/about.js +33 -0
- package/dist/templates/source/web/routes/about.js.map +1 -0
- package/dist/templates/source/web/routes/index.auth.d.ts +2 -0
- package/dist/templates/source/web/routes/index.auth.d.ts.map +1 -0
- package/dist/templates/source/web/routes/index.auth.js +159 -0
- package/dist/templates/source/web/routes/index.auth.js.map +1 -0
- package/dist/templates/source/web/routes/index.default.d.ts +2 -0
- package/dist/templates/source/web/routes/index.default.d.ts.map +1 -0
- package/dist/templates/source/web/routes/index.default.js +60 -0
- package/dist/templates/source/web/routes/index.default.js.map +1 -0
- package/dist/templates/source/web/vite.config.d.ts +3 -0
- package/dist/templates/source/web/vite.config.d.ts.map +1 -0
- package/dist/templates/source/web/vite.config.js +22 -0
- package/dist/templates/source/web/vite.config.js.map +1 -0
- package/package.json +11 -9
- package/src/templates/source/api/config/app.ts +13 -0
- package/src/templates/source/api/config/auth.ts +202 -0
- package/src/templates/source/api/config/database.ts +22 -0
- package/src/templates/source/api/env.auth +22 -0
- package/src/templates/source/api/env.default +13 -0
- package/src/templates/source/api/index.auth.ts +30 -0
- package/src/templates/source/api/index.default.ts +27 -0
- package/src/templates/source/api/package.auth.json +40 -0
- package/src/templates/source/api/package.default.json +38 -0
- package/src/templates/source/api/prisma/schema.auth.prisma +30 -0
- package/src/templates/source/api/prisma/schema.default.prisma +28 -0
- package/src/templates/source/api/prisma.config.ts +15 -0
- package/src/templates/source/api/procedures/auth.ts +285 -0
- package/src/templates/source/api/procedures/health.ts +24 -0
- package/src/templates/source/api/procedures/users.auth.ts +170 -0
- package/src/templates/source/api/procedures/users.default.ts +119 -0
- package/src/templates/source/api/schemas/user.ts +29 -0
- package/src/templates/source/api/tsconfig.json +12 -0
- package/src/templates/source/api/tsup.config.ts +10 -0
- package/src/templates/source/root/CLAUDE.auth.md +148 -0
- package/src/templates/source/root/CLAUDE.default.md +128 -0
- package/src/templates/source/root/README.md +72 -0
- package/src/templates/source/root/gitignore +37 -0
- package/src/templates/source/root/package.json +17 -0
- package/src/templates/source/root/pnpm-workspace.yaml +2 -0
- package/src/templates/source/root/tsconfig.json +19 -0
- package/src/templates/source/web/App.module.css +282 -0
- package/src/templates/source/web/favicon.svg +12 -0
- package/src/templates/source/web/index.html +13 -0
- package/src/templates/source/web/main.tsx +38 -0
- package/src/templates/source/web/package.json +26 -0
- package/src/templates/source/web/routes/__root.tsx +31 -0
- package/src/templates/source/web/routes/about.tsx +36 -0
- package/src/templates/source/web/routes/index.auth.tsx +230 -0
- package/src/templates/source/web/routes/index.default.tsx +79 -0
- package/src/templates/source/web/styles/global.css +90 -0
- package/src/templates/source/web/tsconfig.json +24 -0
- package/src/templates/source/web/vite.config.ts +22 -0
- 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
|
+
}
|