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-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', () => {
@ -30,7 +30,7 @@ describe('User List', () => {
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('#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.get('[data-testid=modal-ok-button').click();
cy.get('[data-testid=modal-ok-button]').click();
cy.wait('@user');
// 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.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(1000);

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

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

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

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

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

@ -258,12 +258,7 @@ export const canMakePermissionsChange = (
user?: User
): boolean =>
// Only let the owner grant admin privileges
!(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)
);
!(hasPermission(Permission.ADMIN, permissions) && user?.id !== 1);
router.put<
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 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>}
<div
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>
{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}
</div>
)}

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

@ -12,9 +12,6 @@ export const messages = defineMessages({
users: 'Manage Users',
usersDescription:
'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',
managerequestsDescription:
'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),
permission: Permission.ADMIN,
},
{
id: 'settings',
name: intl.formatMessage(messages.settings),
description: intl.formatMessage(messages.settingsDescription),
permission: Permission.MANAGE_SETTINGS,
},
{
id: 'users',
name: intl.formatMessage(messages.users),

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

@ -41,8 +41,7 @@ const messages = defineMessages({
toastSettingsFailure: 'Something went wrong while saving settings.',
hideAvailable: 'Hide Available Media',
csrfProtection: 'Enable CSRF Protection',
csrfProtectionTip:
'Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)',
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',
@ -50,7 +49,7 @@ const messages = defineMessages({
'Optimize and store all images locally (consumes a significant amount of disk space)',
trustProxy: 'Enable Proxy Support',
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',
validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
@ -151,6 +150,7 @@ const SettingsMain: React.FC = () => {
trustProxy: values.trustProxy,
});
mutate('/api/v1/settings/public');
mutate('/api/v1/status');
if (setLocale) {
setLocale(
@ -252,7 +252,12 @@ const SettingsMain: React.FC = () => {
</div>
<div className="form-row">
<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">
{intl.formatMessage(messages.trustProxyTip)}
</span>
@ -273,9 +278,12 @@ const SettingsMain: React.FC = () => {
<span className="mr-2">
{intl.formatMessage(messages.csrfProtection)}
</span>
<Badge badgeType="danger">
<Badge badgeType="danger" className="mr-2">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<Badge badgeType="primary">
{intl.formatMessage(globalMessages.restartRequired)}
</Badge>
<span className="label-tip">
{intl.formatMessage(messages.csrfProtectionTip)}
</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.',
mediaTypeMovie: 'movie',
mediaTypeSeries: 'series',
deleteServer: 'Delete {serverType} Server',
});
interface ServerInstanceProps {
@ -256,7 +257,7 @@ const SettingsServices: React.FC = () => {
leaveTo="opacity-0"
>
<Modal
okText="Delete"
okText={intl.formatMessage(globalMessages.delete)}
okButtonType="danger"
onOk={() => deleteServer()}
onCancel={() =>
@ -266,7 +267,10 @@ const SettingsServices: React.FC = () => {
type: 'radarr',
})
}
title="Delete Server"
title={intl.formatMessage(messages.deleteServer, {
serverType:
deleteServerModal.type === 'radarr' ? 'Radarr' : 'Sonarr',
})}
iconSvg={<TrashIcon />}
>
{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"
/>
)}
{currentHasPermission(Permission.MANAGE_SETTINGS) &&
{currentHasPermission(Permission.ADMIN) &&
!passwordGenerationEnabled && (
<Alert
title={intl.formatMessage(

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

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

@ -245,8 +245,6 @@
"components.PermissionEdit.requestMoviesDescription": "Grant permission to submit requests for non-4K movies.",
"components.PermissionEdit.requestTv": "Request 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.usersDescription": "Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.",
"components.PermissionEdit.viewissues": "View Issues",
@ -711,10 +709,11 @@
"components.Settings.copied": "Copied API key to clipboard.",
"components.Settings.csrfProtection": "Enable CSRF Protection",
"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.default": "Default",
"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.email": "Email",
"components.Settings.enablessl": "Use SSL",
@ -789,7 +788,7 @@
"components.Settings.toastTautulliSettingsFailure": "Something went wrong while saving Tautulli settings.",
"components.Settings.toastTautulliSettingsSuccess": "Tautulli settings saved successfully!",
"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.validationApiKey": "You must provide an API key",
"components.Settings.validationApplicationTitle": "You must provide an application title",
@ -818,9 +817,11 @@
"components.Setup.welcome": "Welcome to Overseerr",
"components.StatusBadge.status": "{status}",
"components.StatusBadge.status4k": "4K {status}",
"components.StatusChacker.newversionDescription": "Overseerr has been updated! Please click the button below to reload the page.",
"components.StatusChacker.newversionavailable": "Application Update",
"components.StatusChacker.reloadOverseerr": "Reload",
"components.StatusChecker.appUpdated": "{applicationTitle} Updated",
"components.StatusChecker.appUpdatedDescription": "Please click the button below to reload the application.",
"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.TvCrew.fullseriescrew": "Full Series Crew",
"components.TvDetails.anime": "Anime",
@ -1020,6 +1021,7 @@
"i18n.requested": "Requested",
"i18n.requesting": "Requesting…",
"i18n.resolved": "Resolved",
"i18n.restartRequired": "Restart Required",
"i18n.resultsperpage": "Display {pageSize} results per page",
"i18n.retry": "Retry",
"i18n.retrying": "Retrying…",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save