mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
refactor: clean up auth-related types+utils+functions+hooks into lib/auth/ directory (#12853)
This commit is contained in:
1 parent
698d0305fd
commit
68cd608f26
27 files changed
+272
-268
No files matched your search
@@ -4,7 +4,7 @@ import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { useAuthType } from "@/lib/hooks";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
import { AuthType } from "@/lib/auth/types";
|
||||
import { AccountsAccessSettings } from "@/views/SettingsPage";
|
||||
|
||||
export default function AccountsAccessPage() {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { User } from "@/lib/types";
|
||||
import { getCurrentUserSS } from "@/lib/users/svcSS";
|
||||
import { getAuthTypeMetadataSS, getAuthUrlSS } from "@/lib/auth/svcSS";
|
||||
import { AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { AuthType, AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { redirect } from "next/navigation";
|
||||
import EmailPasswordForm from "../login/EmailPasswordForm";
|
||||
import SignInButton from "@/app/auth/login/SignInButton";
|
||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||
import AuthErrorDisplay from "@/components/auth/AuthErrorDisplay";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
|
||||
const Page = async (props: {
|
||||
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { AuthType, AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import LoginText from "@/app/auth/login/LoginText";
|
||||
import SignInButton from "@/app/auth/login/SignInButton";
|
||||
import ProviderSignInButton from "@/app/auth/login/ProviderSignInButton";
|
||||
import EmailPasswordForm from "./EmailPasswordForm";
|
||||
import { AuthType, NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
import { useSendAuthRequiredMessage } from "@/lib/extension/utils";
|
||||
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
|
||||
import { useSendAuthRequiredMessage } from "@/lib/extension/hooks";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Button, MessageCard } from "@opal/components";
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@opal/components";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
import { AuthType } from "@/lib/auth/types";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import type { IconProps } from "@opal/types";
|
||||
import { useCaptcha } from "@/lib/hooks/useCaptcha";
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { User } from "@/lib/types";
|
||||
import { getCurrentUserSS } from "@/lib/users/svcSS";
|
||||
import { getAuthTypeMetadataSS, getAuthUrlSS } from "@/lib/auth/svcSS";
|
||||
import { AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { AuthType, AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { Route } from "next";
|
||||
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
|
||||
import LoginPage from "./LoginPage";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
|
||||
export interface PageProps {
|
||||
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { User } from "@/lib/types";
|
||||
import { getCurrentUserSS } from "@/lib/users/svcSS";
|
||||
import { getAuthTypeMetadataSS, getAuthUrlSS } from "@/lib/auth/svcSS";
|
||||
import { AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { AuthType, AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { redirect } from "next/navigation";
|
||||
import EmailPasswordForm from "../login/EmailPasswordForm";
|
||||
import SignInButton from "@/app/auth/login/SignInButton";
|
||||
@@ -10,7 +10,6 @@ import ReferralSourceSelector from "./ReferralSourceSelector";
|
||||
import AuthErrorDisplay from "@/components/auth/AuthErrorDisplay";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { cn } from "@opal/utils";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
|
||||
const Page = async (props: {
|
||||
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
|
||||
@@ -4,19 +4,20 @@ import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
import { AuthType } from "@/lib/auth/types";
|
||||
import AppInputBar, { AppInputBarHandle } from "@/sections/input/AppInputBar";
|
||||
import { Button } from "@opal/components";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { useFilters, useLlmManager } from "@/lib/hooks";
|
||||
import Dropzone from "react-dropzone";
|
||||
import { useSendMessageToParent, getPanelOrigin } from "@/lib/extension/utils";
|
||||
import { getPanelOrigin } from "@/lib/extension/utils";
|
||||
import { sendSetDefaultNewTabMessage } from "@/lib/extension/svc";
|
||||
import { useSendMessageToParent } from "@/lib/extension/hooks";
|
||||
import { useNRFPreferences } from "@/components/context/NRFPreferencesContext";
|
||||
import SidePanelHeader from "@/app/nrf/side-panel/SidePanelHeader";
|
||||
import { CHROME_MESSAGE } from "@/lib/extension/constants";
|
||||
import { SettingsPanel } from "@/app/components/nrf/SettingsPanel";
|
||||
import LoginPage from "@/app/auth/login/LoginPage";
|
||||
import { sendSetDefaultNewTabMessage } from "@/lib/extension/utils";
|
||||
import { useAgents } from "@/lib/agents/hooks";
|
||||
import { useProjectsContext } from "@/providers/ProjectsContext";
|
||||
import useDeepResearchToggle from "@/hooks/useDeepResearchToggle";
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import useSWR from "swr";
|
||||
import { AuthType, NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { AuthTypeMetadata } from "@/lib/auth/types";
|
||||
|
||||
interface AuthTypeAPIResponse {
|
||||
auth_type: string;
|
||||
requires_verification: boolean;
|
||||
anonymous_user_enabled: boolean | null;
|
||||
password_min_length: number;
|
||||
has_users: boolean;
|
||||
oauth_enabled: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_AUTH_TYPE_METADATA: AuthTypeMetadata = {
|
||||
authType: NEXT_PUBLIC_CLOUD_ENABLED ? AuthType.CLOUD : AuthType.BASIC,
|
||||
autoRedirect: false,
|
||||
requiresVerification: false,
|
||||
anonymousUserEnabled: null,
|
||||
passwordMinLength: 0,
|
||||
hasUsers: false,
|
||||
oauthEnabled: false,
|
||||
};
|
||||
|
||||
async function fetchAuthTypeMetadata(url: string): Promise<AuthTypeMetadata> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error("Failed to fetch auth type metadata");
|
||||
const data: AuthTypeAPIResponse = await res.json();
|
||||
const authType = NEXT_PUBLIC_CLOUD_ENABLED
|
||||
? AuthType.CLOUD
|
||||
: (data.auth_type as AuthType);
|
||||
return {
|
||||
authType,
|
||||
autoRedirect: authType === AuthType.OIDC || authType === AuthType.SAML,
|
||||
requiresVerification: data.requires_verification,
|
||||
anonymousUserEnabled: data.anonymous_user_enabled,
|
||||
passwordMinLength: data.password_min_length,
|
||||
hasUsers: data.has_users,
|
||||
oauthEnabled: data.oauth_enabled,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAuthTypeMetadata(): {
|
||||
authTypeMetadata: AuthTypeMetadata;
|
||||
isLoading: boolean;
|
||||
error: Error | undefined;
|
||||
} {
|
||||
const { data, error, isLoading } = useSWR<AuthTypeMetadata>(
|
||||
SWR_KEYS.authType,
|
||||
fetchAuthTypeMetadata,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 30_000,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
authTypeMetadata: data ?? DEFAULT_AUTH_TYPE_METADATA,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { User } from "@/lib/types";
|
||||
import { NO_AUTH_USER_ID } from "@/lib/extension/constants";
|
||||
import { AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
|
||||
const REFRESH_INTERVAL = 600000;
|
||||
const MIN_REFRESH_GAP_MS = REFRESH_INTERVAL - 60000;
|
||||
const VISIBILITY_REFRESH_GAP_MS = 60000;
|
||||
|
||||
export function useTokenRefresh(
|
||||
user: User | null,
|
||||
authTypeMetadata: AuthTypeMetadata,
|
||||
authTypeMetadataLoading: boolean,
|
||||
onRefreshFail: () => Promise<void>
|
||||
) {
|
||||
// Refs (not state) so updates do not retrigger the effect. lastAttemptRef
|
||||
// is bumped on every attempt — including failures — so that a 404 cannot
|
||||
// compound into a re-render loop with the gate permanently open.
|
||||
const lastAttemptRef = useRef<number>(Date.now());
|
||||
const isFirstLoadRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
// The SWR fallback for auth type defaults to BASIC; bail until the real
|
||||
// value loads, otherwise SAML/OIDC users will hit /api/auth/refresh
|
||||
// (which is not registered for those auth types) and 404.
|
||||
if (authTypeMetadataLoading) return;
|
||||
|
||||
if (
|
||||
!user ||
|
||||
user.id === NO_AUTH_USER_ID ||
|
||||
// Anonymous users have no session to refresh; a failed refresh churns the
|
||||
// /me query and can bounce them to login.
|
||||
user.is_anonymous_user ||
|
||||
authTypeMetadata.authType === AuthType.OIDC ||
|
||||
authTypeMetadata.authType === AuthType.SAML
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshTokenPeriodically = async () => {
|
||||
const isTimeToRefresh =
|
||||
isFirstLoadRef.current ||
|
||||
Date.now() - lastAttemptRef.current > MIN_REFRESH_GAP_MS;
|
||||
|
||||
if (!isTimeToRefresh) return;
|
||||
|
||||
isFirstLoadRef.current = false;
|
||||
lastAttemptRef.current = Date.now();
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/refresh", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.debug("Auth token refreshed successfully");
|
||||
} else {
|
||||
console.warn("Failed to refresh auth token:", response.status);
|
||||
await onRefreshFail();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error refreshing auth token:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Hidden tabs skip refreshes entirely; the visibilitychange handler below
|
||||
// catches the session up as soon as the user returns.
|
||||
if (!document.hidden) {
|
||||
refreshTokenPeriodically();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
if (document.hidden) return;
|
||||
refreshTokenPeriodically();
|
||||
}, REFRESH_INTERVAL);
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (
|
||||
document.visibilityState === "visible" &&
|
||||
Date.now() - lastAttemptRef.current > VISIBILITY_REFRESH_GAP_MS
|
||||
) {
|
||||
refreshTokenPeriodically();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [user, authTypeMetadata, authTypeMetadataLoading, onRefreshFail]);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useLayoutEffect } from "react";
|
||||
import AdminSidebar from "@/sections/sidebar/AdminSidebar";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useSettings } from "@/lib/settings/hooks";
|
||||
import { useAdminDocumentTitle } from "@/lib/app/hooks";
|
||||
import { ApplicationStatus } from "@/lib/settings/types";
|
||||
import { Button, Text } from "@opal/components";
|
||||
import { markdown } from "@opal/utils";
|
||||
@@ -22,12 +22,8 @@ export default function AdminChrome({ children }: AdminChromeProps) {
|
||||
const { setFolded } = useSidebarState();
|
||||
const { isMobile } = useScreenSize();
|
||||
const pathname = usePathname();
|
||||
const { appName, vectorDbEnabled, isLoading, application_status } =
|
||||
useSettings();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.title = `Admin — ${appName}`;
|
||||
}, [pathname, appName]);
|
||||
const { vectorDbEnabled, isLoading, application_status } = useSettings();
|
||||
useAdminDocumentTitle();
|
||||
|
||||
// Certain admin panels have their own custom sidebar.
|
||||
// For those pages, we skip rendering the default `AdminSidebar` and let those individual pages render their own.
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -67,7 +66,7 @@ import useAppFocus from "@/hooks/useAppFocus";
|
||||
import { useQueryController } from "@/providers/QueryControllerProvider";
|
||||
import { useTierAtLeast } from "@/hooks/useTierAtLeast";
|
||||
import { Tier } from "@/lib/settings/types";
|
||||
import { useCustomFooterContent } from "@/lib/app/hooks";
|
||||
import { useAppDocumentTitle, useCustomFooterContent } from "@/lib/app/hooks";
|
||||
import { useFullWidthChat } from "@/providers/FullWidthChatProvider";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -558,16 +557,7 @@ export default function AppChrome({ children }: AppChromeProps) {
|
||||
const [rightPanel, setRightPanel] = useState<ReactNode>(null);
|
||||
|
||||
const appFocus = useAppFocus();
|
||||
const { appName } = useSettings();
|
||||
const { currentChatSession } = useChatSessions();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const appendChatNameToDocumentTitle =
|
||||
(appFocus.isChat() || appFocus.isSharedChat()) && currentChatSession;
|
||||
document.title = appendChatNameToDocumentTitle
|
||||
? `${currentChatSession.name} — ${appName}`
|
||||
: appName;
|
||||
}, [currentChatSession?.name, appName, appFocus]);
|
||||
useAppDocumentTitle();
|
||||
|
||||
const { hasBackground, appBackgroundUrl } = useAppBackground();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useLayoutEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useSettings } from "@/lib/settings/hooks";
|
||||
import { APP_SLOGAN } from "@/lib/constants";
|
||||
import useAppFocus from "@/hooks/useAppFocus";
|
||||
import useChatSessions from "@/hooks/useChatSessions";
|
||||
|
||||
export function useCustomFooterContent(): string {
|
||||
const settings = useSettings();
|
||||
@@ -8,3 +14,24 @@ export function useCustomFooterContent(): string {
|
||||
`[Onyx ${settings.version ?? "dev"}](https://www.onyx.app/) - ${APP_SLOGAN}`
|
||||
);
|
||||
}
|
||||
|
||||
export function useAppDocumentTitle(): void {
|
||||
const appFocus = useAppFocus();
|
||||
const { appName } = useSettings();
|
||||
const { currentChatSession } = useChatSessions();
|
||||
useLayoutEffect(() => {
|
||||
const appendChatNameToDocumentTitle =
|
||||
(appFocus.isChat() || appFocus.isSharedChat()) && currentChatSession;
|
||||
document.title = appendChatNameToDocumentTitle
|
||||
? `${currentChatSession.name} — ${appName}`
|
||||
: appName;
|
||||
}, [currentChatSession?.name, appName, appFocus]);
|
||||
}
|
||||
|
||||
export function useAdminDocumentTitle(): void {
|
||||
const pathname = usePathname();
|
||||
const { appName } = useSettings();
|
||||
useLayoutEffect(() => {
|
||||
document.title = `Admin — ${appName}`;
|
||||
}, [pathname, appName]);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { setDocumentVisibility } from "@tests/setup/test-utils";
|
||||
import { useTokenRefresh } from "@/hooks/useTokenRefresh";
|
||||
import { AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
import { useTokenRefresh } from "@/lib/auth/hooks";
|
||||
import { AuthType, AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { User } from "@/lib/types";
|
||||
|
||||
const baseAuthMetadata = (authType: AuthType): AuthTypeMetadata => ({
|
||||
@@ -98,16 +97,12 @@ describe("useTokenRefresh", () => {
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
|
||||
|
||||
// Simulate the parent re-render storm that occurs when onRefreshFail
|
||||
// -> mutateUser produces a new user object identity.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await act(async () => {
|
||||
rerender({ user: { ...fakeUser } });
|
||||
});
|
||||
}
|
||||
|
||||
// The time-gate should suppress every follow-up attempt; the original
|
||||
// bug fired ~one extra call per re-render.
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -124,7 +119,6 @@ describe("useTokenRefresh", () => {
|
||||
)
|
||||
);
|
||||
|
||||
// No refresh on hidden mount, and none from interval ticks while hidden.
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(31 * 60 * 1000);
|
||||
});
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { NO_AUTH_USER_ID } from "@/lib/extension/constants";
|
||||
import { AuthType, AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import { User } from "@/lib/types";
|
||||
import { getSecondsUntilExpiration } from "@opal/time";
|
||||
import { logout } from "@/lib/users/svc";
|
||||
@@ -72,3 +77,151 @@ export function useSessionWatcher(): boolean {
|
||||
hasSeenAuthenticatedUserRef.current
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useAuthTypeMetadata
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface AuthTypeAPIResponse {
|
||||
auth_type: string;
|
||||
requires_verification: boolean;
|
||||
anonymous_user_enabled: boolean | null;
|
||||
password_min_length: number;
|
||||
has_users: boolean;
|
||||
oauth_enabled: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_AUTH_TYPE_METADATA: AuthTypeMetadata = {
|
||||
authType: NEXT_PUBLIC_CLOUD_ENABLED ? AuthType.CLOUD : AuthType.BASIC,
|
||||
autoRedirect: false,
|
||||
requiresVerification: false,
|
||||
anonymousUserEnabled: null,
|
||||
passwordMinLength: 0,
|
||||
hasUsers: false,
|
||||
oauthEnabled: false,
|
||||
};
|
||||
|
||||
async function fetchAuthTypeMetadata(url: string): Promise<AuthTypeMetadata> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error("Failed to fetch auth type metadata");
|
||||
const data: AuthTypeAPIResponse = await res.json();
|
||||
const authType = NEXT_PUBLIC_CLOUD_ENABLED
|
||||
? AuthType.CLOUD
|
||||
: (data.auth_type as AuthType);
|
||||
return {
|
||||
authType,
|
||||
autoRedirect: authType === AuthType.OIDC || authType === AuthType.SAML,
|
||||
requiresVerification: data.requires_verification,
|
||||
anonymousUserEnabled: data.anonymous_user_enabled,
|
||||
passwordMinLength: data.password_min_length,
|
||||
hasUsers: data.has_users,
|
||||
oauthEnabled: data.oauth_enabled,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAuthTypeMetadata(): {
|
||||
authTypeMetadata: AuthTypeMetadata;
|
||||
isLoading: boolean;
|
||||
error: Error | undefined;
|
||||
} {
|
||||
const { data, error, isLoading } = useSWR<AuthTypeMetadata>(
|
||||
SWR_KEYS.authType,
|
||||
fetchAuthTypeMetadata,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 30_000,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
authTypeMetadata: data ?? DEFAULT_AUTH_TYPE_METADATA,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useTokenRefresh
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const REFRESH_INTERVAL = 600000;
|
||||
const MIN_REFRESH_GAP_MS = REFRESH_INTERVAL - 60000;
|
||||
const VISIBILITY_REFRESH_GAP_MS = 60000;
|
||||
|
||||
export function useTokenRefresh(
|
||||
user: User | null,
|
||||
authTypeMetadata: AuthTypeMetadata,
|
||||
authTypeMetadataLoading: boolean,
|
||||
onRefreshFail: () => Promise<void>
|
||||
) {
|
||||
const lastAttemptRef = useRef<number>(Date.now());
|
||||
const isFirstLoadRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (authTypeMetadataLoading) return;
|
||||
|
||||
if (
|
||||
!user ||
|
||||
user.id === NO_AUTH_USER_ID ||
|
||||
user.is_anonymous_user ||
|
||||
authTypeMetadata.authType === AuthType.OIDC ||
|
||||
authTypeMetadata.authType === AuthType.SAML
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshTokenPeriodically = async () => {
|
||||
const isTimeToRefresh =
|
||||
isFirstLoadRef.current ||
|
||||
Date.now() - lastAttemptRef.current > MIN_REFRESH_GAP_MS;
|
||||
|
||||
if (!isTimeToRefresh) return;
|
||||
|
||||
isFirstLoadRef.current = false;
|
||||
lastAttemptRef.current = Date.now();
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/refresh", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.debug("Auth token refreshed successfully");
|
||||
} else {
|
||||
console.warn("Failed to refresh auth token:", response.status);
|
||||
await onRefreshFail();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error refreshing auth token:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!document.hidden) {
|
||||
refreshTokenPeriodically();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
if (document.hidden) return;
|
||||
refreshTokenPeriodically();
|
||||
}, REFRESH_INTERVAL);
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (
|
||||
document.visibilityState === "visible" &&
|
||||
Date.now() - lastAttemptRef.current > VISIBILITY_REFRESH_GAP_MS
|
||||
) {
|
||||
refreshTokenPeriodically();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [user, authTypeMetadata, authTypeMetadataLoading, onRefreshFail]);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { buildUrl, UrlBuilder } from "@/lib/utilsSS";
|
||||
import { AuthType, NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
|
||||
import { NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
|
||||
import { User, UserRole } from "@/lib/types";
|
||||
import { getCurrentUserSS } from "@/lib/users/svcSS";
|
||||
import { AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import type { SSOProviderType } from "@/lib/auth/types";
|
||||
import {
|
||||
AuthType,
|
||||
AuthTypeMetadata,
|
||||
type SSOProviderType,
|
||||
} from "@/lib/auth/types";
|
||||
|
||||
export async function getAuthTypeMetadataSS(): Promise<AuthTypeMetadata> {
|
||||
const res = await fetch(buildUrl("/auth/type"));
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { AuthType } from "@/lib/constants";
|
||||
export enum AuthType {
|
||||
BASIC = "basic",
|
||||
GOOGLE_OAUTH = "google_oauth",
|
||||
OIDC = "oidc",
|
||||
SAML = "saml",
|
||||
CLOUD = "cloud",
|
||||
}
|
||||
|
||||
export type SSOProviderType = "GOOGLE_OAUTH" | "OIDC" | "SAML";
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
export const IS_DEV = process.env.NODE_ENV === "development";
|
||||
import { AuthType } from "@/lib/auth/types";
|
||||
|
||||
export enum AuthType {
|
||||
BASIC = "basic",
|
||||
GOOGLE_OAUTH = "google_oauth",
|
||||
OIDC = "oidc",
|
||||
SAML = "saml",
|
||||
CLOUD = "cloud",
|
||||
}
|
||||
export const IS_DEV = process.env.NODE_ENV === "development";
|
||||
|
||||
export const HOST_URL = process.env.WEB_DOMAIN || "http://localhost:3000";
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
sendAuthRequiredMessage,
|
||||
sendMessageToParent,
|
||||
} from "@/lib/extension/svc";
|
||||
|
||||
export function useSendAuthRequiredMessage(): void {
|
||||
useEffect(() => {
|
||||
sendAuthRequiredMessage();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useSendMessageToParent(): void {
|
||||
useEffect(() => {
|
||||
sendMessageToParent();
|
||||
}, []);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { CHROME_MESSAGE } from "@/lib/extension/constants";
|
||||
import { getPanelOrigin } from "@/lib/extension/utils";
|
||||
|
||||
export function sendSetDefaultNewTabMessage(value: boolean): void {
|
||||
if (typeof window !== "undefined" && window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{ type: CHROME_MESSAGE.SET_DEFAULT_NEW_TAB, value },
|
||||
getPanelOrigin()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function sendAuthRequiredMessage(): void {
|
||||
if (typeof window !== "undefined" && window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{ type: CHROME_MESSAGE.AUTH_REQUIRED },
|
||||
getPanelOrigin()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function sendMessageToParent(): void {
|
||||
if (typeof window !== "undefined" && window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{ type: CHROME_MESSAGE.ONYX_APP_LOADED },
|
||||
getPanelOrigin()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useEffect } from "react";
|
||||
import { CHROME_MESSAGE } from "./constants";
|
||||
|
||||
export type ExtensionContext = "new_tab" | "side_panel" | null;
|
||||
|
||||
// Returns the origin of the Chrome extension panel (our parent frame).
|
||||
@@ -27,40 +24,3 @@ export function getExtensionContext(): {
|
||||
}
|
||||
return { isExtension: false, context: null };
|
||||
}
|
||||
export function sendSetDefaultNewTabMessage(value: boolean) {
|
||||
if (typeof window !== "undefined" && window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{ type: CHROME_MESSAGE.SET_DEFAULT_NEW_TAB, value },
|
||||
getPanelOrigin()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const sendAuthRequiredMessage = () => {
|
||||
if (typeof window !== "undefined" && window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{ type: CHROME_MESSAGE.AUTH_REQUIRED },
|
||||
getPanelOrigin()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const useSendAuthRequiredMessage = () => {
|
||||
useEffect(() => {
|
||||
sendAuthRequiredMessage();
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const sendMessageToParent = () => {
|
||||
if (typeof window !== "undefined" && window.parent !== window) {
|
||||
window.parent.postMessage(
|
||||
{ type: CHROME_MESSAGE.ONYX_APP_LOADED },
|
||||
getPanelOrigin()
|
||||
);
|
||||
}
|
||||
};
|
||||
export const useSendMessageToParent = () => {
|
||||
useEffect(() => {
|
||||
sendMessageToParent();
|
||||
}, []);
|
||||
};
|
||||
@@ -30,16 +30,13 @@ import {
|
||||
} from "@/lib/languageModels/types";
|
||||
import { isAnthropic } from "@/lib/languageModels/svc";
|
||||
import { getSourceMetadataForSources } from "./sources";
|
||||
import {
|
||||
AuthType,
|
||||
DEFAULT_AGENT_ID,
|
||||
NEXT_PUBLIC_CLOUD_ENABLED,
|
||||
} from "./constants";
|
||||
import { DEFAULT_AGENT_ID, NEXT_PUBLIC_CLOUD_ENABLED } from "./constants";
|
||||
import { AuthType } from "@/lib/auth/types";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { SEARCH_TOOL_ID } from "@/app/app/components/tools/constants";
|
||||
import { updateTemperatureOverrideForChatSession } from "@/app/app/services/lib";
|
||||
import { useLLMProviders } from "@/lib/languageModels/hooks";
|
||||
import { useAuthTypeMetadata } from "@/hooks/useAuthTypeMetadata";
|
||||
import { useAuthTypeMetadata } from "@/lib/auth/hooks";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
export const usePublicCredentials = () => {
|
||||
|
||||
@@ -17,9 +17,8 @@ import {
|
||||
} from "@/lib/types";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useSettings } from "@/lib/settings/hooks";
|
||||
import { useTokenRefresh } from "@/hooks/useTokenRefresh";
|
||||
import { useCurrentUser } from "@/lib/users/hooks";
|
||||
import { useAuthTypeMetadata } from "@/hooks/useAuthTypeMetadata";
|
||||
import { useAuthTypeMetadata, useTokenRefresh } from "@/lib/auth/hooks";
|
||||
import { AuthTypeMetadata } from "@/lib/auth/types";
|
||||
import {
|
||||
updateUserPersonalization as persistPersonalization,
|
||||
|
||||
+1
-1
@@ -1,11 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
AuthType,
|
||||
SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED,
|
||||
SERVER_SIDE_ONLY__AUTH_TYPE,
|
||||
SERVER_SIDE_ONLY__AUTH_COOKIE_NAME,
|
||||
} from "./lib/constants";
|
||||
import { AuthType } from "@/lib/auth/types";
|
||||
|
||||
// Route prefixes that never allow anonymous access, so we fast-fail at the edge
|
||||
// when no auth cookie is present. "/app" is intentionally excluded: it allows
|
||||
|
||||
@@ -37,7 +37,7 @@ import { updateMCPServerStatus, upsertMCPServer } from "@/lib/tools/mcpService";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { SvgArrowExchange } from "@opal/icons";
|
||||
import { useAuthType } from "@/lib/hooks";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
import { AuthType } from "@/lib/auth/types";
|
||||
|
||||
interface MCPAuthenticationModalProps {
|
||||
mcpServer: MCPServer | null;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { OAuthConfig } from "@/lib/tools/interfaces";
|
||||
import { getOAuthConfig } from "@/lib/oauth/api";
|
||||
import { SvgArrowExchange } from "@opal/icons";
|
||||
import { useAuthType } from "@/lib/hooks";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
import { AuthType } from "@/lib/auth/types";
|
||||
|
||||
export type AuthMethod = "oauth" | "custom-header" | "pt-oauth";
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useCurrentUser } from "@/lib/users/hooks";
|
||||
import NoAgentModal from "@/sections/modals/NoAgentModal";
|
||||
import PreviewModal from "@/sections/modals/PreviewModal";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { useSendMessageToParent } from "@/lib/extension/utils";
|
||||
import { useSendMessageToParent } from "@/lib/extension/hooks";
|
||||
import { SUBMIT_MESSAGE_TYPES } from "@/lib/extension/constants";
|
||||
import { getSourceMetadata } from "@/lib/sources";
|
||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
|
||||
@@ -8,8 +8,8 @@ import IconButton from "@/refresh-components/buttons/IconButton";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import Link from "next/link";
|
||||
import { ADMIN_ROUTES } from "@/lib/admin-routes";
|
||||
import { useAuthTypeMetadata } from "@/hooks/useAuthTypeMetadata";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
import { useAuthTypeMetadata } from "@/lib/auth/hooks";
|
||||
import { AuthType } from "@/lib/auth/types";
|
||||
import InviteOnlyCard from "./InviteOnlyCard";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user