import type { BaseItemDto, NameIdPair, SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client'; import escapeHTML from 'escape-html'; import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import Dashboard from '../../../../utils/dashboard'; import globalize from '../../../../lib/globalize'; import ButtonElement from '../../../../elements/ButtonElement'; import CheckBoxElement from '../../../../elements/CheckBoxElement'; import InputElement from '../../../../elements/InputElement'; import LinkButton from '../../../../elements/emby-button/LinkButton'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; import loading from '../../../../components/loading/loading'; import toast from '../../../../components/toast/toast'; import SelectElement from '../../../../elements/SelectElement'; import Page from '../../../../components/Page'; type ResetProvider = BaseItemDto & { checkedAttribute: string }; const getCheckedElementDataIds = (elements: NodeListOf) => ( Array.prototype.filter.call(elements, e => e.checked) .map(e => e.getAttribute('data-id')) ); function onSaveComplete() { Dashboard.navigate('/dashboard/users') .catch(err => { console.error('[useredit] failed to navigate to user profile', err); }); loading.hide(); toast(globalize.translate('SettingsSaved')); } const UserEdit = () => { const [ searchParams ] = useSearchParams(); const userId = searchParams.get('userId'); const [ userDto, setUserDto ] = useState(); const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState([]); const [ authProviders, setAuthProviders ] = useState([]); const [ passwordResetProviders, setPasswordResetProviders ] = useState([]); const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []); const [ authenticationProviderId, setAuthenticationProviderId ] = useState(''); const [ passwordResetProviderId, setPasswordResetProviderId ] = useState(''); const element = useRef(null); const triggerChange = (select: HTMLInputElement) => { const evt = new Event('change', { bubbles: false, cancelable: true }); select.dispatchEvent(evt); }; const getUser = () => { if (!userId) throw new Error('missing user id'); return window.ApiClient.getUser(userId); }; const loadAuthProviders = useCallback((page: HTMLDivElement, user: UserDto, providers: NameIdPair[]) => { const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement; fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1); setAuthProviders(providers); const currentProviderId = user.Policy?.AuthenticationProviderId || ''; setAuthenticationProviderId(currentProviderId); }, []); const loadPasswordResetProviders = useCallback((page: HTMLDivElement, user: UserDto, providers: NameIdPair[]) => { const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement; fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1); setPasswordResetProviders(providers); const currentProviderId = user.Policy?.PasswordResetProviderId || ''; setPasswordResetProviderId(currentProviderId); }, []); const loadDeleteFolders = useCallback((page: HTMLDivElement, user: UserDto, mediaFolders: BaseItemDto[]) => { window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', { SupportsMediaDeletion: true })).then(function (channelsResult) { let isChecked; let checkedAttribute; const itemsArr: ResetProvider[] = []; for (const mediaFolder of mediaFolders) { isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(mediaFolder.Id || '') != -1; checkedAttribute = isChecked ? ' checked="checked"' : ''; itemsArr.push({ ...mediaFolder, checkedAttribute: checkedAttribute }); } for (const channel of channelsResult.Items) { isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(channel.Id || '') != -1; checkedAttribute = isChecked ? ' checked="checked"' : ''; itemsArr.push({ ...channel, checkedAttribute: checkedAttribute }); } setDeleteFoldersAccess(itemsArr); const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement; chkEnableDeleteAllFolders.checked = user.Policy?.EnableContentDeletion || false; triggerChange(chkEnableDeleteAllFolders); }).catch(err => { console.error('[useredit] failed to fetch channels', err); }); }, []); const loadUser = useCallback((user: UserDto) => { const page = element.current; if (!page) { console.error('[useredit] Unexpected null page reference'); return; } window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) { loadAuthProviders(page, user, providers); }).catch(err => { console.error('[useredit] failed to fetch auth providers', err); }); window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) { loadPasswordResetProviders(page, user, providers); }).catch(err => { console.error('[useredit] failed to fetch password reset providers', err); }); window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', { IsHidden: false })).then(function (folders) { loadDeleteFolders(page, user, folders.Items); }).catch(err => { console.error('[useredit] failed to fetch media folders', err); }); const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement; disabledUserBanner.classList.toggle('hide', !user.Policy?.IsDisabled); const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement; txtUserName.disabled = false; txtUserName.removeAttribute('disabled'); void libraryMenu.then(menu => menu.setTitle(user.Name)); setUserDto(user); (page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name || ''; (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = !!user.Policy?.IsAdministrator; (page.querySelector('.chkDisabled') as HTMLInputElement).checked = !!user.Policy?.IsDisabled; (page.querySelector('.chkIsHidden') as HTMLInputElement).checked = !!user.Policy?.IsHidden; (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = !!user.Policy?.EnableCollectionManagement; (page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked = !!user.Policy?.EnableSubtitleManagement; (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = !!user.Policy?.EnableSharedDeviceControl; (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = !!user.Policy?.EnableRemoteControlOfOtherUsers; (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = !!user.Policy?.EnableContentDownloading; (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = !!user.Policy?.EnableLiveTvManagement; (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = !!user.Policy?.EnableLiveTvAccess; (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = !!user.Policy?.EnableMediaPlayback; (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = !!user.Policy?.EnableAudioPlaybackTranscoding; (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = !!user.Policy?.EnableVideoPlaybackTranscoding; (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = !!user.Policy?.EnablePlaybackRemuxing; (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = !!user.Policy?.ForceRemoteSourceTranscoding; (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy?.EnableRemoteAccess == null || user.Policy?.EnableRemoteAccess; (page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy?.RemoteClientBitrateLimit && user.Policy?.RemoteClientBitrateLimit > 0 ? (user.Policy?.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : ''; (page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = String(user.Policy?.LoginAttemptsBeforeLockout) || '-1'; (page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = String(user.Policy?.MaxActiveSessions) || '0'; (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = String(user.Policy?.SyncPlayAccess); loading.hide(); }, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]); const loadData = useCallback(() => { loading.show(); getUser().then(function (user) { loadUser(user); }).catch(err => { console.error('[useredit] failed to load data', err); }); }, [loadUser]); useEffect(() => { const page = element.current; if (!page) { console.error('[useredit] Unexpected null page reference'); return; } loadData(); const saveUser = (user: UserDto) => { if (!user.Id || !user.Policy) { throw new Error('Unexpected null user id or policy'); } user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value.trim(); user.Policy.IsAdministrator = (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked; user.Policy.IsHidden = (page.querySelector('.chkIsHidden') as HTMLInputElement).checked; user.Policy.IsDisabled = (page.querySelector('.chkDisabled') as HTMLInputElement).checked; user.Policy.EnableRemoteControlOfOtherUsers = (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked; user.Policy.EnableLiveTvManagement = (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked; user.Policy.EnableLiveTvAccess = (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked; user.Policy.EnableSharedDeviceControl = (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked; user.Policy.EnableMediaPlayback = (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked; user.Policy.EnableAudioPlaybackTranscoding = (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked; user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked; user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked; user.Policy.EnableCollectionManagement = (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked; user.Policy.EnableSubtitleManagement = (page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked; user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked; user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked; user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked; user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat((page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value || '0')); user.Policy.LoginAttemptsBeforeLockout = parseInt((page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value || '0', 10); user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0', 10); user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value; user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value; user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked; user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder')); user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType; window.ApiClient.updateUser(user).then(() => ( window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || { PasswordResetProviderId: '', AuthenticationProviderId: '' }) )).then(() => { onSaveComplete(); }).catch(err => { console.error('[useredit] failed to update user', err); }); }; const onSubmit = (e: Event) => { loading.show(); getUser().then(function (result) { saveUser(result); }).catch(err => { console.error('[useredit] failed to fetch user', err); }); e.preventDefault(); e.stopPropagation(); return false; }; (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked); }); window.ApiClient.getNamedConfiguration('network').then(function (config) { (page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess); }).catch(err => { console.error('[useredit] failed to load network config', err); }); (page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit); (page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() { window.history.back(); }); }, [loadData]); const optionLoginProvider = authProviders.map((provider) => { const selected = provider.Id === authenticationProviderId || authProviders.length < 2 ? ' selected' : ''; return ``; }); const optionPasswordResetProvider = passwordResetProviders.map((provider) => { const selected = provider.Id === passwordResetProviderId || passwordResetProviders.length < 2 ? ' selected' : ''; return ``; }); const optionSyncPlayAccess = () => { let content = ''; content += ``; content += ``; content += ``; return content; }; return (
{globalize.translate('ButtonEditOtherUserPreferences')}
{globalize.translate('HeaderThisUserIsCurrentlyDisabled')}
{globalize.translate('MessageReenableUser')}
{optionLoginProvider}
{globalize.translate('AuthProviderHelp')}
{optionPasswordResetProvider}
{globalize.translate('PasswordResetProviderHelp')}
{globalize.translate('AllowRemoteAccessHelp')}

{globalize.translate('HeaderFeatureAccess')}

{globalize.translate('HeaderPlayback')}

{globalize.translate('OptionAllowMediaPlaybackTranscodingHelp')}

{globalize.translate('LabelRemoteClientBitrateLimitHelp')}
{globalize.translate('LabelUserRemoteClientBitrateLimitHelp')}
{optionSyncPlayAccess()}
{globalize.translate('SyncPlayAccessHelp')}

{globalize.translate('HeaderAllowMediaDeletionFrom')}

{deleteFoldersAccess.map(Item => ( ))}

{globalize.translate('HeaderRemoteControl')}

{globalize.translate('OptionAllowRemoteSharedDevicesHelp')}

{globalize.translate('Other')}

{globalize.translate('OptionAllowContentDownloadHelp')}
{globalize.translate('OptionDisableUserHelp')}
{globalize.translate('OptionHideUserFromLoginHelp')}

{globalize.translate('OptionLoginAttemptsBeforeLockout')}
{globalize.translate('OptionLoginAttemptsBeforeLockoutHelp')}

{globalize.translate('OptionMaxActiveSessions')}
{globalize.translate('OptionMaxActiveSessionsHelp')}

); }; export default UserEdit;