mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-25 11:16:43 +02:00
Forgot password feature (#3437)
* forgot password feature * improved config * nit * nit
This commit is contained in:
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
100
web/src/app/auth/forgot-password/page.tsx
Normal file
100
web/src/app/auth/forgot-password/page.tsx
Normal 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;
|
33
web/src/app/auth/forgot-password/utils.ts
Normal file
33
web/src/app/auth/forgot-password/utils.ts
Normal 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");
|
||||
}
|
||||
};
|
@@ -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>
|
||||
|
@@ -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'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'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
|
||||
|
117
web/src/app/auth/reset-password/page.tsx
Normal file
117
web/src/app/auth/reset-password/page.tsx
Normal 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;
|
@@ -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}>
|
||||
|
@@ -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,
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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 =
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user