better-ts-stack 2.0.0 → 2.0.1

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 (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/dist/args.js +2 -2
  4. package/dist/builder/buildPlanner.js +71 -0
  5. package/dist/builder/configGenerator.js +9 -7
  6. package/dist/builder/dependencyInstaller.js +0 -1
  7. package/dist/builder/fileProcessor.js +47 -44
  8. package/dist/builder/gitInitializer.js +20 -1
  9. package/dist/builder/index.js +19 -38
  10. package/dist/builder/moduleSelector.js +0 -1
  11. package/dist/builder/templateContext.js +0 -22
  12. package/dist/index.js +4 -3
  13. package/dist/modules/registry.js +4 -6
  14. package/dist/output/nextSteps.js +0 -1
  15. package/dist/prompts/index.js +0 -1
  16. package/dist/prompts/stack.js +4 -1
  17. package/dist/types/index.js +7 -3
  18. package/dist/ui/index.js +0 -1
  19. package/dist/ui/messages.js +25 -1
  20. package/dist/ui/prompts.js +0 -1
  21. package/dist/ui/theme.js +0 -1
  22. package/dist/validators/index.js +10 -41
  23. package/package.json +2 -1
  24. package/templates/backend/express/.prettierrc +2 -2
  25. package/templates/backend/express/config.json +1 -5
  26. package/templates/backend/express/eslint.config.mjs +40 -40
  27. package/templates/backend/express/gitignore +36 -36
  28. package/templates/backend/express/src/{index.ts → index.ts.hbs} +28 -29
  29. package/templates/backend/express/src/routes/health.ts +12 -12
  30. package/templates/backend/express/tsconfig.json +25 -25
  31. package/templates/backend/nestjs/.prettierrc +16 -16
  32. package/templates/backend/nestjs/config.json +1 -4
  33. package/templates/backend/nestjs/eslint.config.mjs +40 -40
  34. package/templates/backend/nestjs/gitignore +36 -36
  35. package/templates/backend/nestjs/nest-cli.json +9 -9
  36. package/templates/backend/nestjs/src/app.module.ts.hbs +34 -34
  37. package/templates/backend/nestjs/src/health/health.controller.ts +12 -12
  38. package/templates/backend/nestjs/src/main.ts +23 -23
  39. package/templates/backend/nestjs/tsconfig.json +25 -25
  40. package/templates/frontend/nextjs/components.json +19 -19
  41. package/templates/frontend/nextjs/config.json +1 -2
  42. package/templates/frontend/nextjs/gitignore +42 -42
  43. package/templates/frontend/nextjs/lib/utils.ts +6 -6
  44. package/templates/frontend/nextjs/proxy.ts.hbs +24 -24
  45. package/templates/frontend/nextjs/tsconfig.json +34 -34
  46. package/templates/frontend/tanstack/config.json +1 -4
  47. package/templates/frontend/tanstack/src/routeTree.gen.ts +68 -68
  48. package/templates/frontend/tanstack/src/styles/app.css +85 -85
  49. package/templates/modules/auth/express/config.json +1 -8
  50. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +116 -116
  51. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +26 -26
  52. package/templates/modules/auth/express/src/services/userStore.ts.hbs +117 -117
  53. package/templates/modules/auth/nestjs/config.json +1 -5
  54. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts +46 -46
  55. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +36 -36
  56. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +22 -22
  57. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +35 -35
  58. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +70 -70
  59. package/templates/modules/auth/nestjs/src/auth/jwt.ts +27 -27
  60. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +144 -144
  61. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +16 -16
  62. package/templates/modules/auth/nextjs/app/dashboard/page.tsx +96 -96
  63. package/templates/modules/auth/nextjs/app/sign-in/page.tsx +35 -35
  64. package/templates/modules/auth/nextjs/app/sign-up/page.tsx +35 -35
  65. package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +132 -132
  66. package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +50 -50
  67. package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +152 -152
  68. package/templates/modules/auth/nextjs/config.json +2 -15
  69. package/templates/modules/auth/nextjs/lib/auth-schema.ts +39 -39
  70. package/templates/modules/auth/nextjs/lib/auth.ts.hbs +37 -37
  71. package/templates/modules/auth/tanstack/config.json +2 -16
  72. package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +4 -4
  73. package/templates/modules/auth/tanstack/src/routeTree.gen.ts +140 -140
  74. package/templates/modules/docker/.dockerignore +14 -14
  75. package/templates/modules/docker/Dockerfile.hbs +1 -3
  76. package/templates/modules/docker/config.json +1 -5
  77. package/templates/modules/docker/docker-compose.yml.hbs +9 -9
  78. package/templates/modules/drizzle/express/config.json +1 -2
  79. package/templates/modules/drizzle/express/drizzle.config.ts +13 -13
  80. package/templates/modules/drizzle/express/src/lib/db.ts +13 -13
  81. package/templates/modules/drizzle/express/src/lib/schema.ts +10 -10
  82. package/templates/modules/drizzle/nestjs/config.json +1 -2
  83. package/templates/modules/drizzle/nestjs/drizzle.config.ts +13 -13
  84. package/templates/modules/drizzle/nestjs/src/database/database.module.ts +30 -30
  85. package/templates/modules/drizzle/nestjs/src/database/schema.ts +10 -10
  86. package/templates/modules/drizzle/nextjs/config.json +1 -4
  87. package/templates/modules/drizzle/nextjs/lib/db.ts +13 -13
  88. package/templates/modules/drizzle/nextjs/lib/schema.ts.hbs +84 -84
  89. package/templates/modules/drizzle/tanstack/config.json +5 -5
  90. package/templates/modules/mongoose/express/config.json +1 -2
  91. package/templates/modules/mongoose/nestjs/config.json +1 -2
  92. package/templates/modules/mongoose/nestjs/src/users/user.schema.ts +21 -21
  93. package/templates/modules/mongoose/nextjs/config.json +1 -2
  94. package/templates/modules/mongoose/nextjs/lib/db.ts +36 -36
  95. package/templates/modules/mongoose/tanstack/config.json +1 -2
  96. package/templates/modules/mongoose/tanstack/src/lib/db.ts +37 -37
  97. package/templates/modules/prisma/express/config.json +0 -1
  98. package/templates/modules/prisma/express/prisma/schema.prisma +1 -1
  99. package/templates/modules/prisma/express/prisma.config.ts +12 -12
  100. package/templates/modules/prisma/express/src/lib/prisma.ts +24 -24
  101. package/templates/modules/prisma/nestjs/config.json +0 -1
  102. package/templates/modules/prisma/nestjs/prisma/schema.prisma +23 -23
  103. package/templates/modules/prisma/nestjs/prisma.config.ts +12 -12
  104. package/templates/modules/prisma/nestjs/src/prisma/prisma.module.ts +11 -11
  105. package/templates/modules/prisma/nestjs/src/prisma/prisma.service.ts +17 -17
  106. package/templates/modules/prisma/nextjs/config.json +0 -1
  107. package/templates/modules/prisma/nextjs/prisma/schema.prisma.hbs +91 -91
  108. package/templates/modules/prisma/tanstack/config.json +0 -1
  109. package/dist/args.d.ts +0 -12
  110. package/dist/args.d.ts.map +0 -1
  111. package/dist/args.js.map +0 -1
  112. package/dist/builder/configGenerator.d.ts +0 -7
  113. package/dist/builder/configGenerator.d.ts.map +0 -1
  114. package/dist/builder/configGenerator.js.map +0 -1
  115. package/dist/builder/dependencyInstaller.d.ts +0 -3
  116. package/dist/builder/dependencyInstaller.d.ts.map +0 -1
  117. package/dist/builder/dependencyInstaller.js.map +0 -1
  118. package/dist/builder/fileProcessor.d.ts +0 -6
  119. package/dist/builder/fileProcessor.d.ts.map +0 -1
  120. package/dist/builder/fileProcessor.js.map +0 -1
  121. package/dist/builder/gitInitializer.d.ts +0 -2
  122. package/dist/builder/gitInitializer.d.ts.map +0 -1
  123. package/dist/builder/gitInitializer.js.map +0 -1
  124. package/dist/builder/index.d.ts +0 -3
  125. package/dist/builder/index.d.ts.map +0 -1
  126. package/dist/builder/index.js.map +0 -1
  127. package/dist/builder/moduleSelector.d.ts +0 -6
  128. package/dist/builder/moduleSelector.d.ts.map +0 -1
  129. package/dist/builder/moduleSelector.js.map +0 -1
  130. package/dist/builder/templateContext.d.ts +0 -6
  131. package/dist/builder/templateContext.d.ts.map +0 -1
  132. package/dist/builder/templateContext.js.map +0 -1
  133. package/dist/index.d.ts +0 -2
  134. package/dist/index.d.ts.map +0 -1
  135. package/dist/index.js.map +0 -1
  136. package/dist/modules/registry.d.ts +0 -6
  137. package/dist/modules/registry.d.ts.map +0 -1
  138. package/dist/modules/registry.js.map +0 -1
  139. package/dist/output/nextSteps.d.ts +0 -3
  140. package/dist/output/nextSteps.d.ts.map +0 -1
  141. package/dist/output/nextSteps.js.map +0 -1
  142. package/dist/prompts/index.d.ts +0 -4
  143. package/dist/prompts/index.d.ts.map +0 -1
  144. package/dist/prompts/index.js.map +0 -1
  145. package/dist/prompts/stack.d.ts +0 -4
  146. package/dist/prompts/stack.d.ts.map +0 -1
  147. package/dist/prompts/stack.js.map +0 -1
  148. package/dist/types/index.d.ts +0 -137
  149. package/dist/types/index.d.ts.map +0 -1
  150. package/dist/types/index.js.map +0 -1
  151. package/dist/ui/index.d.ts +0 -3
  152. package/dist/ui/index.d.ts.map +0 -1
  153. package/dist/ui/index.js.map +0 -1
  154. package/dist/ui/messages.d.ts +0 -9
  155. package/dist/ui/messages.d.ts.map +0 -1
  156. package/dist/ui/messages.js.map +0 -1
  157. package/dist/ui/prompts.d.ts +0 -20
  158. package/dist/ui/prompts.d.ts.map +0 -1
  159. package/dist/ui/prompts.js.map +0 -1
  160. package/dist/ui/theme.d.ts +0 -9
  161. package/dist/ui/theme.d.ts.map +0 -1
  162. package/dist/ui/theme.js.map +0 -1
  163. package/dist/validators/index.d.ts +0 -6
  164. package/dist/validators/index.d.ts.map +0 -1
  165. package/dist/validators/index.js.map +0 -1
@@ -1,68 +1,68 @@
1
- /* eslint-disable */
2
-
3
- // @ts-nocheck
4
-
5
- // noinspection JSUnusedGlobalSymbols
6
-
7
- // This file was automatically generated by TanStack Router.
8
- // You should NOT make any changes in this file as it will be overwritten.
9
- // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
-
11
- import { Route as rootRouteImport } from './routes/__root'
12
- import { Route as IndexRouteImport } from './routes/index'
13
-
14
- const IndexRoute = IndexRouteImport.update({
15
- id: '/',
16
- path: '/',
17
- getParentRoute: () => rootRouteImport,
18
- } as any)
19
-
20
- export interface FileRoutesByFullPath {
21
- '/': typeof IndexRoute
22
- }
23
- export interface FileRoutesByTo {
24
- '/': typeof IndexRoute
25
- }
26
- export interface FileRoutesById {
27
- __root__: typeof rootRouteImport
28
- '/': typeof IndexRoute
29
- }
30
- export interface FileRouteTypes {
31
- fileRoutesByFullPath: FileRoutesByFullPath
32
- fullPaths: '/'
33
- fileRoutesByTo: FileRoutesByTo
34
- to: '/'
35
- id: '__root__' | '/'
36
- fileRoutesById: FileRoutesById
37
- }
38
- export interface RootRouteChildren {
39
- IndexRoute: typeof IndexRoute
40
- }
41
-
42
- declare module '@tanstack/react-router' {
43
- interface FileRoutesByPath {
44
- '/': {
45
- id: '/'
46
- path: '/'
47
- fullPath: '/'
48
- preLoaderRoute: typeof IndexRouteImport
49
- parentRoute: typeof rootRouteImport
50
- }
51
- }
52
- }
53
-
54
- const rootRouteChildren: RootRouteChildren = {
55
- IndexRoute: IndexRoute,
56
- }
57
- export const routeTree = rootRouteImport
58
- ._addFileChildren(rootRouteChildren)
59
- ._addFileTypes<FileRouteTypes>()
60
-
61
- import type { getRouter } from './router.tsx'
62
- import type { createStart } from '@tanstack/react-start'
63
- declare module '@tanstack/react-start' {
64
- interface Register {
65
- ssr: true
66
- router: Awaited<ReturnType<typeof getRouter>>
67
- }
68
- }
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ import { Route as rootRouteImport } from './routes/__root'
12
+ import { Route as IndexRouteImport } from './routes/index'
13
+
14
+ const IndexRoute = IndexRouteImport.update({
15
+ id: '/',
16
+ path: '/',
17
+ getParentRoute: () => rootRouteImport,
18
+ } as any)
19
+
20
+ export interface FileRoutesByFullPath {
21
+ '/': typeof IndexRoute
22
+ }
23
+ export interface FileRoutesByTo {
24
+ '/': typeof IndexRoute
25
+ }
26
+ export interface FileRoutesById {
27
+ __root__: typeof rootRouteImport
28
+ '/': typeof IndexRoute
29
+ }
30
+ export interface FileRouteTypes {
31
+ fileRoutesByFullPath: FileRoutesByFullPath
32
+ fullPaths: '/'
33
+ fileRoutesByTo: FileRoutesByTo
34
+ to: '/'
35
+ id: '__root__' | '/'
36
+ fileRoutesById: FileRoutesById
37
+ }
38
+ export interface RootRouteChildren {
39
+ IndexRoute: typeof IndexRoute
40
+ }
41
+
42
+ declare module '@tanstack/react-router' {
43
+ interface FileRoutesByPath {
44
+ '/': {
45
+ id: '/'
46
+ path: '/'
47
+ fullPath: '/'
48
+ preLoaderRoute: typeof IndexRouteImport
49
+ parentRoute: typeof rootRouteImport
50
+ }
51
+ }
52
+ }
53
+
54
+ const rootRouteChildren: RootRouteChildren = {
55
+ IndexRoute: IndexRoute,
56
+ }
57
+ export const routeTree = rootRouteImport
58
+ ._addFileChildren(rootRouteChildren)
59
+ ._addFileTypes<FileRouteTypes>()
60
+
61
+ import type { getRouter } from './router.tsx'
62
+ import type { createStart } from '@tanstack/react-start'
63
+ declare module '@tanstack/react-start' {
64
+ interface Register {
65
+ ssr: true
66
+ router: Awaited<ReturnType<typeof getRouter>>
67
+ }
68
+ }
@@ -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
+ }
@@ -16,12 +16,5 @@
16
16
  "envVars": {
17
17
  "JWT_SECRET": "please-change-me",
18
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
- ]
19
+ }
27
20
  }
@@ -1,116 +1,116 @@
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
+ 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
+ }