From aa4bfa2a78044654d07282982f2190241b70ed6e Mon Sep 17 00:00:00 2001 From: pablonyx Date: Thu, 19 Dec 2024 20:53:37 -0800 Subject: [PATCH] Forgot password feature (#3437) * forgot password feature * improved config * nit * nit --- ...-build-push-cloud-web-container-on-tag.yml | 1 + backend/onyx/auth/email_utils.py | 80 ++++++++++++ backend/onyx/auth/users.py | 49 ++------ backend/onyx/configs/app_configs.py | 1 + backend/onyx/server/manage/users.py | 2 +- backend/onyx/server/utils.py | 39 ------ .../docker_compose/docker-compose.dev.yml | 2 +- .../docker-compose.prod-cloud.yml | 1 + .../docker_compose/docker-compose.prod.yml | 1 + web/Dockerfile | 6 + .../app/admin/configuration/llm/interfaces.ts | 6 +- web/src/app/auth/forgot-password/page.tsx | 100 +++++++++++++++ web/src/app/auth/forgot-password/utils.ts | 33 +++++ web/src/app/auth/login/EmailPasswordForm.tsx | 24 ++-- web/src/app/auth/login/page.tsx | 29 +++-- web/src/app/auth/reset-password/page.tsx | 117 ++++++++++++++++++ web/src/components/admin/connectors/Field.tsx | 6 +- web/src/components/icons/icons.tsx | 8 +- web/src/components/ui/input.tsx | 25 +--- web/src/lib/constants.ts | 6 + web/src/lib/llm/utils.ts | 2 +- 21 files changed, 415 insertions(+), 123 deletions(-) create mode 100644 backend/onyx/auth/email_utils.py create mode 100644 web/src/app/auth/forgot-password/page.tsx create mode 100644 web/src/app/auth/forgot-password/utils.ts create mode 100644 web/src/app/auth/reset-password/page.tsx diff --git a/.github/workflows/docker-build-push-cloud-web-container-on-tag.yml b/.github/workflows/docker-build-push-cloud-web-container-on-tag.yml index 99caf6392a0..dfea0a8703d 100644 --- a/.github/workflows/docker-build-push-cloud-web-container-on-tag.yml +++ b/.github/workflows/docker-build-push-cloud-web-container-on-tag.yml @@ -66,6 +66,7 @@ jobs: NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.POSTHOG_HOST }} NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }} NEXT_PUBLIC_GTM_ENABLED=true + NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=true # needed due to weird interactions with the builds for different platforms no-cache: true labels: ${{ steps.meta.outputs.labels }} diff --git a/backend/onyx/auth/email_utils.py b/backend/onyx/auth/email_utils.py new file mode 100644 index 00000000000..4cb36706ec5 --- /dev/null +++ b/backend/onyx/auth/email_utils.py @@ -0,0 +1,80 @@ +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from textwrap import dedent + +from onyx.configs.app_configs import EMAIL_CONFIGURED +from onyx.configs.app_configs import EMAIL_FROM +from onyx.configs.app_configs import SMTP_PASS +from onyx.configs.app_configs import SMTP_PORT +from onyx.configs.app_configs import SMTP_SERVER +from onyx.configs.app_configs import SMTP_USER +from onyx.configs.app_configs import WEB_DOMAIN +from onyx.db.models import User + + +def send_email( + user_email: str, + subject: str, + body: str, + mail_from: str = EMAIL_FROM, +) -> None: + if not EMAIL_CONFIGURED: + raise ValueError("Email is not configured.") + + msg = MIMEMultipart() + msg["Subject"] = subject + msg["To"] = user_email + if mail_from: + msg["From"] = mail_from + + msg.attach(MIMEText(body)) + + try: + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as s: + s.starttls() + s.login(SMTP_USER, SMTP_PASS) + s.send_message(msg) + except Exception as e: + raise e + + +def send_user_email_invite(user_email: str, current_user: User) -> None: + subject = "Invitation to Join Onyx Workspace" + body = dedent( + f"""\ + Hello, + + You have been invited to join a workspace on Onyx. + + To join the workspace, please visit the following link: + + {WEB_DOMAIN}/auth/login + + Best regards, + The Onyx Team + """ + ) + send_email(user_email, subject, body, current_user.email) + + +def send_forgot_password_email( + user_email: str, + token: str, + mail_from: str = EMAIL_FROM, +) -> None: + subject = "Onyx Forgot Password" + link = f"{WEB_DOMAIN}/auth/reset-password?token={token}" + body = f"Click the following link to reset your password: {link}" + send_email(user_email, subject, body, mail_from) + + +def send_user_verification_email( + user_email: str, + token: str, + mail_from: str = EMAIL_FROM, +) -> None: + subject = "Onyx Email Verification" + link = f"{WEB_DOMAIN}/auth/verify-email?token={token}" + body = f"Click the following link to verify your email address: {link}" + send_email(user_email, subject, body, mail_from) diff --git a/backend/onyx/auth/users.py b/backend/onyx/auth/users.py index 69025aab198..eb337da90be 100644 --- a/backend/onyx/auth/users.py +++ b/backend/onyx/auth/users.py @@ -1,10 +1,7 @@ -import smtplib import uuid from collections.abc import AsyncGenerator from datetime import datetime from datetime import timezone -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText from typing import cast from typing import Dict from typing import List @@ -53,19 +50,17 @@ from sqlalchemy.ext.asyncio import AsyncSession from onyx.auth.api_key import get_hashed_api_key_from_request +from onyx.auth.email_utils import send_forgot_password_email +from onyx.auth.email_utils import send_user_verification_email from onyx.auth.invited_users import get_invited_users from onyx.auth.schemas import UserCreate from onyx.auth.schemas import UserRole from onyx.auth.schemas import UserUpdate from onyx.configs.app_configs import AUTH_TYPE from onyx.configs.app_configs import DISABLE_AUTH -from onyx.configs.app_configs import EMAIL_FROM +from onyx.configs.app_configs import EMAIL_CONFIGURED from onyx.configs.app_configs import REQUIRE_EMAIL_VERIFICATION from onyx.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS -from onyx.configs.app_configs import SMTP_PASS -from onyx.configs.app_configs import SMTP_PORT -from onyx.configs.app_configs import SMTP_SERVER -from onyx.configs.app_configs import SMTP_USER from onyx.configs.app_configs import TRACK_EXTERNAL_IDP_EXPIRY from onyx.configs.app_configs import USER_AUTH_SECRET from onyx.configs.app_configs import VALID_EMAIL_DOMAINS @@ -193,30 +188,6 @@ def verify_email_domain(email: str) -> None: ) -def send_user_verification_email( - user_email: str, - token: str, - mail_from: str = EMAIL_FROM, -) -> None: - msg = MIMEMultipart() - msg["Subject"] = "Onyx Email Verification" - msg["To"] = user_email - if mail_from: - msg["From"] = mail_from - - link = f"{WEB_DOMAIN}/auth/verify-email?token={token}" - - body = MIMEText(f"Click the following link to verify your email address: {link}") - msg.attach(body) - - with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as s: - s.starttls() - # If credentials fails with gmail, check (You need an app password, not just the basic email password) - # https://support.google.com/accounts/answer/185833?sjid=8512343437447396151-NA - s.login(SMTP_USER, SMTP_PASS) - s.send_message(msg) - - class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): reset_password_token_secret = USER_AUTH_SECRET verification_token_secret = USER_AUTH_SECRET @@ -506,7 +477,15 @@ async def on_after_register( async def on_after_forgot_password( self, user: User, token: str, request: Optional[Request] = None ) -> None: - logger.notice(f"User {user.id} has forgot their password. Reset token: {token}") + if not EMAIL_CONFIGURED: + logger.error( + "Email is not configured. Please configure email in the admin panel" + ) + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + "Your admin has not enbaled this feature.", + ) + send_forgot_password_email(user.email, token) async def on_after_request_verify( self, user: User, token: str, request: Optional[Request] = None @@ -624,9 +603,7 @@ def get_database_strategy( auth_backend = AuthenticationBackend( - name="jwt" if MULTI_TENANT else "database", - transport=cookie_transport, - get_strategy=get_jwt_strategy if MULTI_TENANT else get_database_strategy, # type: ignore + name="jwt", transport=cookie_transport, get_strategy=get_jwt_strategy ) # type: ignore diff --git a/backend/onyx/configs/app_configs.py b/backend/onyx/configs/app_configs.py index 29f7bea194a..99246fb0a8e 100644 --- a/backend/onyx/configs/app_configs.py +++ b/backend/onyx/configs/app_configs.py @@ -92,6 +92,7 @@ SMTP_PORT = int(os.environ.get("SMTP_PORT") or "587") SMTP_USER = os.environ.get("SMTP_USER", "your-email@gmail.com") SMTP_PASS = os.environ.get("SMTP_PASS", "your-gmail-password") +EMAIL_CONFIGURED = all([SMTP_SERVER, SMTP_USER, SMTP_PASS]) EMAIL_FROM = os.environ.get("EMAIL_FROM") or SMTP_USER # If set, Onyx will listen to the `expires_at` returned by the identity diff --git a/backend/onyx/server/manage/users.py b/backend/onyx/server/manage/users.py index 27b8e23fa58..e2066ef77ef 100644 --- a/backend/onyx/server/manage/users.py +++ b/backend/onyx/server/manage/users.py @@ -21,6 +21,7 @@ from sqlalchemy.orm import Session from ee.onyx.configs.app_configs import SUPER_USERS +from onyx.auth.email_utils import send_user_email_invite from onyx.auth.invited_users import get_invited_users from onyx.auth.invited_users import write_invited_users from onyx.auth.noauth_user import fetch_no_auth_user @@ -61,7 +62,6 @@ from onyx.server.models import InvitedUserSnapshot from onyx.server.models import MinimalUserSnapshot from onyx.server.utils import BasicAuthenticationError -from onyx.server.utils import send_user_email_invite from onyx.utils.logger import setup_logger from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop from shared_configs.configs import MULTI_TENANT diff --git a/backend/onyx/server/utils.py b/backend/onyx/server/utils.py index 82f31ec2870..61b5ef65038 100644 --- a/backend/onyx/server/utils.py +++ b/backend/onyx/server/utils.py @@ -1,21 +1,10 @@ import json -import smtplib from datetime import datetime -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from textwrap import dedent from typing import Any from fastapi import HTTPException from fastapi import status -from onyx.configs.app_configs import SMTP_PASS -from onyx.configs.app_configs import SMTP_PORT -from onyx.configs.app_configs import SMTP_SERVER -from onyx.configs.app_configs import SMTP_USER -from onyx.configs.app_configs import WEB_DOMAIN -from onyx.db.models import User - class BasicAuthenticationError(HTTPException): def __init__(self, detail: str): @@ -62,31 +51,3 @@ def mask_credential_dict(credential_dict: dict[str, Any]) -> dict[str, str]: masked_creds[key] = mask_string(val) return masked_creds - - -def send_user_email_invite(user_email: str, current_user: User) -> None: - msg = MIMEMultipart() - msg["Subject"] = "Invitation to Join Onyx Workspace" - msg["From"] = current_user.email - msg["To"] = user_email - - email_body = dedent( - f"""\ - Hello, - - You have been invited to join a workspace on Onyx. - - To join the workspace, please visit the following link: - - {WEB_DOMAIN}/auth/login - - Best regards, - The Onyx Team - """ - ) - - msg.attach(MIMEText(email_body, "plain")) - with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp_server: - smtp_server.starttls() - smtp_server.login(SMTP_USER, SMTP_PASS) - smtp_server.send_message(msg) diff --git a/deployment/docker_compose/docker-compose.dev.yml b/deployment/docker_compose/docker-compose.dev.yml index 2568ac09630..2607bbb1ac8 100644 --- a/deployment/docker_compose/docker-compose.dev.yml +++ b/deployment/docker_compose/docker-compose.dev.yml @@ -267,7 +267,7 @@ services: - NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-} - NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-} - NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN=${NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN:-} - + - NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-} # Enterprise Edition only - NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-} # DO NOT TURN ON unless you have EXPLICIT PERMISSION from Onyx. diff --git a/deployment/docker_compose/docker-compose.prod-cloud.yml b/deployment/docker_compose/docker-compose.prod-cloud.yml index 49706f525b3..37a032f1d38 100644 --- a/deployment/docker_compose/docker-compose.prod-cloud.yml +++ b/deployment/docker_compose/docker-compose.prod-cloud.yml @@ -72,6 +72,7 @@ services: - NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-} - NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-} - NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-} + - NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-} depends_on: - api_server restart: always diff --git a/deployment/docker_compose/docker-compose.prod.yml b/deployment/docker_compose/docker-compose.prod.yml index bbf9966e3a3..720bc202e27 100644 --- a/deployment/docker_compose/docker-compose.prod.yml +++ b/deployment/docker_compose/docker-compose.prod.yml @@ -99,6 +99,7 @@ services: - NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-} - NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-} - NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-} + - NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-} depends_on: - api_server restart: always diff --git a/web/Dockerfile b/web/Dockerfile index ec5f8201953..e0fa59dcfec 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -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 diff --git a/web/src/app/admin/configuration/llm/interfaces.ts b/web/src/app/admin/configuration/llm/interfaces.ts index 1da0e2e983e..5b0dc73bac3 100644 --- a/web/src/app/admin/configuration/llm/interfaces.ts +++ b/web/src/app/admin/configuration/llm/interfaces.ts @@ -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; } diff --git a/web/src/app/auth/forgot-password/page.tsx b/web/src/app/auth/forgot-password/page.tsx new file mode 100644 index 00000000000..aac32e29acb --- /dev/null +++ b/web/src/app/auth/forgot-password/page.tsx @@ -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 ( + +
+ + {" "} +
+ Forgot Password +
+ {isWorking && } + {popup} + { + 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 }) => ( +
+ + +
+ +
+ + )} +
+
+ + + Back to Login + + +
+
+
+
+ ); +}; + +export default ForgotPasswordPage; diff --git a/web/src/app/auth/forgot-password/utils.ts b/web/src/app/auth/forgot-password/utils.ts new file mode 100644 index 00000000000..fc5176633fc --- /dev/null +++ b/web/src/app/auth/forgot-password/utils.ts @@ -0,0 +1,33 @@ +export const forgotPassword = async (email: string): Promise => { + 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 => { + 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"); + } +}; diff --git a/web/src/app/auth/login/EmailPasswordForm.tsx b/web/src/app/auth/login/EmailPasswordForm.tsx index 61b8116db20..89829a20543 100644 --- a/web/src/app/auth/login/EmailPasswordForm.tsx +++ b/web/src/app/auth/login/EmailPasswordForm.tsx @@ -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="**************" /> -
- -
+ Forgot Password? + + )} + )} diff --git a/web/src/app/auth/login/page.tsx b/web/src/app/auth/login/page.tsx index 7a6459655ca..6722f85eb8f 100644 --- a/web/src/app/auth/login/page.tsx +++ b/web/src/app/auth/login/page.tsx @@ -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: { -
- - Don't have an account?{" "} +
+ + Create an account + + + {NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && ( - Create an account + Reset Password - + )}
)} @@ -123,11 +132,13 @@ const Page = async (props: { -
- +
+ Don't have an account?{" "} Create an account diff --git a/web/src/app/auth/reset-password/page.tsx b/web/src/app/auth/reset-password/page.tsx new file mode 100644 index 00000000000..a18cc92f5c4 --- /dev/null +++ b/web/src/app/auth/reset-password/page.tsx @@ -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 ( + +
+ +
+ Reset Password +
+ {isWorking && } + {popup} + { + 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 }) => ( +
+ + + +
+ +
+ + )} +
+
+ + + Back to Login + + +
+
+
+
+ ); +}; + +export default ResetPasswordPage; diff --git a/web/src/components/admin/connectors/Field.tsx b/web/src/components/admin/connectors/Field.tsx index c45b27b4aca..c7032fdd3f1 100644 --- a/web/src/components/admin/connectors/Field.tsx +++ b/web/src/components/admin/connectors/Field.tsx @@ -211,7 +211,11 @@ export function TextFormField({ return (
-
+
{!removeLabel && (