mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-17 10:11:31 +10:00
refactor(v4.0.0-alpha): beginning of a new era
This commit is contained in:
20
apps/client/src/pages/auth/_components/social-auth.tsx
Normal file
20
apps/client/src/pages/auth/_components/social-auth.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { GithubLogo, GoogleLogo } from "@phosphor-icons/react";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
|
||||
export const SocialAuth = () => (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button asChild size="lg" className="w-full !bg-[#222] !text-white hover:!bg-[#222]/80">
|
||||
<a href="/api/auth/github">
|
||||
<GoogleLogo className="mr-3 h-4 w-4" />
|
||||
GitHub
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button asChild size="lg" className="w-full !bg-[#4285F4] !text-white hover:!bg-[#4285F4]/80">
|
||||
<a href="/api/auth/google">
|
||||
<GithubLogo className="mr-3 h-4 w-4" />
|
||||
Google
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
102
apps/client/src/pages/auth/backup-otp/page.tsx
Normal file
102
apps/client/src/pages/auth/backup-otp/page.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import { twoFactorBackupSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from "@reactive-resume/ui";
|
||||
import { AxiosError } from "axios";
|
||||
import { useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { z } from "zod";
|
||||
|
||||
import { toast } from "@/client/hooks/use-toast";
|
||||
import { useBackupOtp } from "@/client/services/auth";
|
||||
|
||||
type FormValues = z.infer<typeof twoFactorBackupSchema>;
|
||||
|
||||
export const BackupOtpPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { backupOtp, loading } = useBackupOtp();
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
usePasswordToggle(formRef);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(twoFactorBackupSchema),
|
||||
defaultValues: { code: "" },
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
await backupOtp(data);
|
||||
|
||||
navigate("/dashboard");
|
||||
} catch (error) {
|
||||
form.reset();
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const message = error.response?.data.message || error.message;
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to sign in",
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Use your backup code</h2>
|
||||
<h6 className="leading-relaxed opacity-60">
|
||||
Enter one of the 10 backup codes you saved when you enabled two-factor authentication.
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-col gap-y-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
name="code"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Backup Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
pattern="[a-z0-9]{10}"
|
||||
placeholder="a1b2c3d4e5"
|
||||
title="may contain lowercase letters or numbers, and must be exactly 10 characters."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={loading} className="mt-4 w-full">
|
||||
Sign in
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
105
apps/client/src/pages/auth/forgot-password/page.tsx
Normal file
105
apps/client/src/pages/auth/forgot-password/page.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import { forgotPasswordSchema } from "@reactive-resume/dto";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from "@reactive-resume/ui";
|
||||
import { AxiosError } from "axios";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { toast } from "@/client/hooks/use-toast";
|
||||
import { useForgotPassword } from "@/client/services/auth";
|
||||
|
||||
type FormValues = z.infer<typeof forgotPasswordSchema>;
|
||||
|
||||
export const ForgotPasswordPage = () => {
|
||||
const [submitted, setSubmitted] = useState<boolean>(false);
|
||||
const { forgotPassword, loading } = useForgotPassword();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(forgotPasswordSchema),
|
||||
defaultValues: { email: "" },
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
await forgotPassword(data);
|
||||
setSubmitted(true);
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
const message = error.response?.data.message || error.message;
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to send your password recovery email",
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">You've got mail!</h2>
|
||||
<Alert variant="success">
|
||||
<AlertDescription className="pt-0">
|
||||
A password reset link should have been sent to your inbox, if an account existed with
|
||||
the email you provided.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Forgot your password?</h2>
|
||||
<h6 className="leading-relaxed opacity-75">
|
||||
Enter your email address and we will send you a link to reset your password if the account
|
||||
exists.
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form className="flex flex-col gap-y-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john.doe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={loading} className="mt-4 w-full">
|
||||
Send Email
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
58
apps/client/src/pages/auth/layout.tsx
Normal file
58
apps/client/src/pages/auth/layout.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { useMemo } from "react";
|
||||
import { Link, matchRoutes, Outlet, useLocation } from "react-router-dom";
|
||||
|
||||
import { Logo } from "@/client/components/logo";
|
||||
|
||||
import { SocialAuth } from "./_components/social-auth";
|
||||
|
||||
const authRoutes = [{ path: "/auth/login" }, { path: "/auth/register" }];
|
||||
|
||||
export const AuthLayout = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isAuthRoute = useMemo(() => matchRoutes(authRoutes, location) !== null, [location]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen">
|
||||
<div className="flex w-full flex-col justify-center gap-y-8 px-12 sm:mx-auto sm:basis-[420px] sm:px-0 lg:basis-[480px] lg:px-12">
|
||||
<Link to="/" className="h-24 w-24">
|
||||
<Logo className="-ml-3" size={96} />
|
||||
</Link>
|
||||
|
||||
<Outlet />
|
||||
|
||||
{isAuthRoute && (
|
||||
<>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<hr className="flex-1" />
|
||||
<span className="text-xs font-medium">or continue with</span>
|
||||
<hr className="flex-1" />
|
||||
</div>
|
||||
|
||||
<SocialAuth />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative hidden lg:block lg:flex-1">
|
||||
<img
|
||||
width={1920}
|
||||
height={1080}
|
||||
alt="Open books on a table"
|
||||
className="h-screen w-full object-cover object-center"
|
||||
src="/backgrounds/patrick-tomasso-Oaqk7qqNh_c-unsplash.jpg"
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-5 right-5 z-10 bg-primary/30 px-4 py-2 text-xs font-medium text-primary-foreground backdrop-blur-sm">
|
||||
<a
|
||||
href="https://unsplash.com/photos/Oaqk7qqNh_c"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
Photograph by Patrick Tomasso
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
126
apps/client/src/pages/auth/login/page.tsx
Normal file
126
apps/client/src/pages/auth/login/page.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowRight, Warning } from "@phosphor-icons/react";
|
||||
import { loginSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from "@reactive-resume/ui";
|
||||
import { AxiosError } from "axios";
|
||||
import { useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link } from "react-router-dom";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useToast } from "@/client/hooks/use-toast";
|
||||
import { useLogin } from "@/client/services/auth";
|
||||
|
||||
type FormValues = z.infer<typeof loginSchema>;
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { toast } = useToast();
|
||||
const { login, loading } = useLogin();
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
usePasswordToggle(formRef);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: { identifier: "", password: "" },
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
await login(data);
|
||||
} catch (error) {
|
||||
form.reset();
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const message = error.response?.data.message || error.message;
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to sign in",
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Sign in to your account</h2>
|
||||
<h6>
|
||||
<span className="opacity-75">Don't have an account?</span>
|
||||
<Button asChild variant="link" className="px-1.5">
|
||||
<Link to="/auth/register">
|
||||
Create one now <ArrowRight className="ml-1" />
|
||||
</Link>
|
||||
</Button>
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-col gap-y-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
name="identifier"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john.doe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>You can also enter your username.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
|
||||
temporarily.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex items-center gap-x-4">
|
||||
<Button type="submit" disabled={loading} className="flex-1">
|
||||
Sign in
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="link" className="px-4">
|
||||
<Link to="/auth/forgot-password">Forgot Password?</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
155
apps/client/src/pages/auth/register/page.tsx
Normal file
155
apps/client/src/pages/auth/register/page.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowRight, Warning } from "@phosphor-icons/react";
|
||||
import { registerSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from "@reactive-resume/ui";
|
||||
import { AxiosError } from "axios";
|
||||
import { useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { z } from "zod";
|
||||
|
||||
import { toast } from "@/client/hooks/use-toast";
|
||||
import { useRegister } from "@/client/services/auth";
|
||||
|
||||
type FormValues = z.infer<typeof registerSchema>;
|
||||
|
||||
export const RegisterPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { register, loading } = useRegister();
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
usePasswordToggle(formRef);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(registerSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
language: "en",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
await register(data);
|
||||
|
||||
navigate("/auth/verify-email");
|
||||
} catch (error) {
|
||||
form.reset();
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const message = error.response?.data.message || error.message;
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to sign up",
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Create a new account</h2>
|
||||
<h6>
|
||||
<span className="opacity-75">Already have an account?</span>
|
||||
<Button asChild variant="link" className="px-1.5">
|
||||
<Link to="/auth/login">
|
||||
Sign in now <ArrowRight className="ml-1" />
|
||||
</Link>
|
||||
</Button>
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-col gap-y-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
name="name"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="username"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john.doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john.doe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
|
||||
temporarily.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button disabled={loading} className="mt-4 w-full">
|
||||
Sign up
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
110
apps/client/src/pages/auth/reset-password/page.tsx
Normal file
110
apps/client/src/pages/auth/reset-password/page.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import { resetPasswordSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from "@reactive-resume/ui";
|
||||
import { AxiosError } from "axios";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { z } from "zod";
|
||||
|
||||
import { toast } from "@/client/hooks/use-toast";
|
||||
import { useResetPassword } from "@/client/services/auth";
|
||||
|
||||
type FormValues = z.infer<typeof resetPasswordSchema>;
|
||||
|
||||
export const ResetPasswordPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get("token") || "";
|
||||
|
||||
const { resetPassword, loading } = useResetPassword();
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
usePasswordToggle(formRef);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(resetPasswordSchema),
|
||||
defaultValues: { token, password: "" },
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
await resetPassword(data);
|
||||
|
||||
navigate("/auth/login");
|
||||
} catch (error) {
|
||||
form.reset();
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const message = error.response?.data.message || error.message;
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to reset your password",
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Redirect the user to the forgot password page if the token is not present.
|
||||
useEffect(() => {
|
||||
if (!token) navigate("/auth/forgot-password");
|
||||
}, [token, navigate]);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Reset your password</h2>
|
||||
<h6 className="leading-relaxed opacity-75">
|
||||
Enter a new password below, and make sure it's secure.
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-col gap-y-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
|
||||
temporarily.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={loading} className="mt-4 w-full">
|
||||
Update Password
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
80
apps/client/src/pages/auth/verify-email/page.tsx
Normal file
80
apps/client/src/pages/auth/verify-email/page.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { ArrowRight, Info, SealCheck, Warning } from "@phosphor-icons/react";
|
||||
import { Alert, AlertDescription, AlertTitle, Button } from "@reactive-resume/ui";
|
||||
import { AxiosError } from "axios";
|
||||
import { useEffect } from "react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { useToast } from "@/client/hooks/use-toast";
|
||||
import { queryClient } from "@/client/libs/query-client";
|
||||
import { useVerifyEmail } from "@/client/services/auth";
|
||||
|
||||
export const VerifyEmailPage = () => {
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get("token");
|
||||
|
||||
const { verifyEmail, loading } = useVerifyEmail();
|
||||
|
||||
useEffect(() => {
|
||||
const handleVerifyEmail = async (token: string) => {
|
||||
try {
|
||||
await verifyEmail({ token });
|
||||
await queryClient.invalidateQueries({ queryKey: ["user"] });
|
||||
|
||||
toast({
|
||||
variant: "success",
|
||||
icon: <SealCheck size={16} weight="bold" />,
|
||||
title: "Your email address has been verified successfully.",
|
||||
});
|
||||
|
||||
navigate("/dashboard/resumes", { replace: true });
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
const message = error.response?.data.message || error.message;
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to verify your email address",
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!token) return;
|
||||
|
||||
handleVerifyEmail(token);
|
||||
}, [token, navigate, verifyEmail]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Verify your email address</h2>
|
||||
<p className="leading-relaxed opacity-75">
|
||||
You should have received an email from <strong>Reactive Resume</strong> with a link to
|
||||
verify your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert variant="info">
|
||||
<Info size={18} />
|
||||
|
||||
<AlertTitle>Please note that this step is completely optional.</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
We verify your email address only to ensure that we can send you a password reset link in
|
||||
case you forget your password.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button asChild disabled={loading}>
|
||||
<Link to="/dashboard">
|
||||
Continue to Dashboard
|
||||
<ArrowRight className="ml-2" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
104
apps/client/src/pages/auth/verify-otp/page.tsx
Normal file
104
apps/client/src/pages/auth/verify-otp/page.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowRight, Warning } from "@phosphor-icons/react";
|
||||
import { twoFactorSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from "@reactive-resume/ui";
|
||||
import { AxiosError } from "axios";
|
||||
import { useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { z } from "zod";
|
||||
|
||||
import { toast } from "@/client/hooks/use-toast";
|
||||
import { useVerifyOtp } from "@/client/services/auth";
|
||||
|
||||
type FormValues = z.infer<typeof twoFactorSchema>;
|
||||
|
||||
export const VerifyOtpPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { verifyOtp, loading } = useVerifyOtp();
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
usePasswordToggle(formRef);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(twoFactorSchema),
|
||||
defaultValues: { code: "" },
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
await verifyOtp(data);
|
||||
|
||||
navigate("/dashboard");
|
||||
} catch (error) {
|
||||
form.reset();
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const message = error.response?.data.message || error.message;
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to sign in",
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Two Step Verification</h2>
|
||||
<h6>
|
||||
<span className="opacity-75">
|
||||
Enter the one-time password provided by your authenticator app below.
|
||||
</span>
|
||||
<Button asChild variant="link" className="px-1.5">
|
||||
<Link to="/auth/backup-otp">
|
||||
Lost your device? <ArrowRight className="ml-1" />
|
||||
</Link>
|
||||
</Button>
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-col gap-y-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
name="code"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>One-Time Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="123456" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={loading} className="mt-4 w-full">
|
||||
Sign in
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user