diff --git a/cypress/e2e/settings/general-settings.cy.ts b/cypress/e2e/settings/general-settings.cy.ts new file mode 100644 index 00000000..9fb9b82f --- /dev/null +++ b/cypress/e2e/settings/general-settings.cy.ts @@ -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'); + }); +}); diff --git a/cypress/e2e/user/user-list.cy.ts b/cypress/e2e/user/user-list.cy.ts index ccd1000d..d2593d51 100644 --- a/cypress/e2e/user/user-list.cy.ts +++ b/cypress/e2e/user/user-list.cy.ts @@ -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); diff --git a/overseerr-api.yml b/overseerr-api.yml index a00ada89..6bf7f69b 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -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 diff --git a/server/index.ts b/server/index.ts index fb3cb0b1..ba955ac9 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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 ( diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 8e4f66c4..0e5ab45a 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -56,4 +56,5 @@ export interface StatusResponse { commitTag: string; updateAvailable: boolean; commitsBehind: number; + restartRequired: boolean; } diff --git a/server/lib/permissions.ts b/server/lib/permissions.ts index 95160d38..3ccc8738 100644 --- a/server/lib/permissions.ts +++ b/server/lib/permissions.ts @@ -1,7 +1,6 @@ export enum Permission { NONE = 0, ADMIN = 2, - MANAGE_SETTINGS = 4, MANAGE_USERS = 8, MANAGE_REQUESTS = 16, REQUEST = 32, diff --git a/server/routes/index.ts b/server/routes/index.ts index 2be9533b..9c0c2f49 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -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('/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); diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index b1893250..1d06e0fa 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -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, diff --git a/server/utils/restartFlag.ts b/server/utils/restartFlag.ts new file mode 100644 index 00000000..8590137a --- /dev/null +++ b/server/utils/restartFlag.ts @@ -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; diff --git a/src/components/Common/Modal/index.tsx b/src/components/Common/Modal/index.tsx index 777128c0..671a4ff8 100644 --- a/src/components/Common/Modal/index.tsx +++ b/src/components/Common/Modal/index.tsx @@ -130,7 +130,7 @@ const Modal: React.FC = ({ /> )} -
+
{iconSvg &&
{iconSvg}
}
= ({
{children && ( -
+
{children}
)} diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index 6c5daae2..26bdb78b 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -80,7 +80,8 @@ const SidebarLinks: SidebarLinkProps[] = [ messagesKey: 'settings', svgIcon: , activeRegExp: /^\/settings/, - requiredPermission: Permission.MANAGE_SETTINGS, + requiredPermission: Permission.ADMIN, + dataTestId: 'sidebar-menu-settings', }, ]; diff --git a/src/components/PermissionEdit/index.tsx b/src/components/PermissionEdit/index.tsx index c6315b8a..b85c81bc 100644 --- a/src/components/PermissionEdit/index.tsx +++ b/src/components/PermissionEdit/index.tsx @@ -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 = ({ 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), diff --git a/src/components/PermissionOption/index.tsx b/src/components/PermissionOption/index.tsx index 2638419d..8aebc949 100644 --- a/src/components/PermissionOption/index.tsx +++ b/src/components/PermissionOption/index.tsx @@ -67,14 +67,9 @@ const PermissionOption: React.FC = ({ } 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; } diff --git a/src/components/Settings/SettingsMain.tsx b/src/components/Settings/SettingsMain.tsx index 11e88e7f..bc2397b7 100644 --- a/src/components/Settings/SettingsMain.tsx +++ b/src/components/Settings/SettingsMain.tsx @@ -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 = () => {