diff --git a/overseerr-api.yml b/overseerr-api.yml index bcb97771..268fa896 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -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 diff --git a/server/api/plextv.ts b/server/api/plextv.ts index a1152ada..e3e40c73 100644 --- a/server/api/plextv.ts +++ b/server/api/plextv.ts @@ -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 { + 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; diff --git a/server/routes/user.ts b/server/routes/user.ts index e6dd136a..acbdfdb3 100644 --- a/server/routes/user.ts +++ b/server/routes/user.ts @@ -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; diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 37b8752d..27971931 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -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('/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 ; } @@ -116,7 +149,17 @@ const UserList: React.FC = () => { {intl.formatMessage(messages.deleteconfirm)} -
{intl.formatMessage(messages.userlist)}
+
+
{intl.formatMessage(messages.userlist)}
+ +
@@ -134,18 +177,18 @@ const UserList: React.FC = () => {
-
+
-
+
{user.username}
-
+
{user.email}
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 8130c76e..5fae284e 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -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",