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
@@ -3,9 +3,17 @@
3
3
  /**
4
4
  * User Server Actions
5
5
  *
6
- * Type-safe server actions for user operations using the VeloxTS action() helper.
6
+ * Type-safe server actions with built-in security using VeloxTS validated() helper.
7
7
  * These can be called directly from client components with full type inference.
8
8
  *
9
+ * The validated() helper provides:
10
+ * - Input validation via Zod schemas
11
+ * - Input size limits (DoS protection)
12
+ * - Input sanitization (prototype pollution prevention)
13
+ * - Rate limiting (optional)
14
+ * - Authentication checks (optional)
15
+ * - Authorization via custom callbacks (optional)
16
+ *
9
17
  * @example
10
18
  * ```tsx
11
19
  * // In a client component
@@ -19,7 +27,7 @@
19
27
  * ```
20
28
  */
21
29
 
22
- import { action } from '@veloxts/web';
30
+ import { validated, validatedMutation, validatedQuery } from '@veloxts/web/server';
23
31
  import { z } from 'zod';
24
32
 
25
33
  import { db } from '@/api/database';
@@ -36,6 +44,15 @@ const CreateUserSchema = z.object({
36
44
  email: z.string().email('Invalid email address'),
37
45
  });
38
46
 
47
+ /**
48
+ * Schema for updating a user
49
+ */
50
+ const UpdateUserSchema = z.object({
51
+ id: z.string().min(1, 'User ID is required'),
52
+ name: z.string().min(1).max(100).optional(),
53
+ email: z.string().email().optional(),
54
+ });
55
+
39
56
  /**
40
57
  * Schema for deleting a user
41
58
  */
@@ -43,24 +60,90 @@ const DeleteUserSchema = z.object({
43
60
  id: z.string().min(1, 'User ID is required'),
44
61
  });
45
62
 
63
+ /**
64
+ * Schema for searching users
65
+ */
66
+ const SearchUsersSchema = z.object({
67
+ query: z.string().max(100).optional(),
68
+ limit: z.number().min(1).max(100).default(10),
69
+ });
70
+
71
+ // ============================================================================
72
+ // Public Actions (no authentication required)
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Search users - public query action
77
+ *
78
+ * Uses validatedQuery() which allows unauthenticated access by default.
79
+ * Includes input sanitization and validation.
80
+ */
81
+ export const searchUsers = validatedQuery(SearchUsersSchema, async (input) => {
82
+ const users = await db.user.findMany({
83
+ where: input.query
84
+ ? {
85
+ OR: [{ name: { contains: input.query } }, { email: { contains: input.query } }],
86
+ }
87
+ : undefined,
88
+ take: input.limit,
89
+ orderBy: { createdAt: 'desc' },
90
+ });
91
+
92
+ return users;
93
+ });
94
+
46
95
  // ============================================================================
47
- // Actions
96
+ // Mutations (with security options)
48
97
  // ============================================================================
49
98
 
50
99
  /**
51
100
  * Creates a new user.
52
101
  *
53
- * Input is automatically validated against CreateUserSchema.
54
- * Returns a discriminated union for type-safe error handling.
102
+ * Uses validated() with explicit options:
103
+ * - Rate limiting: max 10 requests per minute
104
+ * - Input size limit: 10KB (prevent large payload DoS)
105
+ * - Input sanitization: enabled by default
106
+ */
107
+ export const createUser = validated(
108
+ CreateUserSchema,
109
+ async (input) => {
110
+ const user = await db.user.create({
111
+ data: {
112
+ name: input.name.trim(),
113
+ email: input.email.toLowerCase().trim(),
114
+ },
115
+ });
116
+
117
+ return user;
118
+ },
119
+ {
120
+ // Rate limit: 10 requests per minute per IP
121
+ rateLimit: {
122
+ maxRequests: 10,
123
+ windowMs: 60_000,
124
+ },
125
+ // Limit input size to 10KB
126
+ maxInputSize: 10 * 1024,
127
+ }
128
+ );
129
+
130
+ /**
131
+ * Updates a user.
132
+ *
133
+ * Uses validatedMutation() which requires authentication by default.
134
+ * When requireAuth is true, ctx.user is available and typed.
135
+ *
136
+ * Note: In a real app, add authorization to verify the user owns this record.
55
137
  */
56
- export const createUser = action(CreateUserSchema, async (input) => {
57
- // Input is fully typed as { name: string; email: string }
58
- // Validation has already been performed by the action() helper
138
+ export const updateUser = validatedMutation(UpdateUserSchema, async (input, ctx) => {
139
+ // ctx.user is available because validatedMutation requires auth
140
+ console.log('User updating record:', ctx.user.id);
59
141
 
60
- const user = await db.user.create({
142
+ const user = await db.user.update({
143
+ where: { id: input.id },
61
144
  data: {
62
- name: input.name.trim(),
63
- email: input.email.toLowerCase().trim(),
145
+ ...(input.name && { name: input.name.trim() }),
146
+ ...(input.email && { email: input.email.toLowerCase().trim() }),
64
147
  },
65
148
  });
66
149
 
@@ -70,14 +153,22 @@ export const createUser = action(CreateUserSchema, async (input) => {
70
153
  /**
71
154
  * Deletes a user by ID.
72
155
  *
73
- * Input is automatically validated to ensure a valid ID is provided.
156
+ * Uses validated() with role-based authorization.
157
+ * Only admins can delete users.
74
158
  */
75
- export const deleteUser = action(DeleteUserSchema, async (input) => {
76
- // Input is fully typed as { id: string }
159
+ export const deleteUser = validated(
160
+ DeleteUserSchema,
161
+ async (input) => {
162
+ await db.user.delete({
163
+ where: { id: input.id },
164
+ });
77
165
 
78
- await db.user.delete({
79
- where: { id: input.id },
80
- });
81
-
82
- return { deleted: true };
83
- });
166
+ return { deleted: true };
167
+ },
168
+ {
169
+ // Require authentication
170
+ requireAuth: true,
171
+ // Role-based authorization (requires 'admin' role)
172
+ requireRoles: ['admin'],
173
+ }
174
+ );
@@ -24,12 +24,12 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
24
24
  }
25
25
 
26
26
  .dashboard-sidebar {
27
- width: 200px;
27
+ width: 240px;
28
28
  flex-shrink: 0;
29
- background: white;
29
+ background: #111;
30
30
  border-radius: 8px;
31
- padding: 1rem;
32
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
31
+ padding: 1.5rem;
32
+ border: 1px solid #222;
33
33
  height: fit-content;
34
34
  }
35
35
 
@@ -37,8 +37,10 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
37
37
  font-size: 0.75rem;
38
38
  text-transform: uppercase;
39
39
  color: #888;
40
- margin-bottom: 0.5rem;
41
- padding: 0 0.5rem;
40
+ margin-bottom: 0.75rem;
41
+ padding: 0 0.75rem;
42
+ font-weight: 600;
43
+ letter-spacing: 0.05em;
42
44
  }
43
45
 
44
46
  .sidebar-nav {
@@ -47,39 +49,42 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
47
49
 
48
50
  .sidebar-nav a {
49
51
  display: block;
50
- padding: 0.5rem;
51
- color: #1a1a2e;
52
+ padding: 0.75rem;
53
+ color: #ededed;
52
54
  text-decoration: none;
53
55
  border-radius: 4px;
54
56
  transition: background 0.2s;
55
57
  }
56
58
 
57
59
  .sidebar-nav a:hover {
58
- background: #f0f0f0;
60
+ background: #1a1a1a;
61
+ color: #00d9ff;
59
62
  }
60
63
 
61
64
  .sidebar-nav a.active {
62
- background: #6366f1;
63
- color: white;
65
+ background: #00d9ff;
66
+ color: #000;
64
67
  }
65
68
 
66
69
  .dashboard-content {
67
70
  flex: 1;
68
- background: white;
71
+ background: #111;
69
72
  border-radius: 8px;
70
73
  padding: 1.5rem;
71
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
74
+ border: 1px solid #222;
72
75
  }
73
76
 
74
77
  .dashboard-badge {
75
78
  display: inline-block;
76
- background: #6366f1;
77
- color: white;
79
+ background: #00d9ff;
80
+ color: #000;
78
81
  font-size: 0.625rem;
79
82
  text-transform: uppercase;
80
- padding: 0.25rem 0.5rem;
83
+ padding: 0.5rem 0.75rem;
81
84
  border-radius: 4px;
82
85
  margin-bottom: 1rem;
86
+ font-weight: 600;
87
+ letter-spacing: 0.05em;
83
88
  }
84
89
  `}</style>
85
90
 
@@ -15,6 +15,40 @@ interface MarketingLayoutProps {
15
15
  export default function MarketingLayout({ children }: MarketingLayoutProps) {
16
16
  return (
17
17
  <div className="marketing-layout">
18
+ <style>{`
19
+ .marketing-layout {
20
+ position: relative;
21
+ }
22
+
23
+ .marketing-banner {
24
+ background: #111;
25
+ border: 1px solid #222;
26
+ border-radius: 8px;
27
+ padding: 1rem 1.5rem;
28
+ margin-bottom: 1.5rem;
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 1rem;
32
+ }
33
+
34
+ .marketing-banner .badge {
35
+ display: inline-block;
36
+ background: #00d9ff;
37
+ color: #000;
38
+ font-size: 0.625rem;
39
+ text-transform: uppercase;
40
+ padding: 0.5rem 0.75rem;
41
+ border-radius: 4px;
42
+ font-weight: 600;
43
+ letter-spacing: 0.05em;
44
+ }
45
+
46
+ .marketing-banner span:not(.badge) {
47
+ color: #888;
48
+ font-size: 0.875rem;
49
+ }
50
+ `}</style>
51
+
18
52
  <div className="marketing-banner">
19
53
  <span className="badge">Marketing</span>
20
54
  <span>This page uses the marketing layout</span>
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Minimal Content Layout
3
+ *
4
+ * The content portion of the minimal layout, separate from the HTML document shell.
5
+ * This component is shared between server and client for proper hydration.
6
+ *
7
+ * Server renders: <div id="root"><MinimalContent><Page /></MinimalContent></div>
8
+ * Client hydrates: <MinimalContent><Page /></MinimalContent>
9
+ *
10
+ * This ensures the React tree matches on both sides, enabling proper hydration.
11
+ */
12
+
13
+ import type { ReactNode } from 'react';
14
+
15
+ interface MinimalContentProps {
16
+ children: ReactNode;
17
+ }
18
+
19
+ export default function MinimalContent({ children }: MinimalContentProps) {
20
+ return <div className="minimal-content">{children}</div>;
21
+ }
@@ -1,12 +1,23 @@
1
1
  /**
2
- * Minimal Layout
2
+ * Minimal Layout (Document Shell)
3
3
  *
4
4
  * A stripped-down layout for pages that need minimal chrome,
5
5
  * such as print-friendly pages, embedded widgets, or auth pages.
6
+ *
7
+ * This component provides the HTML document shell (server-only).
8
+ * The actual content wrapper is in MinimalContent (shared with client).
9
+ *
10
+ * Architecture for proper hydration:
11
+ * - Server renders: <MinimalLayout><Page /></MinimalLayout>
12
+ * Which produces: <html>...<div id="root"><MinimalContent><Page /></></div>...</html>
13
+ * - Client hydrates #root with: <MinimalContent><Page /></MinimalContent>
14
+ * This ensures the React tree matches exactly.
6
15
  */
7
16
 
8
17
  import type { ReactNode } from 'react';
9
18
 
19
+ import MinimalContent from './minimal-content.tsx';
20
+
10
21
  interface MinimalLayoutProps {
11
22
  children: ReactNode;
12
23
  params?: Record<string, string>;
@@ -19,11 +30,81 @@ export default function MinimalLayout({ children }: MinimalLayoutProps) {
19
30
  <meta charSet="utf-8" />
20
31
  <meta name="viewport" content="width=device-width, initial-scale=1" />
21
32
  <title>VeloxTS</title>
22
- <link rel="stylesheet" href="/_build/styles.css" />
33
+ <style>{`
34
+ /* Global Reset & Dark Mode Base */
35
+ *,
36
+ *::before,
37
+ *::after {
38
+ box-sizing: border-box;
39
+ margin: 0;
40
+ padding: 0;
41
+ font: inherit;
42
+ }
43
+
44
+ html {
45
+ font-size: 16px;
46
+ -webkit-font-smoothing: antialiased;
47
+ -moz-osx-font-smoothing: grayscale;
48
+ }
49
+
50
+ body {
51
+ background: #0a0a0a;
52
+ color: #ededed;
53
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
54
+ line-height: 1.6;
55
+ min-height: 100svh;
56
+ }
57
+
58
+ h1, h2, h3, h4, h5, h6 {
59
+ text-wrap: balance;
60
+ }
61
+
62
+ p, li, figcaption {
63
+ text-wrap: pretty;
64
+ }
65
+
66
+ a {
67
+ color: #00d9ff;
68
+ text-decoration: none;
69
+ transition: opacity 0.2s;
70
+ }
71
+
72
+ a:hover {
73
+ opacity: 0.8;
74
+ }
75
+
76
+ code {
77
+ font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Monaco, "Courier New", monospace;
78
+ background: #1a1a1a;
79
+ padding: 0.2em 0.4em;
80
+ border-radius: 4px;
81
+ font-size: 0.9em;
82
+ }
83
+
84
+ ::selection {
85
+ background: #00d9ff;
86
+ color: #000;
87
+ }
88
+
89
+ .minimal-body {
90
+ min-height: 100vh;
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ padding: 2rem;
95
+ }
96
+
97
+ .minimal-content {
98
+ width: 100%;
99
+ max-width: 450px;
100
+ }
101
+ `}</style>
23
102
  </head>
24
- <body className="minimal-layout">
25
- {children}
26
- <script src="/_build/entry.client.js" type="module" />
103
+ <body className="minimal-body">
104
+ <div id="root">
105
+ <MinimalContent>{children}</MinimalContent>
106
+ </div>
107
+ <script src="/_build/src/entry.client.tsx" type="module" />
27
108
  </body>
28
109
  </html>
29
110
  );