diff --git a/cypress/e2e/settings/general-settings.cy.ts b/cypress/e2e/settings/general-settings.cy.ts index bcfce1a32..59649ed6b 100644 --- a/cypress/e2e/settings/general-settings.cy.ts +++ b/cypress/e2e/settings/general-settings.cy.ts @@ -13,10 +13,10 @@ describe('General Settings', () => { }); it('modifies setting that requires restart', () => { - cy.visit('/settings'); + cy.visit('/settings/network'); cy.get('#trustProxy').click(); - cy.get('[data-testid=settings-main-form]').submit(); + cy.get('[data-testid=settings-network-form]').submit(); cy.get('[data-testid=modal-title]').should( 'contain', 'Server Restart Required' @@ -26,7 +26,7 @@ describe('General Settings', () => { cy.get('[data-testid=modal-title]').should('not.exist'); cy.get('[type=checkbox]#trustProxy').click(); - cy.get('[data-testid=settings-main-form]').submit(); + cy.get('[data-testid=settings-network-form]').submit(); cy.get('[data-testid=modal-title]').should('not.exist'); }); }); diff --git a/overseerr-api.yml b/overseerr-api.yml index f3be28ad2..641ce5d7c 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -164,12 +164,6 @@ components: applicationUrl: type: string example: https://os.example.com - trustProxy: - type: boolean - example: true - csrfProtection: - type: boolean - example: false hideAvailable: type: boolean example: false @@ -191,12 +185,21 @@ components: enableSpecialEpisodes: type: boolean example: false + NetworkSettings: + type: object + properties: + csrfProtection: + type: boolean + example: false forceIpv4First: type: boolean example: false dnsServers: type: string example: '1.1.1.1' + trustProxy: + type: boolean + example: true PlexLibrary: type: object properties: @@ -2045,6 +2048,37 @@ paths: application/json: schema: $ref: '#/components/schemas/MainSettings' + /settings/network: + get: + summary: Get network settings + description: Retrieves all network settings in a JSON object. + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MainSettings' + post: + summary: Update network settings + description: Updates network settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkSettings' /settings/main/regenerate: post: summary: Get main settings with newly-generated API key diff --git a/server/index.ts b/server/index.ts index dde0d5083..e4e872ab6 100644 --- a/server/index.ts +++ b/server/index.ts @@ -72,23 +72,26 @@ app // Load Settings const settings = await getSettings().load(); - restartFlag.initializeSettings(settings.main); + restartFlag.initializeSettings(settings); // Check if we force IPv4 first - if (process.env.forceIpv4First === 'true' || settings.main.forceIpv4First) { + if ( + process.env.forceIpv4First === 'true' || + settings.network.forceIpv4First + ) { dns.setDefaultResultOrder('ipv4first'); net.setDefaultAutoSelectFamily(false); } - if (settings.main.dnsServers.trim() !== '') { + if (settings.network.dnsServers.trim() !== '') { dns.setServers( - settings.main.dnsServers.split(',').map((server) => server.trim()) + settings.network.dnsServers.split(',').map((server) => server.trim()) ); } // Register HTTP proxy - if (settings.main.proxy.enabled) { - await createCustomProxyAgent(settings.main.proxy); + if (settings.network.proxy.enabled) { + await createCustomProxyAgent(settings.network.proxy); } // Migrate library types @@ -143,7 +146,7 @@ app await DiscoverSlider.bootstrapSliders(); const server = express(); - if (settings.main.trustProxy) { + if (settings.network.trustProxy) { server.enable('trust proxy'); } server.use(cookieParser()); @@ -164,7 +167,7 @@ app next(); } }); - if (settings.main.csrfProtection) { + if (settings.network.csrfProtection) { server.use( csurf({ cookie: { @@ -194,7 +197,7 @@ app cookie: { maxAge: 1000 * 60 * 60 * 24 * 30, httpOnly: true, - sameSite: settings.main.csrfProtection ? 'strict' : 'lax', + sameSite: settings.network.csrfProtection ? 'strict' : 'lax', secure: 'auto', }, store: new TypeormStore({ diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 343c01e2f..258dfe2f4 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -115,7 +115,6 @@ export interface MainSettings { apiKey: string; applicationTitle: string; applicationUrl: string; - csrfProtection: boolean; cacheImages: boolean; defaultPermissions: number; defaultQuotas: { @@ -128,13 +127,17 @@ export interface MainSettings { discoverRegion: string; streamingRegion: string; originalLanguage: string; - trustProxy: boolean; mediaServerType: number; partialRequestsEnabled: boolean; enableSpecialEpisodes: boolean; + locale: string; +} + +export interface NetworkSettings { + csrfProtection: boolean; forceIpv4First: boolean; dnsServers: string; - locale: string; + trustProxy: boolean; proxy: ProxySettings; } @@ -313,6 +316,7 @@ export interface AllSettings { public: PublicSettings; notifications: NotificationSettings; jobs: Record; + network: NetworkSettings; } const SETTINGS_PATH = process.env.CONFIG_DIRECTORY @@ -331,7 +335,6 @@ class Settings { apiKey: '', applicationTitle: 'Jellyseerr', applicationUrl: '', - csrfProtection: false, cacheImages: false, defaultPermissions: Permission.REQUEST, defaultQuotas: { @@ -344,23 +347,10 @@ class Settings { discoverRegion: '', streamingRegion: '', originalLanguage: '', - trustProxy: false, mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, enableSpecialEpisodes: false, - forceIpv4First: false, - dnsServers: '', locale: 'en', - proxy: { - enabled: false, - hostname: '', - port: 8080, - useSsl: false, - user: '', - password: '', - bypassFilter: '', - bypassLocalAddresses: true, - }, }, plex: { name: '', @@ -513,6 +503,22 @@ class Settings { schedule: '0 0 5 * * *', }, }, + network: { + csrfProtection: false, + trustProxy: false, + forceIpv4First: false, + dnsServers: '', + proxy: { + enabled: false, + hostname: '', + port: 8080, + useSsl: false, + user: '', + password: '', + bypassFilter: '', + bypassLocalAddresses: true, + }, + }, }; if (initialSettings) { this.data = merge(this.data, initialSettings); @@ -622,6 +628,14 @@ class Settings { this.data.jobs = data; } + get network(): NetworkSettings { + return this.data.network; + } + + set network(data: NetworkSettings) { + this.data.network = data; + } + get clientId(): string { return this.data.clientId; } diff --git a/server/lib/settings/migrations/0005_migrate_network_settings.ts b/server/lib/settings/migrations/0005_migrate_network_settings.ts new file mode 100644 index 000000000..a6ad48445 --- /dev/null +++ b/server/lib/settings/migrations/0005_migrate_network_settings.ts @@ -0,0 +1,33 @@ +import type { AllSettings } from '@server/lib/settings'; + +const migrateNetworkSettings = (settings: any): AllSettings => { + if (settings.network) { + return settings; + } + const newSettings = { ...settings }; + newSettings.network = { + ...settings.network, + csrfProtection: settings.main.csrfProtection ?? false, + trustProxy: settings.main.trustProxy ?? false, + forceIpv4First: settings.main.forceIpv4First ?? false, + dnsServers: settings.main.dnsServers ?? '', + proxy: settings.main.proxy ?? { + enabled: false, + hostname: '', + port: 8080, + useSsl: false, + user: '', + password: '', + bypassFilter: '', + bypassLocalAddresses: true, + }, + }; + delete settings.main.csrfProtection; + delete settings.main.trustProxy; + delete settings.main.forceIpv4First; + delete settings.main.dnsServers; + delete settings.main.proxy; + return newSettings; +}; + +export default migrateNetworkSettings; diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index bc8c5ef7c..018f7b46e 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -78,6 +78,21 @@ settingsRoutes.post('/main', async (req, res) => { return res.status(200).json(settings.main); }); +settingsRoutes.get('/network', (req, res) => { + const settings = getSettings(); + + res.status(200).json(settings.network); +}); + +settingsRoutes.post('/network', async (req, res) => { + const settings = getSettings(); + + settings.network = merge(settings.network, req.body); + await settings.save(); + + return res.status(200).json(settings.network); +}); + settingsRoutes.post('/main/regenerate', async (req, res, next) => { const settings = getSettings(); diff --git a/server/utils/restartFlag.ts b/server/utils/restartFlag.ts index 6b364d1f0..24282a091 100644 --- a/server/utils/restartFlag.ts +++ b/server/utils/restartFlag.ts @@ -1,22 +1,25 @@ -import type { MainSettings } from '@server/lib/settings'; +import type { AllSettings, NetworkSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; class RestartFlag { - private settings: MainSettings; + private networkSettings: NetworkSettings; - public initializeSettings(settings: MainSettings): void { - this.settings = { ...settings }; + public initializeSettings(settings: AllSettings): void { + this.networkSettings = { + ...settings.network, + proxy: { ...settings.network.proxy }, + }; } public isSet(): boolean { - const settings = getSettings().main; + const networkSettings = getSettings().network; return ( - this.settings.csrfProtection !== settings.csrfProtection || - this.settings.trustProxy !== settings.trustProxy || - this.settings.proxy.enabled !== settings.proxy.enabled || - this.settings.forceIpv4First !== settings.forceIpv4First || - this.settings.dnsServers !== settings.dnsServers + this.networkSettings.csrfProtection !== networkSettings.csrfProtection || + this.networkSettings.trustProxy !== networkSettings.trustProxy || + this.networkSettings.proxy.enabled !== networkSettings.proxy.enabled || + this.networkSettings.forceIpv4First !== networkSettings.forceIpv4First || + this.networkSettings.dnsServers !== networkSettings.dnsServers ); } } diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx index 6336bad01..dd7cd6fa5 100644 --- a/src/components/Settings/SettingsLayout.tsx +++ b/src/components/Settings/SettingsLayout.tsx @@ -13,6 +13,7 @@ const messages = defineMessages('components.Settings', { menuPlexSettings: 'Plex', menuJellyfinSettings: '{mediaServerName}', menuServices: 'Services', + menuNetwork: 'Network', menuNotifications: 'Notifications', menuLogs: 'Logs', menuJobs: 'Jobs & Cache', @@ -53,6 +54,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => { route: '/settings/services', regex: /^\/settings\/services/, }, + { + text: intl.formatMessage(messages.menuNetwork), + route: '/settings/network', + regex: /^\/settings\/network/, + }, { text: intl.formatMessage(messages.menuNotifications), route: '/settings/notifications/email', diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index fb1df6b5e..5ccfaaa2a 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -2,7 +2,6 @@ import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; import SensitiveInput from '@app/components/Common/SensitiveInput'; -import Tooltip from '@app/components/Common/Tooltip'; import LanguageSelector from '@app/components/LanguageSelector'; import RegionSelector from '@app/components/RegionSelector'; import CopyButton from '@app/components/Settings/CopyButton'; @@ -42,39 +41,15 @@ const messages = defineMessages('components.Settings.SettingsMain', { toastSettingsSuccess: 'Settings saved successfully!', toastSettingsFailure: 'Something went wrong while saving settings.', hideAvailable: 'Hide Available Media', - csrfProtection: 'Enable CSRF Protection', - csrfProtectionTip: 'Set external API access to read-only (requires HTTPS)', - csrfProtectionHoverTip: - 'Do NOT enable this setting unless you understand what you are doing!', cacheImages: 'Enable Image Caching', cacheImagesTip: 'Cache externally sourced images (requires a significant amount of disk space)', - trustProxy: 'Enable Proxy Support', - trustProxyTip: - 'Allow Jellyseerr to correctly register client IP addresses behind a proxy', validationApplicationTitle: 'You must provide an application title', validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', partialRequestsEnabled: 'Allow Partial Series Requests', enableSpecialEpisodes: 'Allow Special Episodes Requests', - forceIpv4First: 'IPv4 Resolution First', - forceIpv4FirstTip: - 'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6', - dnsServers: 'Custom DNS Servers', - dnsServersTip: - 'Comma-separated list of custom DNS servers, e.g. "1.1.1.1,[2606:4700:4700::1111]"', locale: 'Display Language', - proxyEnabled: 'HTTP(S) Proxy', - proxyHostname: 'Proxy Hostname', - proxyPort: 'Proxy Port', - proxySsl: 'Use SSL For Proxy', - proxyUser: 'Proxy Username', - proxyPassword: 'Proxy Password', - proxyBypassFilter: 'Proxy Ignored Addresses', - proxyBypassFilterTip: - "Use ',' as a separator, and '*.' as a wildcard for subdomains", - proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses', - validationProxyPort: 'You must provide a valid port', }); const SettingsMain = () => { @@ -105,12 +80,6 @@ const SettingsMain = () => { intl.formatMessage(messages.validationApplicationUrlTrailingSlash), (value) => !value || !value.endsWith('/') ), - proxyPort: Yup.number().when('proxyEnabled', { - is: (proxyEnabled: boolean) => proxyEnabled, - then: Yup.number().required( - intl.formatMessage(messages.validationProxyPort) - ), - }), }); const regenerate = async () => { @@ -158,7 +127,6 @@ const SettingsMain = () => { initialValues={{ applicationTitle: data?.applicationTitle, applicationUrl: data?.applicationUrl, - csrfProtection: data?.csrfProtection, hideAvailable: data?.hideAvailable, locale: data?.locale ?? 'en', discoverRegion: data?.discoverRegion, @@ -166,18 +134,7 @@ const SettingsMain = () => { streamingRegion: data?.streamingRegion || 'US', partialRequestsEnabled: data?.partialRequestsEnabled, enableSpecialEpisodes: data?.enableSpecialEpisodes, - forceIpv4First: data?.forceIpv4First, - dnsServers: data?.dnsServers, - trustProxy: data?.trustProxy, cacheImages: data?.cacheImages, - proxyEnabled: data?.proxy?.enabled, - proxyHostname: data?.proxy?.hostname, - proxyPort: data?.proxy?.port, - proxySsl: data?.proxy?.useSsl, - proxyUser: data?.proxy?.user, - proxyPassword: data?.proxy?.password, - proxyBypassFilter: data?.proxy?.bypassFilter, - proxyBypassLocalAddresses: data?.proxy?.bypassLocalAddresses, }} enableReinitialize validationSchema={MainSettingsSchema} @@ -191,7 +148,6 @@ const SettingsMain = () => { body: JSON.stringify({ applicationTitle: values.applicationTitle, applicationUrl: values.applicationUrl, - csrfProtection: values.csrfProtection, hideAvailable: values.hideAvailable, locale: values.locale, discoverRegion: values.discoverRegion, @@ -199,20 +155,7 @@ const SettingsMain = () => { originalLanguage: values.originalLanguage, partialRequestsEnabled: values.partialRequestsEnabled, enableSpecialEpisodes: values.enableSpecialEpisodes, - forceIpv4First: values.forceIpv4First, - dnsServers: values.dnsServers, - trustProxy: values.trustProxy, cacheImages: values.cacheImages, - proxy: { - enabled: values.proxyEnabled, - hostname: values.proxyHostname, - port: values.proxyPort, - useSsl: values.proxySsl, - user: values.proxyUser, - password: values.proxyPassword, - bypassFilter: values.proxyBypassFilter, - bypassLocalAddresses: values.proxyBypassLocalAddresses, - }, }), }); if (!res.ok) throw new Error(); @@ -321,58 +264,6 @@ const SettingsMain = () => { )} -
- -
- { - setFieldValue('trustProxy', !values.trustProxy); - }} - /> -
-
-
- -
- - { - setFieldValue( - 'csrfProtection', - !values.csrfProtection - ); - }} - /> - -
-
-
- -
- { - setFieldValue('forceIpv4First', !values.forceIpv4First); - }} - /> -
-
-
- -
-
- -
- {errors.dnsServers && - touched.dnsServers && - typeof errors.dnsServers === 'string' && ( -
{errors.dnsServers}
- )} -
-
-
- -
- { - setFieldValue('proxyEnabled', !values.proxyEnabled); - }} - /> -
-
- {values.proxyEnabled && ( - <> -
-
- -
-
- -
- {errors.proxyHostname && - touched.proxyHostname && - typeof errors.proxyHostname === 'string' && ( -
- {errors.proxyHostname} -
- )} -
-
-
- -
-
- -
- {errors.proxyPort && - touched.proxyPort && - typeof errors.proxyPort === 'string' && ( -
{errors.proxyPort}
- )} -
-
-
- -
- { - setFieldValue('proxySsl', !values.proxySsl); - }} - /> -
-
-
- -
-
- -
- {errors.proxyUser && - touched.proxyUser && - typeof errors.proxyUser === 'string' && ( -
{errors.proxyUser}
- )} -
-
-
- -
-
- -
- {errors.proxyPassword && - touched.proxyPassword && - typeof errors.proxyPassword === 'string' && ( -
- {errors.proxyPassword} -
- )} -
-
-
- -
-
- -
- {errors.proxyBypassFilter && - touched.proxyBypassFilter && - typeof errors.proxyBypassFilter === 'string' && ( -
- {errors.proxyBypassFilter} -
- )} -
-
-
- -
- { - setFieldValue( - 'proxyBypassLocalAddresses', - !values.proxyBypassLocalAddresses - ); - }} - /> -
-
-
- - )}
diff --git a/src/components/Settings/SettingsNetwork/index.tsx b/src/components/Settings/SettingsNetwork/index.tsx new file mode 100644 index 000000000..82701cdac --- /dev/null +++ b/src/components/Settings/SettingsNetwork/index.tsx @@ -0,0 +1,461 @@ +import Button from '@app/components/Common/Button'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import Tooltip from '@app/components/Common/Tooltip'; +import SettingsBadge from '@app/components/Settings/SettingsBadge'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; +import type { NetworkSettings } from '@server/lib/settings'; +import { Field, Form, Formik } from 'formik'; +import { useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR, { mutate } from 'swr'; +import * as Yup from 'yup'; + +const messages = defineMessages('components.Settings.SettingsNetwork', { + toastSettingsSuccess: 'Settings saved successfully!', + toastSettingsFailure: 'Something went wrong while saving settings.', + network: 'Network', + networksettings: 'Network Settings', + networksettingsDescription: + 'Configure network settings for your Jellyseerr instance.', + csrfProtection: 'Enable CSRF Protection', + csrfProtectionTip: 'Set external API access to read-only (requires HTTPS)', + csrfProtectionHoverTip: + 'Do NOT enable this setting unless you understand what you are doing!', + trustProxy: 'Enable Proxy Support', + trustProxyTip: + 'Allow Jellyseerr to correctly register client IP addresses behind a proxy', + forceIpv4First: 'IPv4 Resolution First', + forceIpv4FirstTip: + 'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6', + dnsServers: 'Custom DNS Servers', + dnsServersTip: + 'Comma-separated list of custom DNS servers, e.g. "1.1.1.1,[2606:4700:4700::1111]"', + proxyEnabled: 'HTTP(S) Proxy', + proxyHostname: 'Proxy Hostname', + proxyPort: 'Proxy Port', + proxySsl: 'Use SSL For Proxy', + proxyUser: 'Proxy Username', + proxyPassword: 'Proxy Password', + proxyBypassFilter: 'Proxy Ignored Addresses', + proxyBypassFilterTip: + "Use ',' as a separator, and '*.' as a wildcard for subdomains", + proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses', + validationProxyPort: 'You must provide a valid port', +}); + +const SettingsMain = () => { + const { addToast } = useToasts(); + const intl = useIntl(); + const { + data, + error, + mutate: revalidate, + } = useSWR('/api/v1/settings/network'); + + const NetworkSettingsSchema = Yup.object().shape({ + proxyPort: Yup.number().when('proxyEnabled', { + is: (proxyEnabled: boolean) => proxyEnabled, + then: Yup.number().required( + intl.formatMessage(messages.validationProxyPort) + ), + }), + }); + + if (!data && !error) { + return ; + } + + return ( + <> + +
+

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

+

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

+
+
+ { + try { + const res = await fetch('/api/v1/settings/network', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + csrfProtection: values.csrfProtection, + forceIpv4First: values.forceIpv4First, + dnsServers: values.dnsServers, + trustProxy: values.trustProxy, + proxy: { + enabled: values.proxyEnabled, + hostname: values.proxyHostname, + port: values.proxyPort, + useSsl: values.proxySsl, + user: values.proxyUser, + password: values.proxyPassword, + bypassFilter: values.proxyBypassFilter, + bypassLocalAddresses: values.proxyBypassLocalAddresses, + }, + }), + }); + if (!res.ok) throw new Error(); + mutate('/api/v1/settings/public'); + mutate('/api/v1/status'); + + addToast(intl.formatMessage(messages.toastSettingsSuccess), { + autoDismiss: true, + appearance: 'success', + }); + } catch (e) { + addToast(intl.formatMessage(messages.toastSettingsFailure), { + autoDismiss: true, + appearance: 'error', + }); + } finally { + revalidate(); + } + }} + > + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + }) => { + return ( +
+
+ +
+ { + setFieldValue('trustProxy', !values.trustProxy); + }} + /> +
+
+
+ +
+ + { + setFieldValue( + 'csrfProtection', + !values.csrfProtection + ); + }} + /> + +
+
+
+ +
+ { + setFieldValue('forceIpv4First', !values.forceIpv4First); + }} + /> +
+
+
+ +
+
+ +
+ {errors.dnsServers && + touched.dnsServers && + typeof errors.dnsServers === 'string' && ( +
{errors.dnsServers}
+ )} +
+
+
+ +
+ { + setFieldValue('proxyEnabled', !values.proxyEnabled); + }} + /> +
+
+ {values.proxyEnabled && ( + <> +
+
+ +
+
+ +
+ {errors.proxyHostname && + touched.proxyHostname && + typeof errors.proxyHostname === 'string' && ( +
+ {errors.proxyHostname} +
+ )} +
+
+
+ +
+
+ +
+ {errors.proxyPort && + touched.proxyPort && + typeof errors.proxyPort === 'string' && ( +
{errors.proxyPort}
+ )} +
+
+
+ +
+ { + setFieldValue('proxySsl', !values.proxySsl); + }} + /> +
+
+
+ +
+
+ +
+ {errors.proxyUser && + touched.proxyUser && + typeof errors.proxyUser === 'string' && ( +
{errors.proxyUser}
+ )} +
+
+
+ +
+
+ +
+ {errors.proxyPassword && + touched.proxyPassword && + typeof errors.proxyPassword === 'string' && ( +
+ {errors.proxyPassword} +
+ )} +
+
+
+ +
+
+ +
+ {errors.proxyBypassFilter && + touched.proxyBypassFilter && + typeof errors.proxyBypassFilter === 'string' && ( +
+ {errors.proxyBypassFilter} +
+ )} +
+
+
+ +
+ { + setFieldValue( + 'proxyBypassLocalAddresses', + !values.proxyBypassLocalAddresses + ); + }} + /> +
+
+
+ + )} +
+
+ + + +
+
+
+ ); + }} +
+
+ + ); +}; + +export default SettingsMain; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 708aee65c..1bd66d846 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -915,16 +915,9 @@ "components.Settings.SettingsMain.applicationurl": "Application URL", "components.Settings.SettingsMain.cacheImages": "Enable Image Caching", "components.Settings.SettingsMain.cacheImagesTip": "Cache externally sourced images (requires a significant amount of disk space)", - "components.Settings.SettingsMain.csrfProtection": "Enable CSRF Protection", - "components.Settings.SettingsMain.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!", - "components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)", "components.Settings.SettingsMain.discoverRegion": "Discover Region", "components.Settings.SettingsMain.discoverRegionTip": "Filter content by regional availability", - "components.Settings.SettingsMain.dnsServers": "Custom DNS Servers", - "components.Settings.SettingsMain.dnsServersTip": "Comma-separated list of custom DNS servers, e.g. \"1.1.1.1,[2606:4700:4700::1111]\"", "components.Settings.SettingsMain.enableSpecialEpisodes": "Allow Special Episodes Requests", - "components.Settings.SettingsMain.forceIpv4First": "IPv4 Resolution First", - "components.Settings.SettingsMain.forceIpv4FirstTip": "Force Jellyseerr to resolve IPv4 addresses first instead of IPv6", "components.Settings.SettingsMain.general": "General", "components.Settings.SettingsMain.generalsettings": "General Settings", "components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.", @@ -933,27 +926,39 @@ "components.Settings.SettingsMain.originallanguage": "Discover Language", "components.Settings.SettingsMain.originallanguageTip": "Filter content by original language", "components.Settings.SettingsMain.partialRequestsEnabled": "Allow Partial Series Requests", - "components.Settings.SettingsMain.proxyBypassFilter": "Proxy Ignored Addresses", - "components.Settings.SettingsMain.proxyBypassFilterTip": "Use ',' as a separator, and '*.' as a wildcard for subdomains", - "components.Settings.SettingsMain.proxyBypassLocalAddresses": "Bypass Proxy for Local Addresses", - "components.Settings.SettingsMain.proxyEnabled": "HTTP(S) Proxy", - "components.Settings.SettingsMain.proxyHostname": "Proxy Hostname", - "components.Settings.SettingsMain.proxyPassword": "Proxy Password", - "components.Settings.SettingsMain.proxyPort": "Proxy Port", - "components.Settings.SettingsMain.proxySsl": "Use SSL For Proxy", - "components.Settings.SettingsMain.proxyUser": "Proxy Username", "components.Settings.SettingsMain.streamingRegion": "Streaming Region", "components.Settings.SettingsMain.streamingRegionTip": "Show streaming sites by regional availability", "components.Settings.SettingsMain.toastApiKeyFailure": "Something went wrong while generating a new API key.", "components.Settings.SettingsMain.toastApiKeySuccess": "New API key generated successfully!", "components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.", "components.Settings.SettingsMain.toastSettingsSuccess": "Settings saved successfully!", - "components.Settings.SettingsMain.trustProxy": "Enable Proxy Support", - "components.Settings.SettingsMain.trustProxyTip": "Allow Jellyseerr to correctly register client IP addresses behind a proxy", "components.Settings.SettingsMain.validationApplicationTitle": "You must provide an application title", "components.Settings.SettingsMain.validationApplicationUrl": "You must provide a valid URL", "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash", - "components.Settings.SettingsMain.validationProxyPort": "You must provide a valid port", + "components.Settings.SettingsNetwork.csrfProtection": "Enable CSRF Protection", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!", + "components.Settings.SettingsNetwork.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)", + "components.Settings.SettingsNetwork.dnsServers": "Custom DNS Servers", + "components.Settings.SettingsNetwork.dnsServersTip": "Comma-separated list of custom DNS servers, e.g. \"1.1.1.1,[2606:4700:4700::1111]\"", + "components.Settings.SettingsNetwork.forceIpv4First": "IPv4 Resolution First", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "Force Jellyseerr to resolve IPv4 addresses first instead of IPv6", + "components.Settings.SettingsNetwork.network": "Network", + "components.Settings.SettingsNetwork.networksettings": "Network Settings", + "components.Settings.SettingsNetwork.networksettingsDescription": "Configure network settings for your Jellyseerr instance.", + "components.Settings.SettingsNetwork.proxyBypassFilter": "Proxy Ignored Addresses", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "Use ',' as a separator, and '*.' as a wildcard for subdomains", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "Bypass Proxy for Local Addresses", + "components.Settings.SettingsNetwork.proxyEnabled": "HTTP(S) Proxy", + "components.Settings.SettingsNetwork.proxyHostname": "Proxy Hostname", + "components.Settings.SettingsNetwork.proxyPassword": "Proxy Password", + "components.Settings.SettingsNetwork.proxyPort": "Proxy Port", + "components.Settings.SettingsNetwork.proxySsl": "Use SSL For Proxy", + "components.Settings.SettingsNetwork.proxyUser": "Proxy Username", + "components.Settings.SettingsNetwork.toastSettingsFailure": "Something went wrong while saving settings.", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "Settings saved successfully!", + "components.Settings.SettingsNetwork.trustProxy": "Enable Proxy Support", + "components.Settings.SettingsNetwork.trustProxyTip": "Allow Jellyseerr to correctly register client IP addresses behind a proxy", + "components.Settings.SettingsNetwork.validationProxyPort": "You must provide a valid port", "components.Settings.SettingsUsers.defaultPermissions": "Default Permissions", "components.Settings.SettingsUsers.defaultPermissionsTip": "Initial permissions assigned to new users", "components.Settings.SettingsUsers.localLogin": "Enable Local Sign-In", @@ -1069,6 +1074,7 @@ "components.Settings.menuJellyfinSettings": "{mediaServerName}", "components.Settings.menuJobs": "Jobs & Cache", "components.Settings.menuLogs": "Logs", + "components.Settings.menuNetwork": "Network", "components.Settings.menuNotifications": "Notifications", "components.Settings.menuPlexSettings": "Plex", "components.Settings.menuServices": "Services", diff --git a/src/pages/settings/network.tsx b/src/pages/settings/network.tsx new file mode 100644 index 000000000..f5aeeaa95 --- /dev/null +++ b/src/pages/settings/network.tsx @@ -0,0 +1,16 @@ +import SettingsLayout from '@app/components/Settings/SettingsLayout'; +import SettingsNetwork from '@app/components/Settings/SettingsNetwork'; +import useRouteGuard from '@app/hooks/useRouteGuard'; +import { Permission } from '@app/hooks/useUser'; +import type { NextPage } from 'next'; + +const SettingsNetworkPage: NextPage = () => { + useRouteGuard(Permission.ADMIN); + return ( + + + + ); +}; + +export default SettingsNetworkPage;