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,69 +1,117 @@
1
+ import { Suspense } from "react";
2
+
1
3
  import Link from "next/link";
2
- import { headers } from "next/headers";
3
4
  import { redirect } from "next/navigation";
5
+
4
6
  import { ArrowRight, ShieldCheck } from "lucide-react";
5
7
 
8
+ import { ChangePasswordForm } from "@/components/auth/change-password-form";
9
+ import { EmailVerificationCard } from "@/components/auth/email-verification-card";
6
10
  import { SignOutButton } from "@/components/auth/sign-out-button";
7
- import { buttonVariants } from "@/components/ui/button";
11
+ import { Badge } from "@/components/ui/badge";
12
+ import { Button } from "@/components/ui/button";
8
13
  import {
9
14
  Card,
10
15
  CardContent,
11
16
  CardDescription,
17
+ CardFooter,
12
18
  CardHeader,
13
19
  CardTitle,
14
20
  } from "@/components/ui/card";
15
- import { auth } from "@/lib/auth";
16
- import { cn } from "@/lib/utils";
21
+ import { Skeleton } from "@/components/ui/skeleton";
22
+ import { getSession } from "@/lib/session";
17
23
 
18
- export default async function DashboardPage() {
19
- const session = await auth.api.getSession({
20
- headers: await headers(),
21
- });
24
+ // The parts that need the user wait for the session in their own <Suspense>
25
+ // boundary, so the rest of the page is sent without waiting for it
26
+ async function DashboardHeader() {
27
+ const session = await getSession();
22
28
 
29
+ // proxy.ts already sent visitors without a session cookie to /sign-in; this
30
+ // catches an expired or revoked session
23
31
  if (!session) {
24
32
  redirect("/sign-in");
25
33
  }
26
34
 
27
35
  return (
28
- <main className="min-h-screen bg-[linear-gradient(180deg,_#f8fafc_0%,_#eef2ff_100%)] px-6 py-16 sm:px-10">
29
- <div className="mx-auto max-w-5xl space-y-8">
30
- <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">
31
- <div className="space-y-3">
32
- <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">
33
- <ShieldCheck className="size-4" />
34
- Authenticated session
35
- </span>
36
- <div className="space-y-1">
37
- <h1 className="text-3xl font-semibold tracking-tight text-slate-950">
38
- Welcome {session.user.name}
39
- </h1>
40
- <p className="text-sm text-slate-600">{session.user.email}</p>
41
- </div>
42
- </div>
43
- <SignOutButton />
44
- </header>
36
+ <header className="flex flex-col gap-6 md:flex-row md:items-end md:justify-between">
37
+ <div className="flex flex-col gap-3">
38
+ <Badge variant="secondary">
39
+ <ShieldCheck data-icon="inline-start" />
40
+ Authenticated session
41
+ </Badge>
42
+ <div className="flex flex-col gap-1">
43
+ <h1 className="text-3xl font-semibold tracking-tight">
44
+ Welcome {session.user.name}
45
+ </h1>
46
+ <p className="text-sm text-muted-foreground">{session.user.email}</p>
47
+ </div>
48
+ </div>
49
+ <SignOutButton />
50
+ </header>
51
+ );
52
+ }
53
+
54
+ function DashboardHeaderSkeleton() {
55
+ return (
56
+ <div className="flex flex-col gap-3" aria-busy="true">
57
+ <Skeleton className="h-5 w-44" />
58
+ <Skeleton className="h-9 w-64" />
59
+ <Skeleton className="h-4 w-40" />
60
+ </div>
61
+ );
62
+ }
63
+
64
+ async function EmailVerification() {
65
+ const session = await getSession();
66
+
67
+ return session ? (
68
+ <EmailVerificationCard
69
+ email={session.user.email}
70
+ emailVerified={session.user.emailVerified}
71
+ />
72
+ ) : null;
73
+ }
74
+
75
+ export default function DashboardPage() {
76
+ return (
77
+ <main className="min-h-svh bg-muted px-6 py-16 sm:px-10">
78
+ <div className="mx-auto flex max-w-5xl flex-col gap-8">
79
+ <Suspense fallback={<DashboardHeaderSkeleton />}>
80
+ <DashboardHeader />
81
+ </Suspense>
45
82
 
46
83
  <section className="grid gap-5 md:grid-cols-2">
84
+ <Suspense fallback={<Skeleton className="h-40 rounded-xl" />}>
85
+ <EmailVerification />
86
+ </Suspense>
87
+ <ChangePasswordForm />
47
88
  <Card>
48
89
  <CardHeader>
49
90
  <CardTitle>Protected route example</CardTitle>
50
91
  <CardDescription>
51
- This page verifies the session on the server before rendering any
52
- content.
92
+ The page is sent right away; the parts that need the user stream
93
+ in once the server has checked the session.
53
94
  </CardDescription>
54
95
  </CardHeader>
55
- <CardContent className="space-y-4 text-sm text-slate-600">
96
+ <CardContent className="flex flex-col gap-4 text-muted-foreground">
56
97
  <p>
57
- The generated proxy only performs an optimistic cookie check. The
58
- authoritative guard happens here with{" "}
59
- <code className="font-mono text-slate-900">
60
- auth.api.getSession({"{"} headers: await headers() {"}"})
61
- </code>
62
- .
98
+ <code className="font-mono text-foreground">proxy.ts</code>{" "}
99
+ sends visitors without a session cookie to the sign-in page.
100
+ Each part that needs the user calls{" "}
101
+ <code className="font-mono text-foreground">getSession()</code>{" "}
102
+ from{" "}
103
+ <code className="font-mono text-foreground">
104
+ lib/session.ts
105
+ </code>{" "}
106
+ inside its own{" "}
107
+ <code className="font-mono text-foreground">
108
+ &lt;Suspense&gt;
109
+ </code>{" "}
110
+ boundary, and the session is looked up once per request.
63
111
  </p>
64
112
  <p>
65
- That keeps route protection aligned with Better Auth&apos;s
66
- Next.js guidance while staying easy to extend.
113
+ Check the session on the server like this in every protected
114
+ page, route handler, and server action.
67
115
  </p>
68
116
  </CardContent>
69
117
  </Card>
@@ -76,18 +124,26 @@ export default async function DashboardPage() {
76
124
  product.
77
125
  </CardDescription>
78
126
  </CardHeader>
79
- <CardContent className="space-y-3 text-sm text-slate-600">
80
- <p>Replace the demo dashboard with your first authenticated page.</p>
127
+ <CardContent className="flex flex-col gap-3 text-muted-foreground">
128
+ <p>
129
+ Replace the demo dashboard with your first authenticated page.
130
+ </p>
81
131
  <p>Extend the schema with profile or onboarding fields.</p>
82
- <p>Add provider-based auth or email verification later if needed.</p>
83
- <Link
84
- href="/"
85
- className={cn(buttonVariants({ variant: "outline" }), "w-fit gap-2")}
132
+ <p>
133
+ Add a GitHub or Google client ID and secret to .env to turn on
134
+ social sign-in, and RESEND_API_KEY to send real emails.
135
+ </p>
136
+ </CardContent>
137
+ <CardFooter>
138
+ <Button
139
+ variant="outline"
140
+ nativeButton={false}
141
+ render={<Link href="/" />}
86
142
  >
87
143
  Back to home
88
- <ArrowRight className="size-4" />
89
- </Link>
90
- </CardContent>
144
+ <ArrowRight data-icon="inline-end" />
145
+ </Button>
146
+ </CardFooter>
91
147
  </Card>
92
148
  </section>
93
149
  </div>
@@ -0,0 +1,11 @@
1
+ import { ForgotPasswordForm } from "@/components/auth/forgot-password-form";
2
+
3
+ export default function ForgotPasswordPage() {
4
+ return (
5
+ <main className="flex min-h-svh items-center justify-center bg-muted p-6 md:p-10">
6
+ <div className="w-full max-w-md">
7
+ <ForgotPasswordForm />
8
+ </div>
9
+ </main>
10
+ );
11
+ }
@@ -0,0 +1,17 @@
1
+ import { Suspense } from "react";
2
+
3
+ import { ResetPasswordForm } from "@/components/auth/reset-password-form";
4
+
5
+ // A static page: the form reads the token from the URL in the browser, inside
6
+ // <Suspense> as useSearchParams() requires
7
+ export default function ResetPasswordPage() {
8
+ return (
9
+ <main className="flex min-h-svh items-center justify-center bg-muted p-6 md:p-10">
10
+ <div className="w-full max-w-md">
11
+ <Suspense>
12
+ <ResetPasswordForm />
13
+ </Suspense>
14
+ </div>
15
+ </main>
16
+ );
17
+ }
@@ -1,35 +1,41 @@
1
- import { headers } from "next/headers";
1
+ import { Suspense } from "react";
2
+
2
3
  import { redirect } from "next/navigation";
3
4
 
4
5
  import { SignInForm } from "@/components/auth/sign-in-form";
5
- import { auth } from "@/lib/auth";
6
-
7
- export default async function SignInPage() {
8
- const session = await auth.api.getSession({
9
- headers: await headers(),
10
- });
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { socialProviders } from "@/lib/auth";
8
+ import { getSession } from "@/lib/session";
11
9
 
12
- if (session) {
10
+ // Sends a signed-in visitor to the dashboard. It runs in its own <Suspense>
11
+ // boundary, so the form is sent without waiting for the session lookup
12
+ async function RedirectIfSignedIn() {
13
+ if (await getSession()) {
13
14
  redirect("/dashboard");
14
15
  }
15
16
 
17
+ return null;
18
+ }
19
+
20
+ export default function SignInPage() {
16
21
  return (
17
- <main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(14,165,233,0.16),_transparent_30%),linear-gradient(180deg,_#f8fafc_0%,_#eef2ff_100%)] px-6 py-16 sm:px-10">
18
- <div className="mx-auto flex min-h-[calc(100vh-8rem)] max-w-5xl items-center gap-12 lg:grid lg:grid-cols-[0.9fr_1.1fr]">
19
- <section className="hidden space-y-5 lg:block">
20
- <span className="inline-flex rounded-full border border-sky-200 bg-white/80 px-3 py-1 text-xs font-medium uppercase tracking-[0.24em] text-sky-700">
21
- Better Auth
22
- </span>
23
- <h1 className="text-4xl font-semibold tracking-tight text-slate-950">
22
+ <main className="flex min-h-svh items-center justify-center bg-muted p-6 md:p-10">
23
+ <div className="grid w-full max-w-5xl items-center gap-12 lg:grid-cols-[0.9fr_1.1fr]">
24
+ <section className="hidden flex-col gap-5 lg:flex">
25
+ <Badge variant="outline">Better Auth</Badge>
26
+ <h1 className="text-4xl font-semibold tracking-tight">
24
27
  Welcome back.
25
28
  </h1>
26
- <p className="max-w-md text-base leading-7 text-slate-600">
29
+ <p className="max-w-md text-base leading-7 text-muted-foreground">
27
30
  This generated starter includes protected routes, session-aware page
28
31
  guards, and reusable auth form primitives you can extend from here.
29
32
  </p>
30
33
  </section>
31
- <SignInForm />
34
+ <SignInForm providers={socialProviders} />
32
35
  </div>
36
+ <Suspense fallback={null}>
37
+ <RedirectIfSignedIn />
38
+ </Suspense>
33
39
  </main>
34
40
  );
35
41
  }
@@ -1,35 +1,41 @@
1
- import { headers } from "next/headers";
1
+ import { Suspense } from "react";
2
+
2
3
  import { redirect } from "next/navigation";
3
4
 
4
5
  import { SignUpForm } from "@/components/auth/sign-up-form";
5
- import { auth } from "@/lib/auth";
6
-
7
- export default async function SignUpPage() {
8
- const session = await auth.api.getSession({
9
- headers: await headers(),
10
- });
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { socialProviders } from "@/lib/auth";
8
+ import { getSession } from "@/lib/session";
11
9
 
12
- if (session) {
10
+ // Sends a signed-in visitor to the dashboard. It runs in its own <Suspense>
11
+ // boundary, so the form is sent without waiting for the session lookup
12
+ async function RedirectIfSignedIn() {
13
+ if (await getSession()) {
13
14
  redirect("/dashboard");
14
15
  }
15
16
 
17
+ return null;
18
+ }
19
+
20
+ export default function SignUpPage() {
16
21
  return (
17
- <main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(56,189,248,0.18),_transparent_28%),linear-gradient(180deg,_#f8fafc_0%,_#ecfeff_100%)] px-6 py-16 sm:px-10">
18
- <div className="mx-auto flex min-h-[calc(100vh-8rem)] max-w-5xl items-center gap-12 lg:grid lg:grid-cols-[0.9fr_1.1fr]">
19
- <section className="hidden space-y-5 lg:block">
20
- <span className="inline-flex rounded-full border border-sky-200 bg-white/80 px-3 py-1 text-xs font-medium uppercase tracking-[0.24em] text-sky-700">
21
- Better Auth
22
- </span>
23
- <h1 className="text-4xl font-semibold tracking-tight text-slate-950">
22
+ <main className="flex min-h-svh items-center justify-center bg-muted p-6 md:p-10">
23
+ <div className="grid w-full max-w-5xl items-center gap-12 lg:grid-cols-[0.9fr_1.1fr]">
24
+ <section className="hidden flex-col gap-5 lg:flex">
25
+ <Badge variant="outline">Better Auth</Badge>
26
+ <h1 className="text-4xl font-semibold tracking-tight">
24
27
  Create your account.
25
28
  </h1>
26
- <p className="max-w-md text-base leading-7 text-slate-600">
27
- Start with email and password today, then layer in profile data,
28
- onboarding, and provider-based auth when your product needs it.
29
+ <p className="max-w-md text-base leading-7 text-muted-foreground">
30
+ Sign up with email and password, or with GitHub or Google once their
31
+ keys are set. A verification link goes to your inbox.
29
32
  </p>
30
33
  </section>
31
- <SignUpForm />
34
+ <SignUpForm providers={socialProviders} />
32
35
  </div>
36
+ <Suspense fallback={null}>
37
+ <RedirectIfSignedIn />
38
+ </Suspense>
33
39
  </main>
34
40
  );
35
41
  }
@@ -0,0 +1,125 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+ import { zodResolver } from "@hookform/resolvers/zod";
5
+ import { CircleAlert, CircleCheck } from "lucide-react";
6
+ import { useForm } from "react-hook-form";
7
+
8
+ import { authClient } from "@/lib/auth-client";
9
+ import {
10
+ changePasswordSchema,
11
+ getAuthErrorMessage,
12
+ type ChangePasswordValues,
13
+ } from "@/lib/auth-schema";
14
+ import { Alert, AlertDescription } from "@/components/ui/alert";
15
+ import { Button } from "@/components/ui/button";
16
+ import {
17
+ Card,
18
+ CardContent,
19
+ CardDescription,
20
+ CardHeader,
21
+ CardTitle,
22
+ } from "@/components/ui/card";
23
+ import {
24
+ Field,
25
+ FieldError,
26
+ FieldGroup,
27
+ FieldLabel,
28
+ } from "@/components/ui/field";
29
+ import { Input } from "@/components/ui/input";
30
+ import { Spinner } from "@/components/ui/spinner";
31
+
32
+ export function ChangePasswordForm() {
33
+ const [isPending, startTransition] = useTransition();
34
+ const [message, setMessage] = useState<{
35
+ text: string;
36
+ isError: boolean;
37
+ } | null>(null);
38
+ const form = useForm<ChangePasswordValues>({
39
+ resolver: zodResolver(changePasswordSchema),
40
+ defaultValues: { currentPassword: "", newPassword: "" },
41
+ });
42
+
43
+ const onSubmit = form.handleSubmit((values) => {
44
+ setMessage(null);
45
+
46
+ startTransition(async () => {
47
+ // Other devices are signed out; this one stays signed in
48
+ const result = await authClient.changePassword({
49
+ currentPassword: values.currentPassword,
50
+ newPassword: values.newPassword,
51
+ revokeOtherSessions: true,
52
+ });
53
+
54
+ if (result.error) {
55
+ setMessage({ text: getAuthErrorMessage(result.error), isError: true });
56
+ return;
57
+ }
58
+
59
+ form.reset();
60
+ setMessage({ text: "Password changed.", isError: false });
61
+ });
62
+ });
63
+
64
+ return (
65
+ <Card>
66
+ <CardHeader>
67
+ <CardTitle>Change password</CardTitle>
68
+ <CardDescription>
69
+ Other devices are signed out after the change.
70
+ </CardDescription>
71
+ </CardHeader>
72
+ <CardContent>
73
+ <form onSubmit={onSubmit} noValidate>
74
+ <FieldGroup>
75
+ <Field
76
+ data-invalid={Boolean(form.formState.errors.currentPassword)}
77
+ >
78
+ <FieldLabel htmlFor="currentPassword">Current password</FieldLabel>
79
+ <Input
80
+ id="currentPassword"
81
+ type="password"
82
+ autoComplete="current-password"
83
+ aria-invalid={Boolean(form.formState.errors.currentPassword)}
84
+ {...form.register("currentPassword")}
85
+ />
86
+ <FieldError errors={[form.formState.errors.currentPassword]} />
87
+ </Field>
88
+
89
+ <Field data-invalid={Boolean(form.formState.errors.newPassword)}>
90
+ <FieldLabel htmlFor="newPassword">New password</FieldLabel>
91
+ <Input
92
+ id="newPassword"
93
+ type="password"
94
+ autoComplete="new-password"
95
+ aria-invalid={Boolean(form.formState.errors.newPassword)}
96
+ {...form.register("newPassword")}
97
+ />
98
+ <FieldError errors={[form.formState.errors.newPassword]} />
99
+ </Field>
100
+
101
+ {message ? (
102
+ <Alert variant={message.isError ? "destructive" : "default"}>
103
+ {message.isError ? <CircleAlert /> : <CircleCheck />}
104
+ <AlertDescription>{message.text}</AlertDescription>
105
+ </Alert>
106
+ ) : null}
107
+
108
+ <Field orientation="horizontal">
109
+ <Button type="submit" disabled={isPending}>
110
+ {isPending ? (
111
+ <>
112
+ <Spinner data-icon="inline-start" />
113
+ Saving
114
+ </>
115
+ ) : (
116
+ "Change password"
117
+ )}
118
+ </Button>
119
+ </Field>
120
+ </FieldGroup>
121
+ </form>
122
+ </CardContent>
123
+ </Card>
124
+ );
125
+ }
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+ import { BadgeCheck, MailWarning } from "lucide-react";
5
+
6
+ import { authClient } from "@/lib/auth-client";
7
+ import { getAuthErrorMessage } from "@/lib/auth-schema";
8
+ import { Badge } from "@/components/ui/badge";
9
+ import { Button } from "@/components/ui/button";
10
+ import {
11
+ Card,
12
+ CardAction,
13
+ CardContent,
14
+ CardDescription,
15
+ CardHeader,
16
+ CardTitle,
17
+ } from "@/components/ui/card";
18
+ import { Spinner } from "@/components/ui/spinner";
19
+
20
+ // Shows whether the email address is verified, with a button to send the
21
+ // verification email again
22
+ export function EmailVerificationCard({
23
+ email,
24
+ emailVerified,
25
+ }: {
26
+ email: string;
27
+ emailVerified: boolean;
28
+ }) {
29
+ const [isPending, startTransition] = useTransition();
30
+ const [message, setMessage] = useState<string | null>(null);
31
+
32
+ function resend() {
33
+ setMessage(null);
34
+ startTransition(async () => {
35
+ const result = await authClient.sendVerificationEmail({
36
+ email,
37
+ callbackURL: "/dashboard",
38
+ });
39
+ setMessage(
40
+ result.error
41
+ ? getAuthErrorMessage(result.error)
42
+ : "Verification email sent. Without an email provider set up, the link is printed in the server terminal."
43
+ );
44
+ });
45
+ }
46
+
47
+ return (
48
+ <Card>
49
+ <CardHeader>
50
+ <CardTitle>
51
+ {emailVerified ? "Email verified" : "Verify your email"}
52
+ </CardTitle>
53
+ <CardDescription>
54
+ {emailVerified
55
+ ? `${email} is confirmed.`
56
+ : `We sent a link to ${email}. Open it to confirm the address.`}
57
+ </CardDescription>
58
+ <CardAction>
59
+ {emailVerified ? (
60
+ <Badge variant="secondary">
61
+ <BadgeCheck data-icon="inline-start" />
62
+ Verified
63
+ </Badge>
64
+ ) : (
65
+ <Badge variant="outline">
66
+ <MailWarning data-icon="inline-start" />
67
+ Pending
68
+ </Badge>
69
+ )}
70
+ </CardAction>
71
+ </CardHeader>
72
+ {emailVerified ? null : (
73
+ <CardContent className="flex flex-col items-start gap-3">
74
+ <Button variant="outline" onClick={resend} disabled={isPending}>
75
+ {isPending ? (
76
+ <>
77
+ <Spinner data-icon="inline-start" />
78
+ Sending
79
+ </>
80
+ ) : (
81
+ "Send the email again"
82
+ )}
83
+ </Button>
84
+ {message ? (
85
+ <p className="text-sm text-muted-foreground">{message}</p>
86
+ ) : null}
87
+ </CardContent>
88
+ )}
89
+ </Card>
90
+ );
91
+ }