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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import { useState, useTransition } from "react";
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { CircleAlert } from "lucide-react";
|
|
5
|
+
import { useForm } from "react-hook-form";
|
|
6
|
+
|
|
7
|
+
import { authClient } from "~/lib/auth-client";
|
|
8
|
+
import {
|
|
9
|
+
getAuthErrorMessage,
|
|
10
|
+
resetPasswordSchema,
|
|
11
|
+
type ResetPasswordValues,
|
|
12
|
+
} from "~/lib/auth-schema";
|
|
13
|
+
import { Alert, AlertDescription } from "~/components/ui/alert";
|
|
14
|
+
import { Button } from "~/components/ui/button";
|
|
15
|
+
import {
|
|
16
|
+
Card,
|
|
17
|
+
CardContent,
|
|
18
|
+
CardDescription,
|
|
19
|
+
CardFooter,
|
|
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
|
+
// token comes from the link in the reset email; null when the link was
|
|
33
|
+
// invalid or has expired
|
|
34
|
+
export function ResetPasswordForm({ token }: { token: string | null }) {
|
|
35
|
+
const [isPending, startTransition] = useTransition();
|
|
36
|
+
const [authError, setAuthError] = useState<string | null>(null);
|
|
37
|
+
const [isDone, setIsDone] = useState(false);
|
|
38
|
+
const form = useForm<ResetPasswordValues>({
|
|
39
|
+
resolver: zodResolver(resetPasswordSchema),
|
|
40
|
+
defaultValues: { password: "", confirmPassword: "" },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
44
|
+
if (!token) return;
|
|
45
|
+
setAuthError(null);
|
|
46
|
+
|
|
47
|
+
startTransition(async () => {
|
|
48
|
+
const result = await authClient.resetPassword({
|
|
49
|
+
newPassword: values.password,
|
|
50
|
+
token,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (result.error) {
|
|
54
|
+
setAuthError(getAuthErrorMessage(result.error));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setIsDone(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!token || isDone) {
|
|
63
|
+
return (
|
|
64
|
+
<Card>
|
|
65
|
+
<CardHeader>
|
|
66
|
+
<CardTitle>{isDone ? "Password changed" : "Link expired"}</CardTitle>
|
|
67
|
+
<CardDescription>
|
|
68
|
+
{isDone
|
|
69
|
+
? "You were signed out on every device. Log in with your new password."
|
|
70
|
+
: "This reset link is invalid or has expired. Ask for a new one."}
|
|
71
|
+
</CardDescription>
|
|
72
|
+
</CardHeader>
|
|
73
|
+
<CardFooter>
|
|
74
|
+
<Button
|
|
75
|
+
className="w-full"
|
|
76
|
+
nativeButton={false}
|
|
77
|
+
render={<Link to={isDone ? "/sign-in" : "/forgot-password"} />}
|
|
78
|
+
>
|
|
79
|
+
{isDone ? "Go to login" : "Send a new link"}
|
|
80
|
+
</Button>
|
|
81
|
+
</CardFooter>
|
|
82
|
+
</Card>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Card>
|
|
88
|
+
<CardHeader>
|
|
89
|
+
<CardTitle>Choose a new password</CardTitle>
|
|
90
|
+
<CardDescription>
|
|
91
|
+
You will be signed out on every device after the change.
|
|
92
|
+
</CardDescription>
|
|
93
|
+
</CardHeader>
|
|
94
|
+
<CardContent>
|
|
95
|
+
<form onSubmit={onSubmit} noValidate>
|
|
96
|
+
<FieldGroup>
|
|
97
|
+
<Field data-invalid={Boolean(form.formState.errors.password)}>
|
|
98
|
+
<FieldLabel htmlFor="password">New password</FieldLabel>
|
|
99
|
+
<Input
|
|
100
|
+
id="password"
|
|
101
|
+
type="password"
|
|
102
|
+
autoComplete="new-password"
|
|
103
|
+
aria-invalid={Boolean(form.formState.errors.password)}
|
|
104
|
+
{...form.register("password")}
|
|
105
|
+
/>
|
|
106
|
+
<FieldError errors={[form.formState.errors.password]} />
|
|
107
|
+
</Field>
|
|
108
|
+
|
|
109
|
+
<Field
|
|
110
|
+
data-invalid={Boolean(form.formState.errors.confirmPassword)}
|
|
111
|
+
>
|
|
112
|
+
<FieldLabel htmlFor="confirmPassword">
|
|
113
|
+
Confirm new password
|
|
114
|
+
</FieldLabel>
|
|
115
|
+
<Input
|
|
116
|
+
id="confirmPassword"
|
|
117
|
+
type="password"
|
|
118
|
+
autoComplete="new-password"
|
|
119
|
+
aria-invalid={Boolean(form.formState.errors.confirmPassword)}
|
|
120
|
+
{...form.register("confirmPassword")}
|
|
121
|
+
/>
|
|
122
|
+
<FieldError errors={[form.formState.errors.confirmPassword]} />
|
|
123
|
+
</Field>
|
|
124
|
+
|
|
125
|
+
{authError ? (
|
|
126
|
+
<Alert variant="destructive">
|
|
127
|
+
<CircleAlert />
|
|
128
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
129
|
+
</Alert>
|
|
130
|
+
) : null}
|
|
131
|
+
|
|
132
|
+
<Field>
|
|
133
|
+
<Button type="submit" disabled={isPending}>
|
|
134
|
+
{isPending ? (
|
|
135
|
+
<>
|
|
136
|
+
<Spinner data-icon="inline-start" />
|
|
137
|
+
Saving
|
|
138
|
+
</>
|
|
139
|
+
) : (
|
|
140
|
+
"Save new password"
|
|
141
|
+
)}
|
|
142
|
+
</Button>
|
|
143
|
+
</Field>
|
|
144
|
+
</FieldGroup>
|
|
145
|
+
</form>
|
|
146
|
+
</CardContent>
|
|
147
|
+
</Card>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Link, useNavigate } from "@tanstack/react-router";
|
|
2
2
|
import { useState, useTransition } from "react";
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
-
import {
|
|
4
|
+
import { CircleAlert } from "lucide-react";
|
|
5
5
|
import { useForm } from "react-hook-form";
|
|
6
6
|
|
|
7
7
|
import { authClient } from "~/lib/auth-client";
|
|
@@ -9,13 +9,14 @@ import {
|
|
|
9
9
|
getAuthErrorMessage,
|
|
10
10
|
signInSchema,
|
|
11
11
|
type SignInValues,
|
|
12
|
+
type SocialProvider,
|
|
12
13
|
} from "~/lib/auth-schema";
|
|
14
|
+
import { Alert, AlertDescription } from "~/components/ui/alert";
|
|
13
15
|
import { Button } from "~/components/ui/button";
|
|
14
16
|
import {
|
|
15
17
|
Card,
|
|
16
18
|
CardContent,
|
|
17
19
|
CardDescription,
|
|
18
|
-
CardFooter,
|
|
19
20
|
CardHeader,
|
|
20
21
|
CardTitle,
|
|
21
22
|
} from "~/components/ui/card";
|
|
@@ -27,8 +28,11 @@ import {
|
|
|
27
28
|
FieldLabel,
|
|
28
29
|
} from "~/components/ui/field";
|
|
29
30
|
import { Input } from "~/components/ui/input";
|
|
31
|
+
import { Spinner } from "~/components/ui/spinner";
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
import { SocialButtons } from "./social-buttons";
|
|
34
|
+
|
|
35
|
+
export function SignInForm({ providers }: { providers: SocialProvider[] }) {
|
|
32
36
|
const navigate = useNavigate();
|
|
33
37
|
const [isPending, startTransition] = useTransition();
|
|
34
38
|
const [authError, setAuthError] = useState<string | null>(null);
|
|
@@ -59,16 +63,18 @@ export function SignInForm() {
|
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
return (
|
|
62
|
-
<Card
|
|
63
|
-
<CardHeader
|
|
66
|
+
<Card>
|
|
67
|
+
<CardHeader>
|
|
64
68
|
<CardTitle>Login to your account</CardTitle>
|
|
65
69
|
<CardDescription>
|
|
66
70
|
Sign in with your email and password to continue to your dashboard.
|
|
67
71
|
</CardDescription>
|
|
68
72
|
</CardHeader>
|
|
69
73
|
<CardContent>
|
|
70
|
-
<form
|
|
74
|
+
<form onSubmit={onSubmit} noValidate>
|
|
71
75
|
<FieldGroup>
|
|
76
|
+
<SocialButtons providers={providers} />
|
|
77
|
+
|
|
72
78
|
<Field data-invalid={Boolean(form.formState.errors.email)}>
|
|
73
79
|
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
74
80
|
<Input
|
|
@@ -86,7 +92,15 @@ export function SignInForm() {
|
|
|
86
92
|
</Field>
|
|
87
93
|
|
|
88
94
|
<Field data-invalid={Boolean(form.formState.errors.password)}>
|
|
89
|
-
<
|
|
95
|
+
<div className="flex items-center">
|
|
96
|
+
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
97
|
+
<Link
|
|
98
|
+
className="ml-auto text-sm underline-offset-4 hover:underline"
|
|
99
|
+
to="/forgot-password"
|
|
100
|
+
>
|
|
101
|
+
Forgot password?
|
|
102
|
+
</Link>
|
|
103
|
+
</div>
|
|
90
104
|
<Input
|
|
91
105
|
id="password"
|
|
92
106
|
type="password"
|
|
@@ -97,37 +111,32 @@ export function SignInForm() {
|
|
|
97
111
|
/>
|
|
98
112
|
<FieldError errors={[form.formState.errors.password]} />
|
|
99
113
|
</Field>
|
|
100
|
-
</FieldGroup>
|
|
101
114
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
{authError ? (
|
|
116
|
+
<Alert variant="destructive">
|
|
117
|
+
<CircleAlert />
|
|
118
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
119
|
+
</Alert>
|
|
120
|
+
) : null}
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
<Field>
|
|
123
|
+
<Button type="submit" disabled={isPending}>
|
|
124
|
+
{isPending ? (
|
|
125
|
+
<>
|
|
126
|
+
<Spinner data-icon="inline-start" />
|
|
127
|
+
Signing in
|
|
128
|
+
</>
|
|
129
|
+
) : (
|
|
130
|
+
"Login"
|
|
131
|
+
)}
|
|
132
|
+
</Button>
|
|
133
|
+
<FieldDescription className="text-center">
|
|
134
|
+
Need an account? <Link to="/sign-up">Signup</Link>
|
|
135
|
+
</FieldDescription>
|
|
136
|
+
</Field>
|
|
137
|
+
</FieldGroup>
|
|
118
138
|
</form>
|
|
119
139
|
</CardContent>
|
|
120
|
-
<CardFooter className="justify-center">
|
|
121
|
-
<p className="text-sm text-slate-600">
|
|
122
|
-
Need an account?{" "}
|
|
123
|
-
<Link
|
|
124
|
-
className="font-medium text-sky-700 hover:text-sky-800"
|
|
125
|
-
to="/sign-up"
|
|
126
|
-
>
|
|
127
|
-
Signup
|
|
128
|
-
</Link>
|
|
129
|
-
</p>
|
|
130
|
-
</CardFooter>
|
|
131
140
|
</Card>
|
|
132
141
|
);
|
|
133
|
-
}
|
|
142
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useNavigate } from "@tanstack/react-router";
|
|
2
2
|
import { useState, useTransition } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { LogOut } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
import { authClient } from "~/lib/auth-client";
|
|
6
6
|
import { Button } from "~/components/ui/button";
|
|
7
|
+
import { Spinner } from "~/components/ui/spinner";
|
|
7
8
|
|
|
8
9
|
export function SignOutButton() {
|
|
9
10
|
const navigate = useNavigate();
|
|
@@ -11,7 +12,7 @@ export function SignOutButton() {
|
|
|
11
12
|
const [error, setError] = useState<string | null>(null);
|
|
12
13
|
|
|
13
14
|
return (
|
|
14
|
-
<div className="
|
|
15
|
+
<div className="flex flex-col items-start gap-2">
|
|
15
16
|
<Button
|
|
16
17
|
variant="outline"
|
|
17
18
|
onClick={() => {
|
|
@@ -31,17 +32,17 @@ export function SignOutButton() {
|
|
|
31
32
|
>
|
|
32
33
|
{isPending ? (
|
|
33
34
|
<>
|
|
34
|
-
<
|
|
35
|
+
<Spinner data-icon="inline-start" />
|
|
35
36
|
Signing out
|
|
36
37
|
</>
|
|
37
38
|
) : (
|
|
38
39
|
<>
|
|
39
|
-
<LogOut
|
|
40
|
+
<LogOut data-icon="inline-start" />
|
|
40
41
|
Sign out
|
|
41
42
|
</>
|
|
42
43
|
)}
|
|
43
44
|
</Button>
|
|
44
|
-
{error ? <p className="text-sm text-
|
|
45
|
+
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
|
45
46
|
</div>
|
|
46
47
|
);
|
|
47
|
-
}
|
|
48
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Link, useNavigate } from "@tanstack/react-router";
|
|
2
2
|
import { useState, useTransition } from "react";
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
-
import {
|
|
4
|
+
import { CircleAlert } from "lucide-react";
|
|
5
5
|
import { useForm } from "react-hook-form";
|
|
6
6
|
|
|
7
7
|
import { authClient } from "~/lib/auth-client";
|
|
@@ -9,13 +9,14 @@ import {
|
|
|
9
9
|
getAuthErrorMessage,
|
|
10
10
|
signUpSchema,
|
|
11
11
|
type SignUpValues,
|
|
12
|
+
type SocialProvider,
|
|
12
13
|
} from "~/lib/auth-schema";
|
|
14
|
+
import { Alert, AlertDescription } from "~/components/ui/alert";
|
|
13
15
|
import { Button } from "~/components/ui/button";
|
|
14
16
|
import {
|
|
15
17
|
Card,
|
|
16
18
|
CardContent,
|
|
17
19
|
CardDescription,
|
|
18
|
-
CardFooter,
|
|
19
20
|
CardHeader,
|
|
20
21
|
CardTitle,
|
|
21
22
|
} from "~/components/ui/card";
|
|
@@ -27,8 +28,11 @@ import {
|
|
|
27
28
|
FieldLabel,
|
|
28
29
|
} from "~/components/ui/field";
|
|
29
30
|
import { Input } from "~/components/ui/input";
|
|
31
|
+
import { Spinner } from "~/components/ui/spinner";
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
import { SocialButtons } from "./social-buttons";
|
|
34
|
+
|
|
35
|
+
export function SignUpForm({ providers }: { providers: SocialProvider[] }) {
|
|
32
36
|
const navigate = useNavigate();
|
|
33
37
|
const [isPending, startTransition] = useTransition();
|
|
34
38
|
const [authError, setAuthError] = useState<string | null>(null);
|
|
@@ -49,6 +53,8 @@ export function SignUpForm() {
|
|
|
49
53
|
name: values.name,
|
|
50
54
|
email: values.email,
|
|
51
55
|
password: values.password,
|
|
56
|
+
// Where the link in the verification email leads after confirming
|
|
57
|
+
callbackURL: "/dashboard",
|
|
52
58
|
});
|
|
53
59
|
|
|
54
60
|
if (result.error) {
|
|
@@ -61,8 +67,8 @@ export function SignUpForm() {
|
|
|
61
67
|
});
|
|
62
68
|
|
|
63
69
|
return (
|
|
64
|
-
<Card
|
|
65
|
-
<CardHeader
|
|
70
|
+
<Card>
|
|
71
|
+
<CardHeader>
|
|
66
72
|
<CardTitle>Create your account</CardTitle>
|
|
67
73
|
<CardDescription>
|
|
68
74
|
Start with a simple email and password sign-up flow powered by Better
|
|
@@ -70,8 +76,10 @@ export function SignUpForm() {
|
|
|
70
76
|
</CardDescription>
|
|
71
77
|
</CardHeader>
|
|
72
78
|
<CardContent>
|
|
73
|
-
<form
|
|
79
|
+
<form onSubmit={onSubmit} noValidate>
|
|
74
80
|
<FieldGroup>
|
|
81
|
+
<SocialButtons providers={providers} />
|
|
82
|
+
|
|
75
83
|
<Field data-invalid={Boolean(form.formState.errors.name)}>
|
|
76
84
|
<FieldLabel htmlFor="name">Full name</FieldLabel>
|
|
77
85
|
<Input
|
|
@@ -115,37 +123,32 @@ export function SignUpForm() {
|
|
|
115
123
|
</FieldDescription>
|
|
116
124
|
<FieldError errors={[form.formState.errors.password]} />
|
|
117
125
|
</Field>
|
|
118
|
-
</FieldGroup>
|
|
119
126
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
{authError ? (
|
|
128
|
+
<Alert variant="destructive">
|
|
129
|
+
<CircleAlert />
|
|
130
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
131
|
+
</Alert>
|
|
132
|
+
) : null}
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
<Field>
|
|
135
|
+
<Button type="submit" disabled={isPending}>
|
|
136
|
+
{isPending ? (
|
|
137
|
+
<>
|
|
138
|
+
<Spinner data-icon="inline-start" />
|
|
139
|
+
Creating account
|
|
140
|
+
</>
|
|
141
|
+
) : (
|
|
142
|
+
"Signup"
|
|
143
|
+
)}
|
|
144
|
+
</Button>
|
|
145
|
+
<FieldDescription className="text-center">
|
|
146
|
+
Already have an account? <Link to="/sign-in">Login</Link>
|
|
147
|
+
</FieldDescription>
|
|
148
|
+
</Field>
|
|
149
|
+
</FieldGroup>
|
|
136
150
|
</form>
|
|
137
151
|
</CardContent>
|
|
138
|
-
<CardFooter className="justify-center">
|
|
139
|
-
<p className="text-sm text-slate-600">
|
|
140
|
-
Already have an account?{" "}
|
|
141
|
-
<Link
|
|
142
|
-
className="font-medium text-sky-700 hover:text-sky-800"
|
|
143
|
-
to="/sign-in"
|
|
144
|
-
>
|
|
145
|
-
Login
|
|
146
|
-
</Link>
|
|
147
|
-
</p>
|
|
148
|
-
</CardFooter>
|
|
149
152
|
</Card>
|
|
150
153
|
);
|
|
151
|
-
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useState, useTransition } from "react";
|
|
2
|
+
|
|
3
|
+
import { authClient } from "~/lib/auth-client";
|
|
4
|
+
import { getAuthErrorMessage, type SocialProvider } from "~/lib/auth-schema";
|
|
5
|
+
import { Button } from "~/components/ui/button";
|
|
6
|
+
import { Field, FieldError, FieldSeparator } from "~/components/ui/field";
|
|
7
|
+
import { Spinner } from "~/components/ui/spinner";
|
|
8
|
+
|
|
9
|
+
const PROVIDER_LABELS: Record<SocialProvider, string> = {
|
|
10
|
+
github: "GitHub",
|
|
11
|
+
google: "Google",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// One button per provider turned on in lib/auth.ts; renders nothing when
|
|
15
|
+
// GitHub and Google sign-in are both off
|
|
16
|
+
export function SocialButtons({ providers }: { providers: SocialProvider[] }) {
|
|
17
|
+
const [isPending, startTransition] = useTransition();
|
|
18
|
+
// The provider whose button was clicked, for its "Redirecting" label
|
|
19
|
+
const [chosen, setChosen] = useState<SocialProvider | null>(null);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
if (providers.length === 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function signIn(provider: SocialProvider) {
|
|
27
|
+
setError(null);
|
|
28
|
+
setChosen(provider);
|
|
29
|
+
startTransition(async () => {
|
|
30
|
+
// Redirects to the provider; the user comes back to the dashboard
|
|
31
|
+
const result = await authClient.signIn.social({
|
|
32
|
+
provider,
|
|
33
|
+
callbackURL: "/dashboard",
|
|
34
|
+
errorCallbackURL: "/sign-in",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (result.error) {
|
|
38
|
+
setError(getAuthErrorMessage(result.error));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<Field data-invalid={Boolean(error)}>
|
|
46
|
+
{providers.map((provider) => (
|
|
47
|
+
<Button
|
|
48
|
+
key={provider}
|
|
49
|
+
type="button"
|
|
50
|
+
variant="outline"
|
|
51
|
+
disabled={isPending}
|
|
52
|
+
onClick={() => signIn(provider)}
|
|
53
|
+
>
|
|
54
|
+
{isPending && chosen === provider ? (
|
|
55
|
+
<>
|
|
56
|
+
<Spinner data-icon="inline-start" />
|
|
57
|
+
Redirecting
|
|
58
|
+
</>
|
|
59
|
+
) : (
|
|
60
|
+
`Continue with ${PROVIDER_LABELS[provider]}`
|
|
61
|
+
)}
|
|
62
|
+
</Button>
|
|
63
|
+
))}
|
|
64
|
+
{error ? <FieldError>{error}</FieldError> : null}
|
|
65
|
+
</Field>
|
|
66
|
+
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
|
|
67
|
+
Or continue with email
|
|
68
|
+
</FieldSeparator>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { createAuthClient } from "better-auth/react";
|
|
2
|
-
|
|
1
|
+
import { createAuthClient } from "better-auth/react";
|
|
2
|
+
|
|
3
3
|
export const authClient = createAuthClient();
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
-
import { getRequestHeaders } from "@tanstack/react-start/server";
|
|
3
|
-
|
|
4
|
-
import { auth } from "./auth";
|
|
5
|
-
|
|
6
|
-
export const getSession = createServerFn({ method: "GET" }).handler(async () => {
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { getRequestHeaders } from "@tanstack/react-start/server";
|
|
3
|
+
|
|
4
|
+
import { auth, socialProviders } from "./auth";
|
|
5
|
+
|
|
6
|
+
export const getSession = createServerFn({ method: "GET" }).handler(async () => {
|
|
7
7
|
const session = await auth.api.getSession({
|
|
8
8
|
headers: getRequestHeaders(),
|
|
9
9
|
});
|
|
10
|
-
|
|
11
|
-
return session;
|
|
10
|
+
|
|
11
|
+
return session;
|
|
12
12
|
});
|
|
13
|
+
|
|
14
|
+
// GitHub and Google, when their keys are set; read on the server, where the
|
|
15
|
+
// environment variables are
|
|
16
|
+
export const getSocialProviders = createServerFn({ method: "GET" }).handler(
|
|
17
|
+
() => socialProviders
|
|
18
|
+
);
|
|
@@ -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) {
|