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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useState, useTransition } from "react";
|
|
2
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
3
|
+
import { CircleAlert, CircleCheck } from "lucide-react";
|
|
4
|
+
import { useForm } from "react-hook-form";
|
|
5
|
+
|
|
6
|
+
import { authClient } from "~/lib/auth-client";
|
|
7
|
+
import {
|
|
8
|
+
changePasswordSchema,
|
|
9
|
+
getAuthErrorMessage,
|
|
10
|
+
type ChangePasswordValues,
|
|
11
|
+
} from "~/lib/auth-schema";
|
|
12
|
+
import { Alert, AlertDescription } from "~/components/ui/alert";
|
|
13
|
+
import { Button } from "~/components/ui/button";
|
|
14
|
+
import {
|
|
15
|
+
Card,
|
|
16
|
+
CardContent,
|
|
17
|
+
CardDescription,
|
|
18
|
+
CardHeader,
|
|
19
|
+
CardTitle,
|
|
20
|
+
} from "~/components/ui/card";
|
|
21
|
+
import {
|
|
22
|
+
Field,
|
|
23
|
+
FieldError,
|
|
24
|
+
FieldGroup,
|
|
25
|
+
FieldLabel,
|
|
26
|
+
} from "~/components/ui/field";
|
|
27
|
+
import { Input } from "~/components/ui/input";
|
|
28
|
+
import { Spinner } from "~/components/ui/spinner";
|
|
29
|
+
|
|
30
|
+
export function ChangePasswordForm() {
|
|
31
|
+
const [isPending, startTransition] = useTransition();
|
|
32
|
+
const [message, setMessage] = useState<{
|
|
33
|
+
text: string;
|
|
34
|
+
isError: boolean;
|
|
35
|
+
} | null>(null);
|
|
36
|
+
const form = useForm<ChangePasswordValues>({
|
|
37
|
+
resolver: zodResolver(changePasswordSchema),
|
|
38
|
+
defaultValues: { currentPassword: "", newPassword: "" },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
42
|
+
setMessage(null);
|
|
43
|
+
|
|
44
|
+
startTransition(async () => {
|
|
45
|
+
// Other devices are signed out; this one stays signed in
|
|
46
|
+
const result = await authClient.changePassword({
|
|
47
|
+
currentPassword: values.currentPassword,
|
|
48
|
+
newPassword: values.newPassword,
|
|
49
|
+
revokeOtherSessions: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (result.error) {
|
|
53
|
+
setMessage({ text: getAuthErrorMessage(result.error), isError: true });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
form.reset();
|
|
58
|
+
setMessage({ text: "Password changed.", isError: false });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle>Change password</CardTitle>
|
|
66
|
+
<CardDescription>
|
|
67
|
+
Other devices are signed out after the change.
|
|
68
|
+
</CardDescription>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent>
|
|
71
|
+
<form onSubmit={onSubmit} noValidate>
|
|
72
|
+
<FieldGroup>
|
|
73
|
+
<Field
|
|
74
|
+
data-invalid={Boolean(form.formState.errors.currentPassword)}
|
|
75
|
+
>
|
|
76
|
+
<FieldLabel htmlFor="currentPassword">Current password</FieldLabel>
|
|
77
|
+
<Input
|
|
78
|
+
id="currentPassword"
|
|
79
|
+
type="password"
|
|
80
|
+
autoComplete="current-password"
|
|
81
|
+
aria-invalid={Boolean(form.formState.errors.currentPassword)}
|
|
82
|
+
{...form.register("currentPassword")}
|
|
83
|
+
/>
|
|
84
|
+
<FieldError errors={[form.formState.errors.currentPassword]} />
|
|
85
|
+
</Field>
|
|
86
|
+
|
|
87
|
+
<Field data-invalid={Boolean(form.formState.errors.newPassword)}>
|
|
88
|
+
<FieldLabel htmlFor="newPassword">New password</FieldLabel>
|
|
89
|
+
<Input
|
|
90
|
+
id="newPassword"
|
|
91
|
+
type="password"
|
|
92
|
+
autoComplete="new-password"
|
|
93
|
+
aria-invalid={Boolean(form.formState.errors.newPassword)}
|
|
94
|
+
{...form.register("newPassword")}
|
|
95
|
+
/>
|
|
96
|
+
<FieldError errors={[form.formState.errors.newPassword]} />
|
|
97
|
+
</Field>
|
|
98
|
+
|
|
99
|
+
{message ? (
|
|
100
|
+
<Alert variant={message.isError ? "destructive" : "default"}>
|
|
101
|
+
{message.isError ? <CircleAlert /> : <CircleCheck />}
|
|
102
|
+
<AlertDescription>{message.text}</AlertDescription>
|
|
103
|
+
</Alert>
|
|
104
|
+
) : null}
|
|
105
|
+
|
|
106
|
+
<Field orientation="horizontal">
|
|
107
|
+
<Button type="submit" disabled={isPending}>
|
|
108
|
+
{isPending ? (
|
|
109
|
+
<>
|
|
110
|
+
<Spinner data-icon="inline-start" />
|
|
111
|
+
Saving
|
|
112
|
+
</>
|
|
113
|
+
) : (
|
|
114
|
+
"Change password"
|
|
115
|
+
)}
|
|
116
|
+
</Button>
|
|
117
|
+
</Field>
|
|
118
|
+
</FieldGroup>
|
|
119
|
+
</form>
|
|
120
|
+
</CardContent>
|
|
121
|
+
</Card>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState, useTransition } from "react";
|
|
2
|
+
import { BadgeCheck, MailWarning } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { authClient } from "~/lib/auth-client";
|
|
5
|
+
import { getAuthErrorMessage } from "~/lib/auth-schema";
|
|
6
|
+
import { Badge } from "~/components/ui/badge";
|
|
7
|
+
import { Button } from "~/components/ui/button";
|
|
8
|
+
import {
|
|
9
|
+
Card,
|
|
10
|
+
CardAction,
|
|
11
|
+
CardContent,
|
|
12
|
+
CardDescription,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
} from "~/components/ui/card";
|
|
16
|
+
import { Spinner } from "~/components/ui/spinner";
|
|
17
|
+
|
|
18
|
+
// Shows whether the email address is verified, with a button to send the
|
|
19
|
+
// verification email again
|
|
20
|
+
export function EmailVerificationCard({
|
|
21
|
+
email,
|
|
22
|
+
emailVerified,
|
|
23
|
+
}: {
|
|
24
|
+
email: string;
|
|
25
|
+
emailVerified: boolean;
|
|
26
|
+
}) {
|
|
27
|
+
const [isPending, startTransition] = useTransition();
|
|
28
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
29
|
+
|
|
30
|
+
function resend() {
|
|
31
|
+
setMessage(null);
|
|
32
|
+
startTransition(async () => {
|
|
33
|
+
const result = await authClient.sendVerificationEmail({
|
|
34
|
+
email,
|
|
35
|
+
callbackURL: "/dashboard",
|
|
36
|
+
});
|
|
37
|
+
setMessage(
|
|
38
|
+
result.error
|
|
39
|
+
? getAuthErrorMessage(result.error)
|
|
40
|
+
: "Verification email sent. Without an email provider set up, the link is printed in the server terminal."
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Card>
|
|
47
|
+
<CardHeader>
|
|
48
|
+
<CardTitle>
|
|
49
|
+
{emailVerified ? "Email verified" : "Verify your email"}
|
|
50
|
+
</CardTitle>
|
|
51
|
+
<CardDescription>
|
|
52
|
+
{emailVerified
|
|
53
|
+
? `${email} is confirmed.`
|
|
54
|
+
: `We sent a link to ${email}. Open it to confirm the address.`}
|
|
55
|
+
</CardDescription>
|
|
56
|
+
<CardAction>
|
|
57
|
+
{emailVerified ? (
|
|
58
|
+
<Badge variant="secondary">
|
|
59
|
+
<BadgeCheck data-icon="inline-start" />
|
|
60
|
+
Verified
|
|
61
|
+
</Badge>
|
|
62
|
+
) : (
|
|
63
|
+
<Badge variant="outline">
|
|
64
|
+
<MailWarning data-icon="inline-start" />
|
|
65
|
+
Pending
|
|
66
|
+
</Badge>
|
|
67
|
+
)}
|
|
68
|
+
</CardAction>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
{emailVerified ? null : (
|
|
71
|
+
<CardContent className="flex flex-col items-start gap-3">
|
|
72
|
+
<Button variant="outline" onClick={resend} disabled={isPending}>
|
|
73
|
+
{isPending ? (
|
|
74
|
+
<>
|
|
75
|
+
<Spinner data-icon="inline-start" />
|
|
76
|
+
Sending
|
|
77
|
+
</>
|
|
78
|
+
) : (
|
|
79
|
+
"Send the email again"
|
|
80
|
+
)}
|
|
81
|
+
</Button>
|
|
82
|
+
{message ? (
|
|
83
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
84
|
+
) : null}
|
|
85
|
+
</CardContent>
|
|
86
|
+
)}
|
|
87
|
+
</Card>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import { useState, useTransition } from "react";
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { CircleAlert, MailCheck } from "lucide-react";
|
|
5
|
+
import { useForm } from "react-hook-form";
|
|
6
|
+
|
|
7
|
+
import { authClient } from "~/lib/auth-client";
|
|
8
|
+
import {
|
|
9
|
+
forgotPasswordSchema,
|
|
10
|
+
getAuthErrorMessage,
|
|
11
|
+
type ForgotPasswordValues,
|
|
12
|
+
} from "~/lib/auth-schema";
|
|
13
|
+
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
|
14
|
+
import { Button } from "~/components/ui/button";
|
|
15
|
+
import {
|
|
16
|
+
Card,
|
|
17
|
+
CardContent,
|
|
18
|
+
CardDescription,
|
|
19
|
+
CardHeader,
|
|
20
|
+
CardTitle,
|
|
21
|
+
} from "~/components/ui/card";
|
|
22
|
+
import {
|
|
23
|
+
Field,
|
|
24
|
+
FieldDescription,
|
|
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 ForgotPasswordForm() {
|
|
33
|
+
const [isPending, startTransition] = useTransition();
|
|
34
|
+
const [authError, setAuthError] = useState<string | null>(null);
|
|
35
|
+
const [isSent, setIsSent] = useState(false);
|
|
36
|
+
const form = useForm<ForgotPasswordValues>({
|
|
37
|
+
resolver: zodResolver(forgotPasswordSchema),
|
|
38
|
+
defaultValues: { email: "" },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
42
|
+
setAuthError(null);
|
|
43
|
+
|
|
44
|
+
startTransition(async () => {
|
|
45
|
+
// The link in the email opens /reset-password with a one-time token
|
|
46
|
+
const result = await authClient.requestPasswordReset({
|
|
47
|
+
email: values.email,
|
|
48
|
+
redirectTo: "/reset-password",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (result.error) {
|
|
52
|
+
setAuthError(getAuthErrorMessage(result.error));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setIsSent(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Card>
|
|
62
|
+
<CardHeader>
|
|
63
|
+
<CardTitle>Forgot your password?</CardTitle>
|
|
64
|
+
<CardDescription>
|
|
65
|
+
Enter your email and we will send you a link to choose a new one.
|
|
66
|
+
</CardDescription>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent>
|
|
69
|
+
{isSent ? (
|
|
70
|
+
<FieldGroup>
|
|
71
|
+
<Alert>
|
|
72
|
+
<MailCheck />
|
|
73
|
+
<AlertTitle>Check your email</AlertTitle>
|
|
74
|
+
<AlertDescription>
|
|
75
|
+
If an account exists for that email, a reset link is on its
|
|
76
|
+
way. Without an email provider set up, the link is printed in
|
|
77
|
+
the server terminal.
|
|
78
|
+
</AlertDescription>
|
|
79
|
+
</Alert>
|
|
80
|
+
<FieldDescription className="text-center">
|
|
81
|
+
<Link to="/sign-in">Back to login</Link>
|
|
82
|
+
</FieldDescription>
|
|
83
|
+
</FieldGroup>
|
|
84
|
+
) : (
|
|
85
|
+
<form onSubmit={onSubmit} noValidate>
|
|
86
|
+
<FieldGroup>
|
|
87
|
+
<Field data-invalid={Boolean(form.formState.errors.email)}>
|
|
88
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
89
|
+
<Input
|
|
90
|
+
id="email"
|
|
91
|
+
type="email"
|
|
92
|
+
autoComplete="email"
|
|
93
|
+
aria-invalid={Boolean(form.formState.errors.email)}
|
|
94
|
+
placeholder="you@example.com"
|
|
95
|
+
{...form.register("email")}
|
|
96
|
+
/>
|
|
97
|
+
<FieldDescription>
|
|
98
|
+
Use the email you registered with.
|
|
99
|
+
</FieldDescription>
|
|
100
|
+
<FieldError errors={[form.formState.errors.email]} />
|
|
101
|
+
</Field>
|
|
102
|
+
|
|
103
|
+
{authError ? (
|
|
104
|
+
<Alert variant="destructive">
|
|
105
|
+
<CircleAlert />
|
|
106
|
+
<AlertDescription>{authError}</AlertDescription>
|
|
107
|
+
</Alert>
|
|
108
|
+
) : null}
|
|
109
|
+
|
|
110
|
+
<Field>
|
|
111
|
+
<Button type="submit" disabled={isPending}>
|
|
112
|
+
{isPending ? (
|
|
113
|
+
<>
|
|
114
|
+
<Spinner data-icon="inline-start" />
|
|
115
|
+
Sending link
|
|
116
|
+
</>
|
|
117
|
+
) : (
|
|
118
|
+
"Send reset link"
|
|
119
|
+
)}
|
|
120
|
+
</Button>
|
|
121
|
+
<FieldDescription className="text-center">
|
|
122
|
+
Remembered it? <Link to="/sign-in">Back to login</Link>
|
|
123
|
+
</FieldDescription>
|
|
124
|
+
</Field>
|
|
125
|
+
</FieldGroup>
|
|
126
|
+
</form>
|
|
127
|
+
)}
|
|
128
|
+
</CardContent>
|
|
129
|
+
</Card>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -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
|
+
}
|