import React, { useState } from 'react'; import useSWR from 'swr'; import LoadingSpinner from '../Common/LoadingSpinner'; import type { User } from '../../../server/entity/User'; import Badge from '../Common/Badge'; import { FormattedDate, defineMessages, useIntl } from 'react-intl'; import Button from '../Common/Button'; import { hasPermission } from '../../../server/lib/permissions'; import { Permission, UserType, useUser } from '../../hooks/useUser'; import { useRouter } from 'next/router'; import Header from '../Common/Header'; import Table from '../Common/Table'; import Transition from '../Transition'; import Modal from '../Common/Modal'; import axios from 'axios'; import { useToasts } from 'react-toast-notifications'; import globalMessages from '../../i18n/globalMessages'; import { Field, Form, Formik } from 'formik'; import * as Yup from 'yup'; import AddUserIcon from '../../assets/useradd.svg'; import Alert from '../Common/Alert'; import BulkEditModal from './BulkEditModal'; const messages = defineMessages({ userlist: 'User List', importfromplex: 'Import Users from Plex', importfromplexerror: 'Something went wrong while 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', role: 'Role', created: 'Created', lastupdated: 'Last Updated', edit: 'Edit', bulkedit: 'Bulk Edit', delete: 'Delete', admin: 'Admin', user: 'User', plexuser: 'Plex User', deleteuser: 'Delete User', userdeleted: 'User deleted', userdeleteerror: 'Something went wrong while deleting the user.', deleteconfirm: 'Are you sure you want to delete this user? All existing request data from this user will be removed.', localuser: 'Local User', createlocaluser: 'Create Local User', createuser: 'Create User', creating: 'Creating', create: 'Create', validationemailrequired: 'Must enter a valid email address', validationpasswordminchars: 'Password is too short; should be a minimum of 8 characters', usercreatedfailed: 'Something went wrong while creating the user.', usercreatedsuccess: 'User created successfully!', email: 'Email Address', password: 'Password', passwordinfo: 'Password Information', passwordinfodescription: 'Email notifications need to be configured and enabled in order to automatically generate passwords.', autogeneratepassword: 'Automatically generate password', }); const UserList: React.FC = () => { const intl = useIntl(); const router = useRouter(); 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; }>({ isOpen: false, }); const [createModal, setCreateModal] = useState<{ isOpen: boolean; }>({ isOpen: false, }); const [showBulkEditModal, setShowBulkEditModal] = useState(false); const [selectedUsers, setSelectedUsers] = useState([]); const { user: currentUser } = useUser(); const isUserPermsEditable = (userId: number) => userId !== 1 && userId !== currentUser?.id; const isAllUsersSelected = () => { return ( selectedUsers.length === data?.filter((user) => user.id !== currentUser?.id).length ); }; const isUserSelected = (userId: number) => selectedUsers.includes(userId); const toggleAllUsers = () => { if ( data && selectedUsers.length >= 0 && selectedUsers.length < data?.length - 1 ) { setSelectedUsers( data.filter((user) => isUserPermsEditable(user.id)).map((u) => u.id) ); } else { setSelectedUsers([]); } }; const toggleUser = (userId: number) => { if (selectedUsers.includes(userId)) { setSelectedUsers((users) => users.filter((u) => u !== userId)); } else { setSelectedUsers((users) => [...users, userId]); } }; const deleteUser = async () => { setDeleting(true); try { await axios.delete(`/api/v1/user/${deleteModal.user?.id}`); addToast(intl.formatMessage(messages.userdeleted), { autoDismiss: true, appearance: 'success', }); setDeleteModal({ isOpen: false }); } catch (e) { addToast(intl.formatMessage(messages.userdeleteerror), { autoDismiss: true, 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 ; } const CreateUserSchema = Yup.object().shape({ email: Yup.string() .email() .required(intl.formatMessage(messages.validationemailrequired)), password: Yup.lazy((value) => !value ? Yup.string() : Yup.string().min(8) ), }); return ( <> deleteUser()} okText={ isDeleting ? intl.formatMessage(globalMessages.deleting) : intl.formatMessage(globalMessages.delete) } okDisabled={isDeleting} okButtonType="danger" onCancel={() => setDeleteModal({ isOpen: false })} title={intl.formatMessage(messages.deleteuser)} iconSvg={ } > {intl.formatMessage(messages.deleteconfirm)} { try { await axios.post('/api/v1/user', { email: values.email, password: values.genpassword ? null : values.password, }); addToast(intl.formatMessage(messages.usercreatedsuccess), { appearance: 'success', autoDismiss: true, }); setCreateModal({ isOpen: false }); } catch (e) { addToast(intl.formatMessage(messages.usercreatedfailed), { appearance: 'error', autoDismiss: true, }); } finally { revalidate(); } }} > {({ errors, touched, isSubmitting, values, isValid, setFieldValue, handleSubmit, }) => { return ( } onOk={() => handleSubmit()} okText={ isSubmitting ? intl.formatMessage(messages.creating) : intl.formatMessage(messages.create) } okDisabled={isSubmitting || !isValid} okButtonType="primary" onCancel={() => setCreateModal({ isOpen: false })} > {intl.formatMessage(messages.passwordinfodescription)}
{errors.email && touched.email && (
{errors.email}
)}
setFieldValue('password', '')} />
{errors.password && touched.password && (
{errors.password}
)}
); }}
setShowBulkEditModal(false)} onComplete={() => { setShowBulkEditModal(false); revalidate(); }} selectedUserIds={selectedUsers} users={data} />
{intl.formatMessage(messages.userlist)}
{ toggleAllUsers(); }} /> {intl.formatMessage(messages.username)}{intl.formatMessage(messages.totalrequests)}{intl.formatMessage(messages.usertype)}{intl.formatMessage(messages.role)}{intl.formatMessage(messages.created)}{intl.formatMessage(messages.lastupdated)} {data?.map((user) => ( {isUserPermsEditable(user.id) && ( { toggleUser(user.id); }} /> )}
{user.displayName}
{user.email}
{user.requestCount}
{user.userType === UserType.PLEX ? ( {intl.formatMessage(messages.plexuser)} ) : ( {intl.formatMessage(messages.localuser)} )} {hasPermission(Permission.ADMIN, user.permissions) ? intl.formatMessage(messages.admin) : intl.formatMessage(messages.user)} ))}
); }; export default UserList;