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
|
@@ -1,35 +1,41 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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-
|
|
18
|
-
<div className="
|
|
19
|
-
<section className="hidden
|
|
20
|
-
<
|
|
21
|
-
|
|
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-
|
|
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 {
|
|
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 {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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-
|
|
18
|
-
<div className="
|
|
19
|
-
<section className="hidden
|
|
20
|
-
<
|
|
21
|
-
|
|
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-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useState, useTransition } from "react";
|
|
5
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
6
|
+
import { CircleAlert, MailCheck } from "lucide-react";
|
|
7
|
+
import { useForm } from "react-hook-form";
|
|
8
|
+
|
|
9
|
+
import { authClient } from "@/lib/auth-client";
|
|
10
|
+
import {
|
|
11
|
+
forgotPasswordSchema,
|
|
12
|
+
getAuthErrorMessage,
|
|
13
|
+
type ForgotPasswordValues,
|
|
14
|
+
} from "@/lib/auth-schema";
|
|
15
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import {
|
|
18
|
+
Card,
|
|
19
|
+
CardContent,
|
|
20
|
+
CardDescription,
|
|
21
|
+
CardHeader,
|
|
22
|
+
CardTitle,
|
|
23
|
+
} from "@/components/ui/card";
|
|
24
|
+
import {
|
|
25
|
+
Field,
|
|
26
|
+
FieldDescription,
|
|
27
|
+
FieldError,
|
|
28
|
+
FieldGroup,
|
|
29
|
+
FieldLabel,
|
|
30
|
+
} from "@/components/ui/field";
|
|
31
|
+
import { Input } from "@/components/ui/input";
|
|
32
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
33
|
+
|
|
34
|
+
export function ForgotPasswordForm() {
|
|
35
|
+
const [isPending, startTransition] = useTransition();
|
|
36
|
+
const [authError, setAuthError] = useState<string | null>(null);
|
|
37
|
+
const [isSent, setIsSent] = useState(false);
|
|
38
|
+
const form = useForm<ForgotPasswordValues>({
|
|
39
|
+
resolver: zodResolver(forgotPasswordSchema),
|
|
40
|
+
defaultValues: { email: "" },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
44
|
+
setAuthError(null);
|
|
45
|
+
|
|
46
|
+
startTransition(async () => {
|
|
47
|
+
// The link in the email opens /reset-password with a one-time token
|
|
48
|
+
const result = await authClient.requestPasswordReset({
|
|
49
|
+
email: values.email,
|
|
50
|
+
redirectTo: "/reset-password",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (result.error) {
|
|
54
|
+
setAuthError(getAuthErrorMessage(result.error));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setIsSent(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle>Forgot your password?</CardTitle>
|
|
66
|
+
<CardDescription>
|
|
67
|
+
Enter your email and we will send you a link to choose a new one.
|
|
68
|
+
</CardDescription>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent>
|
|
71
|
+
{isSent ? (
|
|
72
|
+
<FieldGroup>
|
|
73
|
+
<Alert>
|
|
74
|
+
<MailCheck />
|
|
75
|
+
<AlertTitle>Check your email</AlertTitle>
|
|
76
|
+
<AlertDescription>
|
|
77
|
+
If an account exists for that email, a reset link is on its
|
|
78
|
+
way. Without an email provider set up, the link is printed in
|
|
79
|
+
the server terminal.
|
|
80
|
+
</AlertDescription>
|
|
81
|
+
</Alert>
|
|
82
|
+
<FieldDescription className="text-center">
|
|
83
|
+
<Link href="/sign-in">Back to login</Link>
|
|
84
|
+
</FieldDescription>
|
|
85
|
+
</FieldGroup>
|
|
86
|
+
) : (
|
|
87
|
+
<form onSubmit={onSubmit} noValidate>
|
|
88
|
+
<FieldGroup>
|
|
89
|
+
<Field data-invalid={Boolean(form.formState.errors.email)}>
|
|
90
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
91
|
+
<Input
|
|
92
|
+
id="email"
|
|
93
|
+
type="email"
|
|
94
|
+
autoComplete="email"
|
|
95
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
96
|
+
placeholder="you@example.com"
|
|
97
|
+
{...form.register("email")}
|
|
98
|
+
/>
|
|
99
|
+
<FieldDescription>
|
|
100
|
+
Use the email you registered with.
|
|
101
|
+
</FieldDescription>
|
|
102
|
+
<FieldError errors={[form.formState.errors.email]} />
|
|
103
|
+
</Field>
|
|
104
|
+
|
|
105
|
+
{authError ? (
|
|
106
|
+
<Alert variant="destructive">
|
|
107
|
+
<CircleAlert />
|
|
108
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
109
|
+
</Alert>
|
|
110
|
+
) : null}
|
|
111
|
+
|
|
112
|
+
<Field>
|
|
113
|
+
<Button type="submit" disabled={isPending}>
|
|
114
|
+
{isPending ? (
|
|
115
|
+
<>
|
|
116
|
+
<Spinner data-icon="inline-start" />
|
|
117
|
+
Sending link
|
|
118
|
+
</>
|
|
119
|
+
) : (
|
|
120
|
+
"Send reset link"
|
|
121
|
+
)}
|
|
122
|
+
</Button>
|
|
123
|
+
<FieldDescription className="text-center">
|
|
124
|
+
Remembered it? <Link href="/sign-in">Back to login</Link>
|
|
125
|
+
</FieldDescription>
|
|
126
|
+
</Field>
|
|
127
|
+
</FieldGroup>
|
|
128
|
+
</form>
|
|
129
|
+
)}
|
|
130
|
+
</CardContent>
|
|
131
|
+
</Card>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useSearchParams } from "next/navigation";
|
|
5
|
+
import { useState, useTransition } from "react";
|
|
6
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
7
|
+
import { CircleAlert } from "lucide-react";
|
|
8
|
+
import { useForm } from "react-hook-form";
|
|
9
|
+
|
|
10
|
+
import { authClient } from "@/lib/auth-client";
|
|
11
|
+
import {
|
|
12
|
+
getAuthErrorMessage,
|
|
13
|
+
resetPasswordSchema,
|
|
14
|
+
type ResetPasswordValues,
|
|
15
|
+
} from "@/lib/auth-schema";
|
|
16
|
+
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
17
|
+
import { Button } from "@/components/ui/button";
|
|
18
|
+
import {
|
|
19
|
+
Card,
|
|
20
|
+
CardContent,
|
|
21
|
+
CardDescription,
|
|
22
|
+
CardFooter,
|
|
23
|
+
CardHeader,
|
|
24
|
+
CardTitle,
|
|
25
|
+
} from "@/components/ui/card";
|
|
26
|
+
import {
|
|
27
|
+
Field,
|
|
28
|
+
FieldError,
|
|
29
|
+
FieldGroup,
|
|
30
|
+
FieldLabel,
|
|
31
|
+
} from "@/components/ui/field";
|
|
32
|
+
import { Input } from "@/components/ui/input";
|
|
33
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
34
|
+
|
|
35
|
+
// The link in the reset email opens /reset-password?token=..., or
|
|
36
|
+
// ?error=INVALID_TOKEN when the token is invalid or has expired. Reading it here,
|
|
37
|
+
// in the browser, lets the page itself be static
|
|
38
|
+
export function ResetPasswordForm() {
|
|
39
|
+
const searchParams = useSearchParams();
|
|
40
|
+
const token = searchParams.get("error") ? null : searchParams.get("token");
|
|
41
|
+
const [isPending, startTransition] = useTransition();
|
|
42
|
+
const [authError, setAuthError] = useState<string | null>(null);
|
|
43
|
+
const [isDone, setIsDone] = useState(false);
|
|
44
|
+
const form = useForm<ResetPasswordValues>({
|
|
45
|
+
resolver: zodResolver(resetPasswordSchema),
|
|
46
|
+
defaultValues: { password: "", confirmPassword: "" },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
50
|
+
if (!token) return;
|
|
51
|
+
setAuthError(null);
|
|
52
|
+
|
|
53
|
+
startTransition(async () => {
|
|
54
|
+
const result = await authClient.resetPassword({
|
|
55
|
+
newPassword: values.password,
|
|
56
|
+
token,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.error) {
|
|
60
|
+
setAuthError(getAuthErrorMessage(result.error));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setIsDone(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!token || isDone) {
|
|
69
|
+
return (
|
|
70
|
+
<Card>
|
|
71
|
+
<CardHeader>
|
|
72
|
+
<CardTitle>{isDone ? "Password changed" : "Link expired"}</CardTitle>
|
|
73
|
+
<CardDescription>
|
|
74
|
+
{isDone
|
|
75
|
+
? "You were signed out on every device. Log in with your new password."
|
|
76
|
+
: "This reset link is invalid or has expired. Ask for a new one."}
|
|
77
|
+
</CardDescription>
|
|
78
|
+
</CardHeader>
|
|
79
|
+
<CardFooter>
|
|
80
|
+
<Button
|
|
81
|
+
className="w-full"
|
|
82
|
+
nativeButton={false}
|
|
83
|
+
render={<Link href={isDone ? "/sign-in" : "/forgot-password"} />}
|
|
84
|
+
>
|
|
85
|
+
{isDone ? "Go to login" : "Send a new link"}
|
|
86
|
+
</Button>
|
|
87
|
+
</CardFooter>
|
|
88
|
+
</Card>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Card>
|
|
94
|
+
<CardHeader>
|
|
95
|
+
<CardTitle>Choose a new password</CardTitle>
|
|
96
|
+
<CardDescription>
|
|
97
|
+
You will be signed out on every device after the change.
|
|
98
|
+
</CardDescription>
|
|
99
|
+
</CardHeader>
|
|
100
|
+
<CardContent>
|
|
101
|
+
<form onSubmit={onSubmit} noValidate>
|
|
102
|
+
<FieldGroup>
|
|
103
|
+
<Field data-invalid={Boolean(form.formState.errors.password)}>
|
|
104
|
+
<FieldLabel htmlFor="password">New password</FieldLabel>
|
|
105
|
+
<Input
|
|
106
|
+
id="password"
|
|
107
|
+
type="password"
|
|
108
|
+
autoComplete="new-password"
|
|
109
|
+
aria-invalid={Boolean(form.formState.errors.password)}
|
|
110
|
+
{...form.register("password")}
|
|
111
|
+
/>
|
|
112
|
+
<FieldError errors={[form.formState.errors.password]} />
|
|
113
|
+
</Field>
|
|
114
|
+
|
|
115
|
+
<Field
|
|
116
|
+
data-invalid={Boolean(form.formState.errors.confirmPassword)}
|
|
117
|
+
>
|
|
118
|
+
<FieldLabel htmlFor="confirmPassword">
|
|
119
|
+
Confirm new password
|
|
120
|
+
</FieldLabel>
|
|
121
|
+
<Input
|
|
122
|
+
id="confirmPassword"
|
|
123
|
+
type="password"
|
|
124
|
+
autoComplete="new-password"
|
|
125
|
+
aria-invalid={Boolean(form.formState.errors.confirmPassword)}
|
|
126
|
+
{...form.register("confirmPassword")}
|
|
127
|
+
/>
|
|
128
|
+
<FieldError errors={[form.formState.errors.confirmPassword]} />
|
|
129
|
+
</Field>
|
|
130
|
+
|
|
131
|
+
{authError ? (
|
|
132
|
+
<Alert variant="destructive">
|
|
133
|
+
<CircleAlert />
|
|
134
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
135
|
+
</Alert>
|
|
136
|
+
) : null}
|
|
137
|
+
|
|
138
|
+
<Field>
|
|
139
|
+
<Button type="submit" disabled={isPending}>
|
|
140
|
+
{isPending ? (
|
|
141
|
+
<>
|
|
142
|
+
<Spinner data-icon="inline-start" />
|
|
143
|
+
Saving
|
|
144
|
+
</>
|
|
145
|
+
) : (
|
|
146
|
+
"Save new password"
|
|
147
|
+
)}
|
|
148
|
+
</Button>
|
|
149
|
+
</Field>
|
|
150
|
+
</FieldGroup>
|
|
151
|
+
</form>
|
|
152
|
+
</CardContent>
|
|
153
|
+
</Card>
|
|
154
|
+
);
|
|
155
|
+
}
|