diff --git a/backend/danswer/auth/users.py b/backend/danswer/auth/users.py index 6b0cfe57b4..82697917a8 100644 --- a/backend/danswer/auth/users.py +++ b/backend/danswer/auth/users.py @@ -68,6 +68,12 @@ def verify_auth_setting() -> None: logger.info(f"Using Auth Type: {AUTH_TYPE.value}") +def user_needs_to_be_verified() -> bool: + # all other auth types besides basic should require users to be + # verified + return AUTH_TYPE != AuthType.BASIC or REQUIRE_EMAIL_VERIFICATION + + def get_user_whitelist() -> list[str]: global _user_whitelist if _user_whitelist is None: @@ -104,10 +110,9 @@ def verify_email_domain(email: str) -> None: def send_user_verification_email(user_email: str, token: str) -> None: msg = MIMEMultipart() msg["Subject"] = "Danswer Email Verification" - msg["From"] = "no-reply@danswer.dev" msg["To"] = user_email - link = f"{WEB_DOMAIN}/verify-email?token={token}" + 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) @@ -256,9 +261,11 @@ fastapi_users = FastAPIUserWithLogoutRouter[User, uuid.UUID]( ) -optional_valid_user = fastapi_users.current_user( - active=True, verified=REQUIRE_EMAIL_VERIFICATION, optional=True -) +# NOTE: verified=REQUIRE_EMAIL_VERIFICATION is not used here since we +# take care of that in `double_check_user` ourself. This is needed, since +# we want the /me endpoint to still return a user even if they are not +# yet verified, so that the frontend knows they exist +optional_valid_user = fastapi_users.current_user(active=True, optional=True) async def double_check_user( @@ -276,6 +283,12 @@ async def double_check_user( detail="Access denied. User is not authenticated.", ) + if user_needs_to_be_verified() and not user.is_verified: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied. User is not verified.", + ) + return user diff --git a/backend/danswer/configs/app_configs.py b/backend/danswer/configs/app_configs.py index 9b86cc9236..7ec2c4334c 100644 --- a/backend/danswer/configs/app_configs.py +++ b/backend/danswer/configs/app_configs.py @@ -75,8 +75,8 @@ OAUTH_CLIENT_SECRET = ( REQUIRE_EMAIL_VERIFICATION = ( os.environ.get("REQUIRE_EMAIL_VERIFICATION", "").lower() == "true" ) -SMTP_SERVER = os.environ.get("SMTP_SERVER", "smtp.gmail.com") -SMTP_PORT = int(os.environ.get("SMTP_PORT", "587")) +SMTP_SERVER = os.environ.get("SMTP_SERVER") or "smtp.gmail.com" +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") diff --git a/backend/danswer/server/manage/get_state.py b/backend/danswer/server/manage/get_state.py index 4108b7cce9..3ca47841b6 100644 --- a/backend/danswer/server/manage/get_state.py +++ b/backend/danswer/server/manage/get_state.py @@ -1,6 +1,7 @@ from fastapi import APIRouter from danswer import __version__ +from danswer.auth.users import user_needs_to_be_verified from danswer.configs.app_configs import AUTH_TYPE from danswer.server.manage.models import AuthTypeResponse from danswer.server.manage.models import VersionResponse @@ -16,7 +17,9 @@ def healthcheck() -> StatusResponse: @router.get("/auth/type") def get_auth_type() -> AuthTypeResponse: - return AuthTypeResponse(auth_type=AUTH_TYPE) + return AuthTypeResponse( + auth_type=AUTH_TYPE, requires_verification=user_needs_to_be_verified() + ) @router.get("/version") diff --git a/backend/danswer/server/manage/models.py b/backend/danswer/server/manage/models.py index d6ad138a67..5209eaee1d 100644 --- a/backend/danswer/server/manage/models.py +++ b/backend/danswer/server/manage/models.py @@ -18,6 +18,9 @@ class VersionResponse(BaseModel): class AuthTypeResponse(BaseModel): auth_type: AuthType + # specifies whether the current auth setup requires + # users to have verified emails + requires_verification: bool class UserInfo(BaseModel): diff --git a/backend/danswer/server/manage/users.py b/backend/danswer/server/manage/users.py index 3a4345c13d..539d7212f2 100644 --- a/backend/danswer/server/manage/users.py +++ b/backend/danswer/server/manage/users.py @@ -1,6 +1,7 @@ from fastapi import APIRouter from fastapi import Depends from fastapi import HTTPException +from fastapi import status from fastapi_users.db import SQLAlchemyUserDatabase from fastapi_users_db_sqlalchemy import UUID_ID from sqlalchemy.ext.asyncio import AsyncSession @@ -10,6 +11,7 @@ from danswer.auth.schemas import UserRead from danswer.auth.schemas import UserRole from danswer.auth.users import current_admin_user from danswer.auth.users import current_user +from danswer.auth.users import optional_valid_user from danswer.db.engine import get_session from danswer.db.engine import get_sqlalchemy_async_engine from danswer.db.models import User @@ -55,9 +57,14 @@ async def get_user_role(user: User = Depends(current_user)) -> UserRoleResponse: @router.get("/me") -def verify_user_logged_in(user: User | None = Depends(current_user)) -> UserInfo: +def verify_user_logged_in(user: User | None = Depends(optional_valid_user)) -> UserInfo: + # NOTE: this does not use `current_user` / `current_admin_user` because we don't want + # to enforce user verification here - the frontend always wants to get the info about + # the current user regardless of if they are currently verified if user is None: - raise HTTPException(status_code=401, detail="User Not Authenticated") + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="User Not Authenticated" + ) return UserInfo( id=str(user.id), diff --git a/deployment/docker_compose/docker-compose.dev.yml b/deployment/docker_compose/docker-compose.dev.yml index c971384264..b482174af6 100644 --- a/deployment/docker_compose/docker-compose.dev.yml +++ b/deployment/docker_compose/docker-compose.dev.yml @@ -22,6 +22,11 @@ services: - VALID_EMAIL_DOMAINS=${VALID_EMAIL_DOMAINS:-} - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-} - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET:-} + - REQUIRE_EMAIL_VERIFICATION=${REQUIRE_EMAIL_VERIFICATION:-} + - SMTP_SERVER=${SMTP_SERVER:-} # For sending verification emails, if unspecified then defaults to 'smtp.gmail.com' + - SMTP_PORT=${SMTP_PORT:-} # For sending verification emails, if unspecified then defaults to '587' + - SMTP_USER=${SMTP_USER:-} + - SMTP_PASS=${SMTP_PASS:-} # Gen AI Settings - GEN_AI_MODEL_PROVIDER=${GEN_AI_MODEL_PROVIDER:-openai} - GEN_AI_MODEL_VERSION=${GEN_AI_MODEL_VERSION:-gpt-3.5-turbo} diff --git a/deployment/docker_compose/env.prod.template b/deployment/docker_compose/env.prod.template index 1f600b63b3..190630f6cb 100644 --- a/deployment/docker_compose/env.prod.template +++ b/deployment/docker_compose/env.prod.template @@ -27,6 +27,7 @@ GEN_AI_MODEL_VERSION=gpt-4 # The following are for configuring User Authentication, supported flows are: # disabled +# basic (standard username / password) # google_oauth (login with google/gmail account) # oidc (only in Danswer enterprise edition) # saml (only in Danswer enterprise edition) @@ -37,6 +38,16 @@ GOOGLE_OAUTH_CLIENT_ID= GOOGLE_OAUTH_CLIENT_SECRET= SECRET= +# if using basic auth and you want to require email verification, +# then uncomment / set the following +#REQUIRE_EMAIL_VERIFICATION=true +#SMTP_USER=your-email@company.com +#SMTP_PASS=your-gmail-password + +# The below are only needed if you aren't using gmail as your SMTP +#SMTP_SERVER= +#SMTP_PORT= + # OpenID Connect (OIDC) #OPENID_CONFIG_URL= diff --git a/deployment/kubernetes/env-configmap.yaml b/deployment/kubernetes/env-configmap.yaml index a3e81b1469..a296fed306 100644 --- a/deployment/kubernetes/env-configmap.yaml +++ b/deployment/kubernetes/env-configmap.yaml @@ -7,6 +7,10 @@ data: AUTH_TYPE: "disabled" # Change this for production uses unless Danswer is only accessible behind VPN SESSION_EXPIRE_TIME_SECONDS: "86400" # 1 Day Default VALID_EMAIL_DOMAINS: "" # Can be something like danswer.ai, as an extra double-check + SMTP_SERVER: "" # For sending verification emails, if unspecified then defaults to 'smtp.gmail.com' + SMTP_PORT: "" # For sending verification emails, if unspecified then defaults to '587' + SMTP_USER: "" # 'your-email@company.com' + SMTP_PASS: "" # 'your-gmail-password' # Gen AI Settings GEN_AI_MODEL_PROVIDER: "openai" GEN_AI_MODEL_VERSION: "gpt-3.5-turbo" # Use GPT-4 if you have it diff --git a/web/src/app/auth/lib.ts b/web/src/app/auth/lib.ts new file mode 100644 index 0000000000..18c3eb8fbb --- /dev/null +++ b/web/src/app/auth/lib.ts @@ -0,0 +1,11 @@ +export async function requestEmailVerification(email: string) { + return await fetch("/api/auth/request-verify-token", { + headers: { + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + email: email, + }), + }); +} diff --git a/web/src/app/auth/login/EmailPasswordForm.tsx b/web/src/app/auth/login/EmailPasswordForm.tsx new file mode 100644 index 0000000000..74dcc1a0a6 --- /dev/null +++ b/web/src/app/auth/login/EmailPasswordForm.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { TextFormField } from "@/components/admin/connectors/Field"; +import { usePopup } from "@/components/admin/connectors/Popup"; +import { basicLogin, basicSignup } from "@/lib/user"; +import { Button } from "@tremor/react"; +import { Form, Formik } from "formik"; +import { useRouter } from "next/navigation"; +import * as Yup from "yup"; +import { requestEmailVerification } from "../lib"; +import { useState } from "react"; +import { Spinner } from "@/components/Spinner"; + +export function EmailPasswordForm({ + isSignup = false, + shouldVerify, +}: { + isSignup?: boolean; + shouldVerify?: boolean; +}) { + const router = useRouter(); + const { popup, setPopup } = usePopup(); + const [isWorking, setIsWorking] = useState(false); + + return ( + <> + {isWorking && } + {popup} + { + if (isSignup) { + // login is fast, no need to show a spinner + setIsWorking(true); + const response = await basicSignup(values.email, values.password); + + if (!response.ok) { + const errorDetail = (await response.json()).detail; + + let errorMsg = "Unknown error"; + if (errorDetail === "REGISTER_USER_ALREADY_EXISTS") { + errorMsg = + "An account already exists with the specified email."; + } + setPopup({ + type: "error", + message: `Failed to sign up - ${errorMsg}`, + }); + return; + } + } + + const loginResponse = await basicLogin(values.email, values.password); + if (loginResponse.ok) { + if (isSignup && shouldVerify) { + await requestEmailVerification(values.email); + router.push("/auth/waiting-on-verification"); + } else { + router.push("/"); + } + } else { + setIsWorking(false); + const errorDetail = (await loginResponse.json()).detail; + + let errorMsg = "Unknown error"; + if (errorDetail === "LOGIN_BAD_CREDENTIALS") { + errorMsg = "Invalid email or password"; + } + setPopup({ + type: "error", + message: `Failed to login - ${errorMsg}`, + }); + } + }} + > + {({ isSubmitting, values }) => ( +
+ + + + +
+ +
+ + )} +
+ + ); +} diff --git a/web/src/app/auth/login/page.tsx b/web/src/app/auth/login/page.tsx index 5977fe24b3..9167e31991 100644 --- a/web/src/app/auth/login/page.tsx +++ b/web/src/app/auth/login/page.tsx @@ -7,9 +7,11 @@ import { AuthTypeMetadata, } from "@/lib/userSS"; import { redirect } from "next/navigation"; -import { getWebVersion, getBackendVersion } from "@/lib/version"; import Image from "next/image"; import { SignInButton } from "./SignInButton"; +import { EmailPasswordForm } from "./EmailPasswordForm"; +import { Card, Title, Text } from "@tremor/react"; +import Link from "next/link"; const Page = async ({ searchParams, @@ -32,24 +34,17 @@ const Page = async ({ console.log(`Some fetch failed for the login page - ${e}`); } - let web_version: string | null = null; - let backend_version: string | null = null; - try { - [web_version, backend_version] = await Promise.all([ - getWebVersion(), - getBackendVersion(), - ]); - } catch (e) { - console.log(`Version info fetch failed for the login page - ${e}`); - } - // simply take the user to the home page if Auth is disabled if (authTypeMetadata?.authType === "disabled") { return redirect("/"); } // if user is already logged in, take them to the main app page - if (currentUser && currentUser.is_active && currentUser.is_verified) { + if (currentUser && currentUser.is_active) { + if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) { + return redirect("/auth/waiting-on-verification"); + } + return redirect("/"); } @@ -77,20 +72,38 @@ const Page = async ({
Logo
-

- Log In to Danswer -

{authUrl && authTypeMetadata && ( - + <> +

+ Log In to Danswer +

+ + + + )} + {authTypeMetadata?.authType === "basic" && ( + +
+ + Log In to Danswer + +
+ +
+ + Don't have an account?{" "} + + Create an account + + +
+
)} -
- VERSION w{web_version} b{backend_version} -
); }; diff --git a/web/src/app/auth/signup/page.tsx b/web/src/app/auth/signup/page.tsx new file mode 100644 index 0000000000..deea6babc7 --- /dev/null +++ b/web/src/app/auth/signup/page.tsx @@ -0,0 +1,83 @@ +import { HealthCheckBanner } from "@/components/health/healthcheck"; +import { User } from "@/lib/types"; +import { + getCurrentUserSS, + getAuthTypeMetadataSS, + AuthTypeMetadata, +} from "@/lib/userSS"; +import { redirect } from "next/navigation"; +import Image from "next/image"; +import { EmailPasswordForm } from "../login/EmailPasswordForm"; +import { Card, Title, Text } from "@tremor/react"; +import Link from "next/link"; + +const Page = async () => { + // catch cases where the backend is completely unreachable here + // without try / catch, will just raise an exception and the page + // will not render + let authTypeMetadata: AuthTypeMetadata | null = null; + let currentUser: User | null = null; + try { + [authTypeMetadata, currentUser] = await Promise.all([ + getAuthTypeMetadataSS(), + getCurrentUserSS(), + ]); + } catch (e) { + console.log(`Some fetch failed for the login page - ${e}`); + } + + // simply take the user to the home page if Auth is disabled + if (authTypeMetadata?.authType === "disabled") { + return redirect("/"); + } + + // if user is already logged in, take them to the main app page + if (currentUser && currentUser.is_active) { + if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) { + return redirect("/"); + } + return redirect("/auth/waiting-on-verification"); + } + + // only enable this page if basic login is enabled + if (authTypeMetadata?.authType !== "basic") { + return redirect("/"); + } + + return ( +
+
+ +
+
+
+
+ Logo +
+ +
+ + Sign Up for Danswer + +
+ + +
+ + Already have an account?{" "} + + Log In + + +
+
+
+
+
+ ); +}; + +export default Page; diff --git a/web/src/app/auth/verify-email/Verify.tsx b/web/src/app/auth/verify-email/Verify.tsx new file mode 100644 index 0000000000..85faa45959 --- /dev/null +++ b/web/src/app/auth/verify-email/Verify.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { HealthCheckBanner } from "@/components/health/healthcheck"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { Text } from "@tremor/react"; +import { RequestNewVerificationEmail } from "../waiting-on-verification/RequestNewVerificationEmail"; +import { User } from "@/lib/types"; + +export function Verify({ user }: { user: User | null }) { + const searchParams = useSearchParams(); + const router = useRouter(); + + const [error, setError] = useState(""); + + async function verify() { + const token = searchParams.get("token"); + if (!token) { + setError( + "Missing verification token. Try requesting a new verification email." + ); + return; + } + + const response = await fetch("/api/auth/verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token }), + }); + + if (response.ok) { + router.push("/"); + } else { + const errorDetail = (await response.json()).detail; + setError( + `Failed to verify your email - ${errorDetail}. Please try requesting a new verification email.` + ); + } + } + + useEffect(() => { + verify(); + }, []); + + return ( +
+
+ +
+
+
+
+ Logo +
+ + {!error ? ( + Verifying your email... + ) : ( +
+ {error} + + {user && ( +
+ + + Get new verification email + + +
+ )} +
+ )} +
+
+
+ ); +} diff --git a/web/src/app/auth/verify-email/page.tsx b/web/src/app/auth/verify-email/page.tsx new file mode 100644 index 0000000000..f6faa075b9 --- /dev/null +++ b/web/src/app/auth/verify-email/page.tsx @@ -0,0 +1,30 @@ +import { + AuthTypeMetadata, + getAuthTypeMetadataSS, + getCurrentUserSS, +} from "@/lib/userSS"; +import { Verify } from "./Verify"; +import { User } from "@/lib/types"; +import { redirect } from "next/navigation"; + +export default async function Page() { + // catch cases where the backend is completely unreachable here + // without try / catch, will just raise an exception and the page + // will not render + let authTypeMetadata: AuthTypeMetadata | null = null; + let currentUser: User | null = null; + try { + [authTypeMetadata, currentUser] = await Promise.all([ + getAuthTypeMetadataSS(), + getCurrentUserSS(), + ]); + } catch (e) { + console.log(`Some fetch failed for the login page - ${e}`); + } + + if (!authTypeMetadata?.requiresVerification || currentUser?.is_verified) { + return redirect("/"); + } + + return ; +} diff --git a/web/src/app/auth/waiting-on-verification/RequestNewVerificationEmail.tsx b/web/src/app/auth/waiting-on-verification/RequestNewVerificationEmail.tsx new file mode 100644 index 0000000000..96c70bdfb9 --- /dev/null +++ b/web/src/app/auth/waiting-on-verification/RequestNewVerificationEmail.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { usePopup } from "@/components/admin/connectors/Popup"; +import { requestEmailVerification } from "../lib"; +import { Spinner } from "@/components/Spinner"; +import { useState } from "react"; + +export function RequestNewVerificationEmail({ + children, + email, +}: { + children: JSX.Element | string; + email: string; +}) { + const { popup, setPopup } = usePopup(); + const [isRequestingVerification, setIsRequestingVerification] = + useState(false); + + return ( + + ); +} diff --git a/web/src/app/auth/waiting-on-verification/page.tsx b/web/src/app/auth/waiting-on-verification/page.tsx new file mode 100644 index 0000000000..a266654b4e --- /dev/null +++ b/web/src/app/auth/waiting-on-verification/page.tsx @@ -0,0 +1,69 @@ +import { + AuthTypeMetadata, + getAuthTypeMetadataSS, + getCurrentUserSS, +} from "@/lib/userSS"; +import { redirect } from "next/navigation"; +import Image from "next/image"; +import { HealthCheckBanner } from "@/components/health/healthcheck"; +import { User } from "@/lib/types"; +import { Text } from "@tremor/react"; +import { RequestNewVerificationEmail } from "./RequestNewVerificationEmail"; + +export default async function Page() { + // catch cases where the backend is completely unreachable here + // without try / catch, will just raise an exception and the page + // will not render + let authTypeMetadata: AuthTypeMetadata | null = null; + let currentUser: User | null = null; + try { + [authTypeMetadata, currentUser] = await Promise.all([ + getAuthTypeMetadataSS(), + getCurrentUserSS(), + ]); + } catch (e) { + console.log(`Some fetch failed for the login page - ${e}`); + } + + if (!currentUser) { + if (authTypeMetadata?.authType === "disabled") { + return redirect("/"); + } + return redirect("/auth/login"); + } + + if (!authTypeMetadata?.requiresVerification || currentUser.is_verified) { + return redirect("/"); + } + + return ( +
+
+ +
+
+
+
+ Logo +
+ +
+ + Hey {currentUser.email} - it looks like you haven't + verified your email yet. +
+ Check your inbox for an email from us to get started! +
+
+ If you don't see anything, click{" "} + + here + {" "} + to request a new email. +
+
+
+
+
+ ); +} diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 76ee745b0b..5da728ad10 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -1,4 +1,8 @@ -import { getAuthDisabledSS, getCurrentUserSS } from "@/lib/userSS"; +import { + AuthTypeMetadata, + getAuthTypeMetadataSS, + getCurrentUserSS, +} from "@/lib/userSS"; import { redirect } from "next/navigation"; import { fetchSS } from "@/lib/utilsSS"; import { Connector, DocumentSet, User, ValidSources } from "@/lib/types"; @@ -31,7 +35,7 @@ export default async function ChatPage({ const currentChatId = chatId ? parseInt(chatId) : null; const tasks = [ - getAuthDisabledSS(), + getAuthTypeMetadataSS(), getCurrentUserSS(), fetchSS("/manage/connector"), fetchSS("/manage/document-set"), @@ -45,7 +49,7 @@ export default async function ChatPage({ // catch cases where the backend is completely unreachable here // without try / catch, will just raise an exception and the page // will not render - let results: (User | Response | boolean | null)[] = [ + let results: (User | Response | AuthTypeMetadata | null)[] = [ null, null, null, @@ -59,7 +63,7 @@ export default async function ChatPage({ } catch (e) { console.log(`Some fetch failed for the main search page - ${e}`); } - const authDisabled = results[0] as boolean; + const authTypeMetadata = results[0] as AuthTypeMetadata; const user = results[1] as User | null; const connectorsResponse = results[2] as Response | null; const documentSetsResponse = results[3] as Response | null; @@ -67,10 +71,15 @@ export default async function ChatPage({ const chatSessionsResponse = results[5] as Response | null; const chatSessionMessagesResponse = results[6] as Response | null; + const authDisabled = authTypeMetadata.authType === "disabled"; if (!authDisabled && !user) { return redirect("/auth/login"); } + if (!user?.is_verified && authTypeMetadata.requiresVerification) { + return redirect("/auth/waiting-on-verification"); + } + let connectors: Connector[] = []; if (connectorsResponse?.ok) { connectors = await connectorsResponse.json(); diff --git a/web/src/app/search/page.tsx b/web/src/app/search/page.tsx index a05c28f5bd..b1147d3bdf 100644 --- a/web/src/app/search/page.tsx +++ b/web/src/app/search/page.tsx @@ -1,6 +1,10 @@ import { SearchSection } from "@/components/search/SearchSection"; import { Header } from "@/components/Header"; -import { getAuthDisabledSS, getCurrentUserSS } from "@/lib/userSS"; +import { + AuthTypeMetadata, + getAuthTypeMetadataSS, + getCurrentUserSS, +} from "@/lib/userSS"; import { redirect } from "next/navigation"; import { HealthCheckBanner } from "@/components/health/healthcheck"; import { ApiKeyModal } from "@/components/openai/ApiKeyModal"; @@ -21,7 +25,7 @@ export default async function Home() { noStore(); const tasks = [ - getAuthDisabledSS(), + getAuthTypeMetadataSS(), getCurrentUserSS(), fetchSS("/manage/connector"), fetchSS("/manage/document-set"), @@ -31,22 +35,32 @@ export default async function Home() { // catch cases where the backend is completely unreachable here // without try / catch, will just raise an exception and the page // will not render - let results: (User | Response | boolean | null)[] = [null, null, null, null]; + let results: (User | Response | AuthTypeMetadata | null)[] = [ + null, + null, + null, + null, + ]; try { results = await Promise.all(tasks); } catch (e) { console.log(`Some fetch failed for the main search page - ${e}`); } - const authDisabled = results[0] as boolean; + const authTypeMetadata = results[0] as AuthTypeMetadata; const user = results[1] as User | null; const connectorsResponse = results[2] as Response | null; const documentSetsResponse = results[3] as Response | null; const personaResponse = results[4] as Response | null; + const authDisabled = authTypeMetadata.authType === "disabled"; if (!authDisabled && !user) { return redirect("/auth/login"); } + if (!user?.is_verified && authTypeMetadata.requiresVerification) { + return redirect("/auth/waiting-on-verification"); + } + let connectors: Connector[] = []; if (connectorsResponse?.ok) { connectors = await connectorsResponse.json(); diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 645d8b795a..bacc89ce05 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -1,4 +1,4 @@ -export type AuthType = "disabled" | "google_oauth" | "oidc" | "saml"; +export type AuthType = "disabled" | "basic" | "google_oauth" | "oidc" | "saml"; export const INTERNAL_URL = process.env.INTERNAL_URL || "http://127.0.0.1:8080"; export const NEXT_PUBLIC_DISABLE_STREAMING = diff --git a/web/src/lib/user.ts b/web/src/lib/user.ts index b3a9133331..e1387236fc 100644 --- a/web/src/lib/user.ts +++ b/web/src/lib/user.ts @@ -19,3 +19,38 @@ export const logout = async (): Promise => { }); return response; }; + +export const basicLogin = async ( + email: string, + password: string +): Promise => { + const params = new URLSearchParams([ + ["username", email], + ["password", password], + ]); + const response = await fetch("/api/auth/login", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params, + }); + return response; +}; + +export const basicSignup = async (email: string, password: string) => { + const response = await fetch("/api/auth/register", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + username: email, + password, + }), + }); + return response; +}; diff --git a/web/src/lib/userSS.ts b/web/src/lib/userSS.ts index 0402c68a6a..6b195d7dee 100644 --- a/web/src/lib/userSS.ts +++ b/web/src/lib/userSS.ts @@ -7,6 +7,7 @@ import { AuthType } from "./constants"; export interface AuthTypeMetadata { authType: AuthType; autoRedirect: boolean; + requiresVerification: boolean; } export const getAuthTypeMetadataSS = async (): Promise => { @@ -15,15 +16,24 @@ export const getAuthTypeMetadataSS = async (): Promise => { throw new Error("Failed to fetch data"); } - const data: { auth_type: string } = await res.json(); + const data: { auth_type: string; requires_verification: boolean } = + await res.json(); const authType = data.auth_type as AuthType; // for SAML / OIDC, we auto-redirect the user to the IdP when the user visits // Danswer in an un-authenticated state if (authType === "oidc" || authType === "saml") { - return { authType, autoRedirect: true }; + return { + authType, + autoRedirect: true, + requiresVerification: data.requires_verification, + }; } - return { authType, autoRedirect: false }; + return { + authType, + autoRedirect: false, + requiresVerification: data.requires_verification, + }; }; export const getAuthDisabledSS = async (): Promise => { @@ -65,6 +75,8 @@ export const getAuthUrlSS = async (authType: AuthType): Promise => { switch (authType) { case "disabled": return ""; + case "basic": + return ""; case "google_oauth": { return await getGoogleOAuthUrlSS(); }