Compare commits

...

4 Commits

Author SHA1 Message Date
pablodanswer
7e3a9661f9 nits 2024-10-14 11:06:00 -07:00
pablodanswer
713f952a39 nits 2024-10-14 11:04:35 -07:00
pablodanswer
6ba31a8773 add proper client side login 2024-10-14 11:01:28 -07:00
pablodanswer
eb76e7544d client side updates 2024-10-11 09:42:57 -07:00
8 changed files with 151 additions and 68 deletions

View File

@@ -355,7 +355,6 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# NOTE: Most IdPs have very short expiry times, and we don't want to force the user to
# re-authenticate that frequently, so by default this is disabled
if expires_at and TRACK_EXTERNAL_IDP_EXPIRY:
oidc_expiry = datetime.fromtimestamp(expires_at, tz=timezone.utc)
await self.user_db.update(

View File

@@ -30,9 +30,7 @@ data:
LANGUAGE_CHAT_NAMING_HINT: ""
QA_PROMPT_OVERRIDE: ""
# Other Services
POSTGRES_HOST: "relational-db-service"
VESPA_HOST: "document-index-service"
REDIS_HOST: "redis-service"
# Internet Search Tool
BING_API_KEY: ""
# Don't change the NLP models unless you know what you're doing

View File

@@ -0,0 +1,99 @@
"use client";
import { useState, useEffect } from "react";
import { SignInButton } from "./SignInButton";
import { AuthTypeMetadata } from "@/lib/userSS";
import { AuthType } from "@/lib/constants";
const getOIDCAuthUrl = async (): Promise<string> => {
const res = await fetch("/api/auth/oidc/authorize");
if (!res.ok) {
throw new Error("Failed to fetch OIDC auth URL");
}
const data: { authorization_url: string } = await res.json();
return data.authorization_url;
};
const getGoogleOAuthUrl = async (): Promise<string> => {
const res = await fetch("/api/auth/oauth/authorize");
if (!res.ok) {
throw new Error("Failed to fetch Google OAuth URL");
}
const data: { authorization_url: string } = await res.json();
return data.authorization_url;
};
const getSAMLAuthUrl = async (): Promise<string> => {
const res = await fetch("/api/auth/saml/authorize");
if (!res.ok) {
throw new Error("Failed to fetch SAML auth URL");
}
const data: { authorization_url: string } = await res.json();
return data.authorization_url;
};
const getAuthUrl = async (authType: string): Promise<string> => {
switch (authType) {
case "oidc":
return await getOIDCAuthUrl();
case "google_oauth":
return await getGoogleOAuthUrl();
case "saml":
return await getSAMLAuthUrl();
default:
throw new Error(`Unsupported auth type: ${authType}`);
}
};
const ClientSideSigninButton = () => {
const [authUrl, setAuthUrl] = useState<string | null>(null);
const [authTypeMetadata, setAuthTypeMetadata] =
useState<AuthTypeMetadata | null>(null);
const [currentUrl, setCurrentUrl] = useState<string>("");
useEffect(() => {
const fetchAuthData = async () => {
try {
// Fetch the auth type metadata
const authTypeMetadataResponse = await fetch("/api/auth/type");
if (!authTypeMetadataResponse.ok) {
throw new Error("Failed to fetch auth type metadata");
}
const authTypeMetadataJson = await authTypeMetadataResponse.json();
const authTypeMetadataData: AuthTypeMetadata = {
authType: authTypeMetadataJson.auth_type as AuthType,
autoRedirect: false, // Default value, adjust if needed
requiresVerification: authTypeMetadataJson.requires_verification,
};
setAuthTypeMetadata(authTypeMetadataData);
// Fetch the auth URL based on the auth type
const authUrlResponse = await getAuthUrl(authTypeMetadataData.authType);
setAuthUrl(authUrlResponse);
} catch (error) {
console.error("Error fetching auth data:", error);
}
};
fetchAuthData();
// Set the current URL safely on the client side
setCurrentUrl(window.location.href);
}, []);
if (!authUrl || !currentUrl || !authTypeMetadata) {
return <p>Loading...</p>;
}
return (
<div className="flex flex-col items-center space-y-4">
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata.authType}
/>
</div>
);
};
export default ClientSideSigninButton;

View File

@@ -6,10 +6,6 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
export const LoginText = () => {
const settings = useContext(SettingsContext);
// if (!settings) {
// throw new Error("SettingsContext is not available");
// }
return (
<>
Log In to{" "}

View File

@@ -9,6 +9,7 @@ export function SignInButton({
authType: AuthType;
}) {
let button;
if (authType === "google_oauth") {
button = (
<div className="mx-auto flex">

View File

@@ -68,7 +68,6 @@ const Page = async ({
if (authTypeMetadata?.autoRedirect && authUrl && !autoRedirectDisabled) {
return redirect(authUrl);
}
return (
<AuthFlowContainer>
<div className="absolute top-10x w-full">
@@ -88,6 +87,7 @@ const Page = async ({
/>
</>
)}
{authTypeMetadata?.authType === "basic" && (
<Card className="mt-4 w-96">
<div className="flex">

View File

@@ -6,8 +6,9 @@ import { Modal } from "../Modal";
import { useCallback, useEffect, useState } from "react";
import { getSecondsUntilExpiration } from "@/lib/time";
import { User } from "@/lib/types";
import { mockedRefreshToken, refreshToken } from "./refreshUtils";
import { refreshToken } from "./refreshUtils";
import { CUSTOM_REFRESH_URL } from "@/lib/constants";
import ClientSideSigninButton from "@/app/auth/login/ClientSideSigninButton";
export const HealthCheckBanner = () => {
const { error } = useSWR("/api/health", errorHandlingFetcher);
@@ -26,7 +27,6 @@ export const HealthCheckBanner = () => {
if (updatedUser) {
const seconds = getSecondsUntilExpiration(updatedUser);
setSecondsUntilExpiration(seconds);
console.debug(`Updated seconds until expiration:! ${seconds}`);
}
}, [mutateUser]);
@@ -35,60 +35,58 @@ export const HealthCheckBanner = () => {
}, [user, updateExpirationTime]);
useEffect(() => {
if (CUSTOM_REFRESH_URL) {
const refreshUrl = CUSTOM_REFRESH_URL;
let refreshTimeoutId: NodeJS.Timeout;
let expireTimeoutId: NodeJS.Timeout;
const refreshUrl = CUSTOM_REFRESH_URL;
let refreshTimeoutId: NodeJS.Timeout;
let expireTimeoutId: NodeJS.Timeout;
const attemptTokenRefresh = async () => {
try {
// NOTE: This is a mocked refresh token for testing purposes.
// const refreshTokenData = mockedRefreshToken();
const attemptTokenRefresh = async () => {
if (!refreshUrl) {
console.debug("No refresh URL, skipping refresh");
return;
}
try {
// NOTE: This is a mocked refresh token for testing purposes.
// const refreshTokenData = mockedRefreshToken();
const refreshTokenData = await refreshToken(refreshUrl);
const refreshTokenData = await refreshToken(refreshUrl);
const response = await fetch(
"/api/enterprise-settings/refresh-token",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(refreshTokenData),
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
await new Promise((resolve) => setTimeout(resolve, 4000));
await mutateUser(undefined, { revalidate: true });
updateExpirationTime();
} catch (error) {
console.error("Error refreshing token:", error);
const response = await fetch("/api/enterprise-settings/refresh-token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(refreshTokenData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
};
await new Promise((resolve) => setTimeout(resolve, 4000));
const scheduleRefreshAndExpire = () => {
if (secondsUntilExpiration !== null) {
const timeUntilRefresh = (secondsUntilExpiration + 0.5) * 1000;
refreshTimeoutId = setTimeout(attemptTokenRefresh, timeUntilRefresh);
await mutateUser(undefined, { revalidate: true });
updateExpirationTime();
} catch (error) {
console.error("Error refreshing token:", error);
}
};
const timeUntilExpire = (secondsUntilExpiration + 10) * 1000;
expireTimeoutId = setTimeout(() => {
console.debug("Session expired. Setting expired state to true.");
setExpired(true);
}, timeUntilExpire);
}
};
const scheduleRefreshAndExpire = () => {
if (secondsUntilExpiration !== null) {
const timeUntilRefresh = (secondsUntilExpiration + 0.5) * 1000;
refreshTimeoutId = setTimeout(attemptTokenRefresh, timeUntilRefresh);
const timeUntilExpire = (secondsUntilExpiration + 2) * 1000;
expireTimeoutId = setTimeout(() => {
console.debug("Session expired. Setting expired state to true.");
setExpired(true);
}, timeUntilExpire);
}
};
scheduleRefreshAndExpire();
scheduleRefreshAndExpire();
return () => {
clearTimeout(refreshTimeoutId);
clearTimeout(expireTimeoutId);
};
}
return () => {
clearTimeout(refreshTimeoutId);
clearTimeout(expireTimeoutId);
};
}, [secondsUntilExpiration, user, mutateUser, updateExpirationTime]);
if (!error && !expired) {
@@ -101,21 +99,12 @@ export const HealthCheckBanner = () => {
if (error instanceof RedirectError || expired) {
return (
<Modal
width="w-1/4"
className="overflow-y-hidden flex flex-col"
title="You've been logged out"
>
<Modal width="max-w-2xl" className="overflow-y-hidden flex flex-col">
<div className="flex flex-col gap-y-4">
<p className="text-sm">
<p className="text-sm text-center">
Your session has expired. Please log in again to continue.
</p>
<a
href="/auth/login"
className="w-full mt-4 mx-auto rounded-md text-text-200 py-2 bg-background-900 text-center hover:bg-emphasis animtate duration-300 transition-bg"
>
Log in
</a>
<ClientSideSigninButton />
</div>
</Modal>
);

View File

@@ -18,6 +18,7 @@ export const getAuthTypeMetadataSS = async (): Promise<AuthTypeMetadata> => {
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