better-ts-stack 2.1.2 → 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 (117) hide show
  1. package/README.md +16 -8
  2. package/dist/args.js +2 -1
  3. package/dist/builder/buildPlanner.js +7 -1
  4. package/dist/builder/configGenerator.js +28 -3
  5. package/dist/builder/index.js +2 -1
  6. package/dist/builder/readmeGenerator.js +2 -2
  7. package/dist/output/nextSteps.js +2 -4
  8. package/dist/prompts/stack.js +3 -5
  9. package/dist/types/index.js +1 -1
  10. package/package.json +1 -1
  11. package/templates/backend/express/src/env.ts.hbs +13 -2
  12. package/templates/backend/express/src/index.ts.hbs +31 -2
  13. package/templates/backend/nestjs/src/app.module.ts.hbs +4 -1
  14. package/templates/backend/nestjs/src/env.ts.hbs +13 -2
  15. package/templates/backend/nestjs/src/health/health.controller.ts.hbs +7 -0
  16. package/templates/backend/nestjs/src/{main.ts → main.ts.hbs} +11 -0
  17. package/templates/frontend/nextjs/app/globals.css +119 -79
  18. package/templates/frontend/nextjs/app/layout.tsx +13 -11
  19. package/templates/frontend/nextjs/app/page.tsx.hbs +63 -43
  20. package/templates/frontend/nextjs/components/ui/alert.tsx +75 -0
  21. package/templates/frontend/nextjs/components/ui/badge.tsx +51 -0
  22. package/templates/frontend/nextjs/components/ui/button.tsx +33 -27
  23. package/templates/frontend/nextjs/components/ui/card.tsx +76 -34
  24. package/templates/frontend/nextjs/components/ui/field.tsx +203 -32
  25. package/templates/frontend/nextjs/components/ui/input.tsx +8 -7
  26. package/templates/frontend/nextjs/components/ui/label.tsx +19 -0
  27. package/templates/frontend/nextjs/components/ui/separator.tsx +24 -0
  28. package/templates/frontend/nextjs/components/ui/skeleton.tsx +13 -0
  29. package/templates/frontend/nextjs/components/ui/spinner.tsx +10 -0
  30. package/templates/frontend/nextjs/components.json +11 -5
  31. package/templates/frontend/nextjs/config.json +4 -3
  32. package/templates/frontend/nextjs/lib/utils.ts +1 -6
  33. package/templates/frontend/tanstack/components.json +12 -5
  34. package/templates/frontend/tanstack/config.json +5 -2
  35. package/templates/frontend/tanstack/src/components/ui/alert.tsx +75 -0
  36. package/templates/frontend/tanstack/src/components/ui/badge.tsx +51 -0
  37. package/templates/frontend/tanstack/src/components/ui/button.tsx +33 -27
  38. package/templates/frontend/tanstack/src/components/ui/card.tsx +76 -34
  39. package/templates/frontend/tanstack/src/components/ui/field.tsx +201 -32
  40. package/templates/frontend/tanstack/src/components/ui/input.tsx +8 -7
  41. package/templates/frontend/tanstack/src/components/ui/label.tsx +17 -0
  42. package/templates/frontend/tanstack/src/components/ui/separator.tsx +24 -0
  43. package/templates/frontend/tanstack/src/components/ui/skeleton.tsx +13 -0
  44. package/templates/frontend/tanstack/src/components/ui/spinner.tsx +10 -0
  45. package/templates/frontend/tanstack/src/lib/utils.ts +1 -6
  46. package/templates/frontend/tanstack/src/routes/index.tsx.hbs +64 -45
  47. package/templates/frontend/tanstack/src/styles/app.css +100 -55
  48. package/templates/modules/agents/AGENTS.md.hbs +29 -21
  49. package/templates/modules/auth/express/config.json +12 -11
  50. package/templates/modules/auth/express/src/lib/auth.ts.hbs +143 -0
  51. package/templates/modules/auth/express/src/lib/email.ts +37 -0
  52. package/templates/modules/auth/express/src/middleware/requireAuth.ts +29 -0
  53. package/templates/modules/auth/express/src/routes/me.ts +13 -0
  54. package/templates/modules/auth/nestjs/config.json +13 -11
  55. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +64 -14
  56. package/templates/modules/auth/nestjs/src/auth/auth.ts.hbs +152 -0
  57. package/templates/modules/auth/nestjs/src/auth/email.ts +37 -0
  58. package/templates/modules/auth/nestjs/src/me/me.controller.ts +15 -0
  59. package/templates/modules/auth/nextjs/app/dashboard/page.tsx +101 -45
  60. package/templates/modules/auth/nextjs/app/forgot-password/page.tsx +11 -0
  61. package/templates/modules/auth/nextjs/app/reset-password/page.tsx +17 -0
  62. package/templates/modules/auth/nextjs/app/sign-in/page.tsx +23 -17
  63. package/templates/modules/auth/nextjs/app/sign-up/page.tsx +25 -19
  64. package/templates/modules/auth/nextjs/components/auth/change-password-form.tsx +125 -0
  65. package/templates/modules/auth/nextjs/components/auth/email-verification-card.tsx +91 -0
  66. package/templates/modules/auth/nextjs/components/auth/forgot-password-form.tsx +133 -0
  67. package/templates/modules/auth/nextjs/components/auth/reset-password-form.tsx +155 -0
  68. package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +46 -32
  69. package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +6 -5
  70. package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +36 -30
  71. package/templates/modules/auth/nextjs/components/auth/social-buttons.tsx +73 -0
  72. package/templates/modules/auth/nextjs/config.json +8 -2
  73. package/templates/modules/auth/nextjs/lib/auth-schema.ts +27 -2
  74. package/templates/modules/auth/nextjs/lib/auth.ts.hbs +65 -0
  75. package/templates/modules/auth/nextjs/lib/email.ts +43 -0
  76. package/templates/modules/auth/nextjs/lib/session.ts +12 -0
  77. package/templates/modules/auth/tanstack/config.json +9 -3
  78. package/templates/modules/auth/tanstack/src/components/auth/change-password-form.tsx +123 -0
  79. package/templates/modules/auth/tanstack/src/components/auth/email-verification-card.tsx +89 -0
  80. package/templates/modules/auth/tanstack/src/components/auth/forgot-password-form.tsx +131 -0
  81. package/templates/modules/auth/tanstack/src/components/auth/reset-password-form.tsx +149 -0
  82. package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +44 -35
  83. package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +7 -6
  84. package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +37 -34
  85. package/templates/modules/auth/tanstack/src/components/auth/social-buttons.tsx +71 -0
  86. package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +7 -1
  87. package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +27 -2
  88. package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +61 -1
  89. package/templates/modules/auth/tanstack/src/lib/email.ts +43 -0
  90. package/templates/modules/auth/tanstack/src/routeTree.gen.ts +61 -3
  91. package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +40 -32
  92. package/templates/modules/auth/tanstack/src/routes/forgot-password.tsx +17 -0
  93. package/templates/modules/auth/tanstack/src/routes/reset-password.tsx +25 -0
  94. package/templates/modules/auth/tanstack/src/routes/sign-in.tsx +19 -12
  95. package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +21 -14
  96. package/templates/modules/drizzle/express/src/lib/schema.ts.hbs +90 -0
  97. package/templates/modules/drizzle/nestjs/src/database/schema.ts.hbs +90 -0
  98. package/templates/modules/mongoose/nestjs/config.json +2 -1
  99. package/templates/modules/prisma/express/prisma/schema.prisma.hbs +92 -0
  100. package/templates/modules/prisma/nestjs/prisma/schema.prisma.hbs +91 -0
  101. package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +0 -8
  102. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +0 -116
  103. package/templates/modules/auth/express/src/lib/jwt.ts.hbs +0 -27
  104. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +0 -25
  105. package/templates/modules/auth/express/src/routes/auth.ts.hbs +0 -27
  106. package/templates/modules/auth/express/src/services/userStore.ts.hbs +0 -117
  107. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts.hbs +0 -61
  108. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +0 -36
  109. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +0 -35
  110. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +0 -70
  111. package/templates/modules/auth/nestjs/src/auth/jwt.ts +0 -27
  112. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +0 -144
  113. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +0 -16
  114. package/templates/modules/drizzle/express/src/lib/schema.ts +0 -12
  115. package/templates/modules/drizzle/nestjs/src/database/schema.ts +0 -12
  116. package/templates/modules/prisma/express/prisma/schema.prisma +0 -24
  117. package/templates/modules/prisma/nestjs/prisma/schema.prisma +0 -23
@@ -4,7 +4,7 @@ import Link from "next/link";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { useState, useTransition } from "react";
6
6
  import { zodResolver } from "@hookform/resolvers/zod";
7
- import { LoaderCircle } from "lucide-react";
7
+ import { CircleAlert } from "lucide-react";
8
8
  import { useForm } from "react-hook-form";
9
9
 
10
10
  import { authClient } from "@/lib/auth-client";
@@ -12,13 +12,14 @@ import {
12
12
  getAuthErrorMessage,
13
13
  signInSchema,
14
14
  type SignInValues,
15
+ type SocialProvider,
15
16
  } from "@/lib/auth-schema";
17
+ import { Alert, AlertDescription } from "@/components/ui/alert";
16
18
  import { Button } from "@/components/ui/button";
17
19
  import {
18
20
  Card,
19
21
  CardContent,
20
22
  CardDescription,
21
- CardFooter,
22
23
  CardHeader,
23
24
  CardTitle,
24
25
  } from "@/components/ui/card";
@@ -30,8 +31,11 @@ import {
30
31
  FieldLabel,
31
32
  } from "@/components/ui/field";
32
33
  import { Input } from "@/components/ui/input";
34
+ import { Spinner } from "@/components/ui/spinner";
33
35
 
34
- export function SignInForm() {
36
+ import { SocialButtons } from "./social-buttons";
37
+
38
+ export function SignInForm({ providers }: { providers: SocialProvider[] }) {
35
39
  const router = useRouter();
36
40
  const [isPending, startTransition] = useTransition();
37
41
  const [authError, setAuthError] = useState<string | null>(null);
@@ -63,16 +67,18 @@ export function SignInForm() {
63
67
  });
64
68
 
65
69
  return (
66
- <Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
67
- <CardHeader className="space-y-2">
70
+ <Card>
71
+ <CardHeader>
68
72
  <CardTitle>Login to your account</CardTitle>
69
73
  <CardDescription>
70
74
  Sign in with your email and password to continue to your dashboard.
71
75
  </CardDescription>
72
76
  </CardHeader>
73
77
  <CardContent>
74
- <form className="space-y-6" onSubmit={onSubmit} noValidate>
78
+ <form onSubmit={onSubmit} noValidate>
75
79
  <FieldGroup>
80
+ <SocialButtons providers={providers} />
81
+
76
82
  <Field data-invalid={Boolean(form.formState.errors.email)}>
77
83
  <FieldLabel htmlFor="email">Email</FieldLabel>
78
84
  <Input
@@ -83,12 +89,22 @@ export function SignInForm() {
83
89
  placeholder="you@example.com"
84
90
  {...form.register("email")}
85
91
  />
86
- <FieldDescription>Use the email you registered with.</FieldDescription>
92
+ <FieldDescription>
93
+ Use the email you registered with.
94
+ </FieldDescription>
87
95
  <FieldError errors={[form.formState.errors.email]} />
88
96
  </Field>
89
97
 
90
98
  <Field data-invalid={Boolean(form.formState.errors.password)}>
91
- <FieldLabel htmlFor="password">Password</FieldLabel>
99
+ <div className="flex items-center">
100
+ <FieldLabel htmlFor="password">Password</FieldLabel>
101
+ <Link
102
+ className="ml-auto text-sm underline-offset-4 hover:underline"
103
+ href="/forgot-password"
104
+ >
105
+ Forgot password?
106
+ </Link>
107
+ </div>
92
108
  <Input
93
109
  id="password"
94
110
  type="password"
@@ -99,34 +115,32 @@ export function SignInForm() {
99
115
  />
100
116
  <FieldError errors={[form.formState.errors.password]} />
101
117
  </Field>
102
- </FieldGroup>
103
118
 
104
- {authError ? (
105
- <div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
106
- {authError}
107
- </div>
108
- ) : null}
119
+ {authError ? (
120
+ <Alert variant="destructive">
121
+ <CircleAlert />
122
+ <AlertDescription>{authError}</AlertDescription>
123
+ </Alert>
124
+ ) : null}
109
125
 
110
- <Button className="w-full" type="submit" disabled={isPending}>
111
- {isPending ? (
112
- <>
113
- <LoaderCircle className="mr-2 size-4 animate-spin" />
114
- Signing in
115
- </>
116
- ) : (
117
- "Login"
118
- )}
119
- </Button>
126
+ <Field>
127
+ <Button type="submit" disabled={isPending}>
128
+ {isPending ? (
129
+ <>
130
+ <Spinner data-icon="inline-start" />
131
+ Signing in
132
+ </>
133
+ ) : (
134
+ "Login"
135
+ )}
136
+ </Button>
137
+ <FieldDescription className="text-center">
138
+ Need an account? <Link href="/sign-up">Signup</Link>
139
+ </FieldDescription>
140
+ </Field>
141
+ </FieldGroup>
120
142
  </form>
121
143
  </CardContent>
122
- <CardFooter className="justify-center">
123
- <p className="text-sm text-slate-600">
124
- Need an account?{" "}
125
- <Link className="font-medium text-sky-700 hover:text-sky-800" href="/sign-up">
126
- Signup
127
- </Link>
128
- </p>
129
- </CardFooter>
130
144
  </Card>
131
145
  );
132
146
  }
@@ -2,10 +2,11 @@
2
2
 
3
3
  import { useRouter } from "next/navigation";
4
4
  import { useState, useTransition } from "react";
5
- import { LoaderCircle, LogOut } from "lucide-react";
5
+ import { LogOut } from "lucide-react";
6
6
 
7
7
  import { authClient } from "@/lib/auth-client";
8
8
  import { Button } from "@/components/ui/button";
9
+ import { Spinner } from "@/components/ui/spinner";
9
10
 
10
11
  export function SignOutButton() {
11
12
  const router = useRouter();
@@ -13,7 +14,7 @@ export function SignOutButton() {
13
14
  const [error, setError] = useState<string | null>(null);
14
15
 
15
16
  return (
16
- <div className="space-y-2">
17
+ <div className="flex flex-col items-start gap-2">
17
18
  <Button
18
19
  variant="outline"
19
20
  onClick={() => {
@@ -34,17 +35,17 @@ export function SignOutButton() {
34
35
  >
35
36
  {isPending ? (
36
37
  <>
37
- <LoaderCircle className="mr-2 size-4 animate-spin" />
38
+ <Spinner data-icon="inline-start" />
38
39
  Signing out
39
40
  </>
40
41
  ) : (
41
42
  <>
42
- <LogOut className="mr-2 size-4" />
43
+ <LogOut data-icon="inline-start" />
43
44
  Sign out
44
45
  </>
45
46
  )}
46
47
  </Button>
47
- {error ? <p className="text-sm text-rose-600">{error}</p> : null}
48
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
48
49
  </div>
49
50
  );
50
51
  }
@@ -4,7 +4,7 @@ import Link from "next/link";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { useState, useTransition } from "react";
6
6
  import { zodResolver } from "@hookform/resolvers/zod";
7
- import { LoaderCircle } from "lucide-react";
7
+ import { CircleAlert } from "lucide-react";
8
8
  import { useForm } from "react-hook-form";
9
9
 
10
10
  import { authClient } from "@/lib/auth-client";
@@ -12,13 +12,14 @@ import {
12
12
  getAuthErrorMessage,
13
13
  signUpSchema,
14
14
  type SignUpValues,
15
+ type SocialProvider,
15
16
  } from "@/lib/auth-schema";
17
+ import { Alert, AlertDescription } from "@/components/ui/alert";
16
18
  import { Button } from "@/components/ui/button";
17
19
  import {
18
20
  Card,
19
21
  CardContent,
20
22
  CardDescription,
21
- CardFooter,
22
23
  CardHeader,
23
24
  CardTitle,
24
25
  } from "@/components/ui/card";
@@ -30,8 +31,11 @@ import {
30
31
  FieldLabel,
31
32
  } from "@/components/ui/field";
32
33
  import { Input } from "@/components/ui/input";
34
+ import { Spinner } from "@/components/ui/spinner";
33
35
 
34
- export function SignUpForm() {
36
+ import { SocialButtons } from "./social-buttons";
37
+
38
+ export function SignUpForm({ providers }: { providers: SocialProvider[] }) {
35
39
  const router = useRouter();
36
40
  const [isPending, startTransition] = useTransition();
37
41
  const [authError, setAuthError] = useState<string | null>(null);
@@ -52,6 +56,8 @@ export function SignUpForm() {
52
56
  name: values.name,
53
57
  email: values.email,
54
58
  password: values.password,
59
+ // Where the link in the verification email leads after confirming
60
+ callbackURL: "/dashboard",
55
61
  });
56
62
 
57
63
  if (result.error) {
@@ -65,8 +71,8 @@ export function SignUpForm() {
65
71
  });
66
72
 
67
73
  return (
68
- <Card className="border-white/80 bg-white/90 shadow-2xl shadow-slate-200/70 backdrop-blur">
69
- <CardHeader className="space-y-2">
74
+ <Card>
75
+ <CardHeader>
70
76
  <CardTitle>Create your account</CardTitle>
71
77
  <CardDescription>
72
78
  Start with a simple email and password sign-up flow powered by Better
@@ -74,8 +80,10 @@ export function SignUpForm() {
74
80
  </CardDescription>
75
81
  </CardHeader>
76
82
  <CardContent>
77
- <form className="space-y-6" onSubmit={onSubmit} noValidate>
83
+ <form onSubmit={onSubmit} noValidate>
78
84
  <FieldGroup>
85
+ <SocialButtons providers={providers} />
86
+
79
87
  <Field data-invalid={Boolean(form.formState.errors.name)}>
80
88
  <FieldLabel htmlFor="name">Full name</FieldLabel>
81
89
  <Input
@@ -119,34 +127,32 @@ export function SignUpForm() {
119
127
  </FieldDescription>
120
128
  <FieldError errors={[form.formState.errors.password]} />
121
129
  </Field>
122
- </FieldGroup>
123
130
 
124
- {authError ? (
125
- <div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
126
- {authError}
127
- </div>
128
- ) : null}
131
+ {authError ? (
132
+ <Alert variant="destructive">
133
+ <CircleAlert />
134
+ <AlertDescription>{authError}</AlertDescription>
135
+ </Alert>
136
+ ) : null}
129
137
 
130
- <Button className="w-full" type="submit" disabled={isPending}>
131
- {isPending ? (
132
- <>
133
- <LoaderCircle className="mr-2 size-4 animate-spin" />
134
- Creating account
135
- </>
136
- ) : (
137
- "Signup"
138
- )}
139
- </Button>
138
+ <Field>
139
+ <Button type="submit" disabled={isPending}>
140
+ {isPending ? (
141
+ <>
142
+ <Spinner data-icon="inline-start" />
143
+ Creating account
144
+ </>
145
+ ) : (
146
+ "Signup"
147
+ )}
148
+ </Button>
149
+ <FieldDescription className="text-center">
150
+ Already have an account? <Link href="/sign-in">Login</Link>
151
+ </FieldDescription>
152
+ </Field>
153
+ </FieldGroup>
140
154
  </form>
141
155
  </CardContent>
142
- <CardFooter className="justify-center">
143
- <p className="text-sm text-slate-600">
144
- Already have an account?{" "}
145
- <Link className="font-medium text-sky-700 hover:text-sky-800" href="/sign-in">
146
- Login
147
- </Link>
148
- </p>
149
- </CardFooter>
150
156
  </Card>
151
157
  );
152
158
  }
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+
5
+ import { authClient } from "@/lib/auth-client";
6
+ import { getAuthErrorMessage, type SocialProvider } from "@/lib/auth-schema";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Field, FieldError, FieldSeparator } from "@/components/ui/field";
9
+ import { Spinner } from "@/components/ui/spinner";
10
+
11
+ const PROVIDER_LABELS: Record<SocialProvider, string> = {
12
+ github: "GitHub",
13
+ google: "Google",
14
+ };
15
+
16
+ // One button per provider turned on in lib/auth.ts; renders nothing when
17
+ // GitHub and Google sign-in are both off
18
+ export function SocialButtons({ providers }: { providers: SocialProvider[] }) {
19
+ const [isPending, startTransition] = useTransition();
20
+ // The provider whose button was clicked, for its "Redirecting" label
21
+ const [chosen, setChosen] = useState<SocialProvider | null>(null);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ if (providers.length === 0) {
25
+ return null;
26
+ }
27
+
28
+ function signIn(provider: SocialProvider) {
29
+ setError(null);
30
+ setChosen(provider);
31
+ startTransition(async () => {
32
+ // Redirects to the provider; the user comes back to the dashboard
33
+ const result = await authClient.signIn.social({
34
+ provider,
35
+ callbackURL: "/dashboard",
36
+ errorCallbackURL: "/sign-in",
37
+ });
38
+
39
+ if (result.error) {
40
+ setError(getAuthErrorMessage(result.error));
41
+ }
42
+ });
43
+ }
44
+
45
+ return (
46
+ <>
47
+ <Field data-invalid={Boolean(error)}>
48
+ {providers.map((provider) => (
49
+ <Button
50
+ key={provider}
51
+ type="button"
52
+ variant="outline"
53
+ disabled={isPending}
54
+ onClick={() => signIn(provider)}
55
+ >
56
+ {isPending && chosen === provider ? (
57
+ <>
58
+ <Spinner data-icon="inline-start" />
59
+ Redirecting
60
+ </>
61
+ ) : (
62
+ `Continue with ${PROVIDER_LABELS[provider]}`
63
+ )}
64
+ </Button>
65
+ ))}
66
+ {error ? <FieldError>{error}</FieldError> : null}
67
+ </Field>
68
+ <FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
69
+ Or continue with email
70
+ </FieldSeparator>
71
+ </>
72
+ );
73
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "auth",
3
3
  "name": "Better Auth",
4
- "description": "Better Auth for Next.js with optional Prisma or Drizzle adapter",
4
+ "description": "Better Auth with email verification, password reset, and GitHub and Google sign-in for Next.js with optional Prisma or Drizzle adapter",
5
5
  "type": "feature",
6
6
  "dependencies": {
7
7
  "@hookform/resolvers": "^5.9.2",
@@ -13,6 +13,12 @@
13
13
  "scripts": {},
14
14
  "envVars": {
15
15
  "BETTER_AUTH_SECRET": "please-change-me-to-a-random-string",
16
- "BETTER_AUTH_URL": "http://localhost:3000"
16
+ "BETTER_AUTH_URL": "http://localhost:3000",
17
+ "GITHUB_CLIENT_ID": "",
18
+ "GITHUB_CLIENT_SECRET": "",
19
+ "GOOGLE_CLIENT_ID": "",
20
+ "GOOGLE_CLIENT_SECRET": "",
21
+ "RESEND_API_KEY": "",
22
+ "EMAIL_FROM": "onboarding@resend.dev"
17
23
  }
18
24
  }
@@ -1,8 +1,13 @@
1
1
  import { z } from "zod";
2
2
 
3
+ const emailSchema = z.email("Enter a valid email address.");
4
+ const passwordSchema = z
5
+ .string()
6
+ .min(8, "Password must be at least 8 characters.");
7
+
3
8
  export const signInSchema = z.object({
4
- email: z.email("Enter a valid email address."),
5
- password: z.string().min(8, "Password must be at least 8 characters."),
9
+ email: emailSchema,
10
+ password: passwordSchema,
6
11
  });
7
12
 
8
13
  export const signUpSchema = signInSchema.extend({
@@ -13,8 +18,28 @@ export const signUpSchema = signInSchema.extend({
13
18
  .max(50, "Name must be 50 characters or fewer."),
14
19
  });
15
20
 
21
+ export const forgotPasswordSchema = z.object({ email: emailSchema });
22
+
23
+ export const resetPasswordSchema = z
24
+ .object({ password: passwordSchema, confirmPassword: z.string() })
25
+ .refine((values) => values.password === values.confirmPassword, {
26
+ message: "Passwords do not match.",
27
+ path: ["confirmPassword"],
28
+ });
29
+
30
+ export const changePasswordSchema = z.object({
31
+ currentPassword: z.string().min(1, "Enter your current password."),
32
+ newPassword: passwordSchema,
33
+ });
34
+
16
35
  export type SignInValues = z.infer<typeof signInSchema>;
17
36
  export type SignUpValues = z.infer<typeof signUpSchema>;
37
+ export type ForgotPasswordValues = z.infer<typeof forgotPasswordSchema>;
38
+ export type ResetPasswordValues = z.infer<typeof resetPasswordSchema>;
39
+ export type ChangePasswordValues = z.infer<typeof changePasswordSchema>;
40
+
41
+ // Sign-in providers the auth server can turn on (see auth.ts)
42
+ export type SocialProvider = "github" | "google";
18
43
 
19
44
  export function getAuthErrorMessage(error: unknown) {
20
45
  if (!error) {
@@ -1,5 +1,9 @@
1
+ import { after } from "next/server";
1
2
  import { betterAuth } from "better-auth";
2
3
  import { nextCookies } from "better-auth/next-js";
4
+
5
+ import type { SocialProvider } from "./auth-schema";
6
+ import { sendEmail } from "./email";
3
7
  {{#if (eq database 'prisma')}}
4
8
  import { prismaAdapter } from "better-auth/adapters/prisma";
5
9
  import client from "./prisma";
@@ -16,6 +20,18 @@ const client = new MongoClient(process.env.MONGODB_URI!);
16
20
  const db = client.db();
17
21
  {{/if}}
18
22
 
23
+ // Sends the email after the response, so the request does not wait for it.
24
+ // after() keeps a serverless function (such as on Vercel) running until the
25
+ // email is sent. A failed email is logged instead of failing the request: the
26
+ // sign-up or reset already happened, and the user can ask for the email again
27
+ function deliver(email: Parameters<typeof sendEmail>[0]): void {
28
+ after(() =>
29
+ sendEmail(email).catch((error: unknown) => {
30
+ console.error(`Could not send "${email.subject}" to ${email.to}:`, error);
31
+ })
32
+ );
33
+ }
34
+
19
35
  export const auth = betterAuth({
20
36
  {{#if (eq database 'prisma')}}
21
37
  database: prismaAdapter(client, { provider: "postgresql" }),
@@ -30,8 +46,57 @@ export const auth = betterAuth({
30
46
  {{/if}}
31
47
  emailAndPassword: {
32
48
  enabled: true,
49
+ // Set to true to block sign-in until the email address is verified
50
+ requireEmailVerification: false,
51
+ // A reset signs the user out everywhere
52
+ revokeSessionsOnPasswordReset: true,
53
+ sendResetPassword: async ({ user, url }) => {
54
+ deliver({
55
+ to: user.email,
56
+ subject: "Reset your password",
57
+ text: `Open this link to choose a new password:
58
+ ${url}`,
59
+ });
60
+ },
61
+ },
62
+ emailVerification: {
63
+ sendOnSignUp: true,
64
+ autoSignInAfterVerification: true,
65
+ sendVerificationEmail: async ({ user, url }) => {
66
+ deliver({
67
+ to: user.email,
68
+ subject: "Verify your email address",
69
+ text: `Open this link to verify your email address:
70
+ ${url}`,
71
+ });
72
+ },
73
+ },
74
+ // Each provider is turned on once its client ID and secret are set
75
+ socialProviders: {
76
+ ...(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
77
+ ? {
78
+ github: {
79
+ clientId: process.env.GITHUB_CLIENT_ID,
80
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
81
+ },
82
+ }
83
+ : {}),
84
+ ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
85
+ ? {
86
+ google: {
87
+ clientId: process.env.GOOGLE_CLIENT_ID,
88
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
89
+ },
90
+ }
91
+ : {}),
33
92
  },
34
93
  plugins: [nextCookies()],
35
94
  secret: process.env.BETTER_AUTH_SECRET,
36
95
  baseURL: process.env.BETTER_AUTH_URL,
37
96
  });
97
+
98
+ // The providers turned on above; the sign-in and sign-up pages show a button
99
+ // for each
100
+ export const socialProviders = Object.keys(
101
+ auth.options.socialProviders
102
+ ) 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
+ }
@@ -0,0 +1,12 @@
1
+ import { cache } from "react";
2
+
3
+ import { headers } from "next/headers";
4
+
5
+ import { auth } from "./auth";
6
+
7
+ // The session of the current request, checked on the server. cache() runs the
8
+ // lookup once per request, however many components ask for it, so each part of
9
+ // a page can read the session inside its own <Suspense> boundary
10
+ export const getSession = cache(async () =>
11
+ auth.api.getSession({ headers: await headers() })
12
+ );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "auth",
3
3
  "name": "Better Auth",
4
- "description": "Better Auth for TanStack Start with optional Prisma, Drizzle, or MongoDB adapter",
4
+ "description": "Better Auth with email verification, password reset, and GitHub and Google sign-in for TanStack Start with optional Prisma, Drizzle, or MongoDB adapter",
5
5
  "type": "feature",
6
6
  "dependencies": {
7
7
  "@hookform/resolvers": "^5.9.2",
@@ -14,6 +14,12 @@
14
14
  "scripts": {},
15
15
  "envVars": {
16
16
  "BETTER_AUTH_SECRET": "please-change-me-to-a-random-string",
17
- "BETTER_AUTH_URL": "http://localhost:3000"
17
+ "BETTER_AUTH_URL": "http://localhost:3000",
18
+ "GITHUB_CLIENT_ID": "",
19
+ "GITHUB_CLIENT_SECRET": "",
20
+ "GOOGLE_CLIENT_ID": "",
21
+ "GOOGLE_CLIENT_SECRET": "",
22
+ "RESEND_API_KEY": "",
23
+ "EMAIL_FROM": "onboarding@resend.dev"
18
24
  }
19
- }
25
+ }