From c446460ba7993ba2aac5db6dd1f8678b7dc2a9f6 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 1 Apr 2025 11:56:51 -0400 Subject: [PATCH] Update ConnectionRequired to prevent wizard access when complete --- src/RootAppRouter.tsx | 2 +- src/apps/dashboard/routes/routes.tsx | 2 +- src/apps/experimental/routes/routes.tsx | 2 +- src/apps/stable/routes/routes.tsx | 2 +- src/apps/wizard/routes/routes.tsx | 2 +- src/components/ConnectionRequired.tsx | 78 +++++++++++++++++++------ 6 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/RootAppRouter.tsx b/src/RootAppRouter.tsx index b6831185a..837c2f38b 100644 --- a/src/RootAppRouter.tsx +++ b/src/RootAppRouter.tsx @@ -10,12 +10,12 @@ import { import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes'; import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes'; import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes'; +import { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes'; import AppHeader from 'components/AppHeader'; import Backdrop from 'components/Backdrop'; import BangRedirect from 'components/router/BangRedirect'; import { createRouterHistory } from 'components/router/routerHistory'; import UserThemeProvider from 'themes/UserThemeProvider'; -import { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes'; const layoutMode = localStorage.getItem('layout'); const isExperimentalLayout = layoutMode === 'experimental'; diff --git a/src/apps/dashboard/routes/routes.tsx b/src/apps/dashboard/routes/routes.tsx index 576bdd620..035bcc352 100644 --- a/src/apps/dashboard/routes/routes.tsx +++ b/src/apps/dashboard/routes/routes.tsx @@ -17,7 +17,7 @@ export const DASHBOARD_APP_PATHS = { export const DASHBOARD_APP_ROUTES: RouteObject[] = [ { - element: , + element: , children: [ { lazy: () => import('../AppLayout'), diff --git a/src/apps/experimental/routes/routes.tsx b/src/apps/experimental/routes/routes.tsx index 533972b89..f783acb49 100644 --- a/src/apps/experimental/routes/routes.tsx +++ b/src/apps/experimental/routes/routes.tsx @@ -36,7 +36,7 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [ { /* Public routes */ - element: , + element: , children: [ ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute), diff --git a/src/apps/stable/routes/routes.tsx b/src/apps/stable/routes/routes.tsx index 230a41953..bcd328ace 100644 --- a/src/apps/stable/routes/routes.tsx +++ b/src/apps/stable/routes/routes.tsx @@ -31,7 +31,7 @@ export const STABLE_APP_ROUTES: RouteObject[] = [ { /* Public routes */ - element: , + element: , children: [ ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute), /* Fallback route for invalid paths */ diff --git a/src/apps/wizard/routes/routes.tsx b/src/apps/wizard/routes/routes.tsx index 81f812cc9..c328d7aae 100644 --- a/src/apps/wizard/routes/routes.tsx +++ b/src/apps/wizard/routes/routes.tsx @@ -60,7 +60,7 @@ const ROUTES: LegacyRoute[] = [ export const WIZARD_APP_ROUTES: RouteObject[] = [ { - element: , + element: , children: [ { Component: AppLayout, diff --git a/src/components/ConnectionRequired.tsx b/src/components/ConnectionRequired.tsx index 8106135e6..443a5e1a0 100644 --- a/src/components/ConnectionRequired.tsx +++ b/src/components/ConnectionRequired.tsx @@ -1,23 +1,49 @@ import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; -import type { ConnectResponse } from 'jellyfin-apiclient'; +import type { ApiClient, ConnectResponse } from 'jellyfin-apiclient'; + +import globalize from 'lib/globalize'; +import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState'; import alert from './alert'; import Loading from './loading/LoadingComponent'; import ServerConnections from './ServerConnections'; -import globalize from '../lib/globalize'; -import { ConnectionState } from '../utils/jellyfin-apiclient/ConnectionState'; + +enum AccessLevel { + /** Requires a user with administrator access */ + Admin = 'admin', + /** No access restrictions */ + Public = 'public', + /** Requires a valid user session */ + User = 'user', + /** Requires the startup wizard to NOT be completed */ + Wizard = 'wizard' +}; + +type AccessLevelValue = `${AccessLevel}`; enum BounceRoutes { Home = '/home', Login = '/login', SelectServer = '/selectserver', - StartWizard = '/wizardstart' + StartWizard = '/wizard/start' } type ConnectionRequiredProps = { - isAdminRequired?: boolean, - isUserRequired?: boolean + level?: AccessLevelValue +}; + +const fetchPublicSystemInfo = async (apiClient: ApiClient) => { + const infoResponse = await fetch( + `${apiClient.serverAddress()}/System/Info/Public`, + { cache: 'no-cache' } + ); + + if (!infoResponse.ok) { + throw new Error('Public system info request failed'); + } + + return infoResponse.json(); }; /** @@ -26,8 +52,7 @@ type ConnectionRequiredProps = { * If a condition fails, this component will navigate to the appropriate page. */ const ConnectionRequired: FunctionComponent = ({ - isAdminRequired = false, - isUserRequired = true + level = 'user' }) => { const navigate = useNavigate(); const location = useLocation(); @@ -76,17 +101,27 @@ const ConnectionRequired: FunctionComponent = ({ } console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname, navigate]); + const handleWizard = useCallback(async (firstConnection: ConnectResponse | null) => { + const apiClient = firstConnection?.ApiClient || ServerConnections.currentApiClient(); + if (!apiClient) { + throw new Error('No ApiClient available'); + } + + const systemInfo = await fetchPublicSystemInfo(apiClient); + if (systemInfo?.StartupWizardCompleted) { + console.info('[ConnectionRequired] startup wizard is complete, redirecting home'); + navigate(BounceRoutes.Home); + } + }, [ navigate ]); + const handleIncompleteWizard = useCallback(async (firstConnection: ConnectResponse) => { if (firstConnection.State === ConnectionState.ServerSignIn) { // Verify the wizard is complete try { - const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`, { cache: 'no-cache' }); - if (!infoResponse.ok) { - throw new Error('Public system info request failed'); - } - const systemInfo = await infoResponse.json(); + const systemInfo = await fetchPublicSystemInfo(firstConnection.ApiClient); if (!systemInfo?.StartupWizardCompleted) { // Update the current ApiClient // TODO: Is there a better place to handle this? @@ -113,7 +148,7 @@ const ConnectionRequired: FunctionComponent = ({ const client = ServerConnections.currentApiClient(); // If this is a user route, ensure a user is logged in - if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) { + if ((level === AccessLevel.Admin || level === AccessLevel.User) && !client?.isLoggedIn()) { try { console.warn('[ConnectionRequired] unauthenticated user attempted to access user route'); bounce(await ServerConnections.connect()) @@ -127,7 +162,7 @@ const ConnectionRequired: FunctionComponent = ({ } // If this is an admin route, ensure the user has access - if (isAdminRequired) { + if (level === AccessLevel.Admin) { try { const user = await client?.getCurrentUser(); if (!user?.Policy?.IsAdministrator) { @@ -145,7 +180,7 @@ const ConnectionRequired: FunctionComponent = ({ } setIsLoading(false); - }, [bounce, isAdminRequired, isUserRequired]); + }, [bounce, level]); useEffect(() => { // Check connection status on initial page load @@ -155,7 +190,14 @@ const ConnectionRequired: FunctionComponent = ({ console.debug('[ConnectionRequired] connection state', firstConnection?.State); ServerConnections.firstConnection = true; - if (firstConnection && firstConnection.State !== ConnectionState.SignedIn && !apiClient?.isLoggedIn()) { + if (level === AccessLevel.Wizard) { + handleWizard(firstConnection) + .catch(err => { + console.error('[ConnectionRequired] could not validate wizard status', err); + }); + } else if ( + firstConnection && firstConnection.State !== ConnectionState.SignedIn && !apiClient?.isLoggedIn() + ) { handleIncompleteWizard(firstConnection) .catch(err => { console.error('[ConnectionRequired] could not start wizard', err); @@ -169,7 +211,7 @@ const ConnectionRequired: FunctionComponent = ({ }).catch(err => { console.error('[ConnectionRequired] failed to connect', err); }); - }, [handleIncompleteWizard, validateUserAccess]); + }, [handleIncompleteWizard, handleWizard, level, validateUserAccess]); if (isLoading) { return ;