feat: add email/password auth backend + forgot/reset password routes
- Configure Better Auth emailAndPassword plugin with Resend - Add email verification and password reset email sending - Create forgot-password and reset-password frontend routes - Add auth schemas to @lila/shared
This commit is contained in:
parent
35e54014b3
commit
6297dff399
10 changed files with 317 additions and 0 deletions
74
apps/web/src/routes/forgot-password.tsx
Normal file
74
apps/web/src/routes/forgot-password.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { useState } from "react";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { authClient } from "../lib/auth-client";
|
||||
import { toast } from "sonner";
|
||||
|
||||
function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsPending(true);
|
||||
await authClient.requestPasswordReset(
|
||||
{ email, redirectTo: `${window.location.origin}/reset-password` },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("Check your email for a reset link.");
|
||||
setIsPending(false);
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message ?? "Something went wrong.");
|
||||
setIsPending(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-6 p-8 max-w-sm mx-auto">
|
||||
<div className="w-full text-center">
|
||||
<h1 className="text-2xl font-black tracking-tight text-(--color-text)">
|
||||
Forgot password
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-(--color-text-muted)">
|
||||
Enter your email and we'll send you a reset link.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void handleSubmit();
|
||||
}}
|
||||
className="w-full flex flex-col gap-3"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-bold hover:bg-(--color-primary-dark) transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isPending ? "Sending..." : "Send reset link"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
className="text-sm text-(--color-text-muted) hover:text-(--color-primary) transition-colors"
|
||||
>
|
||||
Back to home
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/forgot-password")({
|
||||
component: ForgotPasswordPage,
|
||||
});
|
||||
91
apps/web/src/routes/reset-password.tsx
Normal file
91
apps/web/src/routes/reset-password.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { useState } from "react";
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||
import { authClient } from "../lib/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import { ResetPasswordSearchSchema } from "@lila/shared";
|
||||
|
||||
function ResetPasswordPage() {
|
||||
const token = String(Route.useSearch().token);
|
||||
const navigate = useNavigate();
|
||||
const [password, setPassword] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 p-8 max-w-sm mx-auto text-center">
|
||||
<h1 className="text-2xl font-black tracking-tight text-(--color-text)">
|
||||
Invalid link
|
||||
</h1>
|
||||
<p className="text-sm text-(--color-text-muted)">
|
||||
This reset link is invalid or has expired.
|
||||
</p>
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="text-sm text-(--color-primary) hover:underline"
|
||||
>
|
||||
Request a new one
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsPending(true);
|
||||
await authClient.resetPassword(
|
||||
{ newPassword: password, token },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("Password updated. You can now sign in.");
|
||||
void navigate({ to: "/" });
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message ?? "Something went wrong.");
|
||||
setIsPending(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-6 p-8 max-w-sm mx-auto">
|
||||
<div className="w-full text-center">
|
||||
<h1 className="text-2xl font-black tracking-tight text-(--color-text)">
|
||||
Reset password
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-(--color-text-muted)">
|
||||
Enter your new password below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void handleSubmit();
|
||||
}}
|
||||
className="w-full flex flex-col gap-3"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
className="w-full rounded-2xl border border-(--color-primary-light) bg-white px-4 py-3 text-sm text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-bold hover:bg-(--color-primary-dark) transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isPending ? "Updating..." : "Update password"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/reset-password")({
|
||||
component: ResetPasswordPage,
|
||||
validateSearch: ResetPasswordSearchSchema,
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue