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:
|
||||
schema:
|
||||
$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}:
|
||||
get:
|
||||
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 {
|
||||
private authToken: string;
|
||||
private axios: AxiosInstance;
|
||||
@@ -129,6 +144,18 @@ class PlexTvAPI {
|
||||
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;
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import PlexTvAPI from '../api/plextv';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import { User } from '../entity/User';
|
||||
import { hasPermission, Permission } from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
|
||||
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;
|
||||
|
@@ -18,6 +18,10 @@ import globalMessages from '../../i18n/globalMessages';
|
||||
|
||||
const messages = defineMessages({
|
||||
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',
|
||||
totalrequests: 'Total Requests',
|
||||
usertype: 'User Type',
|
||||
@@ -42,6 +46,7 @@ const UserList: React.FC = () => {
|
||||
const { addToast } = useToasts();
|
||||
const { data, error, revalidate } = useSWR<User[]>('/api/v1/user');
|
||||
const [isDeleting, setDeleting] = useState(false);
|
||||
const [isImporting, setImporting] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState<{
|
||||
isOpen: boolean;
|
||||
user?: User;
|
||||
@@ -66,10 +71,38 @@ const UserList: React.FC = () => {
|
||||
appearance: 'error',
|
||||
});
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
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) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
@@ -116,7 +149,17 @@ const UserList: React.FC = () => {
|
||||
{intl.formatMessage(messages.deleteconfirm)}
|
||||
</Modal>
|
||||
</Transition>
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -134,18 +177,18 @@ const UserList: React.FC = () => {
|
||||
<tr key={`user-list-${user.id}`}>
|
||||
<Table.TD>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-10 w-10">
|
||||
<div className="flex-shrink-0 w-10 h-10">
|
||||
<img
|
||||
className="h-10 w-10 rounded-full"
|
||||
className="w-10 h-10 rounded-full"
|
||||
src={user.avatar}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="text-sm leading-5 font-medium">
|
||||
<div className="text-sm font-medium leading-5">
|
||||
{user.username}
|
||||
</div>
|
||||
<div className="text-sm leading-5 text-gray-300">
|
||||
<div className="text-sm text-gray-300 leading-5">
|
||||
{user.email}
|
||||
</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.deleteuser": "Delete User",
|
||||
"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.plexuser": "Plex User",
|
||||
"components.UserList.role": "Role",
|
||||
|
Reference in New Issue
Block a user