mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-10-09 12:47:13 +02:00
Forgot password feature (#3437)
* forgot password feature * improved config * nit * nit
This commit is contained in:
@@ -66,6 +66,7 @@ jobs:
|
|||||||
NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.POSTHOG_HOST }}
|
NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.POSTHOG_HOST }}
|
||||||
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
|
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
|
||||||
NEXT_PUBLIC_GTM_ENABLED=true
|
NEXT_PUBLIC_GTM_ENABLED=true
|
||||||
|
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=true
|
||||||
# needed due to weird interactions with the builds for different platforms
|
# needed due to weird interactions with the builds for different platforms
|
||||||
no-cache: true
|
no-cache: true
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
80
backend/onyx/auth/email_utils.py
Normal file
80
backend/onyx/auth/email_utils.py
Normal file
@@ -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)
|
@@ -1,10 +1,7 @@
|
|||||||
import smtplib
|
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
from email.mime.multipart import MIMEMultipart
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -53,19 +50,17 @@ from sqlalchemy import text
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from onyx.auth.api_key import get_hashed_api_key_from_request
|
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.invited_users import get_invited_users
|
||||||
from onyx.auth.schemas import UserCreate
|
from onyx.auth.schemas import UserCreate
|
||||||
from onyx.auth.schemas import UserRole
|
from onyx.auth.schemas import UserRole
|
||||||
from onyx.auth.schemas import UserUpdate
|
from onyx.auth.schemas import UserUpdate
|
||||||
from onyx.configs.app_configs import AUTH_TYPE
|
from onyx.configs.app_configs import AUTH_TYPE
|
||||||
from onyx.configs.app_configs import DISABLE_AUTH
|
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 REQUIRE_EMAIL_VERIFICATION
|
||||||
from onyx.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS
|
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 TRACK_EXTERNAL_IDP_EXPIRY
|
||||||
from onyx.configs.app_configs import USER_AUTH_SECRET
|
from onyx.configs.app_configs import USER_AUTH_SECRET
|
||||||
from onyx.configs.app_configs import VALID_EMAIL_DOMAINS
|
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]):
|
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||||
reset_password_token_secret = USER_AUTH_SECRET
|
reset_password_token_secret = USER_AUTH_SECRET
|
||||||
verification_token_secret = USER_AUTH_SECRET
|
verification_token_secret = USER_AUTH_SECRET
|
||||||
@@ -506,7 +477,15 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
|||||||
async def on_after_forgot_password(
|
async def on_after_forgot_password(
|
||||||
self, user: User, token: str, request: Optional[Request] = None
|
self, user: User, token: str, request: Optional[Request] = None
|
||||||
) -> 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(
|
async def on_after_request_verify(
|
||||||
self, user: User, token: str, request: Optional[Request] = None
|
self, user: User, token: str, request: Optional[Request] = None
|
||||||
@@ -624,9 +603,7 @@ def get_database_strategy(
|
|||||||
|
|
||||||
|
|
||||||
auth_backend = AuthenticationBackend(
|
auth_backend = AuthenticationBackend(
|
||||||
name="jwt" if MULTI_TENANT else "database",
|
name="jwt", transport=cookie_transport, get_strategy=get_jwt_strategy
|
||||||
transport=cookie_transport,
|
|
||||||
get_strategy=get_jwt_strategy if MULTI_TENANT else get_database_strategy, # type: ignore
|
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@@ -92,6 +92,7 @@ SMTP_SERVER = os.environ.get("SMTP_SERVER") or "smtp.gmail.com"
|
|||||||
SMTP_PORT = int(os.environ.get("SMTP_PORT") or "587")
|
SMTP_PORT = int(os.environ.get("SMTP_PORT") or "587")
|
||||||
SMTP_USER = os.environ.get("SMTP_USER", "your-email@gmail.com")
|
SMTP_USER = os.environ.get("SMTP_USER", "your-email@gmail.com")
|
||||||
SMTP_PASS = os.environ.get("SMTP_PASS", "your-gmail-password")
|
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
|
EMAIL_FROM = os.environ.get("EMAIL_FROM") or SMTP_USER
|
||||||
|
|
||||||
# If set, Onyx will listen to the `expires_at` returned by the identity
|
# If set, Onyx will listen to the `expires_at` returned by the identity
|
||||||
|
@@ -21,6 +21,7 @@ from sqlalchemy.exc import IntegrityError
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ee.onyx.configs.app_configs import SUPER_USERS
|
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 get_invited_users
|
||||||
from onyx.auth.invited_users import write_invited_users
|
from onyx.auth.invited_users import write_invited_users
|
||||||
from onyx.auth.noauth_user import fetch_no_auth_user
|
from onyx.auth.noauth_user import fetch_no_auth_user
|
||||||
@@ -61,7 +62,6 @@ from onyx.server.models import FullUserSnapshot
|
|||||||
from onyx.server.models import InvitedUserSnapshot
|
from onyx.server.models import InvitedUserSnapshot
|
||||||
from onyx.server.models import MinimalUserSnapshot
|
from onyx.server.models import MinimalUserSnapshot
|
||||||
from onyx.server.utils import BasicAuthenticationError
|
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.logger import setup_logger
|
||||||
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
|
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
|
||||||
from shared_configs.configs import MULTI_TENANT
|
from shared_configs.configs import MULTI_TENANT
|
||||||
|
@@ -1,21 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
import smtplib
|
|
||||||
from datetime import datetime
|
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 typing import Any
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from fastapi import status
|
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):
|
class BasicAuthenticationError(HTTPException):
|
||||||
def __init__(self, detail: str):
|
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)
|
masked_creds[key] = mask_string(val)
|
||||||
return masked_creds
|
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)
|
|
||||||
|
@@ -267,7 +267,7 @@ services:
|
|||||||
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||||
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||||
- NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN=${NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN:-}
|
- NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN=${NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN:-}
|
||||||
|
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
|
||||||
# Enterprise Edition only
|
# Enterprise Edition only
|
||||||
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||||
# DO NOT TURN ON unless you have EXPLICIT PERMISSION from Onyx.
|
# DO NOT TURN ON unless you have EXPLICIT PERMISSION from Onyx.
|
||||||
|
@@ -72,6 +72,7 @@ services:
|
|||||||
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||||
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||||
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||||
|
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
|
||||||
depends_on:
|
depends_on:
|
||||||
- api_server
|
- api_server
|
||||||
restart: always
|
restart: always
|
||||||
|
@@ -99,6 +99,7 @@ services:
|
|||||||
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
|
||||||
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
|
||||||
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
|
||||||
|
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
|
||||||
depends_on:
|
depends_on:
|
||||||
- api_server
|
- api_server
|
||||||
restart: always
|
restart: always
|
||||||
|
@@ -75,6 +75,9 @@ ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
|||||||
ARG NEXT_PUBLIC_GTM_ENABLED
|
ARG NEXT_PUBLIC_GTM_ENABLED
|
||||||
ENV NEXT_PUBLIC_GTM_ENABLED=${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
|
RUN npx next build
|
||||||
|
|
||||||
# Step 2. Production image, copy all the files and run next
|
# 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
|
ARG NEXT_PUBLIC_GTM_ENABLED
|
||||||
ENV NEXT_PUBLIC_GTM_ENABLED=${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.
|
# 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
|
# If you want to run this without compose, specify the ports to
|
||||||
# expose via cli
|
# expose via cli
|
||||||
|
@@ -81,13 +81,13 @@ export const getProviderIcon = (providerName: string, modelName?: string) => {
|
|||||||
}
|
}
|
||||||
if (modelName?.toLowerCase().includes("phi")) {
|
if (modelName?.toLowerCase().includes("phi")) {
|
||||||
return MicrosoftIconSVG;
|
return MicrosoftIconSVG;
|
||||||
}
|
}
|
||||||
if (modelName?.toLowerCase().includes("mistral")) {
|
if (modelName?.toLowerCase().includes("mistral")) {
|
||||||
return MistralIcon;
|
return MistralIcon;
|
||||||
}
|
}
|
||||||
if (modelName?.toLowerCase().includes("llama")) {
|
if (modelName?.toLowerCase().includes("llama")) {
|
||||||
return MetaIcon;
|
return MetaIcon;
|
||||||
}
|
}
|
||||||
if (modelName?.toLowerCase().includes("gemini")) {
|
if (modelName?.toLowerCase().includes("gemini")) {
|
||||||
return GeminiIcon;
|
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 { useState } from "react";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { set } from "lodash";
|
import { set } from "lodash";
|
||||||
|
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export function EmailPasswordForm({
|
export function EmailPasswordForm({
|
||||||
isSignup = false,
|
isSignup = false,
|
||||||
@@ -110,15 +112,21 @@ export function EmailPasswordForm({
|
|||||||
placeholder="**************"
|
placeholder="**************"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex">
|
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && !isSignup && (
|
||||||
<Button
|
<Link
|
||||||
type="submit"
|
href="/auth/forgot-password"
|
||||||
disabled={isSubmitting}
|
className="text-sm text-link font-medium whitespace-nowrap"
|
||||||
className="mx-auto w-full"
|
|
||||||
>
|
>
|
||||||
{isSignup ? "Sign Up" : "Log In"}
|
Forgot Password?
|
||||||
</Button>
|
</Link>
|
||||||
</div>
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="mx-auto w-full"
|
||||||
|
>
|
||||||
|
{isSignup ? "Sign Up" : "Log In"}
|
||||||
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
@@ -16,6 +16,7 @@ import { LoginText } from "./LoginText";
|
|||||||
import { getSecondsUntilExpiration } from "@/lib/time";
|
import { getSecondsUntilExpiration } from "@/lib/time";
|
||||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||||
import CardSection from "@/components/admin/CardSection";
|
import CardSection from "@/components/admin/CardSection";
|
||||||
|
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||||
|
|
||||||
const Page = async (props: {
|
const Page = async (props: {
|
||||||
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
|
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
@@ -101,16 +102,24 @@ const Page = async (props: {
|
|||||||
</div>
|
</div>
|
||||||
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
|
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
|
||||||
|
|
||||||
<div className="flex">
|
<div className="flex mt-4 justify-between">
|
||||||
<Text className="mt-4 mx-auto">
|
<Link
|
||||||
Don't have an account?{" "}
|
href={`/auth/signup${
|
||||||
|
searchParams?.next ? `?next=${searchParams.next}` : ""
|
||||||
|
}`}
|
||||||
|
className="text-link font-medium"
|
||||||
|
>
|
||||||
|
Create an account
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
|
||||||
<Link
|
<Link
|
||||||
href={`/auth/signup${searchParams?.next ? `?next=${searchParams.next}` : ""}`}
|
href="/auth/forgot-password"
|
||||||
className="text-link font-medium"
|
className="text-link font-medium"
|
||||||
>
|
>
|
||||||
Create an account
|
Reset Password
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -123,11 +132,13 @@ const Page = async (props: {
|
|||||||
</Title>
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
<EmailPasswordForm nextUrl={nextUrl} />
|
<EmailPasswordForm nextUrl={nextUrl} />
|
||||||
<div className="flex">
|
<div className="flex flex-col gap-y-2 items-center">
|
||||||
<Text className="mt-4 mx-auto">
|
<Text className="mt-4 ">
|
||||||
Don't have an account?{" "}
|
Don't have an account?{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`/auth/signup${searchParams?.next ? `?next=${searchParams.next}` : ""}`}
|
href={`/auth/signup${
|
||||||
|
searchParams?.next ? `?next=${searchParams.next}` : ""
|
||||||
|
}`}
|
||||||
className="text-link font-medium"
|
className="text-link font-medium"
|
||||||
>
|
>
|
||||||
Create an account
|
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 (
|
return (
|
||||||
<div className={`w-full ${width}`}>
|
<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">
|
<div className="flex gap-x-2 items-center">
|
||||||
{!removeLabel && (
|
{!removeLabel && (
|
||||||
<Label className={sizeClass.label} small={small}>
|
<Label className={sizeClass.label} small={small}>
|
||||||
|
@@ -1121,12 +1121,16 @@ export const MetaIcon = ({
|
|||||||
export const MicrosoftIconSVG = ({
|
export const MicrosoftIconSVG = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
}: IconProps) => <LogoIcon size={size} className={className} src={microsoftSVG} />;
|
}: IconProps) => (
|
||||||
|
<LogoIcon size={size} className={className} src={microsoftSVG} />
|
||||||
|
);
|
||||||
|
|
||||||
export const MistralIcon = ({
|
export const MistralIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
}: IconProps) => <LogoIcon size={size} className={className} src={mistralSVG} />;
|
}: IconProps) => (
|
||||||
|
<LogoIcon size={size} className={className} src={mistralSVG} />
|
||||||
|
);
|
||||||
|
|
||||||
export const VoyageIcon = ({
|
export const VoyageIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
|
@@ -2,34 +2,15 @@ import * as React from "react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface InputProps
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
({ className, type, ...props }, ref) => {
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
textClassName,
|
"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",
|
||||||
"w-[1ch] min-w-[1ch] box-content pr-1",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{
|
|
||||||
width: `${Math.max(1, String(props.value || props.defaultValue || "").length)}ch`,
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@@ -75,6 +75,12 @@ export const NEXT_PUBLIC_CLOUD_ENABLED =
|
|||||||
export const REGISTRATION_URL =
|
export const REGISTRATION_URL =
|
||||||
process.env.INTERNAL_URL || "http://127.0.0.1:3001";
|
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 TEST_ENV = process.env.TEST_ENV?.toLowerCase() === "true";
|
||||||
|
|
||||||
export const NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED =
|
export const NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED =
|
||||||
|
@@ -101,7 +101,7 @@ const MODEL_NAMES_SUPPORTING_IMAGE_INPUT = [
|
|||||||
"amazon.nova-pro@v1",
|
"amazon.nova-pro@v1",
|
||||||
// meta models
|
// meta models
|
||||||
"llama-3.2-90b-vision-instruct",
|
"llama-3.2-90b-vision-instruct",
|
||||||
"llama-3.2-11b-vision-instruct"
|
"llama-3.2-11b-vision-instruct",
|
||||||
];
|
];
|
||||||
|
|
||||||
export function checkLLMSupportsImageInput(model: string) {
|
export function checkLLMSupportsImageInput(model: string) {
|
||||||
|
Reference in New Issue
Block a user