diff --git a/overseerr-api.yml b/overseerr-api.yml index dda8286ef..3e0510d01 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -2228,6 +2228,36 @@ paths: application/json: schema: $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: post: summary: Imports all users from Plex @@ -2270,7 +2300,7 @@ paths: put: summary: Update a user by user ID 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. tags: diff --git a/server/routes/user.ts b/server/routes/user.ts index 2bc715f3b..5b3887dca 100644 --- a/server/routes/user.ts +++ b/server/routes/user.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { getRepository } from 'typeorm'; +import { getRepository, Not } from 'typeorm'; import PlexTvAPI from '../api/plextv'; import { MediaRequest } from '../entity/MediaRequest'; 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, + Partial[], + { 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, + ...{ 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) => { try { 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 ( - 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) - ) { + if (!canMakePermissionsChange(req.body.permissions, req.user)) { return next({ status: 403, message: 'You do not have permission to grant this level of access', diff --git a/src/components/PermissionEdit/index.tsx b/src/components/PermissionEdit/index.tsx new file mode 100644 index 000000000..88f278102 --- /dev/null +++ b/src/components/PermissionEdit/index.tsx @@ -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 = ({ + 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) => ( + onUpdate(newPermission)} + /> + ))} + + ); +}; + +export default PermissionEdit; diff --git a/src/components/Settings/SettingsMain.tsx b/src/components/Settings/SettingsMain.tsx index 59254b68b..e29138f84 100644 --- a/src/components/Settings/SettingsMain.tsx +++ b/src/components/Settings/SettingsMain.tsx @@ -9,10 +9,9 @@ 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 PermissionOption, { PermissionItem } from '../PermissionOption'; import Badge from '../Common/Badge'; import globalMessages from '../../i18n/globalMessages'; +import PermissionEdit from '../PermissionEdit'; const messages = defineMessages({ generalsettings: 'General Settings', @@ -59,101 +58,6 @@ const SettingsMain: React.FC = () => { return ; } - 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 ( <>
@@ -298,19 +202,15 @@ const SettingsMain: React.FC = () => {
- {permissionList.map((permissionItem) => ( - - setFieldValue( - 'defaultPermissions', - newPermissions - ) - } - /> - ))} + + setFieldValue( + 'defaultPermissions', + newPermissions + ) + } + />
diff --git a/src/components/UserEdit/index.tsx b/src/components/UserEdit/index.tsx index d1ee87201..d5b2a56cc 100644 --- a/src/components/UserEdit/index.tsx +++ b/src/components/UserEdit/index.tsx @@ -1,13 +1,13 @@ import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import LoadingSpinner from '../Common/LoadingSpinner'; -import { Permission, useUser } from '../../hooks/useUser'; +import { useUser } from '../../hooks/useUser'; import Button from '../Common/Button'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import axios from 'axios'; import { useToasts } from 'react-toast-notifications'; import Header from '../Common/Header'; -import PermissionOption, { PermissionItem } from '../PermissionOption'; +import PermissionEdit from '../PermissionEdit'; export const messages = defineMessages({ edituser: 'Edit User', @@ -15,41 +15,6 @@ export const messages = defineMessages({ avatar: 'Avatar', email: 'Email', 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', saving: 'Saving...', usersaved: 'User saved', @@ -104,91 +69,6 @@ const UserEdit: React.FC = () => { return ; } - 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 ( <>
@@ -278,17 +158,13 @@ const UserEdit: React.FC = () => {
- {permissionList.map((permissionItem) => ( - - setCurrentPermission(newPermission) - } - /> - ))} + + setCurrentPermission(newPermission) + } + />
diff --git a/src/components/UserList/BulkEditModal.tsx b/src/components/UserList/BulkEditModal.tsx new file mode 100644 index 000000000..d38280829 --- /dev/null +++ b/src/components/UserList/BulkEditModal.tsx @@ -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 = ({ + 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(`/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 ( + { + updateUsers(); + }} + okDisabled={isSaving} + okText={intl.formatMessage(userEditMessages.save)} + onCancel={onCancel} + > +
+
+
+ +
+
+
+
+ setCurrentPermission(newPermission)} + /> +
+
+
+
+ ); +}; + +export default BulkEditModal; diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 7b7d6af1b..4c98db270 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -6,7 +6,7 @@ import Badge from '../Common/Badge'; import { FormattedDate, defineMessages, useIntl } from 'react-intl'; import Button from '../Common/Button'; 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 Header from '../Common/Header'; import Table from '../Common/Table'; @@ -19,6 +19,7 @@ import { Field, Form, Formik } from 'formik'; import * as Yup from 'yup'; import AddUserIcon from '../../assets/useradd.svg'; import Alert from '../Common/Alert'; +import BulkEditModal from './BulkEditModal'; const messages = defineMessages({ userlist: 'User List', @@ -33,6 +34,7 @@ const messages = defineMessages({ created: 'Created', lastupdated: 'Last Updated', edit: 'Edit', + bulkedit: 'Bulk Edit', delete: 'Delete', admin: 'Admin', user: 'User', @@ -78,6 +80,39 @@ const UserList: React.FC = () => { }>({ isOpen: false, }); + const [showBulkEditModal, setShowBulkEditModal] = useState(false); + const [selectedUsers, setSelectedUsers] = useState([]); + 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 () => { setDeleting(true); @@ -183,6 +218,7 @@ const UserList: React.FC = () => { {intl.formatMessage(messages.deleteconfirm)} + { }} + + + setShowBulkEditModal(false)} + onComplete={() => { + setShowBulkEditModal(false); + revalidate(); + }} + selectedUserIds={selectedUsers} + users={data} + /> + +
{intl.formatMessage(messages.userlist)}
@@ -333,21 +390,57 @@ const UserList: React.FC = () => {
+ + + { + toggleAllUsers(); + }} + className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox" + /> + {intl.formatMessage(messages.username)}{intl.formatMessage(messages.totalrequests)}{intl.formatMessage(messages.usertype)}{intl.formatMessage(messages.role)}{intl.formatMessage(messages.created)}{intl.formatMessage(messages.lastupdated)} - + + + {data?.map((user) => ( + + {isUserPermsEditable(user.id) && ( + { + toggleUser(user.id); + }} + className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox" + /> + )} +
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 302a47420..47de37d9b 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -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.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.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.ascharacter": "as {character}", "components.PersonDetails.crewmember": "Crew Member", @@ -455,43 +481,18 @@ "components.TvDetails.userrating": "User Rating", "components.TvDetails.viewfullcrew": "View Full Crew", "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.edituser": "Edit User", "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.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.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.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.vote": "Vote", - "components.UserEdit.voteDescription": "Grants permission to vote on requests (voting not yet implemented)", "components.UserList.admin": "Admin", "components.UserList.autogeneratepassword": "Automatically generate password", + "components.UserList.bulkedit": "Bulk Edit", "components.UserList.create": "Create", "components.UserList.created": "Created", "components.UserList.createlocaluser": "Create Local User", @@ -520,6 +521,7 @@ "components.UserList.userdeleteerror": "Something went wrong deleting the user", "components.UserList.userlist": "User List", "components.UserList.username": "Username", + "components.UserList.userssaved": "Users saved", "components.UserList.usertype": "User Type", "components.UserList.validationemailrequired": "Must enter a valid email address.", "components.UserList.validationpasswordminchars": "Password is too short - should be 8 chars minimum.",