refactor(ui): move user-related settings out of "General Settings" (#1143)

pull/1143/merge
TheCatLady 3 years ago committed by GitHub
parent e4686d664b
commit b36aff912a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,7 @@
import React from 'react';
import { hasPermission } from '../../../server/lib/permissions';
import { Permission, User } from '../../hooks/useUser';
import useSettings from '../../hooks/useSettings';
export interface PermissionItem {
id: string;
@ -33,6 +34,8 @@ const PermissionOption: React.FC<PermissionOptionProps> = ({
onUpdate,
parent,
}) => {
const settings = useSettings();
const autoApprovePermissions = [
Permission.AUTO_APPROVE,
Permission.AUTO_APPROVE_MOVIE,
@ -42,34 +45,70 @@ const PermissionOption: React.FC<PermissionOptionProps> = ({
Permission.AUTO_APPROVE_4K_TV,
];
let disabled = false;
let checked = hasPermission(option.permission, currentPermission);
if (
// Permissions for user ID 1 (Plex server owner) cannot be changed
(currentUser && currentUser.id === 1) ||
// Admin permission automatically bypasses/grants all other permissions
(option.permission !== Permission.ADMIN &&
hasPermission(Permission.ADMIN, currentPermission)) ||
// Manage Requests permission automatically grants all Auto-Approve permissions
(autoApprovePermissions.includes(option.permission) &&
hasPermission(Permission.MANAGE_REQUESTS, currentPermission)) ||
// Selecting a parent permission automatically selects all children
(!!parent?.permission &&
hasPermission(parent.permission, currentPermission))
) {
disabled = true;
checked = true;
}
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)
) {
disabled = true;
}
if (
// Some permissions are dependent on others; check requirements are fulfilled
(option.requires &&
!option.requires.every((requirement) =>
hasPermission(requirement.permissions, currentPermission, {
type: requirement.type ?? 'and',
})
)) ||
// Request 4K and Auto-Approve 4K require both 4K movie & 4K series requests to be enabled
((option.permission === Permission.REQUEST_4K ||
option.permission === Permission.AUTO_APPROVE_4K) &&
(!settings.currentSettings.movie4kEnabled ||
!settings.currentSettings.series4kEnabled)) ||
// Request 4K Movie and Auto-Approve 4K Movie require 4K movie requests to be enabled
((option.permission === Permission.REQUEST_4K_MOVIE ||
option.permission === Permission.AUTO_APPROVE_4K_MOVIE) &&
!settings.currentSettings.movie4kEnabled) ||
// Request 4K Series and Auto-Approve 4K Series require 4K series requests to be enabled
((option.permission === Permission.REQUEST_4K_TV ||
option.permission === Permission.AUTO_APPROVE_4K_TV) &&
!settings.currentSettings.series4kEnabled)
) {
disabled = true;
checked = false;
}
return (
<>
<div
className={`relative flex items-start first:mt-0 mt-4 ${
(currentUser && currentUser.id === 1) ||
(option.permission !== Permission.ADMIN &&
hasPermission(Permission.ADMIN, currentPermission)) ||
(autoApprovePermissions.includes(option.permission) &&
hasPermission(Permission.MANAGE_REQUESTS, currentPermission)) ||
(!!parent?.permission &&
hasPermission(parent.permission, currentPermission)) ||
(actingUser &&
!hasPermission(Permission.ADMIN, actingUser.permissions) &&
option.permission === Permission.ADMIN) ||
(actingUser &&
!hasPermission(
Permission.MANAGE_SETTINGS,
actingUser.permissions
) &&
option.permission === Permission.MANAGE_SETTINGS) ||
(option.requires &&
!option.requires.every((requirement) =>
hasPermission(requirement.permissions, currentPermission, {
type: requirement.type ?? 'and',
})
))
? 'opacity-50'
: ''
disabled ? 'opacity-50' : ''
}`}
>
<div className="flex items-center h-6">
@ -77,30 +116,7 @@ const PermissionOption: React.FC<PermissionOptionProps> = ({
id={option.id}
name="permissions"
type="checkbox"
disabled={
(currentUser && currentUser.id === 1) ||
(option.permission !== Permission.ADMIN &&
hasPermission(Permission.ADMIN, currentPermission)) ||
(autoApprovePermissions.includes(option.permission) &&
hasPermission(Permission.MANAGE_REQUESTS, currentPermission)) ||
(!!parent?.permission &&
hasPermission(parent.permission, currentPermission)) ||
(actingUser &&
!hasPermission(Permission.ADMIN, actingUser.permissions) &&
option.permission === Permission.ADMIN) ||
(actingUser &&
!hasPermission(
Permission.MANAGE_SETTINGS,
actingUser.permissions
) &&
option.permission === Permission.MANAGE_SETTINGS) ||
(option.requires &&
!option.requires.every((requirement) =>
hasPermission(requirement.permissions, currentPermission, {
type: requirement.type ?? 'and',
})
))
}
disabled={disabled}
onChange={() => {
onUpdate(
hasPermission(option.permission, currentPermission)
@ -108,22 +124,7 @@ const PermissionOption: React.FC<PermissionOptionProps> = ({
: currentPermission + option.permission
);
}}
checked={
(hasPermission(option.permission, currentPermission) ||
(!!parent?.permission &&
hasPermission(parent.permission, currentPermission)) ||
(autoApprovePermissions.includes(option.permission) &&
hasPermission(
Permission.MANAGE_REQUESTS,
currentPermission
))) &&
(!option.requires ||
option.requires.every((requirement) =>
hasPermission(requirement.permissions, currentPermission, {
type: requirement.type ?? 'and',
})
))
}
checked={checked}
/>
</div>
<div className="ml-3 text-sm leading-6">

@ -7,6 +7,7 @@ import PageTitle from '../Common/PageTitle';
const messages = defineMessages({
settings: 'Settings',
menuGeneralSettings: 'General Settings',
menuUsers: 'Users',
menuPlexSettings: 'Plex',
menuServices: 'Services',
menuNotifications: 'Notifications',
@ -31,6 +32,11 @@ const SettingsLayout: React.FC = ({ children }) => {
route: '/settings/main',
regex: /^\/settings(\/main)?$/,
},
{
text: intl.formatMessage(messages.menuUsers),
route: '/settings/users',
regex: /^\/settings\/users/,
},
{
text: intl.formatMessage(messages.menuPlexSettings),
route: '/settings/plex',

@ -11,7 +11,6 @@ import { useUser, Permission } from '../../hooks/useUser';
import { useToasts } from 'react-toast-notifications';
import Badge from '../Common/Badge';
import globalMessages from '../../i18n/globalMessages';
import PermissionEdit from '../PermissionEdit';
import * as Yup from 'yup';
import RegionSelector from '../RegionSelector';
@ -34,7 +33,6 @@ const messages = defineMessages({
toastApiKeyFailure: 'Something went wrong while generating a new API key.',
toastSettingsSuccess: 'Settings successfully saved!',
toastSettingsFailure: 'Something went wrong while saving settings.',
defaultPermissions: 'Default User Permissions',
hideAvailable: 'Hide Available Media',
csrfProtection: 'Enable CSRF Protection',
csrfProtectionTip:
@ -44,7 +42,6 @@ const messages = defineMessages({
trustProxy: 'Enable Proxy Support',
trustProxyTip:
'Allows Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)',
localLogin: 'Enable Local User Sign-In',
validationApplicationTitle: 'You must provide an application title',
validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
@ -116,9 +113,7 @@ const SettingsMain: React.FC = () => {
applicationTitle: data?.applicationTitle,
applicationUrl: data?.applicationUrl,
csrfProtection: data?.csrfProtection,
defaultPermissions: data?.defaultPermissions ?? 0,
hideAvailable: data?.hideAvailable,
localLogin: data?.localLogin,
region: data?.region,
originalLanguage: data?.originalLanguage,
trustProxy: data?.trustProxy,
@ -131,9 +126,7 @@ const SettingsMain: React.FC = () => {
applicationTitle: values.applicationTitle,
applicationUrl: values.applicationUrl,
csrfProtection: values.csrfProtection,
defaultPermissions: values.defaultPermissions,
hideAvailable: values.hideAvailable,
localLogin: values.localLogin,
region: values.region,
originalLanguage: values.originalLanguage,
trustProxy: values.trustProxy,
@ -345,42 +338,6 @@ const SettingsMain: React.FC = () => {
/>
</div>
</div>
<div className="form-row">
<label htmlFor="localLogin" className="checkbox-label">
<span>{intl.formatMessage(messages.localLogin)}</span>
</label>
<div className="form-input">
<Field
type="checkbox"
id="localLogin"
name="localLogin"
onChange={() => {
setFieldValue('localLogin', !values.localLogin);
}}
/>
</div>
</div>
<div
role="group"
aria-labelledby="group-label"
className="form-group"
>
<div className="form-row">
<span id="group-label" className="group-label">
{intl.formatMessage(messages.defaultPermissions)}
</span>
<div className="form-input">
<div className="max-w-lg">
<PermissionEdit
currentPermission={values.defaultPermissions}
onUpdate={(newPermissions) =>
setFieldValue('defaultPermissions', newPermissions)
}
/>
</div>
</div>
</div>
</div>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">

@ -0,0 +1,133 @@
import React from 'react';
import useSWR from 'swr';
import LoadingSpinner from '../../Common/LoadingSpinner';
import type { MainSettings } from '../../../../server/lib/settings';
import { Form, Formik, Field } from 'formik';
import axios from 'axios';
import Button from '../../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import PermissionEdit from '../../PermissionEdit';
const messages = defineMessages({
userSettings: 'Users',
userSettingsDescription: 'Configure global and default user settings.',
save: 'Save Changes',
saving: 'Saving…',
toastSettingsSuccess: 'Settings successfully saved!',
toastSettingsFailure: 'Something went wrong while saving settings.',
localLogin: 'Enable Local User Sign-In',
defaultPermissions: 'Default User Permissions',
});
const SettingsUsers: React.FC = () => {
const { addToast } = useToasts();
const intl = useIntl();
const { data, error, revalidate } = useSWR<MainSettings>(
'/api/v1/settings/main'
);
if (!data && !error) {
return <LoadingSpinner />;
}
return (
<>
<div className="mb-6">
<h3 className="heading">{intl.formatMessage(messages.userSettings)}</h3>
<p className="description">
{intl.formatMessage(messages.userSettingsDescription)}
</p>
</div>
<div className="section">
<Formik
initialValues={{
localLogin: data?.localLogin,
defaultPermissions: data?.defaultPermissions ?? 0,
}}
enableReinitialize
onSubmit={async (values) => {
try {
await axios.post('/api/v1/settings/main', {
localLogin: values.localLogin,
defaultPermissions: values.defaultPermissions,
});
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
autoDismiss: true,
appearance: 'success',
});
} catch (e) {
addToast(intl.formatMessage(messages.toastSettingsFailure), {
autoDismiss: true,
appearance: 'error',
});
} finally {
revalidate();
}
}}
>
{({ isSubmitting, values, setFieldValue }) => {
return (
<Form className="section">
<div className="form-row">
<label htmlFor="localLogin" className="checkbox-label">
<span>{intl.formatMessage(messages.localLogin)}</span>
</label>
<div className="form-input">
<Field
type="checkbox"
id="localLogin"
name="localLogin"
onChange={() => {
setFieldValue('localLogin', !values.localLogin);
}}
/>
</div>
</div>
<div
role="group"
aria-labelledby="group-label"
className="form-group"
>
<div className="form-row">
<span id="group-label" className="group-label">
{intl.formatMessage(messages.defaultPermissions)}
</span>
<div className="form-input">
<div className="max-w-lg">
<PermissionEdit
currentPermission={values.defaultPermissions}
onUpdate={(newPermissions) =>
setFieldValue('defaultPermissions', newPermissions)
}
/>
</div>
</div>
</div>
</div>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting}
>
{isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div>
</div>
</Form>
);
}}
</Formik>
</div>
</>
);
};
export default SettingsUsers;

@ -441,6 +441,14 @@
"components.Settings.SettingsJobsCache.runnow": "Run Now",
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan",
"components.Settings.SettingsJobsCache.unknownJob": "Unknown Job",
"components.Settings.SettingsUsers.defaultPermissions": "Default User Permissions",
"components.Settings.SettingsUsers.localLogin": "Enable Local User Sign-In",
"components.Settings.SettingsUsers.save": "Save Changes",
"components.Settings.SettingsUsers.saving": "Saving…",
"components.Settings.SettingsUsers.toastSettingsFailure": "Something went wrong while saving settings.",
"components.Settings.SettingsUsers.toastSettingsSuccess": "Settings successfully saved!",
"components.Settings.SettingsUsers.userSettings": "Users",
"components.Settings.SettingsUsers.userSettingsDescription": "Configure global and default user settings.",
"components.Settings.SonarrModal.add": "Add Server",
"components.Settings.SonarrModal.animelanguageprofile": "Anime Language Profile",
"components.Settings.SonarrModal.animequalityprofile": "Anime Quality Profile",
@ -508,7 +516,6 @@
"components.Settings.currentlibrary": "Current Library: {name}",
"components.Settings.default": "Default",
"components.Settings.default4k": "Default 4K",
"components.Settings.defaultPermissions": "Default User Permissions",
"components.Settings.delete": "Delete",
"components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?",
"components.Settings.edit": "Edit",
@ -519,7 +526,6 @@
"components.Settings.hideAvailable": "Hide Available Media",
"components.Settings.hostname": "Hostname/IP",
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
"components.Settings.localLogin": "Enable Local User Sign-In",
"components.Settings.manualscan": "Manual Library Scan",
"components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
"components.Settings.menuAbout": "About",
@ -529,6 +535,7 @@
"components.Settings.menuNotifications": "Notifications",
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuServices": "Services",
"components.Settings.menuUsers": "Users",
"components.Settings.nodefault": "No Default Server",
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
"components.Settings.notificationAgentSettingsDescription": "Choose the types of notifications to send, and which notification agents to use.",

@ -0,0 +1,17 @@
import React from 'react';
import { 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';
const SettingsUsersPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS);
return (
<SettingsLayout>
<SettingsUsers />
</SettingsLayout>
);
};
export default SettingsUsersPage;
Loading…
Cancel
Save