mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-20 13:01:34 +02:00
Add support for basic auth on FE
This commit is contained in:
parent
1e84b0daa4
commit
dab3ba8a41
@ -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
|
||||
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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),
|
||||
|
@ -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}
|
||||
|
@ -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=
|
||||
|
||||
|
@ -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
11
web/src/app/auth/lib.ts
Normal 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,
|
||||
}),
|
||||
});
|
||||
}
|
113
web/src/app/auth/login/EmailPasswordForm.tsx
Normal file
113
web/src/app/auth/login/EmailPasswordForm.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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'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>
|
||||
);
|
||||
};
|
||||
|
83
web/src/app/auth/signup/page.tsx
Normal file
83
web/src/app/auth/signup/page.tsx
Normal 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;
|
80
web/src/app/auth/verify-email/Verify.tsx
Normal file
80
web/src/app/auth/verify-email/Verify.tsx
Normal 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>
|
||||
);
|
||||
}
|
30
web/src/app/auth/verify-email/page.tsx
Normal file
30
web/src/app/auth/verify-email/page.tsx
Normal 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} />;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
69
web/src/app/auth/waiting-on-verification/page.tsx
Normal file
69
web/src/app/auth/waiting-on-verification/page.tsx
Normal 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't
|
||||
verified your email yet.
|
||||
<br />
|
||||
Check your inbox for an email from us to get started!
|
||||
<br />
|
||||
<br />
|
||||
If you don't see anything, click{" "}
|
||||
<RequestNewVerificationEmail email={currentUser.email}>
|
||||
here
|
||||
</RequestNewVerificationEmail>{" "}
|
||||
to request a new email.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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 =
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user