Add support for basic auth on FE

This commit is contained in:
Weves 2023-12-25 01:16:09 -08:00 committed by Chris Weaver
parent 1e84b0daa4
commit dab3ba8a41
21 changed files with 606 additions and 45 deletions

View File

@ -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

View File

@ -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")

View File

@ -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")

View File

@ -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):

View File

@ -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),

View File

@ -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}

View File

@ -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=

View File

@ -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

11
web/src/app/auth/lib.ts Normal file
View File

@ -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,
}),
});
}

View File

@ -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 && <Spinner />}
{popup}
<Formik
initialValues={{
email: "",
password: "",
}}
validationSchema={Yup.object().shape({
email: Yup.string().email().required(),
password: Yup.string().required(),
})}
onSubmit={async (values) => {
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 }) => (
<Form>
<TextFormField
name="email"
label="Email"
type="email"
placeholder="email@yourcompany.com"
/>
<TextFormField
name="password"
label="Password"
type="password"
placeholder="**************"
/>
<div className="flex">
<Button
type="submit"
disabled={isSubmitting}
className="mx-auto w-full"
>
{isSignup ? "Sign Up" : "Log In"}
</Button>
</div>
</Form>
)}
</Formik>
</>
);
}

View File

@ -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 ({
<div className="h-16 w-16 mx-auto">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
</div>
<h2 className="text-center text-xl text-strong font-bold mt-6">
Log In to Danswer
</h2>
{authUrl && authTypeMetadata && (
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata?.authType}
/>
<>
<h2 className="text-center text-xl text-strong font-bold mt-6">
Log In to Danswer
</h2>
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata?.authType}
/>
</>
)}
{authTypeMetadata?.authType === "basic" && (
<Card className="mt-4 w-96">
<div className="flex">
<Title className="mb-2 mx-auto font-bold">
Log In to Danswer
</Title>
</div>
<EmailPasswordForm />
<div className="flex">
<Text className="mt-4 mx-auto">
Don&apos;t have an account?{" "}
<Link href="/auth/signup" className="text-link font-medium">
Create an account
</Link>
</Text>
</div>
</Card>
)}
</div>
</div>
<div className="fixed bottom-4 right-4 z-50 text-slate-400 p-2">
VERSION w{web_version} b{backend_version}
</div>
</main>
);
};

View File

@ -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 (
<main>
<div className="absolute top-10x w-full">
<HealthCheckBanner />
</div>
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div>
<div className="h-16 w-16 mx-auto">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
</div>
<Card className="mt-4 w-96">
<div className="flex">
<Title className="mb-2 mx-auto font-bold">
Sign Up for Danswer
</Title>
</div>
<EmailPasswordForm
isSignup
shouldVerify={authTypeMetadata?.requiresVerification}
/>
<div className="flex">
<Text className="mt-4 mx-auto">
Already have an account?{" "}
<Link href="/auth/login" className="text-link font-medium">
Log In
</Link>
</Text>
</div>
</Card>
</div>
</div>
</main>
);
};
export default Page;

View File

@ -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 (
<main>
<div className="absolute top-10x w-full">
<HealthCheckBanner />
</div>
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div>
<div className="h-16 w-16 mx-auto animate-pulse">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
</div>
{!error ? (
<Text className="mt-2">Verifying your email...</Text>
) : (
<div>
<Text className="mt-2">{error}</Text>
{user && (
<div className="text-center">
<RequestNewVerificationEmail email={user.email}>
<Text className="mt-2 text-link">
Get new verification email
</Text>
</RequestNewVerificationEmail>
</div>
)}
</div>
)}
</div>
</div>
</main>
);
}

View File

@ -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 <Verify user={currentUser} />;
}

View File

@ -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 (
<button
className="text-link"
onClick={async () => {
setIsRequestingVerification(true);
const response = await requestEmailVerification(email);
setIsRequestingVerification(false);
if (response.ok) {
setPopup({
type: "success",
message: "A new verification email has been sent!",
});
} else {
const errorDetail = (await response.json()).detail;
setPopup({
type: "error",
message: `Failed to send a new verification email - ${errorDetail}`,
});
}
}}
>
{isRequestingVerification && <Spinner />}
{popup}
{children}
</button>
);
}

View File

@ -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 (
<main>
<div className="absolute top-10x w-full">
<HealthCheckBanner />
</div>
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div>
<div className="h-16 w-16 mx-auto">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
</div>
<div className="flex">
<Text className="text-center font-medium text-lg mt-6 w-108">
Hey <i>{currentUser.email}</i> - it looks like you haven&apos;t
verified your email yet.
<br />
Check your inbox for an email from us to get started!
<br />
<br />
If you don&apos;t see anything, click{" "}
<RequestNewVerificationEmail email={currentUser.email}>
here
</RequestNewVerificationEmail>{" "}
to request a new email.
</Text>
</div>
</div>
</div>
</main>
);
}

View File

@ -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<any>[] = [];
if (connectorsResponse?.ok) {
connectors = await connectorsResponse.json();

View File

@ -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<any>[] = [];
if (connectorsResponse?.ok) {
connectors = await connectorsResponse.json();

View File

@ -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 =

View File

@ -19,3 +19,38 @@ export const logout = async (): Promise<Response> => {
});
return response;
};
export const basicLogin = async (
email: string,
password: string
): Promise<Response> => {
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;
};

View File

@ -7,6 +7,7 @@ import { AuthType } from "./constants";
export interface AuthTypeMetadata {
authType: AuthType;
autoRedirect: boolean;
requiresVerification: boolean;
}
export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => {
@ -15,15 +16,24 @@ export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => {
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<boolean> => {
@ -65,6 +75,8 @@ export const getAuthUrlSS = async (authType: AuthType): Promise<string> => {
switch (authType) {
case "disabled":
return "";
case "basic":
return "";
case "google_oauth": {
return await getGoogleOAuthUrlSS();
}