Skip to content

Commit

Permalink
feat(plex): selective user import (#2188)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
TheCatLady and sct committed Jan 14, 2022
1 parent 2561639 commit 9cb97db
Show file tree
Hide file tree
Showing 14 changed files with 389 additions and 50 deletions.
4 changes: 2 additions & 2 deletions docs/using-overseerr/users/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 &rarr; 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.

Expand Down
43 changes: 42 additions & 1 deletion overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1994,6 +1994,36 @@ paths:
type: array
items:
$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:
get:
summary: Get Radarr settings
Expand Down Expand Up @@ -3196,11 +3226,22 @@ paths:
post:
summary: Import all users from Plex
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.
tags:
- users
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
plexIds:
type: array
items:
type: string
responses:
'201':
description: A list of the newly created users
Expand Down
2 changes: 1 addition & 1 deletion server/api/plextv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ class PlexTvAPI {

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) {
throw new Error(
Expand Down
1 change: 1 addition & 0 deletions server/interfaces/api/settingsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface PublicSettingsResponse {
enablePushRegistration: boolean;
locale: string;
emailEnabled: boolean;
newPlexLogin: boolean;
}

export interface CacheItem {
Expand Down
2 changes: 2 additions & 0 deletions server/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ interface FullPublicSettings extends PublicSettings {
enablePushRegistration: boolean;
locale: string;
emailEnabled: boolean;
newPlexLogin: boolean;
}

export interface NotificationAgentConfig {
Expand Down Expand Up @@ -469,6 +470,7 @@ class Settings {
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
locale: this.data.main.locale,
emailEnabled: this.data.notifications.agents.email.enabled,
newPlexLogin: this.data.main.newPlexLogin,
};
}

Expand Down
54 changes: 53 additions & 1 deletion server/routes/settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import fs from 'fs';
import { merge, omit } from 'lodash';
import { merge, omit, sortBy } from 'lodash';
import { rescheduleJob } from 'node-schedule';
import path from 'path';
import { getRepository } from 'typeorm';
Expand Down Expand Up @@ -225,6 +225,58 @@ settingsRoutes.post('/plex/sync', (req, res) => {
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(
'/logs',
rateLimit({ windowMs: 60 * 1000, max: 50 }),
Expand Down
3 changes: 2 additions & 1 deletion server/routes/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ router.post(
try {
const settings = getSettings();
const userRepository = getRepository(User);
const body = req.body as { plexIds: string[] } | undefined;

// taken from auth.ts
const mainUser = await userRepository.findOneOrFail({
Expand Down Expand Up @@ -434,7 +435,7 @@ router.post(
user.plexId = parseInt(account.id);
}
await userRepository.save(user);
} else {
} else if (!body || body.plexIds.includes(account.id)) {
if (await mainPlexTv.checkUserAccess(parseInt(account.id))) {
const newUser = new User({
plexUsername: account.username,
Expand Down
4 changes: 2 additions & 2 deletions src/components/Settings/SettingsServices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ const SettingsServices: React.FC = () => {
serverType: 'Radarr',
strong: function strong(msg) {
return (
<strong className="font-semibold text-yellow-100">
<strong className="font-semibold text-white">
{msg}
</strong>
);
Expand Down Expand Up @@ -382,7 +382,7 @@ const SettingsServices: React.FC = () => {
serverType: 'Sonarr',
strong: function strong(msg) {
return (
<strong className="font-semibold text-yellow-100">
<strong className="font-semibold text-white">
{msg}
</strong>
);
Expand Down
Loading

0 comments on commit 9cb97db

Please sign in to comment.