Adding reset password feature.

This commit is contained in:
charlesgauthereau
2025-11-22 17:20:52 +01:00
parent aabdea06a9
commit fe5d8bae1b
29 changed files with 2596 additions and 290 deletions
+25
View File
@@ -0,0 +1,25 @@
import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from "@react-email/components";
import * as React from "react";
import { PropsWithChildren } from "react";
import { getServerUrl } from "@/utils/get-server-url";
const baseUrl = getServerUrl();
export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?: string }>) => {
return (
<Tailwind>
<Html>
<Head />
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
<Body className="bg-gray-100 py-4" style={{ fontFamily: "Arial, sans-serif" }}>
<Container className="bg-white border border-gray-200 p-12">
<Img src={`${baseUrl}/logo-title-black.png`} width="200" height="auto" alt="Logo" />
<Section>{children}</Section>
</Container>
</Body>
</Html>
</Tailwind>
);
};
export default EmailLayout;
@@ -0,0 +1,30 @@
import * as React from "react";
import EmailLayout from "./email-layout";
import {Text, Section, Button} from "@react-email/components";
export interface EmailResetPasswordProps {
url: string;
}
export const EmailResetPassword = ({url}: EmailResetPasswordProps) => {
return (
<EmailLayout preview="Email for password reset of your Portabase account">
<Text className="text-base text-green-800 font-bold ">Hello !</Text>
<Text className="text-base font-light text-green-800 ">You are receiving this email because we
received a password reset request for your account.</Text>{" "}
<Section className="mt-[32px] mb-[32px] text-center">
<Button
className="rounded bg-[#000000] px-5 py-3 text-center font-semibold text-[12px] text-white no-underline"
href={url}
>
Reset Password
</Button>
</Section>
<Text className="text-base font-light text-green-800 ">If you did not request a password reset, no
further action is required.</Text>
<Text className="text-base font-light text-green-800 ">Regards,<br/>Portabase</Text>
</EmailLayout>
);
};
export default EmailResetPassword;
@@ -0,0 +1,15 @@
import { Text } from "@react-email/components";
import * as React from "react";
import EmailLayout from "./email-layout";
export const EmailSettingsTest = () => {
return (
<EmailLayout preview="Email Setup">
<Text className="text-base font-light leading-8 text-green-800 ">Hi, your email settings are setup !</Text>
<Text className="text-base font-light leading-8 text-green-800 ">Best regard,</Text>{" "}
<Text className="text-base font-light leading-8 text-green-800 ">Portabase</Text>
</EmailLayout>
);
};
export default EmailSettingsTest;
+6
View File
@@ -0,0 +1,6 @@
import {Text as ReactEmailText} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
export const EmailText = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
return <ReactEmailText className="text-base font-light leading-8 text-green-800 " {...props} />;
};
@@ -0,0 +1,164 @@
"use client";
import {Ref, useState} from "react";
import {Check, X} from "lucide-react";
import {motion, AnimatePresence} from "framer-motion";
import {
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {PasswordInput} from "@/components/ui/password-input";
interface PasswordStrengthInputProps {
field: {
name: string;
value: string;
onChange: (value: string) => void;
onBlur: () => void;
ref: Ref<HTMLInputElement>;
};
label?: string;
description?: string;
}
const passwordTextsFields = {
label: "Password",
placeholder: "Enter password",
strength: {
enter: "Enter a password",
weak: "Weak password",
medium: "Medium password",
strong: "Strong password"
},
requirementsIntro: "Must contain:",
requirements: {
minLength: "At least 8 characters",
number: "At least 1 number",
lowercase: "At least 1 lowercase letter",
uppercase: "At least 1 uppercase letter",
specialChar: "At least 1 special character"
}
}
export function PasswordStrengthInput({
field,
label,
description,
}: PasswordStrengthInputProps) {
const text = passwordTextsFields
const [isVisible, setIsVisible] = useState(false);
const password = field.value ?? "";
const requirements = [
{regex: /.{8,}/, text: text.requirements.minLength},
{regex: /[0-9]/, text: text.requirements.number},
{regex: /[a-z]/, text: text.requirements.lowercase},
{regex: /[A-Z]/, text: text.requirements.uppercase},
{regex: /[^a-zA-Z0-9]/, text: text.requirements.specialChar},
];
const strength = requirements.map((req) => ({
met: req.regex.test(password),
text: req.text,
}));
const strengthScore = strength.filter((req) => req.met).length;
const getStrengthColor = (score: number) => {
if (score === 0) return "bg-border";
if (score <= 2) return "bg-red-500";
if (score === 3) return "bg-orange-500";
if (score === 4) return "bg-amber-500";
return "bg-emerald-500";
};
const getStrengthText = (score: number) => {
if (score === 0) return text.strength.enter;
if (score <= 2) return text.strength.weak;
if (score === 3 || score === 4) return text.strength.medium;
return text.strength.strong;
};
return (
<FormItem>
<FormLabel>{label ?? text.label}</FormLabel>
<div className="relative">
<FormControl>
<PasswordInput
placeholder={text.placeholder}
value={field.value ?? ""}
onChange={(e) => field.onChange(e.target.value)}
onFocus={() => setIsVisible(true)}
onBlur={(e) => {
field.onBlur();
setIsVisible(false);
}}
ref={field.ref}
name={field.name}
/>
</FormControl>
</div>
<AnimatePresence>
{isVisible && (
<motion.div
initial={{opacity: 0, height: 0, y: -5}}
animate={{opacity: 1, height: "auto", y: 0}}
exit={{opacity: 0, height: 0, y: -5}}
transition={{duration: 0.35, ease: "easeInOut"}}
className="overflow-hidden"
>
<FormDescription>
{description ??
`${getStrengthText(strengthScore)}. ${text.requirementsIntro}`}
</FormDescription>
<FormMessage/>
<div
className="mb-2 h-1 w-full overflow-hidden rounded-full bg-border mt-2"
role="progressbar"
aria-valuenow={strengthScore}
aria-valuemin={0}
aria-valuemax={requirements.length}
>
<motion.div
key={strengthScore}
className={`h-full ${getStrengthColor(
strengthScore
)}`}
initial={{width: 0}}
animate={{
width: `${(strengthScore / requirements.length) * 100}%`,
}}
transition={{duration: 0.5}}
/>
</div>
<ul className="space-y-1.5" aria-label="Password requirements">
{strength.map((req, i) => (
<li key={i} className="flex items-center gap-2">
{req.met ? (
<Check className="h-4 w-4 text-emerald-500"/>
) : (
<X className="h-4 w-4 text-muted-foreground"/>
)}
<span
className={`text-xs ${
req.met ? "text-emerald-600" : "text-muted-foreground"
}`}
>
{req.text}
</span>
</li>
))}
</ul>
</motion.div>
)}
</AnimatePresence>
</FormItem>
);
}
@@ -0,0 +1,113 @@
"use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form";
import {Button} from "@/components/ui/button";
import {toast} from "sonner";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import Link from "next/link";
import {PasswordInput} from "@/components/ui/password-input";
import {LoginSchema, LoginType} from "@/components/wrappers/auth/login/login-form/login-form.schema";
import {SocialAuthButton, SocialProviderType} from "@/components/wrappers/auth/login/button-auth/social-auth-button";
import {authClient, signIn} from "@/lib/auth/auth-client";
import {useRouter} from "next/navigation";
import {Icon} from "@iconify/react";
import {useEffect, useState} from "react";
import {Separator} from "@/components/ui/separator";
import {
ForgotPasswordSchema,
ForgotPasswordType
} from "@/components/wrappers/auth/forgot-password/forgot-password.schema";
import {ArrowLeft} from "lucide-react";
import {getServerUrl} from "@/utils/get-server-url";
export type ForgotPasswordFormProps = {};
export const ForgotPasswordForm = (props: ForgotPasswordFormProps) => {
const router = useRouter();
const form = useZodForm({
schema: ForgotPasswordSchema,
});
const mutation = useMutation({
mutationFn: async (values: ForgotPasswordType) => {
try {
const {data, error} = await authClient.requestPasswordReset({
email: values.email,
redirectTo: `${getServerUrl()}/reset-password`,
});
if (error) {
toast.error(error.message);
} else {
// @ts-ignore
toast.success(data.message);
}
} catch (err) {
console.error(err);
toast.error("Unexpected client error during login");
}
},
onError: (err: any) => {
toast.error(err.message || "Client error");
},
});
return (
<TooltipProvider>
<Card>
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Forgot Password</h1>
<p className="text-balance text-muted-foreground">
Enter your email to reset your password
</p>
</div>
</CardHeader>
<CardContent>
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="email"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
autoComplete="email"
placeholder="example@portabase.io"
{...field}
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<Button type="submit" disabled={mutation.isPending}>
Send reset link
</Button>
</Form>
<div className="mt-4 text-center text-sm flex items-center justify-center gap-1">
<ArrowLeft size={14} className="text-gray-400"/>
<Link href="/login" className="underline">
Go back
</Link>
</div>
</CardContent>
</Card>
</TooltipProvider>
);
};
@@ -0,0 +1,10 @@
import { z } from "zod";
export const ForgotPasswordSchema = z.object({
email: z
.string()
.min(1, "Email is required")
.email("Invalid email address"),
});
export type ForgotPasswordType = z.infer<typeof ForgotPasswordSchema>;
@@ -1,150 +1,3 @@
// "use client";
//
// import {Card, CardContent, CardHeader} from "@/components/ui/card";
// import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
// import {Input} from "@/components/ui/input";
// import {Form} from "@/components/ui/form";
// import {Button} from "@/components/ui/button";
// import {toast} from "sonner";
// import {useMutation} from "@tanstack/react-query";
// import {TooltipProvider} from "@/components/ui/tooltip";
// import Link from "next/link";
// import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
// import {LoginSchema, LoginType} from "@/components/wrappers/auth/login/login-form/login-form.schema";
// import {SocialAuthButton, SocialProviderType} from "@/components/wrappers/auth/login/button-auth/social-auth-button";
// import {signIn} from "@/lib/auth/auth-client";
// import {useRouter} from "next/navigation";
// import {Icon} from "@iconify/react";
// import {useEffect, useState} from "react";
//
// export type loginFormProps = {
// defaultValues?: LoginType;
// authGoogleEnabled: boolean;
//
// };
//
// export const LoginForm = (props: loginFormProps) => {
// const router = useRouter();
//
// const form = useZodForm({
// schema: LoginSchema,
// });
//
// const [urlParams, setUrlParams] = useState<URLSearchParams>();
//
// useEffect(() => {
// const urlParams = new URLSearchParams(window.location.search);
// console.log(urlParams);
// setUrlParams(urlParams);
// const error = urlParams.get("error");
// console.log(urlParams.get("redirect"));
// if (error?.includes("pending")) {
// toast.error("Your account is not active.");
// urlParams.delete("error");
// window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
// }
// }, []);
//
//
// const mutation = useMutation({
// mutationFn: async (values: LoginType) => {
// const {error} = await signIn.email(
// {
// password: values.password,
// email: values.email,
// callbackURL: urlParams?.get("redirect") ?? "/dashboard/profile",
// }, {
// onSuccess: () => {
// toast.success("Login success");
// },
// });
// if (error) {
// toast.error(error.message);
// }
// },
// });
//
// const availableProviders: SocialProviderType[] = [];
//
// if (props.authGoogleEnabled) {
// availableProviders.push(
// {
// id: "google",
// name: "Google",
// icon: <Icon icon={"logos:google-icon"} width="25" height="25"/>,
// },
// )
// }
//
//
// return (
// <TooltipProvider>
// <Card>
// <CardHeader>
// <div className="grid gap-2 text-center mb-2">
// <h1 className="text-3xl font-bold">Login</h1>
// <p className="text-balance text-muted-foreground">Enter your informations below to login</p>
// </div>
// </CardHeader>
// <CardContent>
// <Form
// form={form}
// className="flex flex-col gap-4"
// onSubmit={async (values) => {
// await mutation.mutateAsync(values);
// }}
// >
// <FormField
// control={form.control}
// name="email"
// defaultValue=""
// render={({field}) => (
// <FormItem>
// <FormLabel>Email</FormLabel>
// <FormControl>
// <Input autoComplete="email webauthn"
// placeholder="exemple@portabase.io" {...field} />
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="password"
// defaultValue=""
// render={({field}) => (
// <FormItem>
// <div className="flex items-center">
// <FormLabel>Password</FormLabel>
// {/* <Link href={"/forgot-password"} className="ml-auto inline-block text-sm underline">
// Forgot your password?
// </Link>*/}
// </div>
// <FormControl>
// <PasswordInput autoComplete="current-password webauthn"
// placeholder="Your password" {...field} />
// </FormControl>
// <FormMessage/>
// </FormItem>
// )}
// />
// <Button>Sign in</Button>
// <div className="mt-4 text-center text-sm">
// Don&apos;t have an account?{" "}
// <Link href={"/register"} className="underline">
// Sign up
// </Link>
// </div>
// </Form>
// <SocialAuthButton
// callBackURL={urlParams?.get("redirect") ?? "/dashboard/profile"}
// providers={availableProviders}/>
// </CardContent>
// </Card>
// </TooltipProvider>
// );
// };
"use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
@@ -156,13 +9,14 @@ import {toast} from "sonner";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import Link from "next/link";
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
import {PasswordInput} from "@/components/ui/password-input";
import {LoginSchema, LoginType} from "@/components/wrappers/auth/login/login-form/login-form.schema";
import {SocialAuthButton, SocialProviderType} from "@/components/wrappers/auth/login/button-auth/social-auth-button";
import {signIn} from "@/lib/auth/auth-client";
import {useRouter} from "next/navigation";
import {Icon} from "@iconify/react";
import {useEffect, useState} from "react";
import {Separator} from "@/components/ui/separator";
export type loginFormProps = {
defaultValues?: LoginType;
@@ -271,9 +125,14 @@ export const LoginForm = (props: loginFormProps) => {
defaultValue=""
render={({field}) => (
<FormItem>
<div className="flex items-center">
<div className="flex items-center justify-between">
<FormLabel>Password</FormLabel>
{/* Optional forgot password link */}
<div className="text-center text-sm">
<Link href="/forgot-password" className="hover:underline">
Forgot your password ?
</Link>
</div>
</div>
<FormControl>
<PasswordInput
@@ -289,18 +148,26 @@ export const LoginForm = (props: loginFormProps) => {
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Signing in..." : "Sign in"}
</Button>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<Link href="/register" className="underline">
Sign up
</Link>
</div>
</Form>
<div className="relative my-4 flex items-center justify-center overflow-hidden">
<Separator/>
<div className="px-2 text-center bg-card text-sm">OR</div>
<Separator/>
</div>
<SocialAuthButton
callBackURL={urlParams.get("redirect") ?? "/dashboard/profile"}
providers={availableProviders}
/>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account ?{" "}
<Link href="/register" className="underline">
Sign up
</Link>
</div>
</CardContent>
</Card>
</TooltipProvider>
@@ -1,19 +1,20 @@
"use client";
import { useRouter } from "next/navigation";
import { Info } from "lucide-react";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import {useRouter} from "next/navigation";
import {Info} from "lucide-react";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Form } from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { TooltipProvider, TooltipTrigger, Tooltip, TooltipContent } from "@/components/ui/tooltip";
import { RegisterSchema, RegisterType } from "@/components/wrappers/auth/register/register-form/register-form.schema";
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form";
import {Button} from "@/components/ui/button";
import {TooltipProvider, TooltipTrigger, Tooltip, TooltipContent} from "@/components/ui/tooltip";
import {RegisterSchema, RegisterType} from "@/components/wrappers/auth/register/register-form/register-form.schema";
import {PasswordInput} from "@/components/ui/password-input";
import {signUp} from "@/lib/auth/auth-client";
import Link from "next/link";
export type registerFormProps = {
defaultValues?: RegisterType;
@@ -29,7 +30,7 @@ export const RegisterForm = (props: registerFormProps) => {
mutationFn: async (values: RegisterType) => {
await signUp.email(values, {
onSuccess: () => {
toast.success(`Success`);
toast.success(`Account successfully created`);
router.refresh();
router.push(`/login`);
},
@@ -63,13 +64,13 @@ export const RegisterForm = (props: registerFormProps) => {
control={form.control}
name="name"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -77,13 +78,13 @@ export const RegisterForm = (props: registerFormProps) => {
control={form.control}
name="email"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="exemple@portabase.io" {...field} />
</FormControl>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -91,18 +92,19 @@ export const RegisterForm = (props: registerFormProps) => {
control={form.control}
name="password"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel className="flex">
Password
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-3" size="15" />
<Info className="ml-3" size="15"/>
</TooltipTrigger>
<TooltipContent>
<p>
Min. 8 characters, 1 uppercase (A-Z), 1 lowercase (a-z), 1 number (0-9), 1 special character (!, @,
Min. 8 characters, 1 uppercase (A-Z), 1 lowercase (a-z), 1
number (0-9), 1 special character (!, @,
etc.)
</p>
</TooltipContent>
@@ -112,7 +114,7 @@ export const RegisterForm = (props: registerFormProps) => {
<FormControl>
<PasswordInput placeholder="Your password" {...field} />
</FormControl>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -120,17 +122,26 @@ export const RegisterForm = (props: registerFormProps) => {
control={form.control}
name="confirmPassword"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>Password Confirmation</FormLabel>
<FormControl>
<PasswordInput placeholder="Conform your password" {...field} />
</FormControl>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
<Button>Sign up</Button>
<Button type="submit" disabled={mutation.isPending}>
Sign up
</Button>
<div className="mt-4 text-center text-sm">
Already have an account ?{" "}
<Link href="/login" className="underline">
Sign in
</Link>
</div>
</Form>
</CardContent>
</Card>
@@ -0,0 +1,94 @@
"use client";
import {useRouter} from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import {Form, FormControl, FormField, FormItem, FormLabel, useZodForm} from "@/components/ui/form";
import {PasswordStrengthInput} from "@/components/ui/password-input-indicator";
import {ResetPasswordSchema, ResetPasswordType} from "@/components/wrappers/auth/reset-password/reset-password-schema";
import {PasswordInput} from "@/components/ui/password-input";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {authClient} from "@/lib/auth/auth-client";
import {toast} from "sonner";
type ResetPasswordFormProps = {
token: string;
};
export const ResetPasswordForm = ({token}: ResetPasswordFormProps) => {
const router = useRouter();
const form = useZodForm({
schema: ResetPasswordSchema,
});
const mutationResetPassword = useMutation({
mutationFn: async (data: ResetPasswordType) => {
await authClient.resetPassword({
newPassword: data.password,
token,
}, {
onSuccess: (response) => {
console.log(response);
toast.success("Password changed successfully.");
setTimeout(() => router.push("/"), 1000);
},
onError: (error) => {
console.error(error);
toast.error(error.error.message);
},
});
},
});
return (
<Card>
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Reset Password</h1>
<p className="text-balance text-muted-foreground">Fill information bellow to change your
password</p>
</div>
</CardHeader>
<CardContent>
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutationResetPassword.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="password"
defaultValue=""
render={({field}) => (
<FormItem>
<PasswordStrengthInput label={"Enter new password"} field={field}/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Confirmation Password</FormLabel>
<FormControl>
<PasswordInput placeholder={"Confirm password"} {...field}
value={field.value ?? ""}/>
</FormControl>
</FormItem>
)}
/>
<ButtonWithLoading type="submit" isPending={mutationResetPassword.isPending}>
Validate
</ButtonWithLoading>
</Form>
</CardContent>
</Card>
);
};
@@ -0,0 +1,22 @@
import {z} from "zod";
const passwordRegex = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-])/;
const zPassword = () => z.string().min(8, {message: "New Password too short"}).regex(passwordRegex, {message: "New Password too weak"});
export const ResetPasswordSchema = z
.object({
password: zPassword(),
confirmPassword: zPassword(),
})
.superRefine(({confirmPassword, password}, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
code: "custom",
message: "Passwords don't match.",
path: ["confirmPassword"],
});
}
});
export type ResetPasswordType = z.infer<typeof ResetPasswordSchema>;
@@ -0,0 +1,37 @@
"use client"
import {useRouter, useSearchParams} from "next/navigation";
import {ResetPasswordForm} from "@/components/wrappers/auth/reset-password/reset-password-form";
import {toast} from "sonner";
import {useEffect} from "react";
import {LoadingSpinner} from "@/components/wrappers/common/loading/loading-spinner";
export const ResetPasswordSection = () => {
const router = useRouter();
const searchParams = useSearchParams();
const token = searchParams.get("token");
const error = searchParams.get("error");
useEffect(() => {
if (error || !token) {
if (error == "INVALID_TOKEN") {
toast.error("Invalid reset token");
} else {
toast.error("An error occurred.");
}
setTimeout(() => router.push("/"), 1000);
}
}, [error, token, router]);
if (!token) return (
<div className="w-full h-full flex flex-col items-center justify-center">
<LoadingSpinner size={50}/>
</div>
);
return (
<ResetPasswordForm token={token}/>
)
}
@@ -20,7 +20,7 @@ import {
EmailFormSchema,
EmailFormType
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
import {PasswordInput} from "@/components/ui/password-input";
import {
updateEmailSettingsAction
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.action";
@@ -2,12 +2,13 @@ import {EmailForm} from "@/components/wrappers/dashboard/admin/tabs/admin-email-
import {Send} from "lucide-react";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {useMutation} from "@tanstack/react-query";
import {sendEmail} from "@/utils/email-helper";
import {sendEmail} from "@/lib/email/email-helper";
import {render} from "@react-email/render";
import {toast} from "sonner";
import {Setting} from "@/db/schema/01_setting";
import {EmailFormType} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
import TestEmailSettings from "../../../../../../../emails/TestEmailSettings";
import TestEmailSettings from "@/components/emails/email-settings-test"
export type SettingsEmailTabProps = {
settings: Setting;
@@ -12,7 +12,7 @@ import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/tabs/admin-storage-tab/storage-s3/s3-form.action";
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
import {PasswordInput} from "@/components/ui/password-input";
export type S3FormProps = {
defaultValues?: S3FormType;
@@ -1,5 +1,5 @@
"use client";
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
import {PasswordInput} from "@/components/ui/password-input";
import {useState} from "react";
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
@@ -2,7 +2,7 @@ import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/compon
import {Input} from "@/components/ui/input";
import {UseFormReturn} from "react-hook-form";
import {Separator} from "@/components/ui/separator";
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
import {PasswordInput} from "@/components/ui/password-input";
type NotifierSmtpFormProps = {
+1 -2
View File
@@ -1,8 +1,7 @@
"use server"
import type {EventPayload, DispatchResult} from '../types';
import nodemailer from 'nodemailer';
import {render} from "@react-email/render";
import TestEmailSettings from "../../../../emails/TestEmailSettings";
import TestEmailSettings from "@/components/emails/email-settings-test"
export async function sendSmtp(
config: {
+15 -2
View File
@@ -9,6 +9,9 @@ import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} fro
import {headers} from "next/headers";
import {count, eq} from "drizzle-orm";
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
import {sendEmail} from "@/lib/email/email-helper";
import {render} from "@react-email/render";
import EmailResetPassword from "@/components/emails/email-reset-password";
export const auth = betterAuth({
database: drizzleAdapter(db, {
@@ -19,15 +22,25 @@ export const auth = betterAuth({
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
/*async sendResetPassword(data, request) {
// Send an email to the user with a link to reset their password
sendResetPassword: async ({user, url, token}, request) => {
await sendEmail({
to: user.email,
subject: "Reset your password",
html: await render(EmailResetPassword({url: url})),
});
},
onPasswordReset: async ({user}, request) => {
console.log(`Password for user ${user.email} has been reset.`);
},
/*
async sendVerificationEmail(data, request) {
// Send an email to the user with a link to verify their email
},
async verifyEmail(data, request) {
// Verify the email address
},*/
},
socialProviders: {
google: {
@@ -1,13 +1,13 @@
"use server";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import nodemailer from "nodemailer";
import * as drizzleDb from "@/db";
type Payload = {
to: string;
from: string;
from?: string;
subject: string;
html: any;
};