mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(plex): selective user import (#2188)
* feat(api): allow importing of only selected Plex users * feat(frontend): modal for importing Plex users * feat: add alert if 'Enable New Plex Sign-In' setting is enabled * refactor: fetch all existing Plex users in a single DB query Co-authored-by: Ryan Cohen <ryan@sct.dev>
This commit is contained in:
@@ -8,9 +8,9 @@ The user account created during Overseerr setup is the "Owner" account, which ca
|
|||||||
|
|
||||||
There are currently two methods to add users to Overseerr: importing Plex users and creating "local users." All new users are created with the [default permissions](../settings/README.md#default-permissions) defined in **Settings → Users**.
|
There are currently two methods to add users to Overseerr: importing Plex users and creating "local users." All new users are created with the [default permissions](../settings/README.md#default-permissions) defined in **Settings → Users**.
|
||||||
|
|
||||||
### Importing Users from Plex
|
### Importing Plex Users
|
||||||
|
|
||||||
Clicking the **Import Users from Plex** button on the **User List** page will fetch the list of users with access to the Plex server from [plex.tv](https://www.plex.tv/), and add them to Overseerr automatically.
|
Clicking the **Import Plex Users** button on the **User List** page will fetch the list of users with access to the Plex server from [plex.tv](https://www.plex.tv/), and add them to Overseerr automatically.
|
||||||
|
|
||||||
Importing Plex users is not required, however. Any user with access to the Plex server can log in to Overseerr even if they have not been imported, and will be assigned the configured [default permissions](../settings/README.md#default-permissions) upon their first login.
|
Importing Plex users is not required, however. Any user with access to the Plex server can log in to Overseerr even if they have not been imported, and will be assigned the configured [default permissions](../settings/README.md#default-permissions) upon their first login.
|
||||||
|
|
||||||
|
@@ -1994,6 +1994,36 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/PlexDevice'
|
$ref: '#/components/schemas/PlexDevice'
|
||||||
|
/settings/plex/users:
|
||||||
|
get:
|
||||||
|
summary: Get Plex users
|
||||||
|
description: |
|
||||||
|
Returns a list of Plex users in a JSON array.
|
||||||
|
|
||||||
|
Requires the `MANAGE_USERS` permission.
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
- users
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Plex users
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
thumb:
|
||||||
|
type: string
|
||||||
/settings/radarr:
|
/settings/radarr:
|
||||||
get:
|
get:
|
||||||
summary: Get Radarr settings
|
summary: Get Radarr settings
|
||||||
@@ -3196,11 +3226,22 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: Import all users from Plex
|
summary: Import all users from Plex
|
||||||
description: |
|
description: |
|
||||||
Requests users from the Plex Server and creates a new user for each of them
|
Fetches and imports users from the Plex server. If a list of Plex IDs is provided in the request body, only the specified users will be imported. Otherwise, all users will be imported.
|
||||||
|
|
||||||
Requires the `MANAGE_USERS` permission.
|
Requires the `MANAGE_USERS` permission.
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
plexIds:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: A list of the newly created users
|
description: A list of the newly created users
|
||||||
|
@@ -224,7 +224,7 @@ class PlexTvAPI {
|
|||||||
|
|
||||||
const users = friends.MediaContainer.User;
|
const users = friends.MediaContainer.User;
|
||||||
|
|
||||||
const user = users.find((u) => Number(u.$.id) === userId);
|
const user = users.find((u) => parseInt(u.$.id) === userId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@@ -35,6 +35,7 @@ export interface PublicSettingsResponse {
|
|||||||
enablePushRegistration: boolean;
|
enablePushRegistration: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
|
newPlexLogin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CacheItem {
|
export interface CacheItem {
|
||||||
|
@@ -113,6 +113,7 @@ interface FullPublicSettings extends PublicSettings {
|
|||||||
enablePushRegistration: boolean;
|
enablePushRegistration: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
|
newPlexLogin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationAgentConfig {
|
export interface NotificationAgentConfig {
|
||||||
@@ -469,6 +470,7 @@ class Settings {
|
|||||||
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
||||||
locale: this.data.main.locale,
|
locale: this.data.main.locale,
|
||||||
emailEnabled: this.data.notifications.agents.email.enabled,
|
emailEnabled: this.data.notifications.agents.email.enabled,
|
||||||
|
newPlexLogin: this.data.main.newPlexLogin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { merge, omit } from 'lodash';
|
import { merge, omit, sortBy } from 'lodash';
|
||||||
import { rescheduleJob } from 'node-schedule';
|
import { rescheduleJob } from 'node-schedule';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
@@ -225,6 +225,58 @@ settingsRoutes.post('/plex/sync', (req, res) => {
|
|||||||
return res.status(200).json(plexFullScanner.status());
|
return res.status(200).json(plexFullScanner.status());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
settingsRoutes.get(
|
||||||
|
'/plex/users',
|
||||||
|
isAuthenticated(Permission.MANAGE_USERS),
|
||||||
|
async (req, res) => {
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
const qb = userRepository.createQueryBuilder('user');
|
||||||
|
|
||||||
|
const admin = await userRepository.findOneOrFail({
|
||||||
|
select: ['id', 'plexToken'],
|
||||||
|
order: { id: 'ASC' },
|
||||||
|
});
|
||||||
|
const plexApi = new PlexTvAPI(admin.plexToken ?? '');
|
||||||
|
const plexUsers = (await plexApi.getUsers()).MediaContainer.User.map(
|
||||||
|
(user) => user.$
|
||||||
|
);
|
||||||
|
|
||||||
|
const unimportedPlexUsers: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
thumb: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
const existingUsers = await qb
|
||||||
|
.where('user.plexId IN (:...plexIds)', {
|
||||||
|
plexIds: plexUsers.map((plexUser) => plexUser.id),
|
||||||
|
})
|
||||||
|
.orWhere('user.email IN (:...plexEmails)', {
|
||||||
|
plexEmails: plexUsers.map((plexUser) => plexUser.email.toLowerCase()),
|
||||||
|
})
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
plexUsers.map(async (plexUser) => {
|
||||||
|
if (
|
||||||
|
!existingUsers.find(
|
||||||
|
(user) =>
|
||||||
|
user.plexId === parseInt(plexUser.id) ||
|
||||||
|
user.email === plexUser.email.toLowerCase()
|
||||||
|
) &&
|
||||||
|
(await plexApi.checkUserAccess(parseInt(plexUser.id)))
|
||||||
|
) {
|
||||||
|
unimportedPlexUsers.push(plexUser);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).json(sortBy(unimportedPlexUsers, 'username'));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
settingsRoutes.get(
|
settingsRoutes.get(
|
||||||
'/logs',
|
'/logs',
|
||||||
rateLimit({ windowMs: 60 * 1000, max: 50 }),
|
rateLimit({ windowMs: 60 * 1000, max: 50 }),
|
||||||
|
@@ -400,6 +400,7 @@ router.post(
|
|||||||
try {
|
try {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
|
const body = req.body as { plexIds: string[] } | undefined;
|
||||||
|
|
||||||
// taken from auth.ts
|
// taken from auth.ts
|
||||||
const mainUser = await userRepository.findOneOrFail({
|
const mainUser = await userRepository.findOneOrFail({
|
||||||
@@ -434,7 +435,7 @@ router.post(
|
|||||||
user.plexId = parseInt(account.id);
|
user.plexId = parseInt(account.id);
|
||||||
}
|
}
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
} else {
|
} else if (!body || body.plexIds.includes(account.id)) {
|
||||||
if (await mainPlexTv.checkUserAccess(parseInt(account.id))) {
|
if (await mainPlexTv.checkUserAccess(parseInt(account.id))) {
|
||||||
const newUser = new User({
|
const newUser = new User({
|
||||||
plexUsername: account.username,
|
plexUsername: account.username,
|
||||||
|
@@ -292,7 +292,7 @@ const SettingsServices: React.FC = () => {
|
|||||||
serverType: 'Radarr',
|
serverType: 'Radarr',
|
||||||
strong: function strong(msg) {
|
strong: function strong(msg) {
|
||||||
return (
|
return (
|
||||||
<strong className="font-semibold text-yellow-100">
|
<strong className="font-semibold text-white">
|
||||||
{msg}
|
{msg}
|
||||||
</strong>
|
</strong>
|
||||||
);
|
);
|
||||||
@@ -382,7 +382,7 @@ const SettingsServices: React.FC = () => {
|
|||||||
serverType: 'Sonarr',
|
serverType: 'Sonarr',
|
||||||
strong: function strong(msg) {
|
strong: function strong(msg) {
|
||||||
return (
|
return (
|
||||||
<strong className="font-semibold text-yellow-100">
|
<strong className="font-semibold text-white">
|
||||||
{msg}
|
{msg}
|
||||||
</strong>
|
</strong>
|
||||||
);
|
);
|
||||||
|
250
src/components/UserList/PlexImportModal.tsx
Normal file
250
src/components/UserList/PlexImportModal.tsx
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import { InboxInIcon } from '@heroicons/react/solid';
|
||||||
|
import axios from 'axios';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import useSettings from '../../hooks/useSettings';
|
||||||
|
import globalMessages from '../../i18n/globalMessages';
|
||||||
|
import Alert from '../Common/Alert';
|
||||||
|
import Modal from '../Common/Modal';
|
||||||
|
|
||||||
|
interface PlexImportProps {
|
||||||
|
onCancel?: () => void;
|
||||||
|
onComplete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
importfromplex: 'Import Plex Users',
|
||||||
|
importfromplexerror: 'Something went wrong while importing Plex users.',
|
||||||
|
importedfromplex:
|
||||||
|
'<strong>{userCount}</strong> {userCount, plural, one {user} other {users}} Plex users imported successfully!',
|
||||||
|
user: 'User',
|
||||||
|
nouserstoimport: 'There are no Plex users to import.',
|
||||||
|
newplexsigninenabled:
|
||||||
|
'The <strong>Enable New Plex Sign-In</strong> setting is currently enabled. Plex users with library access do not need to be imported in order to sign in.',
|
||||||
|
});
|
||||||
|
|
||||||
|
const PlexImportModal: React.FC<PlexImportProps> = ({
|
||||||
|
onCancel,
|
||||||
|
onComplete,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const settings = useSettings();
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
const [isImporting, setImporting] = useState(false);
|
||||||
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
|
const { data, error } = useSWR<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
thumb: string;
|
||||||
|
}[]
|
||||||
|
>(`/api/v1/settings/plex/users`, {
|
||||||
|
revalidateOnMount: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const importUsers = async () => {
|
||||||
|
setImporting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: createdUsers } = await axios.post(
|
||||||
|
'/api/v1/user/import-from-plex',
|
||||||
|
{ plexIds: selectedUsers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!createdUsers.length) {
|
||||||
|
throw new Error('No users were imported from Plex.');
|
||||||
|
}
|
||||||
|
|
||||||
|
addToast(
|
||||||
|
intl.formatMessage(messages.importedfromplex, {
|
||||||
|
userCount: createdUsers.length,
|
||||||
|
strong: function strong(msg) {
|
||||||
|
return <strong>{msg}</strong>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'success',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addToast(intl.formatMessage(messages.importfromplexerror), {
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectedUser = (plexId: string): boolean =>
|
||||||
|
selectedUsers.includes(plexId);
|
||||||
|
|
||||||
|
const isAllUsers = (): boolean => selectedUsers.length === data?.length;
|
||||||
|
|
||||||
|
const toggleUser = (plexId: string): void => {
|
||||||
|
if (selectedUsers.includes(plexId)) {
|
||||||
|
setSelectedUsers((users) => users.filter((user) => user !== plexId));
|
||||||
|
} else {
|
||||||
|
setSelectedUsers((users) => [...users, plexId]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAllUsers = (): void => {
|
||||||
|
if (data && selectedUsers.length >= 0 && !isAllUsers()) {
|
||||||
|
setSelectedUsers(data.map((user) => user.id));
|
||||||
|
} else {
|
||||||
|
setSelectedUsers([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
loading={!data && !error}
|
||||||
|
title={intl.formatMessage(messages.importfromplex)}
|
||||||
|
iconSvg={<InboxInIcon />}
|
||||||
|
onOk={() => {
|
||||||
|
importUsers();
|
||||||
|
}}
|
||||||
|
okDisabled={isImporting || !selectedUsers.length}
|
||||||
|
okText={intl.formatMessage(
|
||||||
|
isImporting ? globalMessages.importing : globalMessages.import
|
||||||
|
)}
|
||||||
|
onCancel={onCancel}
|
||||||
|
>
|
||||||
|
{data?.length ? (
|
||||||
|
<>
|
||||||
|
{settings.currentSettings.newPlexLogin && (
|
||||||
|
<Alert
|
||||||
|
title={intl.formatMessage(messages.newplexsigninenabled, {
|
||||||
|
strong: function strong(msg) {
|
||||||
|
return (
|
||||||
|
<strong className="font-semibold text-white">{msg}</strong>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
type="info"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="-mx-4 sm:mx-0">
|
||||||
|
<div className="inline-block min-w-full py-2 align-middle">
|
||||||
|
<div className="overflow-hidden shadow sm:rounded-lg">
|
||||||
|
<table className="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="w-16 px-4 py-3 bg-gray-500">
|
||||||
|
<span
|
||||||
|
role="checkbox"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-checked={isAllUsers()}
|
||||||
|
onClick={() => toggleAllUsers()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === 'Space') {
|
||||||
|
toggleAllUsers();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="relative inline-flex items-center justify-center flex-shrink-0 w-10 h-5 pt-2 cursor-pointer focus:outline-none"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`${
|
||||||
|
isAllUsers() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||||
|
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`${
|
||||||
|
isAllUsers() ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
} absolute left-0 inline-block h-5 w-5 border border-gray-200 rounded-full bg-white shadow transform group-focus:ring group-focus:border-blue-300 transition-transform ease-in-out duration-200`}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th className="px-1 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
|
||||||
|
{intl.formatMessage(messages.user)}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-gray-600 divide-y divide-gray-700">
|
||||||
|
{data?.map((user) => (
|
||||||
|
<tr key={`user-${user.id}`}>
|
||||||
|
<td className="px-4 py-4 text-sm font-medium leading-5 text-gray-100 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
role="checkbox"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-checked={isSelectedUser(user.id)}
|
||||||
|
onClick={() => toggleUser(user.id)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === 'Space') {
|
||||||
|
toggleUser(user.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="relative inline-flex items-center justify-center flex-shrink-0 w-10 h-5 pt-2 cursor-pointer focus:outline-none"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`${
|
||||||
|
isSelectedUser(user.id)
|
||||||
|
? 'bg-indigo-500'
|
||||||
|
: 'bg-gray-800'
|
||||||
|
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`${
|
||||||
|
isSelectedUser(user.id)
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-0'
|
||||||
|
} absolute left-0 inline-block h-5 w-5 border border-gray-200 rounded-full bg-white shadow transform group-focus:ring group-focus:border-blue-300 transition-transform ease-in-out duration-200`}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6 whitespace-nowrap">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
className="flex-shrink-0 w-10 h-10 rounded-full"
|
||||||
|
src={user.thumb}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div className="ml-4">
|
||||||
|
<div className="text-base font-bold leading-5">
|
||||||
|
{user.username}
|
||||||
|
</div>
|
||||||
|
{user.username &&
|
||||||
|
user.username.toLowerCase() !==
|
||||||
|
user.email && (
|
||||||
|
<div className="text-sm leading-5 text-gray-300">
|
||||||
|
{user.email}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Alert
|
||||||
|
title={intl.formatMessage(messages.nouserstoimport)}
|
||||||
|
type="info"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlexImportModal;
|
@@ -33,15 +33,12 @@ import SensitiveInput from '../Common/SensitiveInput';
|
|||||||
import Table from '../Common/Table';
|
import Table from '../Common/Table';
|
||||||
import Transition from '../Transition';
|
import Transition from '../Transition';
|
||||||
import BulkEditModal from './BulkEditModal';
|
import BulkEditModal from './BulkEditModal';
|
||||||
|
import PlexImportModal from './PlexImportModal';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
users: 'Users',
|
users: 'Users',
|
||||||
userlist: 'User List',
|
userlist: 'User List',
|
||||||
importfromplex: 'Import Users from Plex',
|
importfromplex: 'Import Plex Users',
|
||||||
importfromplexerror: 'Something went wrong while importing users from Plex.',
|
|
||||||
importedfromplex:
|
|
||||||
'{userCount, plural, one {# new user} other {# new users}} imported from Plex successfully!',
|
|
||||||
nouserstoimport: 'No new users to import from Plex.',
|
|
||||||
user: 'User',
|
user: 'User',
|
||||||
totalrequests: 'Requests',
|
totalrequests: 'Requests',
|
||||||
accounttype: 'Type',
|
accounttype: 'Type',
|
||||||
@@ -103,7 +100,7 @@ const UserList: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [isDeleting, setDeleting] = useState(false);
|
const [isDeleting, setDeleting] = useState(false);
|
||||||
const [isImporting, setImporting] = useState(false);
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
const [deleteModal, setDeleteModal] = useState<{
|
const [deleteModal, setDeleteModal] = useState<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
user?: User;
|
user?: User;
|
||||||
@@ -193,35 +190,6 @@ const UserList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const importFromPlex = async () => {
|
|
||||||
setImporting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data: createdUsers } = await axios.post(
|
|
||||||
'/api/v1/user/import-from-plex'
|
|
||||||
);
|
|
||||||
addToast(
|
|
||||||
createdUsers.length
|
|
||||||
? intl.formatMessage(messages.importedfromplex, {
|
|
||||||
userCount: createdUsers.length,
|
|
||||||
})
|
|
||||||
: intl.formatMessage(messages.nouserstoimport),
|
|
||||||
{
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'success',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
addToast(intl.formatMessage(messages.importfromplexerror), {
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
revalidate();
|
|
||||||
setImporting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
@@ -354,7 +322,7 @@ const UserList: React.FC = () => {
|
|||||||
title={intl.formatMessage(messages.localLoginDisabled, {
|
title={intl.formatMessage(messages.localLoginDisabled, {
|
||||||
strong: function strong(msg) {
|
strong: function strong(msg) {
|
||||||
return (
|
return (
|
||||||
<strong className="font-semibold text-yellow-100">
|
<strong className="font-semibold text-white">
|
||||||
{msg}
|
{msg}
|
||||||
</strong>
|
</strong>
|
||||||
);
|
);
|
||||||
@@ -481,6 +449,24 @@ const UserList: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</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={showImportModal}
|
||||||
|
>
|
||||||
|
<PlexImportModal
|
||||||
|
onCancel={() => setShowImportModal(false)}
|
||||||
|
onComplete={() => {
|
||||||
|
setShowImportModal(false);
|
||||||
|
revalidate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
|
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
|
||||||
<Header>{intl.formatMessage(messages.userlist)}</Header>
|
<Header>{intl.formatMessage(messages.userlist)}</Header>
|
||||||
<div className="flex flex-col flex-grow mt-2 lg:flex-row lg:flex-grow-0">
|
<div className="flex flex-col flex-grow mt-2 lg:flex-row lg:flex-grow-0">
|
||||||
@@ -496,8 +482,7 @@ const UserList: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
className="flex-grow outline lg:mr-2"
|
className="flex-grow outline lg:mr-2"
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
disabled={isImporting}
|
onClick={() => setShowImportModal(true)}
|
||||||
onClick={() => importFromPlex()}
|
|
||||||
>
|
>
|
||||||
<InboxInIcon />
|
<InboxInIcon />
|
||||||
<span>{intl.formatMessage(messages.importfromplex)}</span>
|
<span>{intl.formatMessage(messages.importfromplex)}</span>
|
||||||
|
@@ -22,6 +22,7 @@ const defaultSettings = {
|
|||||||
enablePushRegistration: false,
|
enablePushRegistration: false,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
emailEnabled: false,
|
emailEnabled: false,
|
||||||
|
newPlexLogin: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsContext = React.createContext<SettingsContextProps>({
|
export const SettingsContext = React.createContext<SettingsContextProps>({
|
||||||
|
@@ -31,6 +31,8 @@ const globalMessages = defineMessages({
|
|||||||
testing: 'Testing…',
|
testing: 'Testing…',
|
||||||
save: 'Save Changes',
|
save: 'Save Changes',
|
||||||
saving: 'Saving…',
|
saving: 'Saving…',
|
||||||
|
import: 'Import',
|
||||||
|
importing: 'Importing…',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
areyousure: 'Are you sure?',
|
areyousure: 'Are you sure?',
|
||||||
|
@@ -835,12 +835,13 @@
|
|||||||
"components.UserList.displayName": "Display Name",
|
"components.UserList.displayName": "Display Name",
|
||||||
"components.UserList.edituser": "Edit User Permissions",
|
"components.UserList.edituser": "Edit User Permissions",
|
||||||
"components.UserList.email": "Email Address",
|
"components.UserList.email": "Email Address",
|
||||||
"components.UserList.importedfromplex": "{userCount, plural, one {# new user} other {# new users}} imported from Plex successfully!",
|
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {user} other {users}} Plex users imported successfully!",
|
||||||
"components.UserList.importfromplex": "Import Users from Plex",
|
"components.UserList.importfromplex": "Import Plex Users",
|
||||||
"components.UserList.importfromplexerror": "Something went wrong while importing users from Plex.",
|
"components.UserList.importfromplexerror": "Something went wrong while importing Plex users.",
|
||||||
"components.UserList.localLoginDisabled": "The <strong>Enable Local Sign-In</strong> setting is currently disabled.",
|
"components.UserList.localLoginDisabled": "The <strong>Enable Local Sign-In</strong> setting is currently disabled.",
|
||||||
"components.UserList.localuser": "Local User",
|
"components.UserList.localuser": "Local User",
|
||||||
"components.UserList.nouserstoimport": "No new users to import from Plex.",
|
"components.UserList.newplexsigninenabled": "The <strong>Enable New Plex Sign-In</strong> setting is currently enabled. Plex users with library access do not need to be imported in order to sign in.",
|
||||||
|
"components.UserList.nouserstoimport": "There are no Plex users to import.",
|
||||||
"components.UserList.owner": "Owner",
|
"components.UserList.owner": "Owner",
|
||||||
"components.UserList.password": "Password",
|
"components.UserList.password": "Password",
|
||||||
"components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.",
|
"components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.",
|
||||||
@@ -974,6 +975,8 @@
|
|||||||
"i18n.edit": "Edit",
|
"i18n.edit": "Edit",
|
||||||
"i18n.experimental": "Experimental",
|
"i18n.experimental": "Experimental",
|
||||||
"i18n.failed": "Failed",
|
"i18n.failed": "Failed",
|
||||||
|
"i18n.import": "Import",
|
||||||
|
"i18n.importing": "Importing…",
|
||||||
"i18n.loading": "Loading…",
|
"i18n.loading": "Loading…",
|
||||||
"i18n.movie": "Movie",
|
"i18n.movie": "Movie",
|
||||||
"i18n.movies": "Movies",
|
"i18n.movies": "Movies",
|
||||||
|
@@ -169,6 +169,7 @@ CoreApp.getInitialProps = async (initialProps) => {
|
|||||||
enablePushRegistration: false,
|
enablePushRegistration: false,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
emailEnabled: false,
|
emailEnabled: false,
|
||||||
|
newPlexLogin: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ctx.res) {
|
if (ctx.res) {
|
||||||
|
Reference in New Issue
Block a user