Forgot password feature (#3437)

* forgot password feature

* improved config

* nit

* nit
This commit is contained in:
pablonyx
2024-12-19 20:53:37 -08:00
committed by GitHub
parent 9011b8a139
commit aa4bfa2a78
21 changed files with 415 additions and 123 deletions

View File

@@ -75,6 +75,9 @@ ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
ARG NEXT_PUBLIC_GTM_ENABLED
ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}
ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}
RUN npx next build
# Step 2. Production image, copy all the files and run next
@@ -150,6 +153,9 @@ ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
ARG NEXT_PUBLIC_GTM_ENABLED
ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}
ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}
# Note: Don't expose ports here, Compose will handle that for us if necessary.
# If you want to run this without compose, specify the ports to
# expose via cli

View File

@@ -81,13 +81,13 @@ export const getProviderIcon = (providerName: string, modelName?: string) => {
}
if (modelName?.toLowerCase().includes("phi")) {
return MicrosoftIconSVG;
}
}
if (modelName?.toLowerCase().includes("mistral")) {
return MistralIcon;
}
}
if (modelName?.toLowerCase().includes("llama")) {
return MetaIcon;
}
}
if (modelName?.toLowerCase().includes("gemini")) {
return GeminiIcon;
}

View File

@@ -0,0 +1,100 @@
"use client";
import React, { useState } from "react";
import { forgotPassword } from "./utils";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import CardSection from "@/components/admin/CardSection";
import Title from "@/components/ui/title";
import Text from "@/components/ui/text";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { TextFormField } from "@/components/admin/connectors/Field";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Spinner } from "@/components/Spinner";
import { redirect } from "next/navigation";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
const ForgotPasswordPage: React.FC = () => {
const { popup, setPopup } = usePopup();
const [isWorking, setIsWorking] = useState(false);
if (!NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED) {
redirect("/auth/login");
}
return (
<AuthFlowContainer>
<div className="flex flex-col w-full justify-center">
<CardSection className="mt-4 w-full">
{" "}
<div className="flex">
<Title className="mb-2 mx-auto font-bold">Forgot Password</Title>
</div>
{isWorking && <Spinner />}
{popup}
<Formik
initialValues={{
email: "",
}}
validationSchema={Yup.object().shape({
email: Yup.string().email().required(),
})}
onSubmit={async (values) => {
setIsWorking(true);
try {
await forgotPassword(values.email);
setPopup({
type: "success",
message:
"Password reset email sent. Please check your inbox.",
});
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: "An error occurred. Please try again.";
setPopup({
type: "error",
message: errorMessage,
});
} finally {
setIsWorking(false);
}
}}
>
{({ isSubmitting }) => (
<Form className="w-full flex flex-col items-stretch mt-2">
<TextFormField
name="email"
label="Email"
type="email"
placeholder="email@yourcompany.com"
/>
<div className="flex">
<Button
type="submit"
disabled={isSubmitting}
className="mx-auto w-full"
>
Reset Password
</Button>
</div>
</Form>
)}
</Formik>
<div className="flex">
<Text className="mt-4 mx-auto">
<Link href="/auth/login" className="text-link font-medium">
Back to Login
</Link>
</Text>
</div>
</CardSection>
</div>
</AuthFlowContainer>
);
};
export default ForgotPasswordPage;

View File

@@ -0,0 +1,33 @@
export const forgotPassword = async (email: string): Promise<void> => {
const response = await fetch(`/api/auth/forgot-password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
const error = await response.json();
const errorMessage =
error?.detail || "An error occurred during password reset.";
throw new Error(errorMessage);
}
};
export const resetPassword = async (
token: string,
password: string
): Promise<void> => {
const response = await fetch(`/api/auth/reset-password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ token, password }),
});
if (!response.ok) {
throw new Error("Failed to reset password");
}
};

View File

@@ -10,6 +10,8 @@ import { requestEmailVerification } from "../lib";
import { useState } from "react";
import { Spinner } from "@/components/Spinner";
import { set } from "lodash";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
import Link from "next/link";
export function EmailPasswordForm({
isSignup = false,
@@ -110,15 +112,21 @@ export function EmailPasswordForm({
placeholder="**************"
/>
<div className="flex">
<Button
type="submit"
disabled={isSubmitting}
className="mx-auto w-full"
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && !isSignup && (
<Link
href="/auth/forgot-password"
className="text-sm text-link font-medium whitespace-nowrap"
>
{isSignup ? "Sign Up" : "Log In"}
</Button>
</div>
Forgot Password?
</Link>
)}
<Button
type="submit"
disabled={isSubmitting}
className="mx-auto w-full"
>
{isSignup ? "Sign Up" : "Log In"}
</Button>
</Form>
)}
</Formik>

View File

@@ -16,6 +16,7 @@ import { LoginText } from "./LoginText";
import { getSecondsUntilExpiration } from "@/lib/time";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import CardSection from "@/components/admin/CardSection";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
const Page = async (props: {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -101,16 +102,24 @@ const Page = async (props: {
</div>
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
<div className="flex">
<Text className="mt-4 mx-auto">
Don&apos;t have an account?{" "}
<div className="flex mt-4 justify-between">
<Link
href={`/auth/signup${
searchParams?.next ? `?next=${searchParams.next}` : ""
}`}
className="text-link font-medium"
>
Create an account
</Link>
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
<Link
href={`/auth/signup${searchParams?.next ? `?next=${searchParams.next}` : ""}`}
href="/auth/forgot-password"
className="text-link font-medium"
>
Create an account
Reset Password
</Link>
</Text>
)}
</div>
</div>
)}
@@ -123,11 +132,13 @@ const Page = async (props: {
</Title>
</div>
<EmailPasswordForm nextUrl={nextUrl} />
<div className="flex">
<Text className="mt-4 mx-auto">
<div className="flex flex-col gap-y-2 items-center">
<Text className="mt-4 ">
Don&apos;t have an account?{" "}
<Link
href={`/auth/signup${searchParams?.next ? `?next=${searchParams.next}` : ""}`}
href={`/auth/signup${
searchParams?.next ? `?next=${searchParams.next}` : ""
}`}
className="text-link font-medium"
>
Create an account

View File

@@ -0,0 +1,117 @@
"use client";
import React, { useState } from "react";
import { resetPassword } from "../forgot-password/utils";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import CardSection from "@/components/admin/CardSection";
import Title from "@/components/ui/title";
import Text from "@/components/ui/text";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { TextFormField } from "@/components/admin/connectors/Field";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Spinner } from "@/components/Spinner";
import { redirect, useSearchParams } from "next/navigation";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
const ResetPasswordPage: React.FC = () => {
const { popup, setPopup } = usePopup();
const [isWorking, setIsWorking] = useState(false);
const searchParams = useSearchParams();
const token = searchParams.get("token");
if (!NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED) {
redirect("/auth/login");
}
return (
<AuthFlowContainer>
<div className="flex flex-col w-full justify-center">
<CardSection className="mt-4 w-full">
<div className="flex">
<Title className="mb-2 mx-auto font-bold">Reset Password</Title>
</div>
{isWorking && <Spinner />}
{popup}
<Formik
initialValues={{
password: "",
confirmPassword: "",
}}
validationSchema={Yup.object().shape({
password: Yup.string().required("Password is required"),
confirmPassword: Yup.string()
.oneOf([Yup.ref("password"), undefined], "Passwords must match")
.required("Confirm Password is required"),
})}
onSubmit={async (values) => {
if (!token) {
setPopup({
type: "error",
message: "Invalid or missing reset token.",
});
return;
}
setIsWorking(true);
try {
await resetPassword(token, values.password);
setPopup({
type: "success",
message:
"Password reset successfully. Redirecting to login...",
});
setTimeout(() => {
redirect("/auth/login");
}, 1000);
} catch (error) {
setPopup({
type: "error",
message: "An error occurred. Please try again.",
});
} finally {
setIsWorking(false);
}
}}
>
{({ isSubmitting }) => (
<Form className="w-full flex flex-col items-stretch mt-2">
<TextFormField
name="password"
label="New Password"
type="password"
placeholder="Enter your new password"
/>
<TextFormField
name="confirmPassword"
label="Confirm New Password"
type="password"
placeholder="Confirm your new password"
/>
<div className="flex">
<Button
type="submit"
disabled={isSubmitting}
className="mx-auto w-full"
>
Reset Password
</Button>
</div>
</Form>
)}
</Formik>
<div className="flex">
<Text className="mt-4 mx-auto">
<Link href="/auth/login" className="text-link font-medium">
Back to Login
</Link>
</Text>
</div>
</CardSection>
</div>
</AuthFlowContainer>
);
};
export default ResetPasswordPage;

View File

@@ -211,7 +211,11 @@ export function TextFormField({
return (
<div className={`w-full ${width}`}>
<div className={`flex ${vertical ? "flex-col" : "flex-row"} items-start`}>
<div
className={`flex ${
vertical ? "flex-col" : "flex-row"
} gap-x-2 items-start`}
>
<div className="flex gap-x-2 items-center">
{!removeLabel && (
<Label className={sizeClass.label} small={small}>

View File

@@ -1121,12 +1121,16 @@ export const MetaIcon = ({
export const MicrosoftIconSVG = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => <LogoIcon size={size} className={className} src={microsoftSVG} />;
}: IconProps) => (
<LogoIcon size={size} className={className} src={microsoftSVG} />
);
export const MistralIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => <LogoIcon size={size} className={className} src={mistralSVG} />;
}: IconProps) => (
<LogoIcon size={size} className={className} src={mistralSVG} />
);
export const VoyageIcon = ({
size = 16,

View File

@@ -2,34 +2,15 @@ import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
isEditing?: boolean;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, isEditing = true, style, ...props }, ref) => {
const textClassName = "text-2xl text-strong dark:text-neutral-50";
if (!isEditing) {
return (
<span className={cn(textClassName, className)}>
{props.value || props.defaultValue}
</span>
);
}
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
textClassName,
"w-[1ch] min-w-[1ch] box-content pr-1",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
style={{
width: `${Math.max(1, String(props.value || props.defaultValue || "").length)}ch`,
...style,
}}
ref={ref}
{...props}
/>

View File

@@ -75,6 +75,12 @@ export const NEXT_PUBLIC_CLOUD_ENABLED =
export const REGISTRATION_URL =
process.env.INTERNAL_URL || "http://127.0.0.1:3001";
export const SERVER_SIDE_ONLY__CLOUD_ENABLED =
process.env.NEXT_PUBLIC_CLOUD_ENABLED?.toLowerCase() === "true";
export const NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED =
process.env.NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED?.toLowerCase() === "true";
export const TEST_ENV = process.env.TEST_ENV?.toLowerCase() === "true";
export const NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED =

View File

@@ -101,7 +101,7 @@ const MODEL_NAMES_SUPPORTING_IMAGE_INPUT = [
"amazon.nova-pro@v1",
// meta models
"llama-3.2-90b-vision-instruct",
"llama-3.2-11b-vision-instruct"
"llama-3.2-11b-vision-instruct",
];
export function checkLLMSupportsImageInput(model: string) {