import { TrashIcon } from '@heroicons/react/outline'; import { ChevronLeftIcon, ChevronRightIcon, InboxInIcon, PencilIcon, SortDescendingIcon, UserAddIcon, } from '@heroicons/react/solid'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; import * as Yup from 'yup'; import type { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces'; import { hasPermission } from '../../../server/lib/permissions'; import useSettings from '../../hooks/useSettings'; import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams'; import type { User } from '../../hooks/useUser'; import { Permission, UserType, useUser } from '../../hooks/useUser'; import globalMessages from '../../i18n/globalMessages'; import Alert from '../Common/Alert'; import Badge from '../Common/Badge'; import Button from '../Common/Button'; import Header from '../Common/Header'; import LoadingSpinner from '../Common/LoadingSpinner'; import Modal from '../Common/Modal'; import PageTitle from '../Common/PageTitle'; import SensitiveInput from '../Common/SensitiveInput'; import Table from '../Common/Table'; import Transition from '../Transition'; import BulkEditModal from './BulkEditModal'; import PlexImportModal from './PlexImportModal'; const messages = defineMessages({ users: 'Users', userlist: 'User List', importfromplex: 'Import Plex Users', user: 'User', totalrequests: 'Requests', accounttype: 'Type', role: 'Role', created: 'Joined', bulkedit: 'Bulk Edit', owner: 'Owner', admin: 'Admin', plexuser: 'Plex User', deleteuser: 'Delete User', userdeleted: 'User deleted successfully!', userdeleteerror: 'Something went wrong while deleting the user.', deleteconfirm: 'Are you sure you want to delete this user? All of their request data will be permanently removed.', localuser: 'Local User', createlocaluser: 'Create Local User', creating: 'Creating…', create: 'Create', validationpasswordminchars: 'Password is too short; should be a minimum of 8 characters', usercreatedfailed: 'Something went wrong while creating the user.', usercreatedfailedexisting: 'The provided email address is already in use by another user.', usercreatedsuccess: 'User created successfully!', displayName: 'Display Name', email: 'Email Address', password: 'Password', passwordinfodescription: 'Configure an application URL and enable email notifications to allow automatic password generation.', autogeneratepassword: 'Automatically Generate Password', autogeneratepasswordTip: 'Email a server-generated password to the user', validationEmail: 'You must provide a valid email address', sortCreated: 'Join Date', sortDisplayName: 'Display Name', sortRequests: 'Request Count', localLoginDisabled: 'The Enable Local Sign-In setting is currently disabled.', }); type Sort = 'created' | 'updated' | 'requests' | 'displayname'; const UserList: React.FC = () => { const intl = useIntl(); const router = useRouter(); const settings = useSettings(); const { addToast } = useToasts(); const { user: currentUser, hasPermission: currentHasPermission } = useUser(); const [currentSort, setCurrentSort] = useState('displayname'); const [currentPageSize, setCurrentPageSize] = useState(10); const page = router.query.page ? Number(router.query.page) : 1; const pageIndex = page - 1; const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); const { data, error, mutate: revalidate, } = useSWR( `/api/v1/user?take=${currentPageSize}&skip=${ pageIndex * currentPageSize }&sort=${currentSort}` ); const [isDeleting, setDeleting] = useState(false); const [showImportModal, setShowImportModal] = 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([]); useEffect(() => { const filterString = window.localStorage.getItem('ul-filter-settings'); if (filterString) { const filterSettings = JSON.parse(filterString); setCurrentSort(filterSettings.currentSort); setCurrentPageSize(filterSettings.currentPageSize); } }, []); useEffect(() => { window.localStorage.setItem( 'ul-filter-settings', JSON.stringify({ currentSort, currentPageSize, }) ); }, [currentSort, currentPageSize]); const isUserPermsEditable = (userId: number) => userId !== 1 && userId !== currentUser?.id; const isAllUsersSelected = () => { return ( selectedUsers.length === data?.results.filter((user) => user.id !== currentUser?.id).length ); }; const isUserSelected = (userId: number) => selectedUsers.includes(userId); const toggleAllUsers = () => { if ( data && selectedUsers.length >= 0 && selectedUsers.length < data?.results.length - 1 ) { setSelectedUsers( data.results .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(); } }; if (!data && !error) { return ; } const CreateUserSchema = Yup.object().shape({ email: Yup.string() .required(intl.formatMessage(messages.validationEmail)) .email(intl.formatMessage(messages.validationEmail)), password: Yup.lazy((value) => !value ? Yup.string() : Yup.string().min( 8, intl.formatMessage(messages.validationpasswordminchars) ) ), }); if (!data) { return ; } const hasNextPage = data.pageInfo.pages > pageIndex + 1; const hasPrevPage = pageIndex > 0; const passwordGenerationEnabled = settings.currentSettings.applicationUrl && settings.currentSettings.emailEnabled; 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', { username: values.displayName, 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( e.response.data.errors?.includes('USER_EXISTS') ? messages.usercreatedfailedexisting : 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 })} > {!settings.currentSettings.localLogin && ( {msg} ); }, })} type="warning" /> )} {currentHasPermission(Permission.MANAGE_SETTINGS) && !passwordGenerationEnabled && ( )}
{errors.email && touched.email && (
{errors.email}
)}
setFieldValue('password', '')} />
{errors.password && touched.password && (
{errors.password}
)}
); }}
setShowBulkEditModal(false)} onComplete={() => { setShowBulkEditModal(false); revalidate(); }} selectedUserIds={selectedUsers} users={data.results} /> setShowImportModal(false)} onComplete={() => { setShowImportModal(false); revalidate(); }} />
{intl.formatMessage(messages.userlist)}
{(data.results ?? []).length > 1 && ( { toggleAllUsers(); }} /> )} {intl.formatMessage(messages.user)}{intl.formatMessage(messages.totalrequests)}{intl.formatMessage(messages.accounttype)}{intl.formatMessage(messages.role)}{intl.formatMessage(messages.created)} {(data.results ?? []).length > 1 && ( )} {data?.results.map((user) => ( {isUserPermsEditable(user.id) && ( { toggleUser(user.id); }} /> )}
{user.displayName} {user.displayName.toLowerCase() !== user.email && (
{user.email}
)}
{user.id === currentUser?.id || currentHasPermission( [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], { type: 'or' } ) ? ( {user.requestCount} ) : ( user.requestCount )} {user.userType === UserType.PLEX ? ( {intl.formatMessage(messages.plexuser)} ) : ( {intl.formatMessage(messages.localuser)} )} {user.id === 1 ? intl.formatMessage(messages.owner) : hasPermission(Permission.ADMIN, user.permissions) ? intl.formatMessage(messages.admin) : intl.formatMessage(messages.user)} {intl.formatDate(user.createdAt, { year: 'numeric', month: 'long', day: 'numeric', })} ))}
); }; export default UserList;