better-ts-stack 2.1.1 → 2.2.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 (174) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +16 -8
  3. package/dist/args.js +7 -0
  4. package/dist/builder/buildPlanner.js +7 -1
  5. package/dist/builder/configGenerator.js +28 -3
  6. package/dist/builder/index.js +2 -1
  7. package/dist/builder/moduleSelector.js +6 -1
  8. package/dist/builder/readmeGenerator.js +8 -2
  9. package/dist/builder/templateContext.js +1 -0
  10. package/dist/output/nextSteps.js +2 -4
  11. package/dist/prompts/stack.js +6 -1
  12. package/dist/types/index.js +2 -0
  13. package/dist/ui/messages.js +2 -0
  14. package/package.json +71 -71
  15. package/templates/backend/express/.prettierignore +52 -52
  16. package/templates/backend/express/.prettierrc +16 -16
  17. package/templates/backend/express/gitignore +3 -1
  18. package/templates/backend/express/src/env.ts.hbs +13 -2
  19. package/templates/backend/express/src/index.ts.hbs +31 -2
  20. package/templates/backend/express/tsconfig.eslint.json +9 -9
  21. package/templates/backend/nestjs/.prettierignore +52 -52
  22. package/templates/backend/nestjs/gitignore +3 -1
  23. package/templates/backend/nestjs/src/app.module.ts.hbs +4 -1
  24. package/templates/backend/nestjs/src/env.ts.hbs +13 -2
  25. package/templates/backend/nestjs/src/health/health.controller.ts.hbs +7 -0
  26. package/templates/backend/nestjs/src/{main.ts → main.ts.hbs} +11 -0
  27. package/templates/backend/nestjs/tsconfig.eslint.json +9 -9
  28. package/templates/frontend/nextjs/app/globals.css +129 -89
  29. package/templates/frontend/nextjs/app/layout.tsx +36 -34
  30. package/templates/frontend/nextjs/app/page.tsx.hbs +63 -43
  31. package/templates/frontend/nextjs/components/ui/alert.tsx +75 -0
  32. package/templates/frontend/nextjs/components/ui/badge.tsx +51 -0
  33. package/templates/frontend/nextjs/components/ui/button.tsx +57 -51
  34. package/templates/frontend/nextjs/components/ui/card.tsx +102 -60
  35. package/templates/frontend/nextjs/components/ui/field.tsx +238 -67
  36. package/templates/frontend/nextjs/components/ui/input.tsx +19 -18
  37. package/templates/frontend/nextjs/components/ui/label.tsx +19 -0
  38. package/templates/frontend/nextjs/components/ui/separator.tsx +24 -0
  39. package/templates/frontend/nextjs/components/ui/skeleton.tsx +13 -0
  40. package/templates/frontend/nextjs/components/ui/spinner.tsx +10 -0
  41. package/templates/frontend/nextjs/components.json +11 -5
  42. package/templates/frontend/nextjs/config.json +36 -35
  43. package/templates/frontend/nextjs/eslint.config.mjs +11 -11
  44. package/templates/frontend/nextjs/lib/utils.ts +1 -6
  45. package/templates/frontend/nextjs/next.config.ts.hbs +12 -12
  46. package/templates/frontend/nextjs/postcss.config.mjs +7 -7
  47. package/templates/frontend/nextjs/public/file.svg +1 -1
  48. package/templates/frontend/nextjs/public/globe.svg +1 -1
  49. package/templates/frontend/nextjs/public/window.svg +1 -1
  50. package/templates/frontend/tanstack/components.json +25 -18
  51. package/templates/frontend/tanstack/config.json +5 -2
  52. package/templates/frontend/tanstack/eslint.config.mjs +23 -23
  53. package/templates/frontend/tanstack/gitignore +39 -39
  54. package/templates/frontend/tanstack/public/favicon.svg +7 -7
  55. package/templates/frontend/tanstack/src/components/ui/alert.tsx +75 -0
  56. package/templates/frontend/tanstack/src/components/ui/badge.tsx +51 -0
  57. package/templates/frontend/tanstack/src/components/ui/button.tsx +57 -51
  58. package/templates/frontend/tanstack/src/components/ui/card.tsx +102 -60
  59. package/templates/frontend/tanstack/src/components/ui/field.tsx +236 -67
  60. package/templates/frontend/tanstack/src/components/ui/input.tsx +19 -18
  61. package/templates/frontend/tanstack/src/components/ui/label.tsx +17 -0
  62. package/templates/frontend/tanstack/src/components/ui/separator.tsx +24 -0
  63. package/templates/frontend/tanstack/src/components/ui/skeleton.tsx +13 -0
  64. package/templates/frontend/tanstack/src/components/ui/spinner.tsx +10 -0
  65. package/templates/frontend/tanstack/src/lib/utils.ts +1 -6
  66. package/templates/frontend/tanstack/src/router.tsx +31 -31
  67. package/templates/frontend/tanstack/src/routes/__root.tsx +69 -69
  68. package/templates/frontend/tanstack/src/routes/index.tsx.hbs +64 -45
  69. package/templates/frontend/tanstack/src/styles/app.css +100 -55
  70. package/templates/frontend/tanstack/tsconfig.json +22 -22
  71. package/templates/modules/agents/AGENTS.md.hbs +245 -0
  72. package/templates/modules/agents/CLAUDE.md +1 -0
  73. package/templates/modules/agents/config.json +10 -0
  74. package/templates/modules/auth/express/config.json +12 -11
  75. package/templates/modules/auth/express/src/lib/auth.ts.hbs +143 -0
  76. package/templates/modules/auth/express/src/lib/email.ts +37 -0
  77. package/templates/modules/auth/express/src/middleware/requireAuth.ts +29 -0
  78. package/templates/modules/auth/express/src/routes/me.ts +13 -0
  79. package/templates/modules/auth/nestjs/config.json +13 -11
  80. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +64 -14
  81. package/templates/modules/auth/nestjs/src/auth/auth.ts.hbs +152 -0
  82. package/templates/modules/auth/nestjs/src/auth/email.ts +37 -0
  83. package/templates/modules/auth/nestjs/src/me/me.controller.ts +15 -0
  84. package/templates/modules/auth/nextjs/app/api/auth/[...all]/route.ts +4 -4
  85. package/templates/modules/auth/nextjs/app/dashboard/page.tsx +101 -45
  86. package/templates/modules/auth/nextjs/app/forgot-password/page.tsx +11 -0
  87. package/templates/modules/auth/nextjs/app/reset-password/page.tsx +17 -0
  88. package/templates/modules/auth/nextjs/app/sign-in/page.tsx +23 -17
  89. package/templates/modules/auth/nextjs/app/sign-up/page.tsx +25 -19
  90. package/templates/modules/auth/nextjs/components/auth/change-password-form.tsx +125 -0
  91. package/templates/modules/auth/nextjs/components/auth/email-verification-card.tsx +91 -0
  92. package/templates/modules/auth/nextjs/components/auth/forgot-password-form.tsx +133 -0
  93. package/templates/modules/auth/nextjs/components/auth/reset-password-form.tsx +155 -0
  94. package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +46 -32
  95. package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +6 -5
  96. package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +36 -30
  97. package/templates/modules/auth/nextjs/components/auth/social-buttons.tsx +73 -0
  98. package/templates/modules/auth/nextjs/config.json +24 -18
  99. package/templates/modules/auth/nextjs/lib/auth-client.ts +3 -3
  100. package/templates/modules/auth/nextjs/lib/auth-schema.ts +27 -2
  101. package/templates/modules/auth/nextjs/lib/auth.ts.hbs +65 -0
  102. package/templates/modules/auth/nextjs/lib/email.ts +43 -0
  103. package/templates/modules/auth/nextjs/lib/session.ts +12 -0
  104. package/templates/modules/auth/tanstack/config.json +25 -19
  105. package/templates/modules/auth/tanstack/src/components/auth/change-password-form.tsx +123 -0
  106. package/templates/modules/auth/tanstack/src/components/auth/email-verification-card.tsx +89 -0
  107. package/templates/modules/auth/tanstack/src/components/auth/forgot-password-form.tsx +131 -0
  108. package/templates/modules/auth/tanstack/src/components/auth/reset-password-form.tsx +149 -0
  109. package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +44 -35
  110. package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +7 -6
  111. package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +37 -34
  112. package/templates/modules/auth/tanstack/src/components/auth/social-buttons.tsx +71 -0
  113. package/templates/modules/auth/tanstack/src/lib/auth-client.ts +2 -2
  114. package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +14 -8
  115. package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +27 -2
  116. package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +98 -38
  117. package/templates/modules/auth/tanstack/src/lib/email.ts +43 -0
  118. package/templates/modules/auth/tanstack/src/routeTree.gen.ts +61 -3
  119. package/templates/modules/auth/tanstack/src/routes/api/auth/$.ts +15 -15
  120. package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +119 -111
  121. package/templates/modules/auth/tanstack/src/routes/forgot-password.tsx +17 -0
  122. package/templates/modules/auth/tanstack/src/routes/reset-password.tsx +25 -0
  123. package/templates/modules/auth/tanstack/src/routes/sign-in.tsx +44 -37
  124. package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +44 -37
  125. package/templates/modules/docker/.dockerignore +2 -0
  126. package/templates/modules/docker/config.json +15 -15
  127. package/templates/modules/drizzle/express/config.json +26 -26
  128. package/templates/modules/drizzle/express/src/lib/schema.ts.hbs +90 -0
  129. package/templates/modules/drizzle/nestjs/config.json +26 -26
  130. package/templates/modules/drizzle/nestjs/src/database/schema.ts.hbs +90 -0
  131. package/templates/modules/drizzle/nextjs/config.json +26 -26
  132. package/templates/modules/drizzle/nextjs/drizzle.config.ts +13 -13
  133. package/templates/modules/drizzle/tanstack/config.json +26 -26
  134. package/templates/modules/drizzle/tanstack/drizzle.config.ts +12 -12
  135. package/templates/modules/drizzle/tanstack/src/lib/db.ts +13 -13
  136. package/templates/modules/mongoose/express/config.json +15 -15
  137. package/templates/modules/mongoose/express/src/lib/db.ts +43 -43
  138. package/templates/modules/mongoose/express/src/models/User.ts +48 -48
  139. package/templates/modules/mongoose/nestjs/config.json +16 -15
  140. package/templates/modules/mongoose/nextjs/config.json +15 -15
  141. package/templates/modules/mongoose/nextjs/models/User.ts +48 -48
  142. package/templates/modules/mongoose/tanstack/config.json +16 -16
  143. package/templates/modules/mongoose/tanstack/src/models/User.ts +48 -48
  144. package/templates/modules/prisma/express/config.json +27 -27
  145. package/templates/modules/prisma/express/prisma/schema.prisma.hbs +92 -0
  146. package/templates/modules/prisma/nestjs/config.json +27 -27
  147. package/templates/modules/prisma/nestjs/prisma/schema.prisma.hbs +91 -0
  148. package/templates/modules/prisma/nextjs/config.json +28 -28
  149. package/templates/modules/prisma/nextjs/lib/prisma.ts +18 -18
  150. package/templates/modules/prisma/nextjs/prisma.config.ts +14 -14
  151. package/templates/modules/prisma/tanstack/config.json +28 -28
  152. package/templates/modules/prisma/tanstack/prisma/schema.prisma.hbs +91 -91
  153. package/templates/modules/prisma/tanstack/prisma.config.ts +14 -14
  154. package/templates/modules/prisma/tanstack/src/lib/prisma.ts +17 -17
  155. package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +0 -8
  156. package/templates/modules/vscode/.vscode/extensions.json.hbs +29 -0
  157. package/templates/modules/vscode/.vscode/settings.json.hbs +22 -0
  158. package/templates/modules/vscode/config.json +10 -0
  159. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +0 -116
  160. package/templates/modules/auth/express/src/lib/jwt.ts.hbs +0 -27
  161. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +0 -25
  162. package/templates/modules/auth/express/src/routes/auth.ts.hbs +0 -27
  163. package/templates/modules/auth/express/src/services/userStore.ts.hbs +0 -117
  164. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts.hbs +0 -61
  165. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +0 -36
  166. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +0 -35
  167. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +0 -70
  168. package/templates/modules/auth/nestjs/src/auth/jwt.ts +0 -27
  169. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +0 -144
  170. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +0 -16
  171. package/templates/modules/drizzle/express/src/lib/schema.ts +0 -12
  172. package/templates/modules/drizzle/nestjs/src/database/schema.ts +0 -12
  173. package/templates/modules/prisma/express/prisma/schema.prisma +0 -24
  174. package/templates/modules/prisma/nestjs/prisma/schema.prisma +0 -23
@@ -1,38 +1,98 @@
1
- import "dotenv/config";
2
- import { betterAuth } from "better-auth";
3
- import { tanstackStartCookies } from "better-auth/tanstack-start";
4
- {{#if (eq database 'prisma')}}
5
- import { prismaAdapter } from "better-auth/adapters/prisma";
6
- import client from "./prisma";
7
- {{/if}}
8
- {{#if (eq database 'drizzle')}}
9
- import { drizzleAdapter } from "better-auth/adapters/drizzle";
10
- import { db } from "./db";
11
- {{/if}}
12
- {{#if (eq database 'mongoose')}}
13
- import { mongodbAdapter } from "better-auth/adapters/mongodb";
14
- import { MongoClient } from "mongodb";
15
-
16
- const client = new MongoClient(process.env.MONGODB_URI!);
17
- const db = client.db();
18
- {{/if}}
19
-
20
- export const auth = betterAuth({
21
- {{#if (eq database 'prisma')}}
22
- database: prismaAdapter(client, { provider: "postgresql" }),
23
- {{/if}}
24
- {{#if (eq database 'drizzle')}}
25
- database: drizzleAdapter(db, { provider: "pg" }),
26
- {{/if}}
27
- {{#if (eq database 'mongoose')}}
28
- // Transactions require a replica set. Create user/session/account/verification
29
- // collections before the first sign-up; see the database setup documentation.
30
- database: mongodbAdapter(db, { client }),
31
- {{/if}}
32
- emailAndPassword: {
33
- enabled: true,
34
- },
35
- plugins: [tanstackStartCookies()],
36
- secret: process.env.BETTER_AUTH_SECRET,
37
- baseURL: process.env.BETTER_AUTH_URL,
38
- });
1
+ import "dotenv/config";
2
+ import { betterAuth } from "better-auth";
3
+ import { tanstackStartCookies } from "better-auth/tanstack-start";
4
+
5
+ import type { SocialProvider } from "./auth-schema";
6
+ import { sendEmail } from "./email";
7
+ {{#if (eq database 'prisma')}}
8
+ import { prismaAdapter } from "better-auth/adapters/prisma";
9
+ import client from "./prisma";
10
+ {{/if}}
11
+ {{#if (eq database 'drizzle')}}
12
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
13
+ import { db } from "./db";
14
+ {{/if}}
15
+ {{#if (eq database 'mongoose')}}
16
+ import { mongodbAdapter } from "better-auth/adapters/mongodb";
17
+ import { MongoClient } from "mongodb";
18
+
19
+ const client = new MongoClient(process.env.MONGODB_URI!);
20
+ const db = client.db();
21
+ {{/if}}
22
+
23
+ // Reports a failed email instead of failing the request: the sign-up or reset
24
+ // already happened, and the user can ask for the email again
25
+ function deliver(email: Parameters<typeof sendEmail>[0]): void {
26
+ sendEmail(email).catch((error: unknown) => {
27
+ console.error(`Could not send "${email.subject}" to ${email.to}:`, error);
28
+ });
29
+ }
30
+
31
+ export const auth = betterAuth({
32
+ {{#if (eq database 'prisma')}}
33
+ database: prismaAdapter(client, { provider: "postgresql" }),
34
+ {{/if}}
35
+ {{#if (eq database 'drizzle')}}
36
+ database: drizzleAdapter(db, { provider: "pg" }),
37
+ {{/if}}
38
+ {{#if (eq database 'mongoose')}}
39
+ // Transactions require a replica set. Create user/session/account/verification
40
+ // collections before the first sign-up; see the database setup documentation.
41
+ database: mongodbAdapter(db, { client }),
42
+ {{/if}}
43
+ emailAndPassword: {
44
+ enabled: true,
45
+ // Set to true to block sign-in until the email address is verified
46
+ requireEmailVerification: false,
47
+ // A reset signs the user out everywhere
48
+ revokeSessionsOnPasswordReset: true,
49
+ sendResetPassword: async ({ user, url }) => {
50
+ deliver({
51
+ to: user.email,
52
+ subject: "Reset your password",
53
+ text: `Open this link to choose a new password:
54
+ ${url}`,
55
+ });
56
+ },
57
+ },
58
+ emailVerification: {
59
+ sendOnSignUp: true,
60
+ autoSignInAfterVerification: true,
61
+ sendVerificationEmail: async ({ user, url }) => {
62
+ deliver({
63
+ to: user.email,
64
+ subject: "Verify your email address",
65
+ text: `Open this link to verify your email address:
66
+ ${url}`,
67
+ });
68
+ },
69
+ },
70
+ // Each provider is turned on once its client ID and secret are set
71
+ socialProviders: {
72
+ ...(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
73
+ ? {
74
+ github: {
75
+ clientId: process.env.GITHUB_CLIENT_ID,
76
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
77
+ },
78
+ }
79
+ : {}),
80
+ ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
81
+ ? {
82
+ google: {
83
+ clientId: process.env.GOOGLE_CLIENT_ID,
84
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
85
+ },
86
+ }
87
+ : {}),
88
+ },
89
+ plugins: [tanstackStartCookies()],
90
+ secret: process.env.BETTER_AUTH_SECRET,
91
+ baseURL: process.env.BETTER_AUTH_URL,
92
+ });
93
+
94
+ // The providers turned on above; the sign-in and sign-up pages show a button
95
+ // for each
96
+ export const socialProviders = Object.keys(
97
+ auth.options.socialProviders
98
+ ) as SocialProvider[];
@@ -0,0 +1,43 @@
1
+ // Sends through Resend when RESEND_API_KEY is set. Without it the email is
2
+ // printed to the server log, so verification and reset links can be copied
3
+ // from the terminal during development
4
+ export async function sendEmail({
5
+ to,
6
+ subject,
7
+ text,
8
+ }: {
9
+ to: string;
10
+ subject: string;
11
+ text: string;
12
+ }): Promise<void> {
13
+ const apiKey = process.env.RESEND_API_KEY;
14
+
15
+ if (!apiKey) {
16
+ console.log(`
17
+ --- Email to ${to}: ${subject}
18
+ ${text}
19
+ ---
20
+ `);
21
+ return;
22
+ }
23
+
24
+ const response = await fetch("https://api.resend.com/emails", {
25
+ method: "POST",
26
+ headers: {
27
+ Authorization: `Bearer ${apiKey}`,
28
+ "Content-Type": "application/json",
29
+ },
30
+ body: JSON.stringify({
31
+ from: process.env.EMAIL_FROM || "onboarding@resend.dev",
32
+ to,
33
+ subject,
34
+ text,
35
+ }),
36
+ });
37
+
38
+ if (!response.ok) {
39
+ throw new Error(
40
+ `Resend rejected the email (${response.status}): ${await response.text()}`
41
+ );
42
+ }
43
+ }
@@ -11,6 +11,8 @@
11
11
  import { Route as rootRouteImport } from './routes/__root'
12
12
  import { Route as IndexRouteImport } from './routes/index'
13
13
  import { Route as DashboardRouteImport } from './routes/dashboard'
14
+ import { Route as ForgotPasswordRouteImport } from './routes/forgot-password'
15
+ import { Route as ResetPasswordRouteImport } from './routes/reset-password'
14
16
  import { Route as SignInRouteImport } from './routes/sign-in'
15
17
  import { Route as SignUpRouteImport } from './routes/sign-up'
16
18
  import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
@@ -25,6 +27,16 @@ const DashboardRoute = DashboardRouteImport.update({
25
27
  path: '/dashboard',
26
28
  getParentRoute: () => rootRouteImport,
27
29
  } as any)
30
+ const ForgotPasswordRoute = ForgotPasswordRouteImport.update({
31
+ id: '/forgot-password',
32
+ path: '/forgot-password',
33
+ getParentRoute: () => rootRouteImport,
34
+ } as any)
35
+ const ResetPasswordRoute = ResetPasswordRouteImport.update({
36
+ id: '/reset-password',
37
+ path: '/reset-password',
38
+ getParentRoute: () => rootRouteImport,
39
+ } as any)
28
40
  const SignInRoute = SignInRouteImport.update({
29
41
  id: '/sign-in',
30
42
  path: '/sign-in',
@@ -44,6 +56,8 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
44
56
  export interface FileRoutesByFullPath {
45
57
  '/': typeof IndexRoute
46
58
  '/dashboard': typeof DashboardRoute
59
+ '/forgot-password': typeof ForgotPasswordRoute
60
+ '/reset-password': typeof ResetPasswordRoute
47
61
  '/sign-in': typeof SignInRoute
48
62
  '/sign-up': typeof SignUpRoute
49
63
  '/api/auth/$': typeof ApiAuthSplatRoute
@@ -51,6 +65,8 @@ export interface FileRoutesByFullPath {
51
65
  export interface FileRoutesByTo {
52
66
  '/': typeof IndexRoute
53
67
  '/dashboard': typeof DashboardRoute
68
+ '/forgot-password': typeof ForgotPasswordRoute
69
+ '/reset-password': typeof ResetPasswordRoute
54
70
  '/sign-in': typeof SignInRoute
55
71
  '/sign-up': typeof SignUpRoute
56
72
  '/api/auth/$': typeof ApiAuthSplatRoute
@@ -59,21 +75,47 @@ export interface FileRoutesById {
59
75
  __root__: typeof rootRouteImport
60
76
  '/': typeof IndexRoute
61
77
  '/dashboard': typeof DashboardRoute
78
+ '/forgot-password': typeof ForgotPasswordRoute
79
+ '/reset-password': typeof ResetPasswordRoute
62
80
  '/sign-in': typeof SignInRoute
63
81
  '/sign-up': typeof SignUpRoute
64
82
  '/api/auth/$': typeof ApiAuthSplatRoute
65
83
  }
66
84
  export interface FileRouteTypes {
67
85
  fileRoutesByFullPath: FileRoutesByFullPath
68
- fullPaths: '/' | '/dashboard' | '/sign-in' | '/sign-up' | '/api/auth/$'
86
+ fullPaths:
87
+ | '/'
88
+ | '/dashboard'
89
+ | '/forgot-password'
90
+ | '/reset-password'
91
+ | '/sign-in'
92
+ | '/sign-up'
93
+ | '/api/auth/$'
69
94
  fileRoutesByTo: FileRoutesByTo
70
- to: '/' | '/dashboard' | '/sign-in' | '/sign-up' | '/api/auth/$'
71
- id: '__root__' | '/' | '/dashboard' | '/sign-in' | '/sign-up' | '/api/auth/$'
95
+ to:
96
+ | '/'
97
+ | '/dashboard'
98
+ | '/forgot-password'
99
+ | '/reset-password'
100
+ | '/sign-in'
101
+ | '/sign-up'
102
+ | '/api/auth/$'
103
+ id:
104
+ | '__root__'
105
+ | '/'
106
+ | '/dashboard'
107
+ | '/forgot-password'
108
+ | '/reset-password'
109
+ | '/sign-in'
110
+ | '/sign-up'
111
+ | '/api/auth/$'
72
112
  fileRoutesById: FileRoutesById
73
113
  }
74
114
  export interface RootRouteChildren {
75
115
  IndexRoute: typeof IndexRoute
76
116
  DashboardRoute: typeof DashboardRoute
117
+ ForgotPasswordRoute: typeof ForgotPasswordRoute
118
+ ResetPasswordRoute: typeof ResetPasswordRoute
77
119
  SignInRoute: typeof SignInRoute
78
120
  SignUpRoute: typeof SignUpRoute
79
121
  ApiAuthSplatRoute: typeof ApiAuthSplatRoute
@@ -95,6 +137,20 @@ declare module '@tanstack/react-router' {
95
137
  preLoaderRoute: typeof DashboardRouteImport
96
138
  parentRoute: typeof rootRouteImport
97
139
  }
140
+ '/forgot-password': {
141
+ id: '/forgot-password'
142
+ path: '/forgot-password'
143
+ fullPath: '/forgot-password'
144
+ preLoaderRoute: typeof ForgotPasswordRouteImport
145
+ parentRoute: typeof rootRouteImport
146
+ }
147
+ '/reset-password': {
148
+ id: '/reset-password'
149
+ path: '/reset-password'
150
+ fullPath: '/reset-password'
151
+ preLoaderRoute: typeof ResetPasswordRouteImport
152
+ parentRoute: typeof rootRouteImport
153
+ }
98
154
  '/sign-in': {
99
155
  id: '/sign-in'
100
156
  path: '/sign-in'
@@ -122,6 +178,8 @@ declare module '@tanstack/react-router' {
122
178
  const rootRouteChildren: RootRouteChildren = {
123
179
  IndexRoute: IndexRoute,
124
180
  DashboardRoute: DashboardRoute,
181
+ ForgotPasswordRoute: ForgotPasswordRoute,
182
+ ResetPasswordRoute: ResetPasswordRoute,
125
183
  SignInRoute: SignInRoute,
126
184
  SignUpRoute: SignUpRoute,
127
185
  ApiAuthSplatRoute: ApiAuthSplatRoute,
@@ -1,16 +1,16 @@
1
- import { createFileRoute } from "@tanstack/react-router";
2
-
3
- import { auth } from "~/lib/auth";
4
-
5
- export const Route = createFileRoute("/api/auth/$")({
6
- server: {
7
- handlers: {
8
- GET: async ({ request }: { request: Request }) => {
9
- return await auth.handler(request);
10
- },
11
- POST: async ({ request }: { request: Request }) => {
12
- return await auth.handler(request);
13
- },
14
- },
15
- },
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+
3
+ import { auth } from "~/lib/auth";
4
+
5
+ export const Route = createFileRoute("/api/auth/$")({
6
+ server: {
7
+ handlers: {
8
+ GET: async ({ request }: { request: Request }) => {
9
+ return await auth.handler(request);
10
+ },
11
+ POST: async ({ request }: { request: Request }) => {
12
+ return await auth.handler(request);
13
+ },
14
+ },
15
+ },
16
16
  });
@@ -1,111 +1,119 @@
1
- import { createFileRoute, redirect } from "@tanstack/react-router";
2
- import { Link } from "@tanstack/react-router";
3
- import { ArrowRight, ShieldCheck } from "lucide-react";
4
-
5
- import { SignOutButton } from "~/components/auth/sign-out-button";
6
- import { buttonVariants } from "~/components/ui/button";
7
- import {
8
- Card,
9
- CardContent,
10
- CardDescription,
11
- CardHeader,
12
- CardTitle,
13
- } from "~/components/ui/card";
14
- import { getSession } from "~/lib/auth-functions";
15
- import { cn } from "~/lib/utils";
16
-
17
- export const Route = createFileRoute("/dashboard")({
18
- beforeLoad: async () => {
19
- const session = await getSession();
20
-
21
- if (!session) {
22
- throw redirect({ to: "/sign-in" });
23
- }
24
-
25
- return { user: session.user };
26
- },
27
- component: Dashboard,
28
- });
29
-
30
- function Dashboard() {
31
- const user = Route.useRouteContext({ select: (ctx) => ctx.user });
32
-
33
- return (
34
- <main className="min-h-screen bg-[linear-gradient(180deg,_#f8fafc_0%,_#eef2ff_100%)] px-6 py-16 sm:px-10">
35
- <div className="mx-auto max-w-5xl space-y-8">
36
- <header className="flex flex-col gap-6 rounded-[2rem] border border-white/70 bg-white/85 p-8 shadow-xl shadow-slate-200/60 backdrop-blur md:flex-row md:items-end md:justify-between">
37
- <div className="space-y-3">
38
- <span className="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium uppercase tracking-[0.24em] text-emerald-700">
39
- <ShieldCheck className="size-4" />
40
- Authenticated session
41
- </span>
42
- <div className="space-y-1">
43
- <h1 className="text-3xl font-semibold tracking-tight text-slate-950">
44
- Welcome {user.name}
45
- </h1>
46
- <p className="text-sm text-slate-600">{user.email}</p>
47
- </div>
48
- </div>
49
- <SignOutButton />
50
- </header>
51
-
52
- <section className="grid gap-5 md:grid-cols-2">
53
- <Card>
54
- <CardHeader>
55
- <CardTitle>Protected route example</CardTitle>
56
- <CardDescription>
57
- This page verifies the session on the server before rendering any
58
- content.
59
- </CardDescription>
60
- </CardHeader>
61
- <CardContent className="space-y-4 text-sm text-slate-600">
62
- <p>
63
- The route guard calls{" "}
64
- <code className="font-mono text-slate-900">getSession()</code> in
65
- <code className="font-mono text-slate-900"> beforeLoad</code>,
66
- which reads the cookie headers server-side via{" "}
67
- <code className="font-mono text-slate-900">
68
- auth.api.getSession
69
- </code>{" "}
70
- on every render and navigation.
71
- </p>
72
- <p>
73
- That keeps route protection aligned with Better Auth&apos;s
74
- TanStack Start guidance while staying easy to extend.
75
- </p>
76
- </CardContent>
77
- </Card>
78
-
79
- <Card>
80
- <CardHeader>
81
- <CardTitle>Suggested next steps</CardTitle>
82
- <CardDescription>
83
- Keep the generated auth flow, then customize the app around your
84
- product.
85
- </CardDescription>
86
- </CardHeader>
87
- <CardContent className="space-y-3 text-sm text-slate-600">
88
- <p>
89
- Replace the demo dashboard with your first authenticated page.
90
- </p>
91
- <p>Extend the schema with profile or onboarding fields.</p>
92
- <p>
93
- Add provider-based auth or email verification later if needed.
94
- </p>
95
- <Link
96
- to="/"
97
- className={cn(
98
- buttonVariants({ variant: "outline" }),
99
- "w-fit gap-2"
100
- )}
101
- >
102
- Back to home
103
- <ArrowRight className="size-4" />
104
- </Link>
105
- </CardContent>
106
- </Card>
107
- </section>
108
- </div>
109
- </main>
110
- );
111
- }
1
+ import { createFileRoute, Link, redirect } from "@tanstack/react-router";
2
+ import { ArrowRight, ShieldCheck } from "lucide-react";
3
+
4
+ import { ChangePasswordForm } from "~/components/auth/change-password-form";
5
+ import { EmailVerificationCard } from "~/components/auth/email-verification-card";
6
+ import { SignOutButton } from "~/components/auth/sign-out-button";
7
+ import { Badge } from "~/components/ui/badge";
8
+ import { Button } from "~/components/ui/button";
9
+ import {
10
+ Card,
11
+ CardContent,
12
+ CardDescription,
13
+ CardFooter,
14
+ CardHeader,
15
+ CardTitle,
16
+ } from "~/components/ui/card";
17
+ import { getSession } from "~/lib/auth-functions";
18
+
19
+ export const Route = createFileRoute("/dashboard")({
20
+ beforeLoad: async () => {
21
+ const session = await getSession();
22
+
23
+ if (!session) {
24
+ throw redirect({ to: "/sign-in" });
25
+ }
26
+
27
+ return { user: session.user };
28
+ },
29
+ component: Dashboard,
30
+ });
31
+
32
+ function Dashboard() {
33
+ const user = Route.useRouteContext({ select: (ctx) => ctx.user });
34
+
35
+ return (
36
+ <main className="min-h-svh bg-muted px-6 py-16 sm:px-10">
37
+ <div className="mx-auto flex max-w-5xl flex-col gap-8">
38
+ <header className="flex flex-col gap-6 md:flex-row md:items-end md:justify-between">
39
+ <div className="flex flex-col gap-3">
40
+ <Badge variant="secondary">
41
+ <ShieldCheck data-icon="inline-start" />
42
+ Authenticated session
43
+ </Badge>
44
+ <div className="flex flex-col gap-1">
45
+ <h1 className="text-3xl font-semibold tracking-tight">
46
+ Welcome {user.name}
47
+ </h1>
48
+ <p className="text-sm text-muted-foreground">{user.email}</p>
49
+ </div>
50
+ </div>
51
+ <SignOutButton />
52
+ </header>
53
+
54
+ <section className="grid gap-5 md:grid-cols-2">
55
+ <EmailVerificationCard
56
+ email={user.email}
57
+ emailVerified={user.emailVerified}
58
+ />
59
+ <ChangePasswordForm />
60
+ <Card>
61
+ <CardHeader>
62
+ <CardTitle>Protected route example</CardTitle>
63
+ <CardDescription>
64
+ This page verifies the session on the server before rendering
65
+ any content.
66
+ </CardDescription>
67
+ </CardHeader>
68
+ <CardContent className="flex flex-col gap-4 text-muted-foreground">
69
+ <p>
70
+ The route guard calls{" "}
71
+ <code className="font-mono text-foreground">getSession()</code>{" "}
72
+ in <code className="font-mono text-foreground">beforeLoad</code>,
73
+ which reads the cookie headers server-side via{" "}
74
+ <code className="font-mono text-foreground">
75
+ auth.api.getSession
76
+ </code>{" "}
77
+ on every render and navigation.
78
+ </p>
79
+ <p>
80
+ That keeps route protection aligned with Better Auth&apos;s
81
+ TanStack Start guidance while staying easy to extend.
82
+ </p>
83
+ </CardContent>
84
+ </Card>
85
+
86
+ <Card>
87
+ <CardHeader>
88
+ <CardTitle>Suggested next steps</CardTitle>
89
+ <CardDescription>
90
+ Keep the generated auth flow, then customize the app around your
91
+ product.
92
+ </CardDescription>
93
+ </CardHeader>
94
+ <CardContent className="flex flex-col gap-3 text-muted-foreground">
95
+ <p>
96
+ Replace the demo dashboard with your first authenticated page.
97
+ </p>
98
+ <p>Extend the schema with profile or onboarding fields.</p>
99
+ <p>
100
+ Add a GitHub or Google client ID and secret to .env to turn on
101
+ social sign-in, and RESEND_API_KEY to send real emails.
102
+ </p>
103
+ </CardContent>
104
+ <CardFooter>
105
+ <Button
106
+ variant="outline"
107
+ nativeButton={false}
108
+ render={<Link to="/" />}
109
+ >
110
+ Back to home
111
+ <ArrowRight data-icon="inline-end" />
112
+ </Button>
113
+ </CardFooter>
114
+ </Card>
115
+ </section>
116
+ </div>
117
+ </main>
118
+ );
119
+ }
@@ -0,0 +1,17 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+
3
+ import { ForgotPasswordForm } from "~/components/auth/forgot-password-form";
4
+
5
+ export const Route = createFileRoute("/forgot-password")({
6
+ component: ForgotPasswordPage,
7
+ });
8
+
9
+ function ForgotPasswordPage() {
10
+ return (
11
+ <main className="flex min-h-svh items-center justify-center bg-muted p-6 md:p-10">
12
+ <div className="w-full max-w-md">
13
+ <ForgotPasswordForm />
14
+ </div>
15
+ </main>
16
+ );
17
+ }
@@ -0,0 +1,25 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+
3
+ import { ResetPasswordForm } from "~/components/auth/reset-password-form";
4
+
5
+ // The link in the reset email lands here with ?token=..., or with ?error=...
6
+ // when the token is invalid or has expired
7
+ export const Route = createFileRoute("/reset-password")({
8
+ validateSearch: (search: Record<string, unknown>) => ({
9
+ token: typeof search.token === "string" ? search.token : undefined,
10
+ error: typeof search.error === "string" ? search.error : undefined,
11
+ }),
12
+ component: ResetPasswordPage,
13
+ });
14
+
15
+ function ResetPasswordPage() {
16
+ const { token, error } = Route.useSearch();
17
+
18
+ return (
19
+ <main className="flex min-h-svh items-center justify-center bg-muted p-6 md:p-10">
20
+ <div className="w-full max-w-md">
21
+ <ResetPasswordForm token={error ? null : (token ?? null)} />
22
+ </div>
23
+ </main>
24
+ );
25
+ }