From 1d5648d02a0340bff95f14d2973c64bca17dd766 Mon Sep 17 00:00:00 2001 From: DoubleThePsycho <27337177+DoubleThePsycho@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:32:58 +0200 Subject: [PATCH 1/6] feat: add runtime base path support --- next.config.ts | 27 ++++ package.json | 2 +- public/sw.js | 47 ++++-- server/index.ts | 42 ++++-- server/lib/settings/index.ts | 2 + server/middleware/basePath.ts | 24 ++++ server/routes/index.ts | 8 +- server/utils/basePath.ts | 38 +++++ server/utils/restartFlag.ts | 3 +- src/components/Common/BaseLink/index.tsx | 41 ++++++ src/components/Common/CachedImage/index.tsx | 5 + src/components/Layout/Sidebar/index.tsx | 9 +- src/components/Login/index.tsx | 5 +- src/components/PWAHeader/index.tsx | 62 ++++---- .../ResetPassword/RequestResetLink.tsx | 15 +- src/components/ResetPassword/index.tsx | 15 +- src/components/ServiceWorkerSetup/index.tsx | 3 +- .../Settings/SettingsBasePath/index.tsx | 135 ++++++++++++++++++ src/components/Setup/index.tsx | 3 +- src/context/UserContext.tsx | 3 +- src/i18n/locale/en.json | 8 ++ src/pages/_app.tsx | 15 +- src/pages/collection/[collectionId]/index.tsx | 14 +- src/pages/movie/[movieId]/index.tsx | 14 +- src/pages/settings/network.tsx | 2 + src/pages/tv/[tvId]/index.tsx | 14 +- src/utils/basePath.ts | 62 ++++++++ src/utils/plex.ts | 3 +- src/utils/router.ts | 107 ++++++++++++++ 29 files changed, 624 insertions(+), 104 deletions(-) create mode 100644 server/middleware/basePath.ts create mode 100644 server/utils/basePath.ts create mode 100644 src/components/Common/BaseLink/index.tsx create mode 100644 src/components/Settings/SettingsBasePath/index.tsx create mode 100644 src/utils/basePath.ts create mode 100644 src/utils/router.ts diff --git a/next.config.ts b/next.config.ts index 25c023ac2e..74e10386c1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,10 +1,12 @@ import type { NextConfig } from 'next'; +import path from 'path'; const nextConfig: NextConfig = { env: { commitTag: process.env.COMMIT_TAG || 'local', }, images: { + unoptimized: true, remotePatterns: [ { hostname: 'gravatar.com' }, { hostname: 'image.tmdb.org' }, @@ -14,6 +16,10 @@ const nextConfig: NextConfig = { }, transpilePackages: ['country-flag-icons'], turbopack: { + resolveAlias: { + 'next/link': './src/components/Common/BaseLink/index.tsx', + 'next/router': './src/utils/router.ts', + }, rules: { '*.svg': { loaders: ['@svgr/webpack'], @@ -21,6 +27,27 @@ const nextConfig: NextConfig = { }, }, }, + webpack: (config) => { + const svgRule = config.module.rules.find((rule: { test?: RegExp }) => + rule?.test?.test?.('.svg') + ); + + if (svgRule) { + svgRule.exclude = /\.svg$/i; + } + + config.module.rules.push({ + test: /\.svg$/i, + use: ['@svgr/webpack'], + }); + + config.resolve.alias['next/link'] = path.resolve( + './src/components/Common/BaseLink/index.tsx' + ); + config.resolve.alias['next/router'] = path.resolve('./src/utils/router.ts'); + + return config; + }, experimental: { scrollRestoration: true, largePageDataBytes: 512 * 1000, diff --git a/package.json b/package.json index 8a5e3dcc11..a6c47edfdf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "postinstall": "next telemetry disable", "dev": "nodemon -e ts,json,yml --watch server --watch seerr-api.yml --exec 'ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts'", "build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && copyfiles -u 2 \"server/i18n/locale/*.json\" dist/i18n && tsc-alias -p server/tsconfig.json", - "build:next": "next build", + "build:next": "next build --webpack", "build": "pnpm build:next && pnpm build:server", "lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache", "lintfix": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --fix", diff --git a/public/sw.js b/public/sw.js index 459985d632..c14e9a872e 100644 --- a/public/sw.js +++ b/public/sw.js @@ -3,10 +3,24 @@ // previously cached resources to be updated from the network. // This variable is intentionally declared and unused. // eslint-disable-next-line @typescript-eslint/no-unused-vars -const OFFLINE_VERSION = 5; +const OFFLINE_VERSION = 6; const CACHE_NAME = 'offline'; +const BASE_PATH = new URL(self.registration.scope).pathname.replace(/\/$/, ''); +const withBasePath = (path) => { + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + + if ( + !BASE_PATH || + normalizedPath === BASE_PATH || + normalizedPath.startsWith(`${BASE_PATH}/`) + ) { + return normalizedPath; + } + + return `${BASE_PATH}${normalizedPath}`; +}; // Customize this with a different URL if needed. -const OFFLINE_URL = '/offline.html'; +const OFFLINE_URL = withBasePath('/offline.html'); self.addEventListener('install', (event) => { event.waitUntil( @@ -75,8 +89,10 @@ self.addEventListener('push', (event) => { const options = { body: payload.message, - badge: 'badge-128x128.png', - icon: payload.image ? payload.image : 'android-chrome-192x192.png', + badge: withBasePath('/badge-128x128.png'), + icon: payload.image + ? payload.image + : withBasePath('/android-chrome-192x192.png'), vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), @@ -137,17 +153,26 @@ self.addEventListener( event.notification.close(); if (event.action === 'approve') { - fetch(`/api/v1/request/${notificationData.requestId}/approve`, { - method: 'POST', - }); + fetch( + withBasePath(`/api/v1/request/${notificationData.requestId}/approve`), + { + method: 'POST', + } + ); } else if (event.action === 'decline') { - fetch(`/api/v1/request/${notificationData.requestId}/decline`, { - method: 'POST', - }); + fetch( + withBasePath(`/api/v1/request/${notificationData.requestId}/decline`), + { + method: 'POST', + } + ); } if (notificationData.actionUrl) { - clients.openWindow(notificationData.actionUrl); + const actionUrl = notificationData.actionUrl.startsWith('/') + ? withBasePath(notificationData.actionUrl) + : notificationData.actionUrl; + clients.openWindow(actionUrl); } }, false diff --git a/server/index.ts b/server/index.ts index db5b13843d..b7d6e97641 100644 --- a/server/index.ts +++ b/server/index.ts @@ -26,6 +26,7 @@ import avatarproxy from '@server/routes/avatarproxy'; import imageproxy from '@server/routes/imageproxy'; import { appDataPermissions } from '@server/utils/appDataVolume'; import { getAppVersion } from '@server/utils/appVersion'; +import { getRuntimeBasePath } from '@server/utils/basePath'; import createCustomProxyAgent, { setForceIpv4First, } from '@server/utils/customProxyAgent'; @@ -46,8 +47,17 @@ import path from 'path'; import swaggerUi from 'swagger-ui-express'; const API_SPEC_PATH = path.join(__dirname, '../seerr-api.yml'); +const basePath = getRuntimeBasePath(); +const cookiePath = basePath || '/'; + +// Expose the resolved value to server-rendered application code without +// compiling the user-facing setting into the browser bundle. +process.env.SEERR_RUNTIME_BASE_PATH = basePath; logger.info(`Starting Seerr version ${getAppVersion()}`); +if (basePath) { + logger.info(`Using base path ${basePath}`, { label: 'Server' }); +} const dev = process.env.NODE_ENV !== 'production'; const app = next({ dev }); const handle = app.getRequestHandler(); @@ -61,6 +71,11 @@ if (!appDataPermissions()) { app .prepare() .then(async () => { + // Next.js basePath is build-time only, but assetPrefix can be changed after + // the production build has been loaded. Express strips the runtime prefix + // before handing requests to Next.js below. + app.setAssetPrefix(basePath); + // Run Overseerr to Seerr migration await checkOverseerrMerge(); @@ -197,7 +212,7 @@ app sameSite: true, secure: !dev, key: '_csrf', - path: '/', + path: cookiePath, }, }) ); @@ -205,14 +220,18 @@ app res.cookie('XSRF-TOKEN', req.csrfToken(), { sameSite: true, secure: !dev, + path: cookiePath, }); next(); }); } + // Mount Seerr-owned HTTP routes below the configured application base path. + const appRouter = express.Router(); + // Set up sessions const sessionRespository = getRepository(Session); - server.use( + appRouter.use( '/api', session({ secret: settings.sessionSecret, @@ -223,6 +242,7 @@ app httpOnly: true, sameSite: settings.network.csrfProtection ? 'strict' : 'lax', secure: 'auto', + path: cookiePath, }, store: new TypeormStore({ cleanupLimit: 2, @@ -232,8 +252,8 @@ app ); const apiSpecContent = await fs.readFile(API_SPEC_PATH, 'utf-8'); const apiDocs = yaml.load(apiSpecContent) as Record; - server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs)); - server.use( + appRouter.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs)); + appRouter.use( OpenApiValidator.middleware({ apiSpec: API_SPEC_PATH, validateRequests: true, @@ -244,20 +264,24 @@ app * OpenAPI validator. Otherwise, they are treated as objects instead of strings * and response validation will fail */ - server.use((_req, res, next) => { + appRouter.use((_req, res, next) => { const original = res.json; res.json = function jsonp(json) { return original.call(this, JSON.parse(JSON.stringify(json))); }; next(); }); - server.use('/api/v1', routes); + appRouter.use('/api/v1', routes); // Do not set cookies so CDNs can cache them - server.use('/imageproxy', clearCookies, imageproxy); - server.use('/avatarproxy', clearCookies, avatarproxy); + appRouter.use('/imageproxy', clearCookies, imageproxy); + appRouter.use('/avatarproxy', clearCookies, avatarproxy); + + server.use(basePath || '/', appRouter); - server.get('*path', (req, res) => handle(req, res)); + // Express removes the mount path from req.url while this middleware runs, + // allowing one root-built Next.js bundle to serve any configured URL base. + server.use(basePath || '/', (req, res) => handle(req, res)); server.use( ( err: { status: number; message: string; errors: string[] }, diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 0a896524eb..ab11b9de46 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -177,6 +177,7 @@ export interface DnsCacheSettings { } export interface NetworkSettings { + basePath: string; csrfProtection: boolean; forceIpv4First: boolean; trustProxy: boolean; @@ -611,6 +612,7 @@ class Settings { }, }, network: { + basePath: '', csrfProtection: false, forceIpv4First: false, trustProxy: false, diff --git a/server/middleware/basePath.ts b/server/middleware/basePath.ts new file mode 100644 index 0000000000..35c2b57fdd --- /dev/null +++ b/server/middleware/basePath.ts @@ -0,0 +1,24 @@ +import { isValidBasePath, normalizeBasePath } from '@server/utils/basePath'; +import type { RequestHandler } from 'express'; + +const validateNetworkBasePath: RequestHandler = (req, _res, next) => { + if ( + req.method !== 'POST' || + req.path !== '/network' || + !Object.prototype.hasOwnProperty.call(req.body ?? {}, 'basePath') + ) { + return next(); + } + + if (!isValidBasePath(req.body.basePath)) { + return next({ + status: 400, + message: 'Invalid URL base path.', + }); + } + + req.body.basePath = normalizeBasePath(req.body.basePath); + return next(); +}; + +export default validateNetworkBasePath; diff --git a/server/routes/index.ts b/server/routes/index.ts index 270104ecf7..34a3abbbdf 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -12,6 +12,7 @@ import { Permission } from '@server/lib/permissions'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { checkUser, isAuthenticated } from '@server/middleware/auth'; +import validateNetworkBasePath from '@server/middleware/basePath'; import deprecatedRoute from '@server/middleware/deprecation'; import { mapProductionCompany } from '@server/models/Movie'; import { mapNetwork } from '@server/models/Tv'; @@ -153,7 +154,12 @@ router.get( } } ); -router.use('/settings', isAuthenticated(Permission.ADMIN), settingsRoutes); +router.use( + '/settings', + isAuthenticated(Permission.ADMIN), + validateNetworkBasePath, + settingsRoutes +); router.use('/search', isAuthenticated(), searchRoutes); router.use('/discover', isAuthenticated(), discoverRoutes); router.use('/request', isAuthenticated(), requestRoutes); diff --git a/server/utils/basePath.ts b/server/utils/basePath.ts new file mode 100644 index 0000000000..5868d21ef6 --- /dev/null +++ b/server/utils/basePath.ts @@ -0,0 +1,38 @@ +import { appDataPath } from '@server/utils/appDataVolume'; +import fs from 'fs'; +import path from 'path'; + +const SETTINGS_PATH = path.join(appDataPath(), 'settings.json'); +const BASE_PATH_PATTERN = /^(?:|\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*)$/; + +export const normalizeBasePath = (value?: string): string => { + const normalized = value?.trim().replace(/^\/+|\/+$/g, '') ?? ''; + + return normalized ? `/${normalized}` : ''; +}; + +export const isValidBasePath = (value: unknown): value is string => + typeof value === 'string' && BASE_PATH_PATTERN.test(value); + +const getStoredBasePath = (): string => { + try { + const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8')) as { + network?: { basePath?: string }; + }; + + return normalizeBasePath(settings.network?.basePath); + } catch { + return ''; + } +}; + +export const getRuntimeBasePath = (): string => { + const hasEnvironmentOverride = Object.prototype.hasOwnProperty.call( + process.env, + 'SEERR_BASE_PATH' + ); + + return normalizeBasePath( + hasEnvironmentOverride ? process.env.SEERR_BASE_PATH : getStoredBasePath() + ); +}; diff --git a/server/utils/restartFlag.ts b/server/utils/restartFlag.ts index d0a492ba37..80e73996f4 100644 --- a/server/utils/restartFlag.ts +++ b/server/utils/restartFlag.ts @@ -17,7 +17,8 @@ class RestartFlag { return ( this.networkSettings.csrfProtection !== networkSettings.csrfProtection || this.networkSettings.trustProxy !== networkSettings.trustProxy || - this.networkSettings.proxy.enabled !== networkSettings.proxy.enabled + this.networkSettings.proxy.enabled !== networkSettings.proxy.enabled || + this.networkSettings.basePath !== networkSettings.basePath ); } } diff --git a/src/components/Common/BaseLink/index.tsx b/src/components/Common/BaseLink/index.tsx new file mode 100644 index 0000000000..42aa8ad302 --- /dev/null +++ b/src/components/Common/BaseLink/index.tsx @@ -0,0 +1,41 @@ +import { withBasePath } from '@app/utils/basePath'; +import { useRouter } from '@app/utils/router'; +import NextLink from 'next/dist/client/link'; +import { resolveHref } from 'next/dist/client/resolve-href'; +import React, { forwardRef } from 'react'; + +type BaseLinkProps = React.ComponentProps; +type LinkUrl = BaseLinkProps['href']; + +const resolveExternalAs = ( + router: ReturnType, + href: LinkUrl, + as?: LinkUrl +): string => { + if (as) { + return withBasePath(resolveHref(router, as)); + } + + const [resolvedHref, resolvedAs] = resolveHref(router, href, true); + return withBasePath(resolvedAs ?? resolvedHref); +}; + +const BaseLink = forwardRef( + ({ href, as, ...props }, ref) => { + const router = useRouter(); + + return ( + + ); + } +); + +BaseLink.displayName = 'BaseLink'; + +export default BaseLink; +export type { LinkProps } from 'next/dist/client/link'; diff --git a/src/components/Common/CachedImage/index.tsx b/src/components/Common/CachedImage/index.tsx index 8af2c722db..a214e4260d 100644 --- a/src/components/Common/CachedImage/index.tsx +++ b/src/components/Common/CachedImage/index.tsx @@ -1,4 +1,5 @@ import useSettings from '@app/hooks/useSettings'; +import { withBasePath } from '@app/utils/basePath'; import type { ImageLoader, ImageProps } from 'next/image'; import Image from 'next/image'; @@ -39,6 +40,10 @@ const CachedImage = ({ src, type, ...props }: CachedImageProps) => { return null; } + if (imageUrl.startsWith('/')) { + imageUrl = withBasePath(imageUrl); + } + return ; }; diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index 60b32291a6..bd63dadf90 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -2,6 +2,7 @@ import Badge from '@app/components/Common/Badge'; import VersionStatus from '@app/components/Layout/VersionStatus'; import useClickOutside from '@app/hooks/useClickOutside'; import { Permission, useUser } from '@app/hooks/useUser'; +import { withBasePath } from '@app/utils/basePath'; import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; import { @@ -192,7 +193,11 @@ const Sidebar = ({
- Logo + Logo
@@ -256,7 +261,7 @@ const Sidebar = ({ Logo { > {/* eslint-disable-next-line @next/next/no-img-element */} {settings.currentSettings.applicationTitle} @@ -164,7 +165,7 @@ const Login = () => {
- Logo + Logo
diff --git a/src/components/PWAHeader/index.tsx b/src/components/PWAHeader/index.tsx index 320ac34a62..4f4a68c355 100644 --- a/src/components/PWAHeader/index.tsx +++ b/src/components/PWAHeader/index.tsx @@ -1,3 +1,5 @@ +import { withBasePath } from '@app/utils/basePath'; + interface PWAHeaderProps { applicationTitle?: string; } @@ -8,148 +10,148 @@ const PWAHeader = ({ applicationTitle = 'Seerr' }: PWAHeaderProps) => { { /> diff --git a/src/components/ResetPassword/RequestResetLink.tsx b/src/components/ResetPassword/RequestResetLink.tsx index 9bebb8b065..f55197cb4e 100644 --- a/src/components/ResetPassword/RequestResetLink.tsx +++ b/src/components/ResetPassword/RequestResetLink.tsx @@ -2,6 +2,7 @@ import Button from '@app/components/Common/Button'; import ImageFader from '@app/components/Common/ImageFader'; import PageTitle from '@app/components/Common/PageTitle'; import LanguagePicker from '@app/components/Layout/LanguagePicker'; +import { withBasePath } from '@app/utils/basePath'; import defineMessages from '@app/utils/defineMessages'; import { ArrowLeftIcon, EnvelopeIcon } from '@heroicons/react/24/solid'; import axios from 'axios'; @@ -44,12 +45,12 @@ const ResetPassword = () => {
@@ -57,7 +58,7 @@ const ResetPassword = () => {
- Logo + Logo

{intl.formatMessage(messages.resetpassword)} diff --git a/src/components/ResetPassword/index.tsx b/src/components/ResetPassword/index.tsx index 57053e936c..42747b8620 100644 --- a/src/components/ResetPassword/index.tsx +++ b/src/components/ResetPassword/index.tsx @@ -3,6 +3,7 @@ import ImageFader from '@app/components/Common/ImageFader'; import SensitiveInput from '@app/components/Common/SensitiveInput'; import LanguagePicker from '@app/components/Layout/LanguagePicker'; import globalMessages from '@app/i18n/globalMessages'; +import { withBasePath } from '@app/utils/basePath'; import defineMessages from '@app/utils/defineMessages'; import { LifebuoyIcon } from '@heroicons/react/24/outline'; import axios from 'axios'; @@ -54,12 +55,12 @@ const ResetPassword = () => {
@@ -67,7 +68,7 @@ const ResetPassword = () => {
- Logo + Logo

{intl.formatMessage(messages.resetpassword)} diff --git a/src/components/ServiceWorkerSetup/index.tsx b/src/components/ServiceWorkerSetup/index.tsx index 929580a7c6..a7424c84fb 100644 --- a/src/components/ServiceWorkerSetup/index.tsx +++ b/src/components/ServiceWorkerSetup/index.tsx @@ -2,6 +2,7 @@ import useSettings from '@app/hooks/useSettings'; import { useUser } from '@app/hooks/useUser'; +import { withBasePath } from '@app/utils/basePath'; import { verifyAndResubscribePushSubscription } from '@app/utils/pushSubscriptionHelpers'; import { useEffect } from 'react'; @@ -12,7 +13,7 @@ const ServiceWorkerSetup = () => { useEffect(() => { if ('serviceWorker' in navigator && user?.id) { navigator.serviceWorker - .register('/sw.js') + .register(withBasePath('/sw.js'), { scope: withBasePath('/') }) .then(async (registration) => { console.log( '[SW] Registration successful, scope is:', diff --git a/src/components/Settings/SettingsBasePath/index.tsx b/src/components/Settings/SettingsBasePath/index.tsx new file mode 100644 index 0000000000..86d07d14cf --- /dev/null +++ b/src/components/Settings/SettingsBasePath/index.tsx @@ -0,0 +1,135 @@ +import Button from '@app/components/Common/Button'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import SettingsBadge from '@app/components/Settings/SettingsBadge'; +import useToasts from '@app/hooks/useToasts'; +import globalMessages from '@app/i18n/globalMessages'; +import { + basePath as activeBasePath, + normalizeBasePath, +} from '@app/utils/basePath'; +import defineMessages from '@app/utils/defineMessages'; +import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; +import type { NetworkSettings } from '@server/lib/settings'; +import axios from 'axios'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import useSWR, { mutate } from 'swr'; +import * as Yup from 'yup'; + +const messages = defineMessages('components.Settings.SettingsBasePath', { + heading: 'URL Base', + description: + 'Host Seerr below a URL path such as /seerr. A restart is required after changing this setting.', + field: 'URL Base', + fieldTip: + 'Leave blank to serve Seerr from /. SEERR_BASE_PATH overrides this setting when provided by the environment.', + active: 'Currently active: {basePath}', + invalid: + 'Use an empty value or a path beginning with / containing only letters, numbers, dots, underscores, tildes, and hyphens.', + toastSettingsSuccess: 'Settings saved successfully!', + toastSettingsFailure: 'Something went wrong while saving settings.', +}); + +const SettingsBasePath = () => { + const intl = useIntl(); + const { addToast } = useToasts(); + const { + data, + error, + mutate: revalidate, + } = useSWR('/api/v1/settings/network'); + + if (!data && !error) { + return ; + } + + const schema = Yup.object().shape({ + basePath: Yup.string().matches( + /^(?:|\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*)$/, + intl.formatMessage(messages.invalid) + ), + }); + + return ( +
+
+

{intl.formatMessage(messages.heading)}

+

+ {intl.formatMessage(messages.description)} +

+
+ { + try { + await axios.post('/api/v1/settings/network', { + basePath: normalizeBasePath(values.basePath), + }); + await revalidate(); + mutate('/api/v1/status'); + + addToast(intl.formatMessage(messages.toastSettingsSuccess), { + autoDismiss: true, + appearance: 'success', + }); + } catch { + addToast(intl.formatMessage(messages.toastSettingsFailure), { + autoDismiss: true, + appearance: 'error', + }); + } + }} + > + {({ errors, touched, isSubmitting, isValid }) => ( +
+
+ +
+
+ +
+ {errors.basePath && touched.basePath && ( +
{errors.basePath}
+ )} +
+
+
+
+ +
+
+
+ )} +
+
+ ); +}; + +export default SettingsBasePath; diff --git a/src/components/Setup/index.tsx b/src/components/Setup/index.tsx index dbf2f65fd8..7b7f627995 100644 --- a/src/components/Setup/index.tsx +++ b/src/components/Setup/index.tsx @@ -13,6 +13,7 @@ import SettingsServices from '@app/components/Settings/SettingsServices'; import SetupSteps from '@app/components/Setup/SetupSteps'; import useLocale from '@app/hooks/useLocale'; import useSettings from '@app/hooks/useSettings'; +import { withBasePath } from '@app/utils/basePath'; import defineMessages from '@app/utils/defineMessages'; import { MediaServerType } from '@server/constants/server'; import type { Library } from '@server/lib/settings'; @@ -130,7 +131,7 @@ const Setup = () => {

- Logo + Logo