mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: ability to edit user settings in bulk (#597)
This commit is contained in:
@@ -2228,6 +2228,36 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/User'
|
$ref: '#/components/schemas/User'
|
||||||
|
put:
|
||||||
|
summary: Update batch of users
|
||||||
|
description: |
|
||||||
|
Update users with given IDs with provided values in request `body.settings`. You cannot update users' plex tokens through this request.
|
||||||
|
|
||||||
|
Requires the `MANAGE_USERS` permission.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ids:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
permissions:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successfully updated user details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
|
||||||
/user/import-from-plex:
|
/user/import-from-plex:
|
||||||
post:
|
post:
|
||||||
summary: Imports all users from Plex
|
summary: Imports all users from Plex
|
||||||
@@ -2270,7 +2300,7 @@ paths:
|
|||||||
put:
|
put:
|
||||||
summary: Update a user by user ID
|
summary: Update a user by user ID
|
||||||
description: |
|
description: |
|
||||||
Update a user with provided values in request body. You cannot update a users plex token through this request.
|
Update a user with provided values in request body. You cannot update a user's plex token through this request.
|
||||||
|
|
||||||
Requires the `MANAGE_USERS` permission.
|
Requires the `MANAGE_USERS` permission.
|
||||||
tags:
|
tags:
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository, Not } from 'typeorm';
|
||||||
import PlexTvAPI from '../api/plextv';
|
import PlexTvAPI from '../api/plextv';
|
||||||
import { MediaRequest } from '../entity/MediaRequest';
|
import { MediaRequest } from '../entity/MediaRequest';
|
||||||
import { User } from '../entity/User';
|
import { User } from '../entity/User';
|
||||||
@@ -70,6 +70,51 @@ router.get<{ id: string }>('/:id', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canMakePermissionsChange = (permissions: number, user?: User) =>
|
||||||
|
// 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)
|
||||||
|
);
|
||||||
|
|
||||||
|
router.put<
|
||||||
|
Record<string, never>,
|
||||||
|
Partial<User>[],
|
||||||
|
{ ids: string[]; permissions: number }
|
||||||
|
>('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const isOwner = req.user?.id === 1;
|
||||||
|
|
||||||
|
if (!canMakePermissionsChange(req.body.permissions, req.user)) {
|
||||||
|
return next({
|
||||||
|
status: 403,
|
||||||
|
message: 'You do not have permission to grant this level of access',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
|
||||||
|
const users = await userRepository.findByIds(req.body.ids, {
|
||||||
|
...(!isOwner ? { id: Not(1) } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedUsers = await Promise.all(
|
||||||
|
users.map(async (user) => {
|
||||||
|
return userRepository.save(<User>{
|
||||||
|
...user,
|
||||||
|
...{ permissions: req.body.permissions },
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).json(updatedUsers);
|
||||||
|
} catch (e) {
|
||||||
|
next({ status: 500, message: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.put<{ id: string }>('/:id', async (req, res, next) => {
|
router.put<{ id: string }>('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
@@ -86,22 +131,7 @@ router.put<{ id: string }>('/:id', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only let the owner grant admin privileges
|
if (!canMakePermissionsChange(req.body.permissions, req.user)) {
|
||||||
if (
|
|
||||||
hasPermission(Permission.ADMIN, req.body.permissions) &&
|
|
||||||
req.user?.id !== 1
|
|
||||||
) {
|
|
||||||
return next({
|
|
||||||
status: 403,
|
|
||||||
message: 'You do not have permission to grant this level of access',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only let users with the manage settings permission, grant the same permission
|
|
||||||
if (
|
|
||||||
hasPermission(Permission.MANAGE_SETTINGS, req.body.permissions) &&
|
|
||||||
!hasPermission(Permission.MANAGE_SETTINGS, req.user?.permissions ?? 0)
|
|
||||||
) {
|
|
||||||
return next({
|
return next({
|
||||||
status: 403,
|
status: 403,
|
||||||
message: 'You do not have permission to grant this level of access',
|
message: 'You do not have permission to grant this level of access',
|
||||||
|
157
src/components/PermissionEdit/index.tsx
Normal file
157
src/components/PermissionEdit/index.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PermissionOption, { PermissionItem } from '../PermissionOption';
|
||||||
|
import { Permission, User } from '../../hooks/useUser';
|
||||||
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
admin: 'Admin',
|
||||||
|
adminDescription:
|
||||||
|
'Full administrator access. Bypasses all permission checks.',
|
||||||
|
users: 'Manage Users',
|
||||||
|
usersDescription:
|
||||||
|
'Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.',
|
||||||
|
settings: 'Manage Settings',
|
||||||
|
settingsDescription:
|
||||||
|
'Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.',
|
||||||
|
managerequests: 'Manage Requests',
|
||||||
|
managerequestsDescription:
|
||||||
|
'Grants permission to manage Overseerr requests. This includes approving and denying requests.',
|
||||||
|
request: 'Request',
|
||||||
|
requestDescription: 'Grants permission to request movies and series.',
|
||||||
|
vote: 'Vote',
|
||||||
|
voteDescription:
|
||||||
|
'Grants permission to vote on requests (voting not yet implemented)',
|
||||||
|
autoapprove: 'Auto Approve',
|
||||||
|
autoapproveDescription:
|
||||||
|
'Grants auto approval for any requests made by this user.',
|
||||||
|
autoapproveMovies: 'Auto Approve Movies',
|
||||||
|
autoapproveMoviesDescription:
|
||||||
|
'Grants auto approve for movie requests made by this user.',
|
||||||
|
autoapproveSeries: 'Auto Approve Series',
|
||||||
|
autoapproveSeriesDescription:
|
||||||
|
'Grants auto approve for series requests made by this user.',
|
||||||
|
request4k: 'Request 4K',
|
||||||
|
request4kDescription: 'Grants permission to request 4K movies and series.',
|
||||||
|
request4kMovies: 'Request 4K Movies',
|
||||||
|
request4kMoviesDescription: 'Grants permission to request 4K movies.',
|
||||||
|
request4kTv: 'Request 4K Series',
|
||||||
|
request4kTvDescription: 'Grants permission to request 4K Series.',
|
||||||
|
advancedrequest: 'Advanced Requests',
|
||||||
|
advancedrequestDescription:
|
||||||
|
'Grants permission to use advanced request options. (Ex. Changing servers/profiles/paths)',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PermissionEditProps {
|
||||||
|
currentPermission: number;
|
||||||
|
user?: User;
|
||||||
|
onUpdate: (newPermissions: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionEdit: React.FC<PermissionEditProps> = ({
|
||||||
|
currentPermission,
|
||||||
|
onUpdate,
|
||||||
|
user,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const permissionList: PermissionItem[] = [
|
||||||
|
{
|
||||||
|
id: 'admin',
|
||||||
|
name: intl.formatMessage(messages.admin),
|
||||||
|
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),
|
||||||
|
description: intl.formatMessage(messages.usersDescription),
|
||||||
|
permission: Permission.MANAGE_USERS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'managerequest',
|
||||||
|
name: intl.formatMessage(messages.managerequests),
|
||||||
|
description: intl.formatMessage(messages.managerequestsDescription),
|
||||||
|
permission: Permission.MANAGE_REQUESTS,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'advancedrequest',
|
||||||
|
name: intl.formatMessage(messages.advancedrequest),
|
||||||
|
description: intl.formatMessage(messages.advancedrequestDescription),
|
||||||
|
permission: Permission.REQUEST_ADVANCED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'request',
|
||||||
|
name: intl.formatMessage(messages.request),
|
||||||
|
description: intl.formatMessage(messages.requestDescription),
|
||||||
|
permission: Permission.REQUEST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'request4k',
|
||||||
|
name: intl.formatMessage(messages.request4k),
|
||||||
|
description: intl.formatMessage(messages.request4kDescription),
|
||||||
|
permission: Permission.REQUEST_4K,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'request4k-movies',
|
||||||
|
name: intl.formatMessage(messages.request4kMovies),
|
||||||
|
description: intl.formatMessage(messages.request4kMoviesDescription),
|
||||||
|
permission: Permission.REQUEST_4K_MOVIE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'request4k-tv',
|
||||||
|
name: intl.formatMessage(messages.request4kTv),
|
||||||
|
description: intl.formatMessage(messages.request4kTvDescription),
|
||||||
|
permission: Permission.REQUEST_4K_TV,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'autoapprove',
|
||||||
|
name: intl.formatMessage(messages.autoapprove),
|
||||||
|
description: intl.formatMessage(messages.autoapproveDescription),
|
||||||
|
permission: Permission.AUTO_APPROVE,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'autoapprovemovies',
|
||||||
|
name: intl.formatMessage(messages.autoapproveMovies),
|
||||||
|
description: intl.formatMessage(
|
||||||
|
messages.autoapproveMoviesDescription
|
||||||
|
),
|
||||||
|
permission: Permission.AUTO_APPROVE_MOVIE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'autoapprovetv',
|
||||||
|
name: intl.formatMessage(messages.autoapproveSeries),
|
||||||
|
description: intl.formatMessage(
|
||||||
|
messages.autoapproveSeriesDescription
|
||||||
|
),
|
||||||
|
permission: Permission.AUTO_APPROVE_TV,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{permissionList.map((permissionItem) => (
|
||||||
|
<PermissionOption
|
||||||
|
key={`permission-option-${permissionItem.id}`}
|
||||||
|
option={permissionItem}
|
||||||
|
user={user}
|
||||||
|
currentPermission={currentPermission}
|
||||||
|
onUpdate={(newPermission) => onUpdate(newPermission)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PermissionEdit;
|
@@ -9,10 +9,9 @@ import Button from '../Common/Button';
|
|||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useUser, Permission } from '../../hooks/useUser';
|
import { useUser, Permission } from '../../hooks/useUser';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import { messages as permissionMessages } from '../UserEdit';
|
|
||||||
import PermissionOption, { PermissionItem } from '../PermissionOption';
|
|
||||||
import Badge from '../Common/Badge';
|
import Badge from '../Common/Badge';
|
||||||
import globalMessages from '../../i18n/globalMessages';
|
import globalMessages from '../../i18n/globalMessages';
|
||||||
|
import PermissionEdit from '../PermissionEdit';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
generalsettings: 'General Settings',
|
generalsettings: 'General Settings',
|
||||||
@@ -59,101 +58,6 @@ const SettingsMain: React.FC = () => {
|
|||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissionList: PermissionItem[] = [
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'advancedrequest',
|
|
||||||
name: intl.formatMessage(permissionMessages.advancedrequest),
|
|
||||||
description: intl.formatMessage(
|
|
||||||
permissionMessages.advancedrequestDescription
|
|
||||||
),
|
|
||||||
permission: Permission.REQUEST_ADVANCED,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'request',
|
|
||||||
name: intl.formatMessage(permissionMessages.request),
|
|
||||||
description: intl.formatMessage(permissionMessages.requestDescription),
|
|
||||||
permission: Permission.REQUEST,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'request4k',
|
|
||||||
name: intl.formatMessage(permissionMessages.request4k),
|
|
||||||
description: intl.formatMessage(permissionMessages.request4kDescription),
|
|
||||||
permission: Permission.REQUEST_4K,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'request4k-movies',
|
|
||||||
name: intl.formatMessage(permissionMessages.request4kMovies),
|
|
||||||
description: intl.formatMessage(
|
|
||||||
permissionMessages.request4kMoviesDescription
|
|
||||||
),
|
|
||||||
permission: Permission.REQUEST_4K_MOVIE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'request4k-tv',
|
|
||||||
name: intl.formatMessage(permissionMessages.request4kTv),
|
|
||||||
description: intl.formatMessage(
|
|
||||||
permissionMessages.request4kTvDescription
|
|
||||||
),
|
|
||||||
permission: Permission.REQUEST_4K_TV,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'autoapprove',
|
|
||||||
name: intl.formatMessage(permissionMessages.autoapprove),
|
|
||||||
description: intl.formatMessage(
|
|
||||||
permissionMessages.autoapproveDescription
|
|
||||||
),
|
|
||||||
permission: Permission.AUTO_APPROVE,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'autoapprovemovies',
|
|
||||||
name: intl.formatMessage(permissionMessages.autoapproveMovies),
|
|
||||||
description: intl.formatMessage(
|
|
||||||
permissionMessages.autoapproveMoviesDescription
|
|
||||||
),
|
|
||||||
permission: Permission.AUTO_APPROVE_MOVIE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'autoapprovetv',
|
|
||||||
name: intl.formatMessage(permissionMessages.autoapproveSeries),
|
|
||||||
description: intl.formatMessage(
|
|
||||||
permissionMessages.autoapproveSeriesDescription
|
|
||||||
),
|
|
||||||
permission: Permission.AUTO_APPROVE_TV,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
@@ -298,19 +202,15 @@ const SettingsMain: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 sm:mt-0 sm:col-span-2">
|
<div className="mt-4 sm:mt-0 sm:col-span-2">
|
||||||
<div className="max-w-lg">
|
<div className="max-w-lg">
|
||||||
{permissionList.map((permissionItem) => (
|
<PermissionEdit
|
||||||
<PermissionOption
|
currentPermission={values.defaultPermissions}
|
||||||
key={`permission-option-${permissionItem.id}`}
|
onUpdate={(newPermissions) =>
|
||||||
option={permissionItem}
|
setFieldValue(
|
||||||
currentPermission={values.defaultPermissions}
|
'defaultPermissions',
|
||||||
onUpdate={(newPermissions) =>
|
newPermissions
|
||||||
setFieldValue(
|
)
|
||||||
'defaultPermissions',
|
}
|
||||||
newPermissions
|
/>
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||||
import { Permission, useUser } from '../../hooks/useUser';
|
import { useUser } from '../../hooks/useUser';
|
||||||
import Button from '../Common/Button';
|
import Button from '../Common/Button';
|
||||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import Header from '../Common/Header';
|
import Header from '../Common/Header';
|
||||||
import PermissionOption, { PermissionItem } from '../PermissionOption';
|
import PermissionEdit from '../PermissionEdit';
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
edituser: 'Edit User',
|
edituser: 'Edit User',
|
||||||
@@ -15,41 +15,6 @@ export const messages = defineMessages({
|
|||||||
avatar: 'Avatar',
|
avatar: 'Avatar',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
permissions: 'Permissions',
|
permissions: 'Permissions',
|
||||||
admin: 'Admin',
|
|
||||||
adminDescription:
|
|
||||||
'Full administrator access. Bypasses all permission checks.',
|
|
||||||
users: 'Manage Users',
|
|
||||||
usersDescription:
|
|
||||||
'Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.',
|
|
||||||
settings: 'Manage Settings',
|
|
||||||
settingsDescription:
|
|
||||||
'Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.',
|
|
||||||
managerequests: 'Manage Requests',
|
|
||||||
managerequestsDescription:
|
|
||||||
'Grants permission to manage Overseerr requests. This includes approving and denying requests.',
|
|
||||||
request: 'Request',
|
|
||||||
requestDescription: 'Grants permission to request movies and series.',
|
|
||||||
vote: 'Vote',
|
|
||||||
voteDescription:
|
|
||||||
'Grants permission to vote on requests (voting not yet implemented)',
|
|
||||||
autoapprove: 'Auto Approve',
|
|
||||||
autoapproveDescription:
|
|
||||||
'Grants auto approval for any requests made by this user.',
|
|
||||||
autoapproveMovies: 'Auto Approve Movies',
|
|
||||||
autoapproveMoviesDescription:
|
|
||||||
'Grants auto approve for movie requests made by this user.',
|
|
||||||
autoapproveSeries: 'Auto Approve Series',
|
|
||||||
autoapproveSeriesDescription:
|
|
||||||
'Grants auto approve for series requests made by this user.',
|
|
||||||
request4k: 'Request 4K',
|
|
||||||
request4kDescription: 'Grants permission to request 4K movies and series.',
|
|
||||||
request4kMovies: 'Request 4K Movies',
|
|
||||||
request4kMoviesDescription: 'Grants permission to request 4K movies.',
|
|
||||||
request4kTv: 'Request 4K Series',
|
|
||||||
request4kTvDescription: 'Grants permission to request 4K Series.',
|
|
||||||
advancedrequest: 'Advanced Requests',
|
|
||||||
advancedrequestDescription:
|
|
||||||
'Grants permission to use advanced request options. (Ex. Changing servers/profiles/paths)',
|
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
usersaved: 'User saved',
|
usersaved: 'User saved',
|
||||||
@@ -104,91 +69,6 @@ const UserEdit: React.FC = () => {
|
|||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissionList: PermissionItem[] = [
|
|
||||||
{
|
|
||||||
id: 'admin',
|
|
||||||
name: intl.formatMessage(messages.admin),
|
|
||||||
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),
|
|
||||||
description: intl.formatMessage(messages.usersDescription),
|
|
||||||
permission: Permission.MANAGE_USERS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'managerequest',
|
|
||||||
name: intl.formatMessage(messages.managerequests),
|
|
||||||
description: intl.formatMessage(messages.managerequestsDescription),
|
|
||||||
permission: Permission.MANAGE_REQUESTS,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'advancedrequest',
|
|
||||||
name: intl.formatMessage(messages.advancedrequest),
|
|
||||||
description: intl.formatMessage(messages.advancedrequestDescription),
|
|
||||||
permission: Permission.REQUEST_ADVANCED,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'request',
|
|
||||||
name: intl.formatMessage(messages.request),
|
|
||||||
description: intl.formatMessage(messages.requestDescription),
|
|
||||||
permission: Permission.REQUEST,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'request4k',
|
|
||||||
name: intl.formatMessage(messages.request4k),
|
|
||||||
description: intl.formatMessage(messages.request4kDescription),
|
|
||||||
permission: Permission.REQUEST_4K,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'request4k-movies',
|
|
||||||
name: intl.formatMessage(messages.request4kMovies),
|
|
||||||
description: intl.formatMessage(messages.request4kMoviesDescription),
|
|
||||||
permission: Permission.REQUEST_4K_MOVIE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'request4k-tv',
|
|
||||||
name: intl.formatMessage(messages.request4kTv),
|
|
||||||
description: intl.formatMessage(messages.request4kTvDescription),
|
|
||||||
permission: Permission.REQUEST_4K_TV,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'autoapprove',
|
|
||||||
name: intl.formatMessage(messages.autoapprove),
|
|
||||||
description: intl.formatMessage(messages.autoapproveDescription),
|
|
||||||
permission: Permission.AUTO_APPROVE,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 'autoapprovemovies',
|
|
||||||
name: intl.formatMessage(messages.autoapproveMovies),
|
|
||||||
description: intl.formatMessage(
|
|
||||||
messages.autoapproveMoviesDescription
|
|
||||||
),
|
|
||||||
permission: Permission.AUTO_APPROVE_MOVIE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'autoapprovetv',
|
|
||||||
name: intl.formatMessage(messages.autoapproveSeries),
|
|
||||||
description: intl.formatMessage(
|
|
||||||
messages.autoapproveSeriesDescription
|
|
||||||
),
|
|
||||||
permission: Permission.AUTO_APPROVE_TV,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -278,17 +158,13 @@ const UserEdit: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 sm:mt-0 sm:col-span-2">
|
<div className="mt-4 sm:mt-0 sm:col-span-2">
|
||||||
<div className="max-w-lg">
|
<div className="max-w-lg">
|
||||||
{permissionList.map((permissionItem) => (
|
<PermissionEdit
|
||||||
<PermissionOption
|
user={currentUser}
|
||||||
key={`permission-option-${permissionItem.id}`}
|
currentPermission={currentPermission}
|
||||||
option={permissionItem}
|
onUpdate={(newPermission) =>
|
||||||
user={currentUser}
|
setCurrentPermission(newPermission)
|
||||||
currentPermission={currentPermission}
|
}
|
||||||
onUpdate={(newPermission) =>
|
/>
|
||||||
setCurrentPermission(newPermission)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
115
src/components/UserList/BulkEditModal.tsx
Normal file
115
src/components/UserList/BulkEditModal.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PermissionEdit from '../PermissionEdit';
|
||||||
|
import Modal from '../Common/Modal';
|
||||||
|
import { User, useUser } from '../../hooks/useUser';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import { messages as userEditMessages } from '../UserEdit';
|
||||||
|
|
||||||
|
interface BulkEditProps {
|
||||||
|
selectedUserIds: number[];
|
||||||
|
users?: User[];
|
||||||
|
onCancel?: () => void;
|
||||||
|
onComplete?: (updatedUsers: User[]) => void;
|
||||||
|
onSaving?: (isSaving: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
userssaved: 'Users saved',
|
||||||
|
});
|
||||||
|
|
||||||
|
const BulkEditModal: React.FC<BulkEditProps> = ({
|
||||||
|
selectedUserIds,
|
||||||
|
users,
|
||||||
|
onCancel,
|
||||||
|
onComplete,
|
||||||
|
onSaving,
|
||||||
|
}) => {
|
||||||
|
const { user: currentUser } = useUser();
|
||||||
|
const intl = useIntl();
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
const [currentPermission, setCurrentPermission] = useState(0);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onSaving) {
|
||||||
|
onSaving(isSaving);
|
||||||
|
}
|
||||||
|
}, [isSaving, onSaving]);
|
||||||
|
|
||||||
|
const updateUsers = async () => {
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
const { data: updated } = await axios.put<User[]>(`/api/v1/user`, {
|
||||||
|
ids: selectedUserIds,
|
||||||
|
permissions: currentPermission,
|
||||||
|
});
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete(updated);
|
||||||
|
}
|
||||||
|
addToast(intl.formatMessage(messages.userssaved), {
|
||||||
|
appearance: 'success',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addToast(intl.formatMessage(userEditMessages.userfail), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (users) {
|
||||||
|
const selectedUsers = users.filter((u) => selectedUserIds.includes(u.id));
|
||||||
|
const { permissions: allPermissionsEqual } = selectedUsers.reduce(
|
||||||
|
({ permissions: aPerms }, { permissions: bPerms }) => {
|
||||||
|
return {
|
||||||
|
permissions: aPerms === bPerms ? aPerms : NaN,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ permissions: selectedUsers[0].permissions }
|
||||||
|
);
|
||||||
|
if (allPermissionsEqual) {
|
||||||
|
setCurrentPermission(allPermissionsEqual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [users, selectedUserIds]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={intl.formatMessage(userEditMessages.edituser)}
|
||||||
|
onOk={() => {
|
||||||
|
updateUsers();
|
||||||
|
}}
|
||||||
|
okDisabled={isSaving}
|
||||||
|
okText={intl.formatMessage(userEditMessages.save)}
|
||||||
|
onCancel={onCancel}
|
||||||
|
>
|
||||||
|
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="text-base font-medium leading-6 sm:text-sm sm:leading-5"
|
||||||
|
id="label-permissions"
|
||||||
|
>
|
||||||
|
<FormattedMessage {...userEditMessages.permissions} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg">
|
||||||
|
<PermissionEdit
|
||||||
|
user={currentUser}
|
||||||
|
currentPermission={currentPermission}
|
||||||
|
onUpdate={(newPermission) => setCurrentPermission(newPermission)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BulkEditModal;
|
@@ -6,7 +6,7 @@ import Badge from '../Common/Badge';
|
|||||||
import { FormattedDate, defineMessages, useIntl } from 'react-intl';
|
import { FormattedDate, defineMessages, useIntl } from 'react-intl';
|
||||||
import Button from '../Common/Button';
|
import Button from '../Common/Button';
|
||||||
import { hasPermission } from '../../../server/lib/permissions';
|
import { hasPermission } from '../../../server/lib/permissions';
|
||||||
import { Permission, UserType } from '../../hooks/useUser';
|
import { Permission, UserType, useUser } from '../../hooks/useUser';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Header from '../Common/Header';
|
import Header from '../Common/Header';
|
||||||
import Table from '../Common/Table';
|
import Table from '../Common/Table';
|
||||||
@@ -19,6 +19,7 @@ import { Field, Form, Formik } from 'formik';
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import AddUserIcon from '../../assets/useradd.svg';
|
import AddUserIcon from '../../assets/useradd.svg';
|
||||||
import Alert from '../Common/Alert';
|
import Alert from '../Common/Alert';
|
||||||
|
import BulkEditModal from './BulkEditModal';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
userlist: 'User List',
|
userlist: 'User List',
|
||||||
@@ -33,6 +34,7 @@ const messages = defineMessages({
|
|||||||
created: 'Created',
|
created: 'Created',
|
||||||
lastupdated: 'Last Updated',
|
lastupdated: 'Last Updated',
|
||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
|
bulkedit: 'Bulk Edit',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
admin: 'Admin',
|
admin: 'Admin',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
@@ -78,6 +80,39 @@ const UserList: React.FC = () => {
|
|||||||
}>({
|
}>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
});
|
});
|
||||||
|
const [showBulkEditModal, setShowBulkEditModal] = useState(false);
|
||||||
|
const [selectedUsers, setSelectedUsers] = useState<number[]>([]);
|
||||||
|
const { user: currentUser } = useUser();
|
||||||
|
|
||||||
|
const isUserPermsEditable = (userId: number) =>
|
||||||
|
userId !== 1 && userId !== currentUser?.id;
|
||||||
|
const isAllUsersSelected = () => {
|
||||||
|
return (
|
||||||
|
selectedUsers.length ===
|
||||||
|
data?.filter((user) => user.id !== currentUser?.id).length
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const isUserSelected = (userId: number) => selectedUsers.includes(userId);
|
||||||
|
const toggleAllUsers = () => {
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
selectedUsers.length >= 0 &&
|
||||||
|
selectedUsers.length < data?.length - 1
|
||||||
|
) {
|
||||||
|
setSelectedUsers(
|
||||||
|
data.filter((user) => isUserPermsEditable(user.id)).map((u) => u.id)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedUsers([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const toggleUser = (userId: number) => {
|
||||||
|
if (selectedUsers.includes(userId)) {
|
||||||
|
setSelectedUsers((users) => users.filter((u) => u !== userId));
|
||||||
|
} else {
|
||||||
|
setSelectedUsers((users) => [...users, userId]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteUser = async () => {
|
const deleteUser = async () => {
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
@@ -183,6 +218,7 @@ const UserList: React.FC = () => {
|
|||||||
{intl.formatMessage(messages.deleteconfirm)}
|
{intl.formatMessage(messages.deleteconfirm)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
enter="opacity-0 transition duration-300"
|
enter="opacity-0 transition duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
@@ -313,6 +349,27 @@ const UserList: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
</Formik>
|
</Formik>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
show={showBulkEditModal}
|
||||||
|
>
|
||||||
|
<BulkEditModal
|
||||||
|
onCancel={() => setShowBulkEditModal(false)}
|
||||||
|
onComplete={() => {
|
||||||
|
setShowBulkEditModal(false);
|
||||||
|
revalidate();
|
||||||
|
}}
|
||||||
|
selectedUserIds={selectedUsers}
|
||||||
|
users={data}
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<div className="flex flex-col justify-between sm:flex-row">
|
<div className="flex flex-col justify-between sm:flex-row">
|
||||||
<Header>{intl.formatMessage(messages.userlist)}</Header>
|
<Header>{intl.formatMessage(messages.userlist)}</Header>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -333,21 +390,57 @@ const UserList: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<Table.TH>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="selectAll"
|
||||||
|
name="selectAll"
|
||||||
|
checked={isAllUsersSelected()}
|
||||||
|
onChange={() => {
|
||||||
|
toggleAllUsers();
|
||||||
|
}}
|
||||||
|
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||||
|
/>
|
||||||
|
</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.username)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.username)}</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.totalrequests)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.totalrequests)}</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.usertype)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.usertype)}</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.role)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.role)}</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.created)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.created)}</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.lastupdated)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.lastupdated)}</Table.TH>
|
||||||
<Table.TH></Table.TH>
|
<Table.TH className="text-right">
|
||||||
|
<Button
|
||||||
|
buttonSize="sm"
|
||||||
|
buttonType="warning"
|
||||||
|
onClick={() => setShowBulkEditModal(true)}
|
||||||
|
disabled={selectedUsers.length === 0}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.bulkedit)}
|
||||||
|
</Button>
|
||||||
|
</Table.TH>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<Table.TBody>
|
<Table.TBody>
|
||||||
{data?.map((user) => (
|
{data?.map((user) => (
|
||||||
<tr key={`user-list-${user.id}`}>
|
<tr key={`user-list-${user.id}`}>
|
||||||
|
<Table.TD>
|
||||||
|
{isUserPermsEditable(user.id) && (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`user-list-select-${user.id}`}
|
||||||
|
name={`user-list-select-${user.id}`}
|
||||||
|
checked={isUserSelected(user.id)}
|
||||||
|
onChange={() => {
|
||||||
|
toggleUser(user.id);
|
||||||
|
}}
|
||||||
|
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Table.TD>
|
||||||
<Table.TD>
|
<Table.TD>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-shrink-0 w-10 h-10">
|
<div className="flex-shrink-0 w-10 h-10">
|
||||||
|
@@ -78,6 +78,32 @@
|
|||||||
"components.NotificationTypeSelector.mediafailedDescription": "Sends a notification when media fails to be added to services (Radarr/Sonarr). For certain agents, this will only send the notification to admins or users with the \"Manage Requests\" permission.",
|
"components.NotificationTypeSelector.mediafailedDescription": "Sends a notification when media fails to be added to services (Radarr/Sonarr). For certain agents, this will only send the notification to admins or users with the \"Manage Requests\" permission.",
|
||||||
"components.NotificationTypeSelector.mediarequested": "Media Requested",
|
"components.NotificationTypeSelector.mediarequested": "Media Requested",
|
||||||
"components.NotificationTypeSelector.mediarequestedDescription": "Sends a notification when new media is requested. For certain agents, this will only send the notification to admins or users with the \"Manage Requests\" permission.",
|
"components.NotificationTypeSelector.mediarequestedDescription": "Sends a notification when new media is requested. For certain agents, this will only send the notification to admins or users with the \"Manage Requests\" permission.",
|
||||||
|
"components.PermissionEdit.admin": "Admin",
|
||||||
|
"components.PermissionEdit.adminDescription": "Full administrator access. Bypasses all permission checks.",
|
||||||
|
"components.PermissionEdit.advancedrequest": "Advanced Requests",
|
||||||
|
"components.PermissionEdit.advancedrequestDescription": "Grants permission to use advanced request options. (Ex. Changing servers/profiles/paths)",
|
||||||
|
"components.PermissionEdit.autoapprove": "Auto Approve",
|
||||||
|
"components.PermissionEdit.autoapproveDescription": "Grants auto approval for any requests made by this user.",
|
||||||
|
"components.PermissionEdit.autoapproveMovies": "Auto Approve Movies",
|
||||||
|
"components.PermissionEdit.autoapproveMoviesDescription": "Grants auto approve for movie requests made by this user.",
|
||||||
|
"components.PermissionEdit.autoapproveSeries": "Auto Approve Series",
|
||||||
|
"components.PermissionEdit.autoapproveSeriesDescription": "Grants auto approve for series requests made by this user.",
|
||||||
|
"components.PermissionEdit.managerequests": "Manage Requests",
|
||||||
|
"components.PermissionEdit.managerequestsDescription": "Grants permission to manage Overseerr requests. This includes approving and denying requests.",
|
||||||
|
"components.PermissionEdit.request": "Request",
|
||||||
|
"components.PermissionEdit.request4k": "Request 4K",
|
||||||
|
"components.PermissionEdit.request4kDescription": "Grants permission to request 4K movies and series.",
|
||||||
|
"components.PermissionEdit.request4kMovies": "Request 4K Movies",
|
||||||
|
"components.PermissionEdit.request4kMoviesDescription": "Grants permission to request 4K movies.",
|
||||||
|
"components.PermissionEdit.request4kTv": "Request 4K Series",
|
||||||
|
"components.PermissionEdit.request4kTvDescription": "Grants permission to request 4K Series.",
|
||||||
|
"components.PermissionEdit.requestDescription": "Grants permission to request movies and series.",
|
||||||
|
"components.PermissionEdit.settings": "Manage Settings",
|
||||||
|
"components.PermissionEdit.settingsDescription": "Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.",
|
||||||
|
"components.PermissionEdit.users": "Manage Users",
|
||||||
|
"components.PermissionEdit.usersDescription": "Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.",
|
||||||
|
"components.PermissionEdit.vote": "Vote",
|
||||||
|
"components.PermissionEdit.voteDescription": "Grants permission to vote on requests (voting not yet implemented)",
|
||||||
"components.PersonDetails.appearsin": "Appears in",
|
"components.PersonDetails.appearsin": "Appears in",
|
||||||
"components.PersonDetails.ascharacter": "as {character}",
|
"components.PersonDetails.ascharacter": "as {character}",
|
||||||
"components.PersonDetails.crewmember": "Crew Member",
|
"components.PersonDetails.crewmember": "Crew Member",
|
||||||
@@ -455,43 +481,18 @@
|
|||||||
"components.TvDetails.userrating": "User Rating",
|
"components.TvDetails.userrating": "User Rating",
|
||||||
"components.TvDetails.viewfullcrew": "View Full Crew",
|
"components.TvDetails.viewfullcrew": "View Full Crew",
|
||||||
"components.TvDetails.watchtrailer": "Watch Trailer",
|
"components.TvDetails.watchtrailer": "Watch Trailer",
|
||||||
"components.UserEdit.admin": "Admin",
|
|
||||||
"components.UserEdit.adminDescription": "Full administrator access. Bypasses all permission checks.",
|
|
||||||
"components.UserEdit.advancedrequest": "Advanced Requests",
|
|
||||||
"components.UserEdit.advancedrequestDescription": "Grants permission to use advanced request options. (Ex. Changing servers/profiles/paths)",
|
|
||||||
"components.UserEdit.autoapprove": "Auto Approve",
|
|
||||||
"components.UserEdit.autoapproveDescription": "Grants auto approval for any requests made by this user.",
|
|
||||||
"components.UserEdit.autoapproveMovies": "Auto Approve Movies",
|
|
||||||
"components.UserEdit.autoapproveMoviesDescription": "Grants auto approve for movie requests made by this user.",
|
|
||||||
"components.UserEdit.autoapproveSeries": "Auto Approve Series",
|
|
||||||
"components.UserEdit.autoapproveSeriesDescription": "Grants auto approve for series requests made by this user.",
|
|
||||||
"components.UserEdit.avatar": "Avatar",
|
"components.UserEdit.avatar": "Avatar",
|
||||||
"components.UserEdit.edituser": "Edit User",
|
"components.UserEdit.edituser": "Edit User",
|
||||||
"components.UserEdit.email": "Email",
|
"components.UserEdit.email": "Email",
|
||||||
"components.UserEdit.managerequests": "Manage Requests",
|
|
||||||
"components.UserEdit.managerequestsDescription": "Grants permission to manage Overseerr requests. This includes approving and denying requests.",
|
|
||||||
"components.UserEdit.permissions": "Permissions",
|
"components.UserEdit.permissions": "Permissions",
|
||||||
"components.UserEdit.request": "Request",
|
|
||||||
"components.UserEdit.request4k": "Request 4K",
|
|
||||||
"components.UserEdit.request4kDescription": "Grants permission to request 4K movies and series.",
|
|
||||||
"components.UserEdit.request4kMovies": "Request 4K Movies",
|
|
||||||
"components.UserEdit.request4kMoviesDescription": "Grants permission to request 4K movies.",
|
|
||||||
"components.UserEdit.request4kTv": "Request 4K Series",
|
|
||||||
"components.UserEdit.request4kTvDescription": "Grants permission to request 4K Series.",
|
|
||||||
"components.UserEdit.requestDescription": "Grants permission to request movies and series.",
|
|
||||||
"components.UserEdit.save": "Save",
|
"components.UserEdit.save": "Save",
|
||||||
"components.UserEdit.saving": "Saving…",
|
"components.UserEdit.saving": "Saving…",
|
||||||
"components.UserEdit.settings": "Manage Settings",
|
|
||||||
"components.UserEdit.settingsDescription": "Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.",
|
|
||||||
"components.UserEdit.userfail": "Something went wrong saving the user.",
|
"components.UserEdit.userfail": "Something went wrong saving the user.",
|
||||||
"components.UserEdit.username": "Username",
|
"components.UserEdit.username": "Username",
|
||||||
"components.UserEdit.users": "Manage Users",
|
|
||||||
"components.UserEdit.usersDescription": "Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.",
|
|
||||||
"components.UserEdit.usersaved": "User saved",
|
"components.UserEdit.usersaved": "User saved",
|
||||||
"components.UserEdit.vote": "Vote",
|
|
||||||
"components.UserEdit.voteDescription": "Grants permission to vote on requests (voting not yet implemented)",
|
|
||||||
"components.UserList.admin": "Admin",
|
"components.UserList.admin": "Admin",
|
||||||
"components.UserList.autogeneratepassword": "Automatically generate password",
|
"components.UserList.autogeneratepassword": "Automatically generate password",
|
||||||
|
"components.UserList.bulkedit": "Bulk Edit",
|
||||||
"components.UserList.create": "Create",
|
"components.UserList.create": "Create",
|
||||||
"components.UserList.created": "Created",
|
"components.UserList.created": "Created",
|
||||||
"components.UserList.createlocaluser": "Create Local User",
|
"components.UserList.createlocaluser": "Create Local User",
|
||||||
@@ -520,6 +521,7 @@
|
|||||||
"components.UserList.userdeleteerror": "Something went wrong deleting the user",
|
"components.UserList.userdeleteerror": "Something went wrong deleting the user",
|
||||||
"components.UserList.userlist": "User List",
|
"components.UserList.userlist": "User List",
|
||||||
"components.UserList.username": "Username",
|
"components.UserList.username": "Username",
|
||||||
|
"components.UserList.userssaved": "Users saved",
|
||||||
"components.UserList.usertype": "User Type",
|
"components.UserList.usertype": "User Type",
|
||||||
"components.UserList.validationemailrequired": "Must enter a valid email address.",
|
"components.UserList.validationemailrequired": "Must enter a valid email address.",
|
||||||
"components.UserList.validationpasswordminchars": "Password is too short - should be 8 chars minimum.",
|
"components.UserList.validationpasswordminchars": "Password is too short - should be 8 chars minimum.",
|
||||||
|
Reference in New Issue
Block a user