refactor(settings): move network settings to their own settings tab (#1287)
* refactor(settings): move network settings to their own settings tab This PR moves the network settings out of the General Settings section to a new Netowrk Settings tab. * fix: add missing translations * fix: fix cypress tests for network settings * refactor: create a separate section for network settings
This commit is contained in:
@@ -13,10 +13,10 @@ describe('General Settings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('modifies setting that requires restart', () => {
|
it('modifies setting that requires restart', () => {
|
||||||
cy.visit('/settings');
|
cy.visit('/settings/network');
|
||||||
|
|
||||||
cy.get('#trustProxy').click();
|
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(
|
cy.get('[data-testid=modal-title]').should(
|
||||||
'contain',
|
'contain',
|
||||||
'Server Restart Required'
|
'Server Restart Required'
|
||||||
@@ -26,7 +26,7 @@ describe('General Settings', () => {
|
|||||||
cy.get('[data-testid=modal-title]').should('not.exist');
|
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||||
|
|
||||||
cy.get('[type=checkbox]#trustProxy').click();
|
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');
|
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -164,12 +164,6 @@ components:
|
|||||||
applicationUrl:
|
applicationUrl:
|
||||||
type: string
|
type: string
|
||||||
example: https://os.example.com
|
example: https://os.example.com
|
||||||
trustProxy:
|
|
||||||
type: boolean
|
|
||||||
example: true
|
|
||||||
csrfProtection:
|
|
||||||
type: boolean
|
|
||||||
example: false
|
|
||||||
hideAvailable:
|
hideAvailable:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
@@ -191,12 +185,21 @@ components:
|
|||||||
enableSpecialEpisodes:
|
enableSpecialEpisodes:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
|
NetworkSettings:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
csrfProtection:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
forceIpv4First:
|
forceIpv4First:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
dnsServers:
|
dnsServers:
|
||||||
type: string
|
type: string
|
||||||
example: '1.1.1.1'
|
example: '1.1.1.1'
|
||||||
|
trustProxy:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
PlexLibrary:
|
PlexLibrary:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -2045,6 +2048,37 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/MainSettings'
|
$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:
|
/settings/main/regenerate:
|
||||||
post:
|
post:
|
||||||
summary: Get main settings with newly-generated API key
|
summary: Get main settings with newly-generated API key
|
||||||
|
|||||||
@@ -72,23 +72,26 @@ app
|
|||||||
|
|
||||||
// Load Settings
|
// Load Settings
|
||||||
const settings = await getSettings().load();
|
const settings = await getSettings().load();
|
||||||
restartFlag.initializeSettings(settings.main);
|
restartFlag.initializeSettings(settings);
|
||||||
|
|
||||||
// Check if we force IPv4 first
|
// 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');
|
dns.setDefaultResultOrder('ipv4first');
|
||||||
net.setDefaultAutoSelectFamily(false);
|
net.setDefaultAutoSelectFamily(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.main.dnsServers.trim() !== '') {
|
if (settings.network.dnsServers.trim() !== '') {
|
||||||
dns.setServers(
|
dns.setServers(
|
||||||
settings.main.dnsServers.split(',').map((server) => server.trim())
|
settings.network.dnsServers.split(',').map((server) => server.trim())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register HTTP proxy
|
// Register HTTP proxy
|
||||||
if (settings.main.proxy.enabled) {
|
if (settings.network.proxy.enabled) {
|
||||||
await createCustomProxyAgent(settings.main.proxy);
|
await createCustomProxyAgent(settings.network.proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate library types
|
// Migrate library types
|
||||||
@@ -143,7 +146,7 @@ app
|
|||||||
await DiscoverSlider.bootstrapSliders();
|
await DiscoverSlider.bootstrapSliders();
|
||||||
|
|
||||||
const server = express();
|
const server = express();
|
||||||
if (settings.main.trustProxy) {
|
if (settings.network.trustProxy) {
|
||||||
server.enable('trust proxy');
|
server.enable('trust proxy');
|
||||||
}
|
}
|
||||||
server.use(cookieParser());
|
server.use(cookieParser());
|
||||||
@@ -164,7 +167,7 @@ app
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (settings.main.csrfProtection) {
|
if (settings.network.csrfProtection) {
|
||||||
server.use(
|
server.use(
|
||||||
csurf({
|
csurf({
|
||||||
cookie: {
|
cookie: {
|
||||||
@@ -194,7 +197,7 @@ app
|
|||||||
cookie: {
|
cookie: {
|
||||||
maxAge: 1000 * 60 * 60 * 24 * 30,
|
maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: settings.main.csrfProtection ? 'strict' : 'lax',
|
sameSite: settings.network.csrfProtection ? 'strict' : 'lax',
|
||||||
secure: 'auto',
|
secure: 'auto',
|
||||||
},
|
},
|
||||||
store: new TypeormStore({
|
store: new TypeormStore({
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ export interface MainSettings {
|
|||||||
apiKey: string;
|
apiKey: string;
|
||||||
applicationTitle: string;
|
applicationTitle: string;
|
||||||
applicationUrl: string;
|
applicationUrl: string;
|
||||||
csrfProtection: boolean;
|
|
||||||
cacheImages: boolean;
|
cacheImages: boolean;
|
||||||
defaultPermissions: number;
|
defaultPermissions: number;
|
||||||
defaultQuotas: {
|
defaultQuotas: {
|
||||||
@@ -128,13 +127,17 @@ export interface MainSettings {
|
|||||||
discoverRegion: string;
|
discoverRegion: string;
|
||||||
streamingRegion: string;
|
streamingRegion: string;
|
||||||
originalLanguage: string;
|
originalLanguage: string;
|
||||||
trustProxy: boolean;
|
|
||||||
mediaServerType: number;
|
mediaServerType: number;
|
||||||
partialRequestsEnabled: boolean;
|
partialRequestsEnabled: boolean;
|
||||||
enableSpecialEpisodes: boolean;
|
enableSpecialEpisodes: boolean;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkSettings {
|
||||||
|
csrfProtection: boolean;
|
||||||
forceIpv4First: boolean;
|
forceIpv4First: boolean;
|
||||||
dnsServers: string;
|
dnsServers: string;
|
||||||
locale: string;
|
trustProxy: boolean;
|
||||||
proxy: ProxySettings;
|
proxy: ProxySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +316,7 @@ export interface AllSettings {
|
|||||||
public: PublicSettings;
|
public: PublicSettings;
|
||||||
notifications: NotificationSettings;
|
notifications: NotificationSettings;
|
||||||
jobs: Record<JobId, JobSettings>;
|
jobs: Record<JobId, JobSettings>;
|
||||||
|
network: NetworkSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
||||||
@@ -331,7 +335,6 @@ class Settings {
|
|||||||
apiKey: '',
|
apiKey: '',
|
||||||
applicationTitle: 'Jellyseerr',
|
applicationTitle: 'Jellyseerr',
|
||||||
applicationUrl: '',
|
applicationUrl: '',
|
||||||
csrfProtection: false,
|
|
||||||
cacheImages: false,
|
cacheImages: false,
|
||||||
defaultPermissions: Permission.REQUEST,
|
defaultPermissions: Permission.REQUEST,
|
||||||
defaultQuotas: {
|
defaultQuotas: {
|
||||||
@@ -344,23 +347,10 @@ class Settings {
|
|||||||
discoverRegion: '',
|
discoverRegion: '',
|
||||||
streamingRegion: '',
|
streamingRegion: '',
|
||||||
originalLanguage: '',
|
originalLanguage: '',
|
||||||
trustProxy: false,
|
|
||||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||||
partialRequestsEnabled: true,
|
partialRequestsEnabled: true,
|
||||||
enableSpecialEpisodes: false,
|
enableSpecialEpisodes: false,
|
||||||
forceIpv4First: false,
|
|
||||||
dnsServers: '',
|
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
proxy: {
|
|
||||||
enabled: false,
|
|
||||||
hostname: '',
|
|
||||||
port: 8080,
|
|
||||||
useSsl: false,
|
|
||||||
user: '',
|
|
||||||
password: '',
|
|
||||||
bypassFilter: '',
|
|
||||||
bypassLocalAddresses: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
plex: {
|
plex: {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -513,6 +503,22 @@ class Settings {
|
|||||||
schedule: '0 0 5 * * *',
|
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) {
|
if (initialSettings) {
|
||||||
this.data = merge(this.data, initialSettings);
|
this.data = merge(this.data, initialSettings);
|
||||||
@@ -622,6 +628,14 @@ class Settings {
|
|||||||
this.data.jobs = data;
|
this.data.jobs = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get network(): NetworkSettings {
|
||||||
|
return this.data.network;
|
||||||
|
}
|
||||||
|
|
||||||
|
set network(data: NetworkSettings) {
|
||||||
|
this.data.network = data;
|
||||||
|
}
|
||||||
|
|
||||||
get clientId(): string {
|
get clientId(): string {
|
||||||
return this.data.clientId;
|
return this.data.clientId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -78,6 +78,21 @@ settingsRoutes.post('/main', async (req, res) => {
|
|||||||
return res.status(200).json(settings.main);
|
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) => {
|
settingsRoutes.post('/main/regenerate', async (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
|
||||||
class RestartFlag {
|
class RestartFlag {
|
||||||
private settings: MainSettings;
|
private networkSettings: NetworkSettings;
|
||||||
|
|
||||||
public initializeSettings(settings: MainSettings): void {
|
public initializeSettings(settings: AllSettings): void {
|
||||||
this.settings = { ...settings };
|
this.networkSettings = {
|
||||||
|
...settings.network,
|
||||||
|
proxy: { ...settings.network.proxy },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSet(): boolean {
|
public isSet(): boolean {
|
||||||
const settings = getSettings().main;
|
const networkSettings = getSettings().network;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.settings.csrfProtection !== settings.csrfProtection ||
|
this.networkSettings.csrfProtection !== networkSettings.csrfProtection ||
|
||||||
this.settings.trustProxy !== settings.trustProxy ||
|
this.networkSettings.trustProxy !== networkSettings.trustProxy ||
|
||||||
this.settings.proxy.enabled !== settings.proxy.enabled ||
|
this.networkSettings.proxy.enabled !== networkSettings.proxy.enabled ||
|
||||||
this.settings.forceIpv4First !== settings.forceIpv4First ||
|
this.networkSettings.forceIpv4First !== networkSettings.forceIpv4First ||
|
||||||
this.settings.dnsServers !== settings.dnsServers
|
this.networkSettings.dnsServers !== networkSettings.dnsServers
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const messages = defineMessages('components.Settings', {
|
|||||||
menuPlexSettings: 'Plex',
|
menuPlexSettings: 'Plex',
|
||||||
menuJellyfinSettings: '{mediaServerName}',
|
menuJellyfinSettings: '{mediaServerName}',
|
||||||
menuServices: 'Services',
|
menuServices: 'Services',
|
||||||
|
menuNetwork: 'Network',
|
||||||
menuNotifications: 'Notifications',
|
menuNotifications: 'Notifications',
|
||||||
menuLogs: 'Logs',
|
menuLogs: 'Logs',
|
||||||
menuJobs: 'Jobs & Cache',
|
menuJobs: 'Jobs & Cache',
|
||||||
@@ -53,6 +54,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => {
|
|||||||
route: '/settings/services',
|
route: '/settings/services',
|
||||||
regex: /^\/settings\/services/,
|
regex: /^\/settings\/services/,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage(messages.menuNetwork),
|
||||||
|
route: '/settings/network',
|
||||||
|
regex: /^\/settings\/network/,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: intl.formatMessage(messages.menuNotifications),
|
text: intl.formatMessage(messages.menuNotifications),
|
||||||
route: '/settings/notifications/email',
|
route: '/settings/notifications/email',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import Button from '@app/components/Common/Button';
|
|||||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||||
import PageTitle from '@app/components/Common/PageTitle';
|
import PageTitle from '@app/components/Common/PageTitle';
|
||||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||||
import Tooltip from '@app/components/Common/Tooltip';
|
|
||||||
import LanguageSelector from '@app/components/LanguageSelector';
|
import LanguageSelector from '@app/components/LanguageSelector';
|
||||||
import RegionSelector from '@app/components/RegionSelector';
|
import RegionSelector from '@app/components/RegionSelector';
|
||||||
import CopyButton from '@app/components/Settings/CopyButton';
|
import CopyButton from '@app/components/Settings/CopyButton';
|
||||||
@@ -42,39 +41,15 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
|||||||
toastSettingsSuccess: 'Settings saved successfully!',
|
toastSettingsSuccess: 'Settings saved successfully!',
|
||||||
toastSettingsFailure: 'Something went wrong while saving settings.',
|
toastSettingsFailure: 'Something went wrong while saving settings.',
|
||||||
hideAvailable: 'Hide Available Media',
|
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',
|
cacheImages: 'Enable Image Caching',
|
||||||
cacheImagesTip:
|
cacheImagesTip:
|
||||||
'Cache externally sourced images (requires a significant amount of disk space)',
|
'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',
|
validationApplicationTitle: 'You must provide an application title',
|
||||||
validationApplicationUrl: 'You must provide a valid URL',
|
validationApplicationUrl: 'You must provide a valid URL',
|
||||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||||
enableSpecialEpisodes: 'Allow Special Episodes 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',
|
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 = () => {
|
const SettingsMain = () => {
|
||||||
@@ -105,12 +80,6 @@ const SettingsMain = () => {
|
|||||||
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
|
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
|
||||||
(value) => !value || !value.endsWith('/')
|
(value) => !value || !value.endsWith('/')
|
||||||
),
|
),
|
||||||
proxyPort: Yup.number().when('proxyEnabled', {
|
|
||||||
is: (proxyEnabled: boolean) => proxyEnabled,
|
|
||||||
then: Yup.number().required(
|
|
||||||
intl.formatMessage(messages.validationProxyPort)
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const regenerate = async () => {
|
const regenerate = async () => {
|
||||||
@@ -158,7 +127,6 @@ const SettingsMain = () => {
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
applicationTitle: data?.applicationTitle,
|
applicationTitle: data?.applicationTitle,
|
||||||
applicationUrl: data?.applicationUrl,
|
applicationUrl: data?.applicationUrl,
|
||||||
csrfProtection: data?.csrfProtection,
|
|
||||||
hideAvailable: data?.hideAvailable,
|
hideAvailable: data?.hideAvailable,
|
||||||
locale: data?.locale ?? 'en',
|
locale: data?.locale ?? 'en',
|
||||||
discoverRegion: data?.discoverRegion,
|
discoverRegion: data?.discoverRegion,
|
||||||
@@ -166,18 +134,7 @@ const SettingsMain = () => {
|
|||||||
streamingRegion: data?.streamingRegion || 'US',
|
streamingRegion: data?.streamingRegion || 'US',
|
||||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||||
enableSpecialEpisodes: data?.enableSpecialEpisodes,
|
enableSpecialEpisodes: data?.enableSpecialEpisodes,
|
||||||
forceIpv4First: data?.forceIpv4First,
|
|
||||||
dnsServers: data?.dnsServers,
|
|
||||||
trustProxy: data?.trustProxy,
|
|
||||||
cacheImages: data?.cacheImages,
|
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
|
enableReinitialize
|
||||||
validationSchema={MainSettingsSchema}
|
validationSchema={MainSettingsSchema}
|
||||||
@@ -191,7 +148,6 @@ const SettingsMain = () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
applicationTitle: values.applicationTitle,
|
applicationTitle: values.applicationTitle,
|
||||||
applicationUrl: values.applicationUrl,
|
applicationUrl: values.applicationUrl,
|
||||||
csrfProtection: values.csrfProtection,
|
|
||||||
hideAvailable: values.hideAvailable,
|
hideAvailable: values.hideAvailable,
|
||||||
locale: values.locale,
|
locale: values.locale,
|
||||||
discoverRegion: values.discoverRegion,
|
discoverRegion: values.discoverRegion,
|
||||||
@@ -199,20 +155,7 @@ const SettingsMain = () => {
|
|||||||
originalLanguage: values.originalLanguage,
|
originalLanguage: values.originalLanguage,
|
||||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||||
enableSpecialEpisodes: values.enableSpecialEpisodes,
|
enableSpecialEpisodes: values.enableSpecialEpisodes,
|
||||||
forceIpv4First: values.forceIpv4First,
|
|
||||||
dnsServers: values.dnsServers,
|
|
||||||
trustProxy: values.trustProxy,
|
|
||||||
cacheImages: values.cacheImages,
|
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();
|
if (!res.ok) throw new Error();
|
||||||
@@ -321,58 +264,6 @@ const SettingsMain = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="trustProxy" className="checkbox-label">
|
|
||||||
<span className="mr-2">
|
|
||||||
{intl.formatMessage(messages.trustProxy)}
|
|
||||||
</span>
|
|
||||||
<SettingsBadge badgeType="restartRequired" />
|
|
||||||
<span className="label-tip">
|
|
||||||
{intl.formatMessage(messages.trustProxyTip)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="trustProxy"
|
|
||||||
name="trustProxy"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue('trustProxy', !values.trustProxy);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="csrfProtection" className="checkbox-label">
|
|
||||||
<span className="mr-2">
|
|
||||||
{intl.formatMessage(messages.csrfProtection)}
|
|
||||||
</span>
|
|
||||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
|
||||||
<SettingsBadge badgeType="restartRequired" />
|
|
||||||
<span className="label-tip">
|
|
||||||
{intl.formatMessage(messages.csrfProtectionTip)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<Tooltip
|
|
||||||
content={intl.formatMessage(
|
|
||||||
messages.csrfProtectionHoverTip
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="csrfProtection"
|
|
||||||
name="csrfProtection"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue(
|
|
||||||
'csrfProtection',
|
|
||||||
!values.csrfProtection
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="cacheImages" className="checkbox-label">
|
<label htmlFor="cacheImages" className="checkbox-label">
|
||||||
<span className="mr-2">
|
<span className="mr-2">
|
||||||
@@ -534,231 +425,6 @@ const SettingsMain = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="forceIpv4First" className="checkbox-label">
|
|
||||||
<span className="mr-2">
|
|
||||||
{intl.formatMessage(messages.forceIpv4First)}
|
|
||||||
</span>
|
|
||||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
|
||||||
<SettingsBadge badgeType="restartRequired" />
|
|
||||||
<span className="label-tip">
|
|
||||||
{intl.formatMessage(messages.forceIpv4FirstTip)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="forceIpv4First"
|
|
||||||
name="forceIpv4First"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue('forceIpv4First', !values.forceIpv4First);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="dnsServers" className="checkbox-label">
|
|
||||||
<span className="mr-2">
|
|
||||||
{intl.formatMessage(messages.dnsServers)}
|
|
||||||
</span>
|
|
||||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
|
||||||
<SettingsBadge badgeType="restartRequired" />
|
|
||||||
<span className="label-tip">
|
|
||||||
{intl.formatMessage(messages.dnsServersTip)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
id="dnsServers"
|
|
||||||
name="dnsServers"
|
|
||||||
type="text"
|
|
||||||
inputMode="url"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.dnsServers &&
|
|
||||||
touched.dnsServers &&
|
|
||||||
typeof errors.dnsServers === 'string' && (
|
|
||||||
<div className="error">{errors.dnsServers}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
|
||||||
<span className="mr-2">
|
|
||||||
{intl.formatMessage(messages.proxyEnabled)}
|
|
||||||
</span>
|
|
||||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
|
||||||
<SettingsBadge badgeType="restartRequired" />
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="proxyEnabled"
|
|
||||||
name="proxyEnabled"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue('proxyEnabled', !values.proxyEnabled);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{values.proxyEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="mr-2 ml-4">
|
|
||||||
<div className="form-row">
|
|
||||||
<label
|
|
||||||
htmlFor="proxyHostname"
|
|
||||||
className="checkbox-label"
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages.proxyHostname)}
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
id="proxyHostname"
|
|
||||||
name="proxyHostname"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.proxyHostname &&
|
|
||||||
touched.proxyHostname &&
|
|
||||||
typeof errors.proxyHostname === 'string' && (
|
|
||||||
<div className="error">
|
|
||||||
{errors.proxyHostname}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="proxyPort" className="checkbox-label">
|
|
||||||
{intl.formatMessage(messages.proxyPort)}
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
id="proxyPort"
|
|
||||||
name="proxyPort"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.proxyPort &&
|
|
||||||
touched.proxyPort &&
|
|
||||||
typeof errors.proxyPort === 'string' && (
|
|
||||||
<div className="error">{errors.proxyPort}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="proxySsl" className="checkbox-label">
|
|
||||||
{intl.formatMessage(messages.proxySsl)}
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="proxySsl"
|
|
||||||
name="proxySsl"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue('proxySsl', !values.proxySsl);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="proxyUser" className="checkbox-label">
|
|
||||||
{intl.formatMessage(messages.proxyUser)}
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
id="proxyUser"
|
|
||||||
name="proxyUser"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.proxyUser &&
|
|
||||||
touched.proxyUser &&
|
|
||||||
typeof errors.proxyUser === 'string' && (
|
|
||||||
<div className="error">{errors.proxyUser}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label
|
|
||||||
htmlFor="proxyPassword"
|
|
||||||
className="checkbox-label"
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages.proxyPassword)}
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
id="proxyPassword"
|
|
||||||
name="proxyPassword"
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.proxyPassword &&
|
|
||||||
touched.proxyPassword &&
|
|
||||||
typeof errors.proxyPassword === 'string' && (
|
|
||||||
<div className="error">
|
|
||||||
{errors.proxyPassword}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label
|
|
||||||
htmlFor="proxyBypassFilter"
|
|
||||||
className="checkbox-label"
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages.proxyBypassFilter)}
|
|
||||||
<span className="label-tip">
|
|
||||||
{intl.formatMessage(messages.proxyBypassFilterTip)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
id="proxyBypassFilter"
|
|
||||||
name="proxyBypassFilter"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.proxyBypassFilter &&
|
|
||||||
touched.proxyBypassFilter &&
|
|
||||||
typeof errors.proxyBypassFilter === 'string' && (
|
|
||||||
<div className="error">
|
|
||||||
{errors.proxyBypassFilter}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label
|
|
||||||
htmlFor="proxyBypassLocalAddresses"
|
|
||||||
className="checkbox-label"
|
|
||||||
>
|
|
||||||
{intl.formatMessage(
|
|
||||||
messages.proxyBypassLocalAddresses
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="proxyBypassLocalAddresses"
|
|
||||||
name="proxyBypassLocalAddresses"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue(
|
|
||||||
'proxyBypassLocalAddresses',
|
|
||||||
!values.proxyBypassLocalAddresses
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
|||||||
461
src/components/Settings/SettingsNetwork/index.tsx
Normal file
461
src/components/Settings/SettingsNetwork/index.tsx
Normal file
@@ -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<NetworkSettings>('/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 <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle
|
||||||
|
title={[
|
||||||
|
intl.formatMessage(messages.network),
|
||||||
|
intl.formatMessage(globalMessages.settings),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="heading">
|
||||||
|
{intl.formatMessage(messages.networksettings)}
|
||||||
|
</h3>
|
||||||
|
<p className="description">
|
||||||
|
{intl.formatMessage(messages.networksettingsDescription)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="section">
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
csrfProtection: data?.csrfProtection,
|
||||||
|
forceIpv4First: data?.forceIpv4First,
|
||||||
|
dnsServers: data?.dnsServers,
|
||||||
|
trustProxy: data?.trustProxy,
|
||||||
|
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={NetworkSettingsSchema}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
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 (
|
||||||
|
<Form className="section" data-testid="settings-network-form">
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="trustProxy" className="checkbox-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.trustProxy)}
|
||||||
|
</span>
|
||||||
|
<SettingsBadge badgeType="restartRequired" />
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.trustProxyTip)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="trustProxy"
|
||||||
|
name="trustProxy"
|
||||||
|
onChange={() => {
|
||||||
|
setFieldValue('trustProxy', !values.trustProxy);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="csrfProtection" className="checkbox-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.csrfProtection)}
|
||||||
|
</span>
|
||||||
|
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||||
|
<SettingsBadge badgeType="restartRequired" />
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.csrfProtectionTip)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Tooltip
|
||||||
|
content={intl.formatMessage(
|
||||||
|
messages.csrfProtectionHoverTip
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="csrfProtection"
|
||||||
|
name="csrfProtection"
|
||||||
|
onChange={() => {
|
||||||
|
setFieldValue(
|
||||||
|
'csrfProtection',
|
||||||
|
!values.csrfProtection
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="forceIpv4First" className="checkbox-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.forceIpv4First)}
|
||||||
|
</span>
|
||||||
|
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||||
|
<SettingsBadge badgeType="restartRequired" />
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.forceIpv4FirstTip)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="forceIpv4First"
|
||||||
|
name="forceIpv4First"
|
||||||
|
onChange={() => {
|
||||||
|
setFieldValue('forceIpv4First', !values.forceIpv4First);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="dnsServers" className="checkbox-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.dnsServers)}
|
||||||
|
</span>
|
||||||
|
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||||
|
<SettingsBadge badgeType="restartRequired" />
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.dnsServersTip)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
id="dnsServers"
|
||||||
|
name="dnsServers"
|
||||||
|
type="text"
|
||||||
|
inputMode="url"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.dnsServers &&
|
||||||
|
touched.dnsServers &&
|
||||||
|
typeof errors.dnsServers === 'string' && (
|
||||||
|
<div className="error">{errors.dnsServers}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="proxyEnabled" className="checkbox-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.proxyEnabled)}
|
||||||
|
</span>
|
||||||
|
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||||
|
<SettingsBadge badgeType="restartRequired" />
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="proxyEnabled"
|
||||||
|
name="proxyEnabled"
|
||||||
|
onChange={() => {
|
||||||
|
setFieldValue('proxyEnabled', !values.proxyEnabled);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{values.proxyEnabled && (
|
||||||
|
<>
|
||||||
|
<div className="mr-2 ml-4">
|
||||||
|
<div className="form-row">
|
||||||
|
<label
|
||||||
|
htmlFor="proxyHostname"
|
||||||
|
className="checkbox-label"
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.proxyHostname)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
id="proxyHostname"
|
||||||
|
name="proxyHostname"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.proxyHostname &&
|
||||||
|
touched.proxyHostname &&
|
||||||
|
typeof errors.proxyHostname === 'string' && (
|
||||||
|
<div className="error">
|
||||||
|
{errors.proxyHostname}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="proxyPort" className="checkbox-label">
|
||||||
|
{intl.formatMessage(messages.proxyPort)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
id="proxyPort"
|
||||||
|
name="proxyPort"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.proxyPort &&
|
||||||
|
touched.proxyPort &&
|
||||||
|
typeof errors.proxyPort === 'string' && (
|
||||||
|
<div className="error">{errors.proxyPort}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="proxySsl" className="checkbox-label">
|
||||||
|
{intl.formatMessage(messages.proxySsl)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="proxySsl"
|
||||||
|
name="proxySsl"
|
||||||
|
onChange={() => {
|
||||||
|
setFieldValue('proxySsl', !values.proxySsl);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="proxyUser" className="checkbox-label">
|
||||||
|
{intl.formatMessage(messages.proxyUser)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
id="proxyUser"
|
||||||
|
name="proxyUser"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.proxyUser &&
|
||||||
|
touched.proxyUser &&
|
||||||
|
typeof errors.proxyUser === 'string' && (
|
||||||
|
<div className="error">{errors.proxyUser}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label
|
||||||
|
htmlFor="proxyPassword"
|
||||||
|
className="checkbox-label"
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.proxyPassword)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
id="proxyPassword"
|
||||||
|
name="proxyPassword"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.proxyPassword &&
|
||||||
|
touched.proxyPassword &&
|
||||||
|
typeof errors.proxyPassword === 'string' && (
|
||||||
|
<div className="error">
|
||||||
|
{errors.proxyPassword}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label
|
||||||
|
htmlFor="proxyBypassFilter"
|
||||||
|
className="checkbox-label"
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.proxyBypassFilter)}
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.proxyBypassFilterTip)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
id="proxyBypassFilter"
|
||||||
|
name="proxyBypassFilter"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.proxyBypassFilter &&
|
||||||
|
touched.proxyBypassFilter &&
|
||||||
|
typeof errors.proxyBypassFilter === 'string' && (
|
||||||
|
<div className="error">
|
||||||
|
{errors.proxyBypassFilter}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label
|
||||||
|
htmlFor="proxyBypassLocalAddresses"
|
||||||
|
className="checkbox-label"
|
||||||
|
>
|
||||||
|
{intl.formatMessage(
|
||||||
|
messages.proxyBypassLocalAddresses
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="proxyBypassLocalAddresses"
|
||||||
|
name="proxyBypassLocalAddresses"
|
||||||
|
onChange={() => {
|
||||||
|
setFieldValue(
|
||||||
|
'proxyBypassLocalAddresses',
|
||||||
|
!values.proxyBypassLocalAddresses
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="actions">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
|
<ArrowDownOnSquareIcon />
|
||||||
|
<span>
|
||||||
|
{isSubmitting
|
||||||
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
: intl.formatMessage(globalMessages.save)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsMain;
|
||||||
@@ -915,16 +915,9 @@
|
|||||||
"components.Settings.SettingsMain.applicationurl": "Application URL",
|
"components.Settings.SettingsMain.applicationurl": "Application URL",
|
||||||
"components.Settings.SettingsMain.cacheImages": "Enable Image Caching",
|
"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.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.discoverRegion": "Discover Region",
|
||||||
"components.Settings.SettingsMain.discoverRegionTip": "Filter content by regional availability",
|
"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.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.general": "General",
|
||||||
"components.Settings.SettingsMain.generalsettings": "General Settings",
|
"components.Settings.SettingsMain.generalsettings": "General Settings",
|
||||||
"components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",
|
"components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",
|
||||||
@@ -933,27 +926,39 @@
|
|||||||
"components.Settings.SettingsMain.originallanguage": "Discover Language",
|
"components.Settings.SettingsMain.originallanguage": "Discover Language",
|
||||||
"components.Settings.SettingsMain.originallanguageTip": "Filter content by original language",
|
"components.Settings.SettingsMain.originallanguageTip": "Filter content by original language",
|
||||||
"components.Settings.SettingsMain.partialRequestsEnabled": "Allow Partial Series Requests",
|
"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.streamingRegion": "Streaming Region",
|
||||||
"components.Settings.SettingsMain.streamingRegionTip": "Show streaming sites by regional availability",
|
"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.toastApiKeyFailure": "Something went wrong while generating a new API key.",
|
||||||
"components.Settings.SettingsMain.toastApiKeySuccess": "New API key generated successfully!",
|
"components.Settings.SettingsMain.toastApiKeySuccess": "New API key generated successfully!",
|
||||||
"components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.",
|
"components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.",
|
||||||
"components.Settings.SettingsMain.toastSettingsSuccess": "Settings saved successfully!",
|
"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.validationApplicationTitle": "You must provide an application title",
|
||||||
"components.Settings.SettingsMain.validationApplicationUrl": "You must provide a valid URL",
|
"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.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.defaultPermissions": "Default Permissions",
|
||||||
"components.Settings.SettingsUsers.defaultPermissionsTip": "Initial permissions assigned to new users",
|
"components.Settings.SettingsUsers.defaultPermissionsTip": "Initial permissions assigned to new users",
|
||||||
"components.Settings.SettingsUsers.localLogin": "Enable Local Sign-In",
|
"components.Settings.SettingsUsers.localLogin": "Enable Local Sign-In",
|
||||||
@@ -1069,6 +1074,7 @@
|
|||||||
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
|
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
|
||||||
"components.Settings.menuJobs": "Jobs & Cache",
|
"components.Settings.menuJobs": "Jobs & Cache",
|
||||||
"components.Settings.menuLogs": "Logs",
|
"components.Settings.menuLogs": "Logs",
|
||||||
|
"components.Settings.menuNetwork": "Network",
|
||||||
"components.Settings.menuNotifications": "Notifications",
|
"components.Settings.menuNotifications": "Notifications",
|
||||||
"components.Settings.menuPlexSettings": "Plex",
|
"components.Settings.menuPlexSettings": "Plex",
|
||||||
"components.Settings.menuServices": "Services",
|
"components.Settings.menuServices": "Services",
|
||||||
|
|||||||
16
src/pages/settings/network.tsx
Normal file
16
src/pages/settings/network.tsx
Normal file
@@ -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 (
|
||||||
|
<SettingsLayout>
|
||||||
|
<SettingsNetwork />
|
||||||
|
</SettingsLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsNetworkPage;
|
||||||
Reference in New Issue
Block a user