+
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 (
+
+
+
+ );
+}
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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ 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();
}