feat: default user permissions added to settings

closes #388
pull/425/head
sct 4 years ago
parent f35dae56a5
commit e7ee85c29b

@ -58,6 +58,9 @@ components:
applicationUrl:
type: string
example: https://os.example.com
defaultPermissions:
type: number
example: 32
PlexLibrary:
type: object
properties:

@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { merge } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { Permission } from './permissions';
export interface Library {
id: string;
@ -47,6 +48,7 @@ export interface SonarrSettings extends DVRSettings {
export interface MainSettings {
apiKey: string;
applicationUrl: string;
defaultPermissions: number;
}
interface PublicSettings {
@ -105,6 +107,7 @@ class Settings {
main: {
apiKey: '',
applicationUrl: '',
defaultPermissions: Permission.REQUEST,
},
plex: {
name: '',

@ -5,6 +5,7 @@ import PlexTvAPI from '../api/plextv';
import { isAuthenticated } from '../middleware/auth';
import { Permission } from '../lib/permissions';
import logger from '../logger';
import { getSettings } from '../lib/settings';
const authRoutes = Router();
@ -25,6 +26,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
});
authRoutes.post('/login', async (req, res, next) => {
const settings = getSettings();
const userRepository = getRepository(User);
const body = req.body as { authToken?: string };
@ -82,7 +84,7 @@ authRoutes.post('/login', async (req, res, next) => {
username: account.username,
plexId: account.id,
plexToken: account.authToken,
permissions: Permission.REQUEST,
permissions: settings.main.defaultPermissions,
avatar: account.thumb,
});
await userRepository.save(user);

@ -16,7 +16,7 @@ import logger from '../logger';
import { scheduledJobs } from '../job/schedule';
import { Permission } from '../lib/permissions';
import { isAuthenticated } from '../middleware/auth';
import { merge } from 'lodash';
import { merge, omit } from 'lodash';
import Media from '../entity/Media';
import { MediaRequest } from '../entity/MediaRequest';
import { getAppVersion } from '../utils/appVersion';
@ -32,9 +32,7 @@ const filteredMainSettings = (
main: MainSettings
): Partial<MainSettings> => {
if (!user?.hasPermission(Permission.ADMIN)) {
return {
applicationUrl: main.applicationUrl,
};
return omit(main, 'apiKey');
}
return main;

@ -9,6 +9,8 @@ import Button from '../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import { useUser, Permission } from '../../hooks/useUser';
import { useToasts } from 'react-toast-notifications';
import { messages as permissionMessages } from '../UserEdit';
import { hasPermission } from '../../../server/lib/permissions';
const messages = defineMessages({
generalsettings: 'General Settings',
@ -22,11 +24,19 @@ const messages = defineMessages({
toastApiKeyFailure: 'Something went wrong generating a new API Key.',
toastSettingsSuccess: 'Settings saved.',
toastSettingsFailure: 'Something went wrong saving settings.',
defaultPermissions: 'Default User Permissions',
});
interface PermissionOption {
id: string;
name: string;
description: string;
permission: Permission;
}
const SettingsMain: React.FC = () => {
const { addToast } = useToasts();
const { hasPermission } = useUser();
const { hasPermission: userHasPermission } = useUser();
const intl = useIntl();
const { data, error, revalidate } = useSWR<MainSettings>(
'/api/v1/settings/main'
@ -53,13 +63,62 @@ const SettingsMain: React.FC = () => {
return <LoadingSpinner />;
}
const permissionList: PermissionOption[] = [
{
id: 'admin',
name: intl.formatMessage(permissionMessages.admin),
description: intl.formatMessage(permissionMessages.adminDescription),
permission: Permission.ADMIN,
},
{
id: 'settings',
name: intl.formatMessage(permissionMessages.settings),
description: intl.formatMessage(permissionMessages.settingsDescription),
permission: Permission.MANAGE_SETTINGS,
},
{
id: 'users',
name: intl.formatMessage(permissionMessages.users),
description: intl.formatMessage(permissionMessages.usersDescription),
permission: Permission.MANAGE_USERS,
},
{
id: 'managerequest',
name: intl.formatMessage(permissionMessages.managerequests),
description: intl.formatMessage(
permissionMessages.managerequestsDescription
),
permission: Permission.MANAGE_REQUESTS,
},
{
id: 'request',
name: intl.formatMessage(permissionMessages.request),
description: intl.formatMessage(permissionMessages.requestDescription),
permission: Permission.REQUEST,
},
{
id: 'vote',
name: intl.formatMessage(permissionMessages.vote),
description: intl.formatMessage(permissionMessages.voteDescription),
permission: Permission.VOTE,
},
{
id: 'autoapprove',
name: intl.formatMessage(permissionMessages.autoapprove),
description: intl.formatMessage(
permissionMessages.autoapproveDescription
),
permission: Permission.AUTO_APPROVE,
},
];
return (
<>
<div>
<h3 className="text-lg leading-6 font-medium text-gray-200">
<h3 className="text-lg font-medium leading-6 text-gray-200">
{intl.formatMessage(messages.generalsettings)}
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
{intl.formatMessage(messages.generalsettingsDescription)}
</p>
</div>
@ -67,11 +126,14 @@ const SettingsMain: React.FC = () => {
<Formik
initialValues={{
applicationUrl: data?.applicationUrl,
defaultPermissions: data?.defaultPermissions ?? 0,
}}
enableReinitialize
onSubmit={async (values) => {
try {
await axios.post('/api/v1/settings/main', {
applicationUrl: values.applicationUrl,
defaultPermissions: values.defaultPermissions,
});
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
@ -88,10 +150,10 @@ const SettingsMain: React.FC = () => {
}
}}
>
{({ isSubmitting }) => {
{({ isSubmitting, values, setFieldValue }) => {
return (
<Form>
{hasPermission(Permission.ADMIN) && (
{userHasPermission(Permission.ADMIN) && (
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="username"
@ -100,11 +162,11 @@ const SettingsMain: React.FC = () => {
{intl.formatMessage(messages.apikey)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<div className="flex max-w-lg rounded-md shadow-sm">
<input
type="text"
id="apiKey"
className="flex-1 form-input block w-full min-w-0 rounded-none rounded-l-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-none form-input rounded-l-md sm:text-sm sm:leading-5"
value={data?.apiKey}
readOnly
/>
@ -117,7 +179,7 @@ const SettingsMain: React.FC = () => {
e.preventDefault();
regenerate();
}}
className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium rounded-r-md text-white bg-indigo-500 hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
className="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium leading-5 text-white transition duration-150 ease-in-out bg-indigo-500 border border-gray-500 rounded-r-md hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700"
>
<svg
className="w-5 h-5"
@ -144,20 +206,98 @@ const SettingsMain: React.FC = () => {
{intl.formatMessage(messages.applicationurl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="applicationUrl"
name="applicationUrl"
type="text"
placeholder="https://os.example.com"
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
/>
</div>
</div>
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="mt-6">
<div role="group" aria-labelledby="label-permissions">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
<div>
<div
className="text-base font-medium leading-6 text-gray-400 sm:text-sm sm:leading-5"
id="label-permissions"
>
{intl.formatMessage(messages.defaultPermissions)}
</div>
</div>
<div className="mt-4 sm:mt-0 sm:col-span-2">
<div className="max-w-lg">
{permissionList.map((permissionOption) => (
<div
className={`relative flex items-start first:mt-0 mt-4 ${
permissionOption.permission !==
Permission.ADMIN &&
hasPermission(
Permission.ADMIN,
values.defaultPermissions
)
? 'opacity-50'
: ''
}`}
key={`permission-option-${permissionOption.id}`}
>
<div className="flex items-center h-5">
<input
id={permissionOption.id}
name="permissions"
type="checkbox"
className="w-4 h-4 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
disabled={
permissionOption.permission !==
Permission.ADMIN &&
hasPermission(
Permission.ADMIN,
values.defaultPermissions
)
}
onClick={() => {
setFieldValue(
'defaultPermissions',
hasPermission(
permissionOption.permission,
values.defaultPermissions
)
? values.defaultPermissions -
permissionOption.permission
: values.defaultPermissions +
permissionOption.permission
);
}}
checked={hasPermission(
permissionOption.permission,
values.defaultPermissions
)}
/>
</div>
<div className="ml-3 text-sm leading-5">
<label
htmlFor={permissionOption.id}
className="font-medium"
>
{permissionOption.name}
</label>
<p className="text-gray-500">
{permissionOption.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"

@ -9,7 +9,7 @@ import axios from 'axios';
import { useToasts } from 'react-toast-notifications';
import Header from '../Common/Header';
const messages = defineMessages({
export const messages = defineMessages({
edituser: 'Edit User',
username: 'Username',
avatar: 'Avatar',
@ -148,7 +148,7 @@ const UserEdit: React.FC = () => {
<FormattedMessage {...messages.edituser} />
</Header>
<div className="px-4 space-y-6 sm:p-6 lg:pb-8">
<div className="flex flex-col space-y-6 lg:flex-row lg:space-y-0 lg:space-x-6 text-white">
<div className="flex flex-col space-y-6 text-white lg:flex-row lg:space-y-0 lg:space-x-6">
<div className="flex-grow space-y-6">
<div className="space-y-1">
<label
@ -157,11 +157,11 @@ const UserEdit: React.FC = () => {
>
<FormattedMessage {...messages.username} />
</label>
<div className="rounded-md shadow-sm flex">
<div className="flex rounded-md shadow-sm">
<input
id="username"
type="text"
className="form-input flex-grow block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
className="flex-grow block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
value={user?.username}
readOnly
/>
@ -174,11 +174,11 @@ const UserEdit: React.FC = () => {
>
<FormattedMessage {...messages.email} />
</label>
<div className="rounded-md shadow-sm flex">
<div className="flex rounded-md shadow-sm">
<input
id="email"
type="text"
className="form-input flex-grow block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
className="flex-grow block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
value={user?.email}
readOnly
/>
@ -188,7 +188,7 @@ const UserEdit: React.FC = () => {
<div className="flex-grow space-y-1 lg:flex-grow-0 lg:flex-shrink-0">
<p
className="block text-sm leading-5 font-medium text-gray-400"
className="block text-sm font-medium leading-5 text-gray-400"
aria-hidden="true"
>
<FormattedMessage {...messages.avatar} />
@ -196,11 +196,11 @@ const UserEdit: React.FC = () => {
<div className="lg:hidden">
<div className="flex items-center">
<div
className="flex-shrink-0 inline-block rounded-full overflow-hidden h-12 w-12"
className="flex-shrink-0 inline-block w-12 h-12 overflow-hidden rounded-full"
aria-hidden="true"
>
<img
className="rounded-full h-full w-full"
className="w-full h-full rounded-full"
src={user?.avatar}
alt=""
/>
@ -208,9 +208,9 @@ const UserEdit: React.FC = () => {
</div>
</div>
<div className="hidden relative rounded-full overflow-hidden lg:block transition duration-150 ease-in-out">
<div className="relative hidden overflow-hidden transition duration-150 ease-in-out rounded-full lg:block">
<img
className="relative rounded-full w-40 h-40"
className="relative w-40 h-40 rounded-full"
src={user?.avatar}
alt=""
/>
@ -223,7 +223,7 @@ const UserEdit: React.FC = () => {
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
<div>
<div
className="text-base leading-6 font-medium sm:text-sm sm:leading-5"
className="text-base font-medium leading-6 sm:text-sm sm:leading-5"
id="label-permissions"
>
<FormattedMessage {...messages.permissions} />
@ -254,7 +254,7 @@ const UserEdit: React.FC = () => {
id={permissionOption.id}
name="permissions"
type="checkbox"
className="form-checkbox h-4 w-4 rounded-md text-indigo-600 transition duration-150 ease-in-out"
className="w-4 h-4 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
disabled={
(permissionOption.permission !==
Permission.ADMIN &&
@ -305,9 +305,9 @@ const UserEdit: React.FC = () => {
</div>
</div>
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"

@ -214,6 +214,7 @@
"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",

Loading…
Cancel
Save