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.
- package/LICENSE +21 -21
- package/README.md +16 -8
- package/dist/args.js +7 -0
- package/dist/builder/buildPlanner.js +7 -1
- package/dist/builder/configGenerator.js +28 -3
- package/dist/builder/index.js +2 -1
- package/dist/builder/moduleSelector.js +6 -1
- package/dist/builder/readmeGenerator.js +8 -2
- package/dist/builder/templateContext.js +1 -0
- package/dist/output/nextSteps.js +2 -4
- package/dist/prompts/stack.js +6 -1
- package/dist/types/index.js +2 -0
- package/dist/ui/messages.js +2 -0
- package/package.json +71 -71
- package/templates/backend/express/.prettierignore +52 -52
- package/templates/backend/express/.prettierrc +16 -16
- package/templates/backend/express/gitignore +3 -1
- package/templates/backend/express/src/env.ts.hbs +13 -2
- package/templates/backend/express/src/index.ts.hbs +31 -2
- package/templates/backend/express/tsconfig.eslint.json +9 -9
- package/templates/backend/nestjs/.prettierignore +52 -52
- package/templates/backend/nestjs/gitignore +3 -1
- 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/backend/nestjs/tsconfig.eslint.json +9 -9
- package/templates/frontend/nextjs/app/globals.css +129 -89
- package/templates/frontend/nextjs/app/layout.tsx +36 -34
- 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 +57 -51
- package/templates/frontend/nextjs/components/ui/card.tsx +102 -60
- package/templates/frontend/nextjs/components/ui/field.tsx +238 -67
- package/templates/frontend/nextjs/components/ui/input.tsx +19 -18
- 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 +36 -35
- package/templates/frontend/nextjs/eslint.config.mjs +11 -11
- package/templates/frontend/nextjs/lib/utils.ts +1 -6
- package/templates/frontend/nextjs/next.config.ts.hbs +12 -12
- package/templates/frontend/nextjs/postcss.config.mjs +7 -7
- package/templates/frontend/nextjs/public/file.svg +1 -1
- package/templates/frontend/nextjs/public/globe.svg +1 -1
- package/templates/frontend/nextjs/public/window.svg +1 -1
- package/templates/frontend/tanstack/components.json +25 -18
- package/templates/frontend/tanstack/config.json +5 -2
- package/templates/frontend/tanstack/eslint.config.mjs +23 -23
- package/templates/frontend/tanstack/gitignore +39 -39
- package/templates/frontend/tanstack/public/favicon.svg +7 -7
- 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 +57 -51
- package/templates/frontend/tanstack/src/components/ui/card.tsx +102 -60
- package/templates/frontend/tanstack/src/components/ui/field.tsx +236 -67
- package/templates/frontend/tanstack/src/components/ui/input.tsx +19 -18
- 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/router.tsx +31 -31
- package/templates/frontend/tanstack/src/routes/__root.tsx +69 -69
- package/templates/frontend/tanstack/src/routes/index.tsx.hbs +64 -45
- package/templates/frontend/tanstack/src/styles/app.css +100 -55
- package/templates/frontend/tanstack/tsconfig.json +22 -22
- package/templates/modules/agents/AGENTS.md.hbs +245 -0
- package/templates/modules/agents/CLAUDE.md +1 -0
- package/templates/modules/agents/config.json +10 -0
- 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/api/auth/[...all]/route.ts +4 -4
- 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 +24 -18
- package/templates/modules/auth/nextjs/lib/auth-client.ts +3 -3
- 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 +25 -19
- 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-client.ts +2 -2
- package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +14 -8
- package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +27 -2
- package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +98 -38
- 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/api/auth/$.ts +15 -15
- package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +119 -111
- 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 +44 -37
- package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +44 -37
- package/templates/modules/docker/.dockerignore +2 -0
- package/templates/modules/docker/config.json +15 -15
- package/templates/modules/drizzle/express/config.json +26 -26
- package/templates/modules/drizzle/express/src/lib/schema.ts.hbs +90 -0
- package/templates/modules/drizzle/nestjs/config.json +26 -26
- package/templates/modules/drizzle/nestjs/src/database/schema.ts.hbs +90 -0
- package/templates/modules/drizzle/nextjs/config.json +26 -26
- package/templates/modules/drizzle/nextjs/drizzle.config.ts +13 -13
- package/templates/modules/drizzle/tanstack/config.json +26 -26
- package/templates/modules/drizzle/tanstack/drizzle.config.ts +12 -12
- package/templates/modules/drizzle/tanstack/src/lib/db.ts +13 -13
- package/templates/modules/mongoose/express/config.json +15 -15
- package/templates/modules/mongoose/express/src/lib/db.ts +43 -43
- package/templates/modules/mongoose/express/src/models/User.ts +48 -48
- package/templates/modules/mongoose/nestjs/config.json +16 -15
- package/templates/modules/mongoose/nextjs/config.json +15 -15
- package/templates/modules/mongoose/nextjs/models/User.ts +48 -48
- package/templates/modules/mongoose/tanstack/config.json +16 -16
- package/templates/modules/mongoose/tanstack/src/models/User.ts +48 -48
- package/templates/modules/prisma/express/config.json +27 -27
- package/templates/modules/prisma/express/prisma/schema.prisma.hbs +92 -0
- package/templates/modules/prisma/nestjs/config.json +27 -27
- package/templates/modules/prisma/nestjs/prisma/schema.prisma.hbs +91 -0
- package/templates/modules/prisma/nextjs/config.json +28 -28
- package/templates/modules/prisma/nextjs/lib/prisma.ts +18 -18
- package/templates/modules/prisma/nextjs/prisma.config.ts +14 -14
- package/templates/modules/prisma/tanstack/config.json +28 -28
- package/templates/modules/prisma/tanstack/prisma/schema.prisma.hbs +91 -91
- package/templates/modules/prisma/tanstack/prisma.config.ts +14 -14
- package/templates/modules/prisma/tanstack/src/lib/prisma.ts +17 -17
- package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +0 -8
- package/templates/modules/vscode/.vscode/extensions.json.hbs +29 -0
- package/templates/modules/vscode/.vscode/settings.json.hbs +22 -0
- package/templates/modules/vscode/config.json +10 -0
- 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,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 {
|
|
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 {
|
|
16
|
-
import {
|
|
21
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
22
|
+
import { getSession } from "@/lib/session";
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
<
|
|
29
|
-
<div className="
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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="
|
|
96
|
+
<CardContent className="flex flex-col gap-4 text-muted-foreground">
|
|
56
97
|
<p>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
<Suspense>
|
|
109
|
+
</code>{" "}
|
|
110
|
+
boundary, and the session is looked up once per request.
|
|
63
111
|
</p>
|
|
64
112
|
<p>
|
|
65
|
-
|
|
66
|
-
|
|
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="
|
|
80
|
-
<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>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
89
|
-
</
|
|
90
|
-
</
|
|
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 {
|
|
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
|
+
}
|