mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: import users from plex (#428)
* feat: import users from plex fix #281 * fix(frontend): re-enable delete user confirmation button after finished
This commit is contained in:
@@ -1673,6 +1673,24 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/User'
|
$ref: '#/components/schemas/User'
|
||||||
|
/user/import-from-plex:
|
||||||
|
post:
|
||||||
|
summary: Imports all users from Plex
|
||||||
|
description: |
|
||||||
|
Requests users from the Plex Server and creates a new user for each of them
|
||||||
|
|
||||||
|
Requires the `MANAGE_USERS` permission.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: A list of the newly created users
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
|
||||||
/user/{userId}:
|
/user/{userId}:
|
||||||
get:
|
get:
|
||||||
summary: Retrieve a user by ID
|
summary: Retrieve a user by ID
|
||||||
|
@@ -56,6 +56,21 @@ interface FriendResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UsersResponse {
|
||||||
|
MediaContainer: {
|
||||||
|
User: {
|
||||||
|
$: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
thumb: string;
|
||||||
|
};
|
||||||
|
Server: ServerResponse[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class PlexTvAPI {
|
class PlexTvAPI {
|
||||||
private authToken: string;
|
private authToken: string;
|
||||||
private axios: AxiosInstance;
|
private axios: AxiosInstance;
|
||||||
@@ -129,6 +144,18 @@ class PlexTvAPI {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getUsers(): Promise<UsersResponse> {
|
||||||
|
const response = await this.axios.get('/api/users', {
|
||||||
|
transformResponse: [],
|
||||||
|
responseType: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedXml = (await xml2js.parseStringPromise(
|
||||||
|
response.data
|
||||||
|
)) as UsersResponse;
|
||||||
|
return parsedXml;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PlexTvAPI;
|
export default PlexTvAPI;
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
|
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';
|
||||||
import { hasPermission, Permission } from '../lib/permissions';
|
import { hasPermission, Permission } from '../lib/permissions';
|
||||||
|
import { getSettings } from '../lib/settings';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -142,4 +144,51 @@ router.delete<{ id: string }>('/:id', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/import-from-plex', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const settings = getSettings();
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
|
||||||
|
// taken from auth.ts
|
||||||
|
const mainUser = await userRepository.findOneOrFail({
|
||||||
|
select: ['id', 'plexToken'],
|
||||||
|
order: { id: 'ASC' },
|
||||||
|
});
|
||||||
|
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
||||||
|
|
||||||
|
const plexUsersResponse = await mainPlexTv.getUsers();
|
||||||
|
const createdUsers: User[] = [];
|
||||||
|
for (const rawUser of plexUsersResponse.MediaContainer.User) {
|
||||||
|
const account = rawUser.$;
|
||||||
|
const user = await userRepository.findOne({
|
||||||
|
where: { plexId: account.id },
|
||||||
|
});
|
||||||
|
if (user) {
|
||||||
|
// Update the users avatar with their plex thumbnail (incase it changed)
|
||||||
|
user.avatar = account.thumb;
|
||||||
|
user.email = account.email;
|
||||||
|
user.username = account.username;
|
||||||
|
await userRepository.save(user);
|
||||||
|
} else {
|
||||||
|
// Check to make sure it's a real account
|
||||||
|
if (account.email && account.username) {
|
||||||
|
const newUser = new User({
|
||||||
|
username: account.username,
|
||||||
|
email: account.email,
|
||||||
|
permissions: settings.main.defaultPermissions,
|
||||||
|
plexId: parseInt(account.id),
|
||||||
|
plexToken: '',
|
||||||
|
avatar: account.thumb,
|
||||||
|
});
|
||||||
|
await userRepository.save(newUser);
|
||||||
|
createdUsers.push(newUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.status(201).json(User.filterMany(createdUsers));
|
||||||
|
} catch (e) {
|
||||||
|
next({ status: 500, message: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@@ -18,6 +18,10 @@ import globalMessages from '../../i18n/globalMessages';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
userlist: 'User List',
|
userlist: 'User List',
|
||||||
|
importfromplex: 'Import Users From Plex',
|
||||||
|
importfromplexerror: 'Something went wrong importing users from Plex',
|
||||||
|
importedfromplex:
|
||||||
|
'{userCount, plural, =0 {No new users} one {# new user} other {# new users}} imported from Plex',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
totalrequests: 'Total Requests',
|
totalrequests: 'Total Requests',
|
||||||
usertype: 'User Type',
|
usertype: 'User Type',
|
||||||
@@ -42,6 +46,7 @@ const UserList: React.FC = () => {
|
|||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const { data, error, revalidate } = useSWR<User[]>('/api/v1/user');
|
const { data, error, revalidate } = useSWR<User[]>('/api/v1/user');
|
||||||
const [isDeleting, setDeleting] = useState(false);
|
const [isDeleting, setDeleting] = useState(false);
|
||||||
|
const [isImporting, setImporting] = useState(false);
|
||||||
const [deleteModal, setDeleteModal] = useState<{
|
const [deleteModal, setDeleteModal] = useState<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
user?: User;
|
user?: User;
|
||||||
@@ -66,10 +71,38 @@ const UserList: React.FC = () => {
|
|||||||
appearance: 'error',
|
appearance: 'error',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
revalidate();
|
revalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const importFromPlex = async () => {
|
||||||
|
setImporting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: createdUsers } = await axios.post(
|
||||||
|
'/api/v1/user/import-from-plex'
|
||||||
|
);
|
||||||
|
addToast(
|
||||||
|
intl.formatMessage(messages.importedfromplex, {
|
||||||
|
userCount: createdUsers.length,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
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 />;
|
||||||
}
|
}
|
||||||
@@ -116,7 +149,17 @@ const UserList: React.FC = () => {
|
|||||||
{intl.formatMessage(messages.deleteconfirm)}
|
{intl.formatMessage(messages.deleteconfirm)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<Header extraMargin={4}>{intl.formatMessage(messages.userlist)}</Header>
|
<Header extraMargin={4}>{intl.formatMessage(messages.userlist)}</Header>
|
||||||
|
<Button
|
||||||
|
className="mx-4 my-8"
|
||||||
|
buttonType="primary"
|
||||||
|
disabled={isImporting}
|
||||||
|
onClick={() => importFromPlex()}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.importfromplex)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -134,18 +177,18 @@ const UserList: React.FC = () => {
|
|||||||
<tr key={`user-list-${user.id}`}>
|
<tr key={`user-list-${user.id}`}>
|
||||||
<Table.TD>
|
<Table.TD>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-shrink-0 h-10 w-10">
|
<div className="flex-shrink-0 w-10 h-10">
|
||||||
<img
|
<img
|
||||||
className="h-10 w-10 rounded-full"
|
className="w-10 h-10 rounded-full"
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<div className="text-sm leading-5 font-medium">
|
<div className="text-sm font-medium leading-5">
|
||||||
{user.username}
|
{user.username}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm leading-5 text-gray-300">
|
<div className="text-sm text-gray-300 leading-5">
|
||||||
{user.email}
|
{user.email}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -334,6 +334,9 @@
|
|||||||
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All existing request data from this user will be removed.",
|
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All existing request data from this user will be removed.",
|
||||||
"components.UserList.deleteuser": "Delete User",
|
"components.UserList.deleteuser": "Delete User",
|
||||||
"components.UserList.edit": "Edit",
|
"components.UserList.edit": "Edit",
|
||||||
|
"components.UserList.importedfromplex": "{userCount, plural, =0 {No new users} one {# new user} other {# new users}} imported from Plex",
|
||||||
|
"components.UserList.importfromplex": "Import Users From Plex",
|
||||||
|
"components.UserList.importfromplexerror": "Something went wrong importing users from Plex",
|
||||||
"components.UserList.lastupdated": "Last Updated",
|
"components.UserList.lastupdated": "Last Updated",
|
||||||
"components.UserList.plexuser": "Plex User",
|
"components.UserList.plexuser": "Plex User",
|
||||||
"components.UserList.role": "Role",
|
"components.UserList.role": "Role",
|
||||||
|
Reference in New Issue
Block a user