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.
- package/README.md +16 -8
- package/dist/args.js +2 -1
- package/dist/builder/buildPlanner.js +7 -1
- package/dist/builder/configGenerator.js +28 -3
- package/dist/builder/index.js +2 -1
- package/dist/builder/readmeGenerator.js +2 -2
- package/dist/output/nextSteps.js +2 -4
- package/dist/prompts/stack.js +3 -5
- package/dist/types/index.js +1 -1
- package/package.json +1 -1
- package/templates/backend/express/src/env.ts.hbs +13 -2
- package/templates/backend/express/src/index.ts.hbs +31 -2
- package/templates/backend/nestjs/src/app.module.ts.hbs +4 -1
- package/templates/backend/nestjs/src/env.ts.hbs +13 -2
- package/templates/backend/nestjs/src/health/health.controller.ts.hbs +7 -0
- package/templates/backend/nestjs/src/{main.ts → main.ts.hbs} +11 -0
- package/templates/frontend/nextjs/app/globals.css +119 -79
- package/templates/frontend/nextjs/app/layout.tsx +13 -11
- package/templates/frontend/nextjs/app/page.tsx.hbs +63 -43
- package/templates/frontend/nextjs/components/ui/alert.tsx +75 -0
- package/templates/frontend/nextjs/components/ui/badge.tsx +51 -0
- package/templates/frontend/nextjs/components/ui/button.tsx +33 -27
- package/templates/frontend/nextjs/components/ui/card.tsx +76 -34
- package/templates/frontend/nextjs/components/ui/field.tsx +203 -32
- package/templates/frontend/nextjs/components/ui/input.tsx +8 -7
- package/templates/frontend/nextjs/components/ui/label.tsx +19 -0
- package/templates/frontend/nextjs/components/ui/separator.tsx +24 -0
- package/templates/frontend/nextjs/components/ui/skeleton.tsx +13 -0
- package/templates/frontend/nextjs/components/ui/spinner.tsx +10 -0
- package/templates/frontend/nextjs/components.json +11 -5
- package/templates/frontend/nextjs/config.json +4 -3
- package/templates/frontend/nextjs/lib/utils.ts +1 -6
- package/templates/frontend/tanstack/components.json +12 -5
- package/templates/frontend/tanstack/config.json +5 -2
- package/templates/frontend/tanstack/src/components/ui/alert.tsx +75 -0
- package/templates/frontend/tanstack/src/components/ui/badge.tsx +51 -0
- package/templates/frontend/tanstack/src/components/ui/button.tsx +33 -27
- package/templates/frontend/tanstack/src/components/ui/card.tsx +76 -34
- package/templates/frontend/tanstack/src/components/ui/field.tsx +201 -32
- package/templates/frontend/tanstack/src/components/ui/input.tsx +8 -7
- package/templates/frontend/tanstack/src/components/ui/label.tsx +17 -0
- package/templates/frontend/tanstack/src/components/ui/separator.tsx +24 -0
- package/templates/frontend/tanstack/src/components/ui/skeleton.tsx +13 -0
- package/templates/frontend/tanstack/src/components/ui/spinner.tsx +10 -0
- package/templates/frontend/tanstack/src/lib/utils.ts +1 -6
- package/templates/frontend/tanstack/src/routes/index.tsx.hbs +64 -45
- package/templates/frontend/tanstack/src/styles/app.css +100 -55
- package/templates/modules/agents/AGENTS.md.hbs +29 -21
- package/templates/modules/auth/express/config.json +12 -11
- package/templates/modules/auth/express/src/lib/auth.ts.hbs +143 -0
- package/templates/modules/auth/express/src/lib/email.ts +37 -0
- package/templates/modules/auth/express/src/middleware/requireAuth.ts +29 -0
- package/templates/modules/auth/express/src/routes/me.ts +13 -0
- package/templates/modules/auth/nestjs/config.json +13 -11
- package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +64 -14
- package/templates/modules/auth/nestjs/src/auth/auth.ts.hbs +152 -0
- package/templates/modules/auth/nestjs/src/auth/email.ts +37 -0
- package/templates/modules/auth/nestjs/src/me/me.controller.ts +15 -0
- package/templates/modules/auth/nextjs/app/dashboard/page.tsx +101 -45
- package/templates/modules/auth/nextjs/app/forgot-password/page.tsx +11 -0
- package/templates/modules/auth/nextjs/app/reset-password/page.tsx +17 -0
- package/templates/modules/auth/nextjs/app/sign-in/page.tsx +23 -17
- package/templates/modules/auth/nextjs/app/sign-up/page.tsx +25 -19
- package/templates/modules/auth/nextjs/components/auth/change-password-form.tsx +125 -0
- package/templates/modules/auth/nextjs/components/auth/email-verification-card.tsx +91 -0
- package/templates/modules/auth/nextjs/components/auth/forgot-password-form.tsx +133 -0
- package/templates/modules/auth/nextjs/components/auth/reset-password-form.tsx +155 -0
- package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +46 -32
- package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +6 -5
- package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +36 -30
- package/templates/modules/auth/nextjs/components/auth/social-buttons.tsx +73 -0
- package/templates/modules/auth/nextjs/config.json +8 -2
- package/templates/modules/auth/nextjs/lib/auth-schema.ts +27 -2
- package/templates/modules/auth/nextjs/lib/auth.ts.hbs +65 -0
- package/templates/modules/auth/nextjs/lib/email.ts +43 -0
- package/templates/modules/auth/nextjs/lib/session.ts +12 -0
- package/templates/modules/auth/tanstack/config.json +9 -3
- package/templates/modules/auth/tanstack/src/components/auth/change-password-form.tsx +123 -0
- package/templates/modules/auth/tanstack/src/components/auth/email-verification-card.tsx +89 -0
- package/templates/modules/auth/tanstack/src/components/auth/forgot-password-form.tsx +131 -0
- package/templates/modules/auth/tanstack/src/components/auth/reset-password-form.tsx +149 -0
- package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +44 -35
- package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +7 -6
- package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +37 -34
- package/templates/modules/auth/tanstack/src/components/auth/social-buttons.tsx +71 -0
- package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +7 -1
- package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +27 -2
- package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +61 -1
- package/templates/modules/auth/tanstack/src/lib/email.ts +43 -0
- package/templates/modules/auth/tanstack/src/routeTree.gen.ts +61 -3
- package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +40 -32
- package/templates/modules/auth/tanstack/src/routes/forgot-password.tsx +17 -0
- package/templates/modules/auth/tanstack/src/routes/reset-password.tsx +25 -0
- package/templates/modules/auth/tanstack/src/routes/sign-in.tsx +19 -12
- package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +21 -14
- package/templates/modules/drizzle/express/src/lib/schema.ts.hbs +90 -0
- package/templates/modules/drizzle/nestjs/src/database/schema.ts.hbs +90 -0
- package/templates/modules/mongoose/nestjs/config.json +2 -1
- package/templates/modules/prisma/express/prisma/schema.prisma.hbs +92 -0
- package/templates/modules/prisma/nestjs/prisma/schema.prisma.hbs +91 -0
- package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +0 -8
- package/templates/modules/auth/express/src/controllers/authController.ts.hbs +0 -116
- package/templates/modules/auth/express/src/lib/jwt.ts.hbs +0 -27
- package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +0 -25
- package/templates/modules/auth/express/src/routes/auth.ts.hbs +0 -27
- package/templates/modules/auth/express/src/services/userStore.ts.hbs +0 -117
- package/templates/modules/auth/nestjs/src/auth/auth.controller.ts.hbs +0 -61
- package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +0 -36
- package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +0 -35
- package/templates/modules/auth/nestjs/src/auth/auth.service.ts +0 -70
- package/templates/modules/auth/nestjs/src/auth/jwt.ts +0 -27
- package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +0 -144
- package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +0 -16
- package/templates/modules/drizzle/express/src/lib/schema.ts +0 -12
- package/templates/modules/drizzle/nestjs/src/database/schema.ts +0 -12
- package/templates/modules/prisma/express/prisma/schema.prisma +0 -24
- 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 {
|
|
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
|
-
|
|
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
|
|
67
|
-
<CardHeader
|
|
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
|
|
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>
|
|
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
|
-
<
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
{authError ? (
|
|
120
|
+
<Alert variant="destructive">
|
|
121
|
+
<CircleAlert />
|
|
122
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
123
|
+
</Alert>
|
|
124
|
+
) : null}
|
|
109
125
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 {
|
|
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="
|
|
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
|
-
<
|
|
38
|
+
<Spinner data-icon="inline-start" />
|
|
38
39
|
Signing out
|
|
39
40
|
</>
|
|
40
41
|
) : (
|
|
41
42
|
<>
|
|
42
|
-
<LogOut
|
|
43
|
+
<LogOut data-icon="inline-start" />
|
|
43
44
|
Sign out
|
|
44
45
|
</>
|
|
45
46
|
)}
|
|
46
47
|
</Button>
|
|
47
|
-
{error ? <p className="text-sm text-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
69
|
-
<CardHeader
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
{authError ? (
|
|
132
|
+
<Alert variant="destructive">
|
|
133
|
+
<CircleAlert />
|
|
134
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
135
|
+
</Alert>
|
|
136
|
+
) : null}
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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:
|
|
5
|
-
password:
|
|
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
|
+
}
|