feat: show alert/prompt when settings changes require restart (#2401)

* fix: correct 'StatusChecker' typo

* feat: add restart required check to StatusChecker

* fix(perms): remove MANAGE_SETTINGS permission

* fix: allow alert to be dismissed

* fix(lang): add missing string in SettingsServices

* fix(frontend): fix modal icon border

* fix(frontend): un-dismiss alert if setting reverted not require server restart

* fix(backend): restart flag only needs to track main settings

* fix: rebase issue

* refactor: appease Prettier

* refactor: swap settings badge order

* fix: type import for MainSettings

* test: add cypress test for restart prompt
pull/2917/head
TheCatLady 2 years ago committed by GitHub
parent 70dc4c4b3b
commit f3e56da3b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,32 @@
describe('General Settings', () => {
beforeEach(() => {
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
});
it('opens the settings page from the home page', () => {
cy.visit('/');
cy.get('[data-testid=sidebar-toggle]').click();
cy.get('[data-testid=sidebar-menu-settings-mobile]').click();
cy.get('.heading').should('contain', 'General Settings');
});
it('modifies setting that requires restart', () => {
cy.visit('/settings');
cy.get('#trustProxy').click();
cy.get('form').submit();
cy.get('[data-testid=modal-title]').should(
'contain',
'Server Restart Required'
);
cy.get('[data-testid=modal-ok-button]').click();
cy.get('[data-testid=modal-title]').should('not.exist');
cy.get('[type=checkbox]#trustProxy').click();
cy.get('form').submit();
cy.get('[data-testid=modal-title]').should('not.exist');
});
});

@ -15,7 +15,7 @@ describe('User List', () => {
cy.get('[data-testid=sidebar-toggle]').click(); cy.get('[data-testid=sidebar-toggle]').click();
cy.get('[data-testid=sidebar-menu-users-mobile]').click(); cy.get('[data-testid=sidebar-menu-users-mobile]').click();
cy.get('[data-testid=page-header').should('contain', 'User List'); cy.get('[data-testid=page-header]').should('contain', 'User List');
}); });
it('can find the admin user and friend user in the user list', () => { it('can find the admin user and friend user in the user list', () => {
@ -30,7 +30,7 @@ describe('User List', () => {
cy.contains('Create Local User').click(); cy.contains('Create Local User').click();
cy.get('[data-testid=modal-title').should('contain', 'Create Local User'); cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
cy.get('#displayName').type(testUser.displayName); cy.get('#displayName').type(testUser.displayName);
cy.get('#email').type(testUser.emailAddress); cy.get('#email').type(testUser.emailAddress);
@ -38,7 +38,7 @@ describe('User List', () => {
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user'); cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
cy.get('[data-testid=modal-ok-button').click(); cy.get('[data-testid=modal-ok-button]').click();
cy.wait('@user'); cy.wait('@user');
// Wait a little longer for the user list to fully re-render // Wait a little longer for the user list to fully re-render
@ -58,7 +58,7 @@ describe('User List', () => {
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user'); cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
cy.get('[data-testid=modal-ok-button').should('contain', 'Delete').click(); cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();
cy.wait('@user'); cy.wait('@user');
cy.wait(1000); cy.wait(1000);

@ -1793,14 +1793,14 @@ components:
paths: paths:
/status: /status:
get: get:
summary: Get Overseerr version summary: Get Overseerr status
description: Returns the current Overseerr version in a JSON object. description: Returns the current Overseerr status in a JSON object.
security: [] security: []
tags: tags:
- public - public
responses: responses:
'200': '200':
description: Returned version description: Returned status
content: content:
application/json: application/json:
schema: schema:
@ -1811,6 +1811,12 @@ paths:
example: 1.0.0 example: 1.0.0
commitTag: commitTag:
type: string type: string
updateAvailable:
type: boolean
commitsBehind:
type: number
restartRequired:
type: boolean
/status/appdata: /status/appdata:
get: get:
summary: Get application data volume status summary: Get application data volume status

@ -31,6 +31,7 @@ import { getSettings } from './lib/settings';
import logger from './logger'; import logger from './logger';
import routes from './routes'; import routes from './routes';
import { getAppVersion } from './utils/appVersion'; import { getAppVersion } from './utils/appVersion';
import restartFlag from './utils/restartFlag';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
@ -53,6 +54,7 @@ app
// Load Settings // Load Settings
const settings = getSettings().load(); const settings = getSettings().load();
restartFlag.initializeSettings(settings.main);
// Migrate library types // Migrate library types
if ( if (

@ -56,4 +56,5 @@ export interface StatusResponse {
commitTag: string; commitTag: string;
updateAvailable: boolean; updateAvailable: boolean;
commitsBehind: number; commitsBehind: number;
restartRequired: boolean;
} }

@ -1,7 +1,6 @@
export enum Permission { export enum Permission {
NONE = 0, NONE = 0,
ADMIN = 2, ADMIN = 2,
MANAGE_SETTINGS = 4,
MANAGE_USERS = 8, MANAGE_USERS = 8,
MANAGE_REQUESTS = 16, MANAGE_REQUESTS = 16,
REQUEST = 32, REQUEST = 32,

@ -14,6 +14,7 @@ import { mapProductionCompany } from '../models/Movie';
import { mapNetwork } from '../models/Tv'; import { mapNetwork } from '../models/Tv';
import { appDataPath, appDataStatus } from '../utils/appDataVolume'; import { appDataPath, appDataStatus } from '../utils/appDataVolume';
import { getAppVersion, getCommitTag } from '../utils/appVersion'; import { getAppVersion, getCommitTag } from '../utils/appVersion';
import restartFlag from '../utils/restartFlag';
import { isPerson } from '../utils/typeHelpers'; import { isPerson } from '../utils/typeHelpers';
import authRoutes from './auth'; import authRoutes from './auth';
import collectionRoutes from './collection'; import collectionRoutes from './collection';
@ -78,6 +79,7 @@ router.get<unknown, StatusResponse>('/status', async (req, res) => {
commitTag: getCommitTag(), commitTag: getCommitTag(),
updateAvailable, updateAvailable,
commitsBehind, commitsBehind,
restartRequired: restartFlag.isSet(),
}); });
}); });
@ -100,11 +102,7 @@ router.get('/settings/public', async (req, res) => {
return res.status(200).json(settings.fullPublicSettings); return res.status(200).json(settings.fullPublicSettings);
} }
}); });
router.use( router.use('/settings', isAuthenticated(Permission.ADMIN), settingsRoutes);
'/settings',
isAuthenticated(Permission.MANAGE_SETTINGS),
settingsRoutes
);
router.use('/search', isAuthenticated(), searchRoutes); router.use('/search', isAuthenticated(), searchRoutes);
router.use('/discover', isAuthenticated(), discoverRoutes); router.use('/discover', isAuthenticated(), discoverRoutes);
router.use('/request', isAuthenticated(), requestRoutes); router.use('/request', isAuthenticated(), requestRoutes);

@ -258,12 +258,7 @@ export const canMakePermissionsChange = (
user?: User user?: User
): boolean => ): boolean =>
// Only let the owner grant admin privileges // Only let the owner grant admin privileges
!(hasPermission(Permission.ADMIN, permissions) && user?.id !== 1) || !(hasPermission(Permission.ADMIN, permissions) && user?.id !== 1);
// Only let users with the manage settings permission, grant the same permission
!(
hasPermission(Permission.MANAGE_SETTINGS, permissions) &&
!hasPermission(Permission.MANAGE_SETTINGS, user?.permissions ?? 0)
);
router.put< router.put<
Record<string, never>, Record<string, never>,

@ -0,0 +1,23 @@
import type { MainSettings } from '../lib/settings';
import { getSettings } from '../lib/settings';
class RestartFlag {
private settings: MainSettings;
public initializeSettings(settings: MainSettings): void {
this.settings = { ...settings };
}
public isSet(): boolean {
const settings = getSettings().main;
return (
this.settings.csrfProtection !== settings.csrfProtection ||
this.settings.trustProxy !== settings.trustProxy
);
}
}
const restartFlag = new RestartFlag();
export default restartFlag;

@ -130,7 +130,7 @@ const Modal: React.FC<ModalProps> = ({
/> />
</div> </div>
)} )}
<div className="relative overflow-x-hidden sm:flex sm:items-center"> <div className="relative overflow-x-hidden p-0.5 sm:flex sm:items-center">
{iconSvg && <div className="modal-icon">{iconSvg}</div>} {iconSvg && <div className="modal-icon">{iconSvg}</div>}
<div <div
className={`mt-3 truncate text-center text-white sm:mt-0 sm:text-left ${ className={`mt-3 truncate text-center text-white sm:mt-0 sm:text-left ${
@ -149,7 +149,11 @@ const Modal: React.FC<ModalProps> = ({
</div> </div>
</div> </div>
{children && ( {children && (
<div className="relative mt-4 text-sm leading-5 text-gray-300"> <div
className={`relative mt-4 text-sm leading-5 text-gray-300 ${
!(onCancel || onOk || onSecondary || onTertiary) ? 'mb-3' : ''
}`}
>
{children} {children}
</div> </div>
)} )}

@ -80,7 +80,8 @@ const SidebarLinks: SidebarLinkProps[] = [
messagesKey: 'settings', messagesKey: 'settings',
svgIcon: <CogIcon className="mr-3 h-6 w-6" />, svgIcon: <CogIcon className="mr-3 h-6 w-6" />,
activeRegExp: /^\/settings/, activeRegExp: /^\/settings/,
requiredPermission: Permission.MANAGE_SETTINGS, requiredPermission: Permission.ADMIN,
dataTestId: 'sidebar-menu-settings',
}, },
]; ];

@ -12,9 +12,6 @@ export const messages = defineMessages({
users: 'Manage Users', users: 'Manage Users',
usersDescription: usersDescription:
'Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.', 'Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.',
settings: 'Manage Settings',
settingsDescription:
'Grant permission to modify global settings. A user must have this permission to grant it to others.',
managerequests: 'Manage Requests', managerequests: 'Manage Requests',
managerequestsDescription: managerequestsDescription:
'Grant permission to manage media requests. All requests made by a user with this permission will be automatically approved.', 'Grant permission to manage media requests. All requests made by a user with this permission will be automatically approved.',
@ -88,12 +85,6 @@ export const PermissionEdit: React.FC<PermissionEditProps> = ({
description: intl.formatMessage(messages.adminDescription), description: intl.formatMessage(messages.adminDescription),
permission: Permission.ADMIN, permission: Permission.ADMIN,
}, },
{
id: 'settings',
name: intl.formatMessage(messages.settings),
description: intl.formatMessage(messages.settingsDescription),
permission: Permission.MANAGE_SETTINGS,
},
{ {
id: 'users', id: 'users',
name: intl.formatMessage(messages.users), name: intl.formatMessage(messages.users),

@ -67,14 +67,9 @@ const PermissionOption: React.FC<PermissionOptionProps> = ({
} }
if ( if (
// Non-Admin users cannot modify the Admin permission // Only the owner can modify the Admin permission
(actingUser && actingUser?.id !== 1 &&
!hasPermission(Permission.ADMIN, actingUser.permissions) && option.permission === Permission.ADMIN
option.permission === Permission.ADMIN) ||
// Users without the Manage Settings permission cannot modify/grant that permission
(actingUser &&
!hasPermission(Permission.MANAGE_SETTINGS, actingUser.permissions) &&
option.permission === Permission.MANAGE_SETTINGS)
) { ) {
disabled = true; disabled = true;
} }

@ -41,8 +41,7 @@ const messages = defineMessages({
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', csrfProtection: 'Enable CSRF Protection',
csrfProtectionTip: csrfProtectionTip: 'Set external API access to read-only (requires HTTPS)',
'Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)',
csrfProtectionHoverTip: csrfProtectionHoverTip:
'Do NOT enable this setting unless you understand what you are doing!', 'Do NOT enable this setting unless you understand what you are doing!',
cacheImages: 'Enable Image Caching', cacheImages: 'Enable Image Caching',
@ -50,7 +49,7 @@ const messages = defineMessages({
'Optimize and store all images locally (consumes a significant amount of disk space)', 'Optimize and store all images locally (consumes a significant amount of disk space)',
trustProxy: 'Enable Proxy Support', trustProxy: 'Enable Proxy Support',
trustProxyTip: trustProxyTip:
'Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)', 'Allow Overseerr 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',
@ -151,6 +150,7 @@ const SettingsMain: React.FC = () => {
trustProxy: values.trustProxy, trustProxy: values.trustProxy,
}); });
mutate('/api/v1/settings/public'); mutate('/api/v1/settings/public');
mutate('/api/v1/status');
if (setLocale) { if (setLocale) {
setLocale( setLocale(
@ -252,7 +252,12 @@ const SettingsMain: React.FC = () => {
</div> </div>
<div className="form-row"> <div className="form-row">
<label htmlFor="trustProxy" className="checkbox-label"> <label htmlFor="trustProxy" className="checkbox-label">
<span>{intl.formatMessage(messages.trustProxy)}</span> <span className="mr-2">
{intl.formatMessage(messages.trustProxy)}
</span>
<Badge badgeType="primary">
{intl.formatMessage(globalMessages.restartRequired)}
</Badge>
<span className="label-tip"> <span className="label-tip">
{intl.formatMessage(messages.trustProxyTip)} {intl.formatMessage(messages.trustProxyTip)}
</span> </span>
@ -273,9 +278,12 @@ const SettingsMain: React.FC = () => {
<span className="mr-2"> <span className="mr-2">
{intl.formatMessage(messages.csrfProtection)} {intl.formatMessage(messages.csrfProtection)}
</span> </span>
<Badge badgeType="danger"> <Badge badgeType="danger" className="mr-2">
{intl.formatMessage(globalMessages.advanced)} {intl.formatMessage(globalMessages.advanced)}
</Badge> </Badge>
<Badge badgeType="primary">
{intl.formatMessage(globalMessages.restartRequired)}
</Badge>
<span className="label-tip"> <span className="label-tip">
{intl.formatMessage(messages.csrfProtectionTip)} {intl.formatMessage(messages.csrfProtectionTip)}
</span> </span>

@ -43,6 +43,7 @@ const messages = defineMessages({
'A 4K {serverType} server must be marked as default in order to enable users to submit 4K {mediaType} requests.', 'A 4K {serverType} server must be marked as default in order to enable users to submit 4K {mediaType} requests.',
mediaTypeMovie: 'movie', mediaTypeMovie: 'movie',
mediaTypeSeries: 'series', mediaTypeSeries: 'series',
deleteServer: 'Delete {serverType} Server',
}); });
interface ServerInstanceProps { interface ServerInstanceProps {
@ -256,7 +257,7 @@ const SettingsServices: React.FC = () => {
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Modal <Modal
okText="Delete" okText={intl.formatMessage(globalMessages.delete)}
okButtonType="danger" okButtonType="danger"
onOk={() => deleteServer()} onOk={() => deleteServer()}
onCancel={() => onCancel={() =>
@ -266,7 +267,10 @@ const SettingsServices: React.FC = () => {
type: 'radarr', type: 'radarr',
}) })
} }
title="Delete Server" title={intl.formatMessage(messages.deleteServer, {
serverType:
deleteServerModal.type === 'radarr' ? 'Radarr' : 'Sonarr',
})}
iconSvg={<TrashIcon />} iconSvg={<TrashIcon />}
> >
{intl.formatMessage(messages.deleteserverconfirm)} {intl.formatMessage(messages.deleteserverconfirm)}

@ -1,54 +0,0 @@
import { SparklesIcon } from '@heroicons/react/outline';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { StatusResponse } from '../../../server/interfaces/api/settingsInterfaces';
import Modal from '../Common/Modal';
import Transition from '../Transition';
const messages = defineMessages({
newversionavailable: 'Application Update',
newversionDescription:
'Overseerr has been updated! Please click the button below to reload the page.',
reloadOverseerr: 'Reload',
});
const StatusChecker: React.FC = () => {
const intl = useIntl();
const { data, error } = useSWR<StatusResponse>('/api/v1/status', {
refreshInterval: 60 * 1000,
});
if (!data && !error) {
return null;
}
if (!data) {
return null;
}
return (
<Transition
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="opacity-100 transition duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
appear
show={data.commitTag !== process.env.commitTag}
>
<Modal
iconSvg={<SparklesIcon />}
title={intl.formatMessage(messages.newversionavailable)}
onOk={() => location.reload()}
okText={intl.formatMessage(messages.reloadOverseerr)}
backgroundClickable={false}
>
{intl.formatMessage(messages.newversionDescription)}
</Modal>
</Transition>
);
};
export default StatusChecker;

@ -0,0 +1,94 @@
import { RefreshIcon, SparklesIcon } from '@heroicons/react/outline';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import type { StatusResponse } from '../../../server/interfaces/api/settingsInterfaces';
import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import Modal from '../Common/Modal';
import Transition from '../Transition';
const messages = defineMessages({
appUpdated: '{applicationTitle} Updated',
appUpdatedDescription:
'Please click the button below to reload the application.',
reloadApp: 'Reload {applicationTitle}',
restartRequired: 'Server Restart Required',
restartRequiredDescription:
'Please restart the server to apply the updated settings.',
});
const StatusChecker: React.FC = () => {
const intl = useIntl();
const settings = useSettings();
const { hasPermission } = useUser();
const { data, error } = useSWR<StatusResponse>('/api/v1/status', {
refreshInterval: 60 * 1000,
});
const [alertDismissed, setAlertDismissed] = useState(false);
useEffect(() => {
if (!data?.restartRequired) {
setAlertDismissed(false);
}
}, [data?.restartRequired]);
if (!data && !error) {
return null;
}
if (!data) {
return null;
}
return (
<Transition
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="opacity-100 transition duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
appear
show={
!alertDismissed &&
((hasPermission(Permission.ADMIN) && data.restartRequired) ||
data.commitTag !== process.env.commitTag)
}
>
{hasPermission(Permission.ADMIN) && data.restartRequired ? (
<Modal
iconSvg={<RefreshIcon />}
title={intl.formatMessage(messages.restartRequired)}
backgroundClickable={false}
onOk={() => {
setAlertDismissed(true);
if (data.commitTag !== process.env.commitTag) {
location.reload();
}
}}
okText={intl.formatMessage(globalMessages.close)}
>
{intl.formatMessage(messages.restartRequiredDescription)}
</Modal>
) : (
<Modal
iconSvg={<SparklesIcon />}
title={intl.formatMessage(messages.appUpdated, {
applicationTitle: settings.currentSettings.applicationTitle,
})}
onOk={() => location.reload()}
okText={intl.formatMessage(messages.reloadApp, {
applicationTitle: settings.currentSettings.applicationTitle,
})}
backgroundClickable={false}
>
{intl.formatMessage(messages.appUpdatedDescription)}
</Modal>
)}
</Transition>
);
};
export default StatusChecker;

@ -336,7 +336,7 @@ const UserList: React.FC = () => {
type="warning" type="warning"
/> />
)} )}
{currentHasPermission(Permission.MANAGE_SETTINGS) && {currentHasPermission(Permission.ADMIN) &&
!passwordGenerationEnabled && ( !passwordGenerationEnabled && (
<Alert <Alert
title={intl.formatMessage( title={intl.formatMessage(

@ -54,10 +54,7 @@ const UserSettings: React.FC = ({ children }) => {
regex: /\/settings\/password/, regex: /\/settings\/password/,
hidden: hidden:
(!settings.currentSettings.localLogin && (!settings.currentSettings.localLogin &&
!hasPermission( !hasPermission(Permission.ADMIN, currentUser?.permissions ?? 0)) ||
Permission.MANAGE_SETTINGS,
currentUser?.permissions ?? 0
)) ||
(currentUser?.id !== 1 && (currentUser?.id !== 1 &&
currentUser?.id !== user?.id && currentUser?.id !== user?.id &&
hasPermission(Permission.ADMIN, user?.permissions ?? 0)), hasPermission(Permission.ADMIN, user?.permissions ?? 0)),

@ -43,6 +43,7 @@ const globalMessages = defineMessages({
all: 'All', all: 'All',
experimental: 'Experimental', experimental: 'Experimental',
advanced: 'Advanced', advanced: 'Advanced',
restartRequired: 'Restart Required',
loading: 'Loading…', loading: 'Loading…',
settings: 'Settings', settings: 'Settings',
usersettings: 'User Settings', usersettings: 'User Settings',

@ -245,8 +245,6 @@
"components.PermissionEdit.requestMoviesDescription": "Grant permission to submit requests for non-4K movies.", "components.PermissionEdit.requestMoviesDescription": "Grant permission to submit requests for non-4K movies.",
"components.PermissionEdit.requestTv": "Request Series", "components.PermissionEdit.requestTv": "Request Series",
"components.PermissionEdit.requestTvDescription": "Grant permission to submit requests for non-4K series.", "components.PermissionEdit.requestTvDescription": "Grant permission to submit requests for non-4K series.",
"components.PermissionEdit.settings": "Manage Settings",
"components.PermissionEdit.settingsDescription": "Grant permission to modify global settings. A user must have this permission to grant it to others.",
"components.PermissionEdit.users": "Manage Users", "components.PermissionEdit.users": "Manage Users",
"components.PermissionEdit.usersDescription": "Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.", "components.PermissionEdit.usersDescription": "Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.",
"components.PermissionEdit.viewissues": "View Issues", "components.PermissionEdit.viewissues": "View Issues",
@ -711,10 +709,11 @@
"components.Settings.copied": "Copied API key to clipboard.", "components.Settings.copied": "Copied API key to clipboard.",
"components.Settings.csrfProtection": "Enable CSRF Protection", "components.Settings.csrfProtection": "Enable CSRF Protection",
"components.Settings.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!", "components.Settings.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
"components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)", "components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
"components.Settings.currentlibrary": "Current Library: {name}", "components.Settings.currentlibrary": "Current Library: {name}",
"components.Settings.default": "Default", "components.Settings.default": "Default",
"components.Settings.default4k": "Default 4K", "components.Settings.default4k": "Default 4K",
"components.Settings.deleteServer": "Delete {serverType} Server",
"components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?", "components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?",
"components.Settings.email": "Email", "components.Settings.email": "Email",
"components.Settings.enablessl": "Use SSL", "components.Settings.enablessl": "Use SSL",
@ -789,7 +788,7 @@
"components.Settings.toastTautulliSettingsFailure": "Something went wrong while saving Tautulli settings.", "components.Settings.toastTautulliSettingsFailure": "Something went wrong while saving Tautulli settings.",
"components.Settings.toastTautulliSettingsSuccess": "Tautulli settings saved successfully!", "components.Settings.toastTautulliSettingsSuccess": "Tautulli settings saved successfully!",
"components.Settings.trustProxy": "Enable Proxy Support", "components.Settings.trustProxy": "Enable Proxy Support",
"components.Settings.trustProxyTip": "Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)", "components.Settings.trustProxyTip": "Allow Overseerr to correctly register client IP addresses behind a proxy",
"components.Settings.urlBase": "URL Base", "components.Settings.urlBase": "URL Base",
"components.Settings.validationApiKey": "You must provide an API key", "components.Settings.validationApiKey": "You must provide an API key",
"components.Settings.validationApplicationTitle": "You must provide an application title", "components.Settings.validationApplicationTitle": "You must provide an application title",
@ -818,9 +817,11 @@
"components.Setup.welcome": "Welcome to Overseerr", "components.Setup.welcome": "Welcome to Overseerr",
"components.StatusBadge.status": "{status}", "components.StatusBadge.status": "{status}",
"components.StatusBadge.status4k": "4K {status}", "components.StatusBadge.status4k": "4K {status}",
"components.StatusChacker.newversionDescription": "Overseerr has been updated! Please click the button below to reload the page.", "components.StatusChecker.appUpdated": "{applicationTitle} Updated",
"components.StatusChacker.newversionavailable": "Application Update", "components.StatusChecker.appUpdatedDescription": "Please click the button below to reload the application.",
"components.StatusChacker.reloadOverseerr": "Reload", "components.StatusChecker.reloadApp": "Reload {applicationTitle}",
"components.StatusChecker.restartRequired": "Server Restart Required",
"components.StatusChecker.restartRequiredDescription": "Please restart the server to apply the updated settings.",
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast", "components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
"components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew", "components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew",
"components.TvDetails.anime": "Anime", "components.TvDetails.anime": "Anime",
@ -1020,6 +1021,7 @@
"i18n.requested": "Requested", "i18n.requested": "Requested",
"i18n.requesting": "Requesting…", "i18n.requesting": "Requesting…",
"i18n.resolved": "Resolved", "i18n.resolved": "Resolved",
"i18n.restartRequired": "Restart Required",
"i18n.resultsperpage": "Display {pageSize} results per page", "i18n.resultsperpage": "Display {pageSize} results per page",
"i18n.retry": "Retry", "i18n.retry": "Retry",
"i18n.retrying": "Retrying…", "i18n.retrying": "Retrying…",

@ -11,7 +11,7 @@ import Layout from '../components/Layout';
import LoadingBar from '../components/LoadingBar'; import LoadingBar from '../components/LoadingBar';
import PWAHeader from '../components/PWAHeader'; import PWAHeader from '../components/PWAHeader';
import ServiceWorkerSetup from '../components/ServiceWorkerSetup'; import ServiceWorkerSetup from '../components/ServiceWorkerSetup';
import StatusChecker from '../components/StatusChacker'; import StatusChecker from '../components/StatusChecker';
import Toast from '../components/Toast'; import Toast from '../components/Toast';
import ToastContainer from '../components/ToastContainer'; import ToastContainer from '../components/ToastContainer';
import { InteractionProvider } from '../context/InteractionContext'; import { InteractionProvider } from '../context/InteractionContext';

@ -6,7 +6,7 @@ import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser'; import { Permission } from '../../hooks/useUser';
const SettingsAboutPage: NextPage = () => { const SettingsAboutPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsAbout /> <SettingsAbout />

@ -6,7 +6,7 @@ import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser'; import { Permission } from '../../hooks/useUser';
const SettingsPage: NextPage = () => { const SettingsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsMain /> <SettingsMain />

@ -1,12 +1,12 @@
import React from 'react';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import SettingsLayout from '../../components/Settings/SettingsLayout'; import React from 'react';
import SettingsJobs from '../../components/Settings/SettingsJobsCache'; import SettingsJobs from '../../components/Settings/SettingsJobsCache';
import { Permission } from '../../hooks/useUser'; import SettingsLayout from '../../components/Settings/SettingsLayout';
import useRouteGuard from '../../hooks/useRouteGuard'; import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser';
const SettingsMainPage: NextPage = () => { const SettingsMainPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsJobs /> <SettingsJobs />

@ -6,7 +6,7 @@ import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser'; import { Permission } from '../../hooks/useUser';
const SettingsLogsPage: NextPage = () => { const SettingsLogsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsLogs /> <SettingsLogs />

@ -2,11 +2,11 @@ import React from 'react';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import SettingsLayout from '../../components/Settings/SettingsLayout'; import SettingsLayout from '../../components/Settings/SettingsLayout';
import SettingsMain from '../../components/Settings/SettingsMain'; import SettingsMain from '../../components/Settings/SettingsMain';
import { Permission } from '../../hooks/useUser';
import useRouteGuard from '../../hooks/useRouteGuard'; import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser';
const SettingsMainPage: NextPage = () => { const SettingsMainPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsMain /> <SettingsMain />

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsPage: NextPage = () => { const NotificationsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsPage: NextPage = () => { const NotificationsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsPage: NextPage = () => { const NotificationsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsPage: NextPage = () => { const NotificationsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsPage: NextPage = () => { const NotificationsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsPage: NextPage = () => { const NotificationsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsSlackPage: NextPage = () => { const NotificationsSlackPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsPage: NextPage = () => { const NotificationsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsPage: NextPage = () => { const NotificationsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -7,7 +7,7 @@ import useRouteGuard from '../../../hooks/useRouteGuard';
import { Permission } from '../../../hooks/useUser'; import { Permission } from '../../../hooks/useUser';
const NotificationsWebPushPage: NextPage = () => { const NotificationsWebPushPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsNotifications> <SettingsNotifications>

@ -1,12 +1,12 @@
import React from 'react';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import React from 'react';
import SettingsLayout from '../../components/Settings/SettingsLayout'; import SettingsLayout from '../../components/Settings/SettingsLayout';
import SettingsPlex from '../../components/Settings/SettingsPlex'; import SettingsPlex from '../../components/Settings/SettingsPlex';
import { Permission } from '../../hooks/useUser';
import useRouteGuard from '../../hooks/useRouteGuard'; import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser';
const PlexSettingsPage: NextPage = () => { const PlexSettingsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsPlex /> <SettingsPlex />

@ -1,12 +1,12 @@
import React from 'react';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import React from 'react';
import SettingsLayout from '../../components/Settings/SettingsLayout'; import SettingsLayout from '../../components/Settings/SettingsLayout';
import SettingsServices from '../../components/Settings/SettingsServices'; import SettingsServices from '../../components/Settings/SettingsServices';
import { Permission } from '../../hooks/useUser';
import useRouteGuard from '../../hooks/useRouteGuard'; import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser';
const ServicesSettingsPage: NextPage = () => { const ServicesSettingsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsServices /> <SettingsServices />

@ -2,11 +2,11 @@ import React from 'react';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import SettingsLayout from '../../components/Settings/SettingsLayout'; import SettingsLayout from '../../components/Settings/SettingsLayout';
import SettingsUsers from '../../components/Settings/SettingsUsers'; import SettingsUsers from '../../components/Settings/SettingsUsers';
import { Permission } from '../../hooks/useUser';
import useRouteGuard from '../../hooks/useRouteGuard'; import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser';
const SettingsUsersPage: NextPage = () => { const SettingsUsersPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS); useRouteGuard(Permission.ADMIN);
return ( return (
<SettingsLayout> <SettingsLayout>
<SettingsUsers /> <SettingsUsers />

Loading…
Cancel
Save