better-ts-stack 1.1.3 → 2.0.0

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 (162) hide show
  1. package/README.md +113 -82
  2. package/dist/args.d.ts +12 -0
  3. package/dist/args.d.ts.map +1 -0
  4. package/dist/args.js +90 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/builder/configGenerator.d.ts +1 -0
  7. package/dist/builder/configGenerator.d.ts.map +1 -1
  8. package/dist/builder/configGenerator.js +46 -6
  9. package/dist/builder/configGenerator.js.map +1 -1
  10. package/dist/builder/dependencyInstaller.d.ts.map +1 -1
  11. package/dist/builder/dependencyInstaller.js +8 -14
  12. package/dist/builder/dependencyInstaller.js.map +1 -1
  13. package/dist/builder/gitInitializer.d.ts.map +1 -1
  14. package/dist/builder/gitInitializer.js +8 -41
  15. package/dist/builder/gitInitializer.js.map +1 -1
  16. package/dist/builder/index.d.ts.map +1 -1
  17. package/dist/builder/index.js +27 -63
  18. package/dist/builder/index.js.map +1 -1
  19. package/dist/builder/moduleSelector.d.ts +1 -4
  20. package/dist/builder/moduleSelector.d.ts.map +1 -1
  21. package/dist/builder/moduleSelector.js +11 -15
  22. package/dist/builder/moduleSelector.js.map +1 -1
  23. package/dist/builder/templateContext.d.ts +2 -17
  24. package/dist/builder/templateContext.d.ts.map +1 -1
  25. package/dist/builder/templateContext.js +6 -5
  26. package/dist/builder/templateContext.js.map +1 -1
  27. package/dist/index.js +45 -20
  28. package/dist/index.js.map +1 -1
  29. package/dist/output/nextSteps.d.ts +1 -2
  30. package/dist/output/nextSteps.d.ts.map +1 -1
  31. package/dist/output/nextSteps.js +10 -19
  32. package/dist/output/nextSteps.js.map +1 -1
  33. package/dist/prompts/index.d.ts +3 -3
  34. package/dist/prompts/index.d.ts.map +1 -1
  35. package/dist/prompts/index.js +22 -58
  36. package/dist/prompts/index.js.map +1 -1
  37. package/dist/prompts/stack.d.ts +3 -2
  38. package/dist/prompts/stack.d.ts.map +1 -1
  39. package/dist/prompts/stack.js +81 -70
  40. package/dist/prompts/stack.js.map +1 -1
  41. package/dist/types/index.d.ts +20 -42
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/dist/types/index.js +40 -38
  44. package/dist/types/index.js.map +1 -1
  45. package/dist/ui/index.d.ts +3 -0
  46. package/dist/ui/index.d.ts.map +1 -0
  47. package/dist/ui/index.js +19 -0
  48. package/dist/ui/index.js.map +1 -0
  49. package/dist/ui/messages.d.ts +9 -0
  50. package/dist/ui/messages.d.ts.map +1 -0
  51. package/dist/ui/messages.js +100 -0
  52. package/dist/ui/messages.js.map +1 -0
  53. package/dist/ui/prompts.d.ts +20 -0
  54. package/dist/ui/prompts.d.ts.map +1 -0
  55. package/dist/ui/prompts.js +71 -0
  56. package/dist/ui/prompts.js.map +1 -0
  57. package/dist/ui/theme.d.ts +9 -0
  58. package/dist/ui/theme.d.ts.map +1 -0
  59. package/dist/ui/theme.js +24 -0
  60. package/dist/ui/theme.js.map +1 -0
  61. package/dist/validators/index.d.ts +1 -0
  62. package/dist/validators/index.d.ts.map +1 -1
  63. package/dist/validators/index.js +6 -0
  64. package/dist/validators/index.js.map +1 -1
  65. package/package.json +70 -66
  66. package/templates/backend/express/config.json +44 -44
  67. package/templates/backend/express/eslint.config.mjs +40 -40
  68. package/templates/backend/express/gitignore +36 -36
  69. package/templates/backend/express/src/index.ts +76 -76
  70. package/templates/backend/express/src/routes/health.ts +12 -12
  71. package/templates/backend/express/tsconfig.json +25 -25
  72. package/templates/backend/nestjs/.prettierignore +52 -0
  73. package/templates/backend/nestjs/.prettierrc +16 -0
  74. package/templates/backend/nestjs/config.json +44 -0
  75. package/templates/backend/nestjs/eslint.config.mjs +40 -0
  76. package/templates/backend/nestjs/gitignore +36 -0
  77. package/templates/backend/nestjs/nest-cli.json +9 -0
  78. package/templates/backend/nestjs/src/app.module.ts.hbs +34 -0
  79. package/templates/backend/nestjs/src/health/health.controller.ts +12 -0
  80. package/templates/backend/nestjs/src/main.ts +23 -0
  81. package/templates/backend/nestjs/tsconfig.eslint.json +9 -0
  82. package/templates/backend/nestjs/tsconfig.json +25 -0
  83. package/templates/frontend/nextjs/app/layout.tsx +13 -13
  84. package/templates/frontend/nextjs/app/page.tsx.hbs +97 -97
  85. package/templates/frontend/nextjs/components/ui/button.tsx +51 -51
  86. package/templates/frontend/nextjs/components/ui/card.tsx +60 -60
  87. package/templates/frontend/nextjs/components/ui/field.tsx +67 -67
  88. package/templates/frontend/nextjs/components/ui/input.tsx +18 -18
  89. package/templates/frontend/nextjs/components.json +19 -19
  90. package/templates/frontend/nextjs/config.json +38 -38
  91. package/templates/frontend/nextjs/eslint.config.mjs +11 -11
  92. package/templates/frontend/nextjs/gitignore +42 -42
  93. package/templates/frontend/nextjs/lib/utils.ts +6 -6
  94. package/templates/frontend/nextjs/next.config.ts.hbs +12 -12
  95. package/templates/frontend/nextjs/proxy.ts.hbs +24 -24
  96. package/templates/frontend/nextjs/tsconfig.json +34 -34
  97. package/templates/frontend/tanstack/config.json +48 -48
  98. package/templates/frontend/tanstack/src/routeTree.gen.ts +68 -68
  99. package/templates/frontend/tanstack/src/styles/app.css +85 -85
  100. package/templates/modules/auth/express/config.json +27 -26
  101. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +116 -130
  102. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +26 -26
  103. package/templates/modules/auth/express/src/services/userStore.ts.hbs +117 -117
  104. package/templates/modules/auth/nestjs/config.json +24 -0
  105. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts +46 -0
  106. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +36 -0
  107. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +22 -0
  108. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +35 -0
  109. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +70 -0
  110. package/templates/modules/auth/nestjs/src/auth/jwt.ts +27 -0
  111. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +144 -0
  112. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +16 -0
  113. package/templates/modules/auth/nextjs/app/dashboard/page.tsx +96 -96
  114. package/templates/modules/auth/nextjs/app/sign-in/page.tsx +35 -35
  115. package/templates/modules/auth/nextjs/app/sign-up/page.tsx +35 -35
  116. package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +132 -132
  117. package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +50 -50
  118. package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +152 -152
  119. package/templates/modules/auth/nextjs/config.json +31 -31
  120. package/templates/modules/auth/nextjs/lib/auth-schema.ts +39 -39
  121. package/templates/modules/auth/nextjs/lib/auth.ts.hbs +37 -37
  122. package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +4 -4
  123. package/templates/modules/auth/tanstack/src/routeTree.gen.ts +140 -140
  124. package/templates/modules/docker/.dockerignore +14 -14
  125. package/templates/modules/docker/Dockerfile.hbs +137 -130
  126. package/templates/modules/docker/config.json +19 -19
  127. package/templates/modules/docker/docker-compose.yml.hbs +9 -9
  128. package/templates/modules/drizzle/express/config.json +27 -27
  129. package/templates/modules/drizzle/express/drizzle.config.ts +13 -13
  130. package/templates/modules/drizzle/express/src/lib/db.ts +13 -13
  131. package/templates/modules/drizzle/express/src/lib/schema.ts +10 -10
  132. package/templates/modules/drizzle/nestjs/config.json +27 -0
  133. package/templates/modules/drizzle/nestjs/drizzle.config.ts +13 -0
  134. package/templates/modules/drizzle/nestjs/src/database/database.module.ts +30 -0
  135. package/templates/modules/drizzle/nestjs/src/database/schema.ts +10 -0
  136. package/templates/modules/drizzle/nextjs/config.json +29 -29
  137. package/templates/modules/drizzle/nextjs/lib/db.ts +13 -13
  138. package/templates/modules/drizzle/nextjs/lib/schema.ts.hbs +84 -84
  139. package/templates/modules/mongoose/express/config.json +16 -16
  140. package/templates/modules/mongoose/nestjs/config.json +16 -0
  141. package/templates/modules/mongoose/nestjs/src/users/user.schema.ts +21 -0
  142. package/templates/modules/mongoose/nextjs/config.json +16 -16
  143. package/templates/modules/mongoose/nextjs/lib/db.ts +36 -36
  144. package/templates/modules/mongoose/tanstack/config.json +17 -17
  145. package/templates/modules/mongoose/tanstack/src/lib/db.ts +37 -37
  146. package/templates/modules/prisma/express/config.json +28 -28
  147. package/templates/modules/prisma/express/prisma/schema.prisma +1 -1
  148. package/templates/modules/prisma/express/prisma.config.ts +12 -12
  149. package/templates/modules/prisma/express/src/lib/prisma.ts +24 -24
  150. package/templates/modules/prisma/nestjs/config.json +28 -0
  151. package/templates/modules/prisma/nestjs/prisma/schema.prisma +23 -0
  152. package/templates/modules/prisma/nestjs/prisma.config.ts +12 -0
  153. package/templates/modules/prisma/nestjs/src/prisma/prisma.module.ts +11 -0
  154. package/templates/modules/prisma/nestjs/src/prisma/prisma.service.ts +17 -0
  155. package/templates/modules/prisma/nextjs/config.json +29 -29
  156. package/templates/modules/prisma/nextjs/prisma/schema.prisma.hbs +91 -91
  157. package/templates/modules/prisma/tanstack/config.json +5 -5
  158. package/dist/intro.d.ts +0 -2
  159. package/dist/intro.d.ts.map +0 -1
  160. package/dist/intro.js +0 -27
  161. package/dist/intro.js.map +0 -1
  162. package/templates/backend/express/.eslintrc.js +0 -24
@@ -1,85 +1,85 @@
1
- @import "tailwindcss" source("../");
2
-
3
- @theme inline {
4
- --color-background: var(--background);
5
- --color-foreground: var(--foreground);
6
- --color-card: var(--card);
7
- --color-card-foreground: var(--card-foreground);
8
- --color-primary: var(--primary);
9
- --color-primary-foreground: var(--primary-foreground);
10
- --color-secondary: var(--secondary);
11
- --color-secondary-foreground: var(--secondary-foreground);
12
- --color-muted: var(--muted);
13
- --color-muted-foreground: var(--muted-foreground);
14
- --color-accent: var(--accent);
15
- --color-accent-foreground: var(--accent-foreground);
16
- --color-destructive: var(--destructive);
17
- --color-border: var(--border);
18
- --color-input: var(--input);
19
- --color-ring: var(--ring);
20
- }
21
-
22
- :root {
23
- --background: oklch(0.985 0 0);
24
- --foreground: oklch(0.145 0 0);
25
- --card: oklch(1 0 0);
26
- --card-foreground: oklch(0.145 0 0);
27
- --primary: oklch(0.205 0 0);
28
- --primary-foreground: oklch(0.985 0 0);
29
- --secondary: oklch(0.97 0 0);
30
- --secondary-foreground: oklch(0.205 0 0);
31
- --muted: oklch(0.97 0 0);
32
- --muted-foreground: oklch(0.556 0 0);
33
- --accent: oklch(0.97 0 0);
34
- --accent-foreground: oklch(0.205 0 0);
35
- --destructive: oklch(0.577 0.245 27.325);
36
- --border: oklch(0.922 0 0);
37
- --input: oklch(0.922 0 0);
38
- --ring: oklch(0.708 0 0);
39
- }
40
-
41
- @media (prefers-color-scheme: dark) {
42
- :root {
43
- --background: oklch(0.145 0 0);
44
- --foreground: oklch(0.985 0 0);
45
- --card: oklch(0.205 0 0);
46
- --card-foreground: oklch(0.985 0 0);
47
- --primary: oklch(0.922 0 0);
48
- --primary-foreground: oklch(0.205 0 0);
49
- --secondary: oklch(0.269 0 0);
50
- --secondary-foreground: oklch(0.985 0 0);
51
- --muted: oklch(0.269 0 0);
52
- --muted-foreground: oklch(0.708 0 0);
53
- --accent: oklch(0.269 0 0);
54
- --accent-foreground: oklch(0.985 0 0);
55
- --destructive: oklch(0.704 0.191 22.216);
56
- --border: oklch(1 0 0 / 10%);
57
- --input: oklch(1 0 0 / 15%);
58
- --ring: oklch(0.556 0 0);
59
- }
60
- }
61
-
62
- @layer base {
63
- *,
64
- ::after,
65
- ::before,
66
- ::backdrop,
67
- ::file-selector-button {
68
- border-color: var(--color-gray-200, currentcolor);
69
- }
70
- }
71
-
72
- @layer base {
73
- html {
74
- color-scheme: light dark;
75
- }
76
-
77
- * {
78
- @apply border-gray-200 dark:border-gray-800;
79
- }
80
-
81
- html,
82
- body {
83
- @apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200;
84
- }
85
- }
1
+ @import "tailwindcss" source("../");
2
+
3
+ @theme inline {
4
+ --color-background: var(--background);
5
+ --color-foreground: var(--foreground);
6
+ --color-card: var(--card);
7
+ --color-card-foreground: var(--card-foreground);
8
+ --color-primary: var(--primary);
9
+ --color-primary-foreground: var(--primary-foreground);
10
+ --color-secondary: var(--secondary);
11
+ --color-secondary-foreground: var(--secondary-foreground);
12
+ --color-muted: var(--muted);
13
+ --color-muted-foreground: var(--muted-foreground);
14
+ --color-accent: var(--accent);
15
+ --color-accent-foreground: var(--accent-foreground);
16
+ --color-destructive: var(--destructive);
17
+ --color-border: var(--border);
18
+ --color-input: var(--input);
19
+ --color-ring: var(--ring);
20
+ }
21
+
22
+ :root {
23
+ --background: oklch(0.985 0 0);
24
+ --foreground: oklch(0.145 0 0);
25
+ --card: oklch(1 0 0);
26
+ --card-foreground: oklch(0.145 0 0);
27
+ --primary: oklch(0.205 0 0);
28
+ --primary-foreground: oklch(0.985 0 0);
29
+ --secondary: oklch(0.97 0 0);
30
+ --secondary-foreground: oklch(0.205 0 0);
31
+ --muted: oklch(0.97 0 0);
32
+ --muted-foreground: oklch(0.556 0 0);
33
+ --accent: oklch(0.97 0 0);
34
+ --accent-foreground: oklch(0.205 0 0);
35
+ --destructive: oklch(0.577 0.245 27.325);
36
+ --border: oklch(0.922 0 0);
37
+ --input: oklch(0.922 0 0);
38
+ --ring: oklch(0.708 0 0);
39
+ }
40
+
41
+ @media (prefers-color-scheme: dark) {
42
+ :root {
43
+ --background: oklch(0.145 0 0);
44
+ --foreground: oklch(0.985 0 0);
45
+ --card: oklch(0.205 0 0);
46
+ --card-foreground: oklch(0.985 0 0);
47
+ --primary: oklch(0.922 0 0);
48
+ --primary-foreground: oklch(0.205 0 0);
49
+ --secondary: oklch(0.269 0 0);
50
+ --secondary-foreground: oklch(0.985 0 0);
51
+ --muted: oklch(0.269 0 0);
52
+ --muted-foreground: oklch(0.708 0 0);
53
+ --accent: oklch(0.269 0 0);
54
+ --accent-foreground: oklch(0.985 0 0);
55
+ --destructive: oklch(0.704 0.191 22.216);
56
+ --border: oklch(1 0 0 / 10%);
57
+ --input: oklch(1 0 0 / 15%);
58
+ --ring: oklch(0.556 0 0);
59
+ }
60
+ }
61
+
62
+ @layer base {
63
+ *,
64
+ ::after,
65
+ ::before,
66
+ ::backdrop,
67
+ ::file-selector-button {
68
+ border-color: var(--color-gray-200, currentcolor);
69
+ }
70
+ }
71
+
72
+ @layer base {
73
+ html {
74
+ color-scheme: light dark;
75
+ }
76
+
77
+ * {
78
+ @apply border-gray-200 dark:border-gray-800;
79
+ }
80
+
81
+ html,
82
+ body {
83
+ @apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200;
84
+ }
85
+ }
@@ -1,26 +1,27 @@
1
- {
2
- "id": "auth",
3
- "name": "JWT Auth",
4
- "description": "JWT authentication with bcrypt password hashing",
5
- "type": "feature",
6
- "dependencies": {
7
- "bcrypt": "^6.0.0",
8
- "jsonwebtoken": "^9.0.3"
9
- },
10
- "devDependencies": {
11
- "@types/bcrypt": "^6.0.0",
12
- "@types/jsonwebtoken": "^9.0.10"
13
- },
14
- "scripts": {},
15
- "envVars": {
16
- "JWT_SECRET": "please-change-me",
17
- "JWT_EXPIRES_IN": "1h"
18
- },
19
- "templateFiles": [
20
- "src/lib/jwt.ts.hbs",
21
- "src/middleware/requireAuth.ts.hbs",
22
- "src/services/userStore.ts.hbs",
23
- "src/routes/auth.ts.hbs",
24
- "src/controllers/authController.ts.hbs"
25
- ]
26
- }
1
+ {
2
+ "id": "auth",
3
+ "name": "JWT Auth",
4
+ "description": "JWT authentication with bcrypt password hashing",
5
+ "type": "feature",
6
+ "dependencies": {
7
+ "bcrypt": "^6.0.0",
8
+ "jsonwebtoken": "^9.0.3",
9
+ "zod": "^4.6.5"
10
+ },
11
+ "devDependencies": {
12
+ "@types/bcrypt": "^6.0.0",
13
+ "@types/jsonwebtoken": "^9.0.10"
14
+ },
15
+ "scripts": {},
16
+ "envVars": {
17
+ "JWT_SECRET": "please-change-me",
18
+ "JWT_EXPIRES_IN": "1h"
19
+ },
20
+ "templateFiles": [
21
+ "src/lib/jwt.ts.hbs",
22
+ "src/middleware/requireAuth.ts.hbs",
23
+ "src/services/userStore.ts.hbs",
24
+ "src/routes/auth.ts.hbs",
25
+ "src/controllers/authController.ts.hbs"
26
+ ]
27
+ }
@@ -1,130 +1,116 @@
1
- import { Request, Response } from "express";
2
- import bcrypt from "bcrypt";
3
- import { signToken } from "../lib/jwt";
4
- import { AuthenticatedRequest } from "../middleware/requireAuth";
5
- import {
6
- createUser,
7
- findUserByEmail,
8
- isDuplicateEmailError,
9
- toPublicUser,
10
- } from "../services/userStore";
11
-
12
- const SALT_ROUNDS = 10;
13
- const MIN_PASSWORD_LENGTH = 8;
14
- // bcrypt ignores bytes beyond 72, so longer passwords would be silently truncated
15
- const MAX_PASSWORD_BYTES = 72;
16
-
17
- // Request bodies are untrusted JSON, so fields may have any type
18
- interface RegisterBody {
19
- email?: unknown;
20
- password?: unknown;
21
- name?: unknown;
22
- }
23
-
24
- interface LoginBody {
25
- email?: unknown;
26
- password?: unknown;
27
- }
28
-
29
- // Returns a trimmed, lowercased email, or null when the value is not a usable string
30
- function normalizeEmail(email: unknown): string | null {
31
- if (typeof email !== "string") return null;
32
- const normalized = email.trim().toLowerCase();
33
- return normalized || null;
34
- }
35
-
36
- export async function register(
37
- req: Request<object, unknown, RegisterBody | undefined>,
38
- res: Response,
39
- ): Promise<void> {
40
- // Express 5 leaves req.body undefined when the request has no body
41
- const body = req.body ?? {};
42
- const email = normalizeEmail(body.email);
43
- const password = body.password;
44
- const name = body.name ?? null;
45
-
46
- if (!email || typeof password !== "string" || !password) {
47
- res.status(400).json({ error: "Email and password are required" });
48
- return;
49
- }
50
-
51
- if (
52
- password.length < MIN_PASSWORD_LENGTH ||
53
- Buffer.byteLength(password, "utf8") > MAX_PASSWORD_BYTES
54
- ) {
55
- res.status(400).json({
56
- error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters and at most ${MAX_PASSWORD_BYTES} bytes`,
57
- });
58
- return;
59
- }
60
-
61
- if (name !== null && typeof name !== "string") {
62
- res.status(400).json({ error: "Name must be a string" });
63
- return;
64
- }
65
-
66
- const existingUser = await findUserByEmail(email);
67
- if (existingUser) {
68
- res.status(409).json({ error: "User already exists" });
69
- return;
70
- }
71
-
72
- const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
73
-
74
- let user;
75
- try {
76
- user = await createUser({ email, name, password: hashedPassword });
77
- } catch (error) {
78
- // A concurrent request registered the same email after the check above
79
- if (isDuplicateEmailError(error)) {
80
- res.status(409).json({ error: "User already exists" });
81
- return;
82
- }
83
- throw error;
84
- }
85
-
86
- const token = signToken({ sub: user.id, email: user.email, name: user.name });
87
-
88
- res.status(201).json({ user: toPublicUser(user), token });
89
- }
90
-
91
- export async function login(
92
- req: Request<object, unknown, LoginBody | undefined>,
93
- res: Response,
94
- ): Promise<void> {
95
- const body = req.body ?? {};
96
- const email = normalizeEmail(body.email);
97
- const password = body.password;
98
-
99
- if (!email || typeof password !== "string" || !password) {
100
- res.status(400).json({ error: "Email and password are required" });
101
- return;
102
- }
103
-
104
- const user = await findUserByEmail(email);
105
-
106
- if (!user) {
107
- res.status(401).json({ error: "Invalid credentials" });
108
- return;
109
- }
110
-
111
- const isValidPassword = await bcrypt.compare(password, user.password);
112
-
113
- if (!isValidPassword) {
114
- res.status(401).json({ error: "Invalid credentials" });
115
- return;
116
- }
117
-
118
- const token = signToken({ sub: user.id, email: user.email, name: user.name });
119
-
120
- res.json({ user: toPublicUser(user), token });
121
- }
122
-
123
- export function me(req: AuthenticatedRequest, res: Response): void {
124
- if (!req.user) {
125
- res.status(401).json({ error: "Unauthorized" });
126
- return;
127
- }
128
-
129
- res.json({ user: req.user });
130
- }
1
+ import { Request, Response } from "express";
2
+ import bcrypt from "bcrypt";
3
+ import { z } from "zod";
4
+ import { signToken } from "../lib/jwt";
5
+ import { AuthenticatedRequest } from "../middleware/requireAuth";
6
+ import {
7
+ createUser,
8
+ findUserByEmail,
9
+ isDuplicateEmailError,
10
+ toPublicUser,
11
+ } from "../services/userStore";
12
+
13
+ const SALT_ROUNDS = 10;
14
+ const MIN_PASSWORD_LENGTH = 8;
15
+ // bcrypt ignores bytes beyond 72, so longer passwords would be silently truncated
16
+ const MAX_PASSWORD_BYTES = 72;
17
+
18
+ // Emails are trimmed and lowercased so sign-in is case-insensitive
19
+ const emailSchema = z
20
+ .string("Email and password are required")
21
+ .trim()
22
+ .toLowerCase()
23
+ .pipe(z.email("Email must be a valid email address"));
24
+
25
+ const registerSchema = z.object({
26
+ email: emailSchema,
27
+ password: z
28
+ .string("Email and password are required")
29
+ .min(
30
+ MIN_PASSWORD_LENGTH,
31
+ `Password must be at least ${MIN_PASSWORD_LENGTH} characters`,
32
+ )
33
+ .refine(
34
+ (password) => Buffer.byteLength(password, "utf8") <= MAX_PASSWORD_BYTES,
35
+ `Password must be at most ${MAX_PASSWORD_BYTES} bytes`,
36
+ ),
37
+ name: z.string("Name must be a string").nullish(),
38
+ });
39
+
40
+ const loginSchema = z.object({
41
+ email: emailSchema,
42
+ password: z.string("Email and password are required").min(1),
43
+ });
44
+
45
+ // Sends a 400 with the first validation message and returns null when the body is invalid
46
+ function parseBody<T>(schema: z.ZodType<T>, req: Request, res: Response): T | null {
47
+ // Express 5 leaves req.body undefined when the request has no body
48
+ const result = schema.safeParse(req.body ?? {});
49
+ if (result.success) return result.data;
50
+
51
+ res.status(400).json({ error: result.error.issues[0].message });
52
+ return null;
53
+ }
54
+
55
+ export async function register(req: Request, res: Response): Promise<void> {
56
+ const body = parseBody(registerSchema, req, res);
57
+ if (!body) return;
58
+ const { email, password, name = null } = body;
59
+
60
+ const existingUser = await findUserByEmail(email);
61
+ if (existingUser) {
62
+ res.status(409).json({ error: "User already exists" });
63
+ return;
64
+ }
65
+
66
+ const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
67
+
68
+ let user;
69
+ try {
70
+ user = await createUser({ email, name, password: hashedPassword });
71
+ } catch (error) {
72
+ // A concurrent request registered the same email after the check above
73
+ if (isDuplicateEmailError(error)) {
74
+ res.status(409).json({ error: "User already exists" });
75
+ return;
76
+ }
77
+ throw error;
78
+ }
79
+
80
+ const token = signToken({ sub: user.id, email: user.email, name: user.name });
81
+
82
+ res.status(201).json({ user: toPublicUser(user), token });
83
+ }
84
+
85
+ export async function login(req: Request, res: Response): Promise<void> {
86
+ const body = parseBody(loginSchema, req, res);
87
+ if (!body) return;
88
+ const { email, password } = body;
89
+
90
+ const user = await findUserByEmail(email);
91
+
92
+ if (!user) {
93
+ res.status(401).json({ error: "Invalid credentials" });
94
+ return;
95
+ }
96
+
97
+ const isValidPassword = await bcrypt.compare(password, user.password);
98
+
99
+ if (!isValidPassword) {
100
+ res.status(401).json({ error: "Invalid credentials" });
101
+ return;
102
+ }
103
+
104
+ const token = signToken({ sub: user.id, email: user.email, name: user.name });
105
+
106
+ res.json({ user: toPublicUser(user), token });
107
+ }
108
+
109
+ export function me(req: AuthenticatedRequest, res: Response): void {
110
+ if (!req.user) {
111
+ res.status(401).json({ error: "Unauthorized" });
112
+ return;
113
+ }
114
+
115
+ res.json({ user: req.user });
116
+ }
@@ -1,26 +1,26 @@
1
- import { NextFunction, Request, Response } from 'express';
2
- import { AuthTokenPayload, verifyToken } from '../lib/jwt';
3
-
4
- export interface AuthenticatedRequest extends Request {
5
- user?: AuthTokenPayload;
6
- }
7
-
8
- export function requireAuth(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
9
- const authHeader = req.headers.authorization;
10
-
11
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
12
- res.status(401).json({ error: 'Authorization header missing or invalid' });
13
- return;
14
- }
15
-
16
- const token = authHeader.substring('Bearer '.length);
17
-
18
- try {
19
- const payload = verifyToken(token);
20
- req.user = payload;
21
- next();
22
- } catch (error) {
23
- console.error('JWT verification failed:', error);
24
- res.status(401).json({ error: 'Invalid or expired token' });
25
- }
26
- }
1
+ import { NextFunction, Request, Response } from 'express';
2
+ import { AuthTokenPayload, verifyToken } from '../lib/jwt';
3
+
4
+ export interface AuthenticatedRequest extends Request {
5
+ user?: AuthTokenPayload;
6
+ }
7
+
8
+ export function requireAuth(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
9
+ const authHeader = req.headers.authorization;
10
+
11
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
12
+ res.status(401).json({ error: 'Authorization header missing or invalid' });
13
+ return;
14
+ }
15
+
16
+ const token = authHeader.substring('Bearer '.length);
17
+
18
+ try {
19
+ const payload = verifyToken(token);
20
+ req.user = payload;
21
+ next();
22
+ } catch (error) {
23
+ console.error('JWT verification failed:', error);
24
+ res.status(401).json({ error: 'Invalid or expired token' });
25
+ }
26
+ }