diff --git a/src/apps/dashboard/routes/users/access.tsx b/src/apps/dashboard/routes/users/access.tsx index 812ad80c5b8..727e21fc924 100644 --- a/src/apps/dashboard/routes/users/access.tsx +++ b/src/apps/dashboard/routes/users/access.tsx @@ -1,5 +1,6 @@ import type { UserDto } from '@jellyfin/sdk/lib/generated-client'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; +import { useSearchParams } from 'react-router-dom'; import loading from '../../../../components/loading/loading'; import libraryMenu from '../../../../scripts/libraryMenu'; @@ -7,7 +8,6 @@ import globalize from '../../../../scripts/globalize'; import toast from '../../../../components/toast/toast'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; import ButtonElement from '../../../../elements/ButtonElement'; -import { getParameterByName } from '../../../../utils/url'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; import AccessContainer from '../../../../components/dashboard/users/AccessContainer'; import CheckBoxElement from '../../../../elements/CheckBoxElement'; @@ -21,6 +21,8 @@ type ItemsArr = { }; const UserLibraryAccess: FunctionComponent = () => { + const [ searchParams ] = useSearchParams(); + const userId = searchParams.get('userId'); const [ userName, setUserName ] = useState(''); const [channelsItems, setChannelsItems] = useState([]); const [mediaFoldersItems, setMediaFoldersItems] = useState([]); @@ -37,7 +39,7 @@ const UserLibraryAccess: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userlibraryaccess] Unexpected null page reference'); return; } @@ -64,7 +66,7 @@ const UserLibraryAccess: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userlibraryaccess] Unexpected null page reference'); return; } @@ -97,7 +99,7 @@ const UserLibraryAccess: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userlibraryaccess] Unexpected null page reference'); return; } @@ -138,7 +140,6 @@ const UserLibraryAccess: FunctionComponent = () => { const loadData = useCallback(() => { loading.show(); - const userId = getParameterByName('userId'); const promise1 = userId ? window.ApiClient.getUser(userId) : Promise.resolve({ Configuration: {} }); const promise2 = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', { IsHidden: false @@ -150,21 +151,25 @@ const UserLibraryAccess: FunctionComponent = () => { }).catch(err => { console.error('[userlibraryaccess] failed to load data', err); }); - }, [loadUser]); + }, [loadUser, userId]); useEffect(() => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userlibraryaccess] Unexpected null page reference'); return; } loadData(); const onSubmit = (e: Event) => { + if (!userId) { + console.error('[userlibraryaccess] missing user id'); + return; + } + loading.show(); - const userId = getParameterByName('userId'); window.ApiClient.getUser(userId).then(function (result) { saveUser(result); }).catch(err => { diff --git a/src/apps/dashboard/routes/users/parentalcontrol.tsx b/src/apps/dashboard/routes/users/parentalcontrol.tsx index 14231245cc1..a995a1695cb 100644 --- a/src/apps/dashboard/routes/users/parentalcontrol.tsx +++ b/src/apps/dashboard/routes/users/parentalcontrol.tsx @@ -1,7 +1,8 @@ import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client'; import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week'; -import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; import escapeHTML from 'escape-html'; +import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; +import { useSearchParams } from 'react-router-dom'; import globalize from '../../../../scripts/globalize'; import LibraryMenu from '../../../../scripts/libraryMenu'; @@ -12,7 +13,6 @@ 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 { getParameterByName } from '../../../../utils/url'; import CheckBoxElement from '../../../../elements/CheckBoxElement'; import SelectElement from '../../../../elements/SelectElement'; import Page from '../../../../components/Page'; @@ -57,6 +57,8 @@ function handleSaveUser( } const UserParentalControl: FunctionComponent = () => { + const [ searchParams ] = useSearchParams(); + const userId = searchParams.get('userId'); const [ userName, setUserName ] = useState(''); const [ parentalRatings, setParentalRatings ] = useState([]); const [ unratedItems, setUnratedItems ] = useState([]); @@ -95,7 +97,7 @@ const UserParentalControl: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userparentalcontrol] Unexpected null page reference'); return; } @@ -144,7 +146,7 @@ const UserParentalControl: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userparentalcontrol] Unexpected null page reference'); return; } @@ -165,7 +167,7 @@ const UserParentalControl: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userparentalcontrol] Unexpected null page reference'); return; } @@ -186,7 +188,7 @@ const UserParentalControl: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userparentalcontrol] Unexpected null page reference'); return; } @@ -208,7 +210,7 @@ const UserParentalControl: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userparentalcontrol] Unexpected null page reference'); return; } @@ -241,8 +243,12 @@ const UserParentalControl: FunctionComponent = () => { }, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]); const loadData = useCallback(() => { + if (!userId) { + console.error('[userparentalcontrol.loadData] missing user id'); + return; + } + loading.show(); - const userId = getParameterByName('userId'); const promise1 = window.ApiClient.getUser(userId); const promise2 = window.ApiClient.getParentalRatings(); Promise.all([promise1, promise2]).then(function (responses) { @@ -250,13 +256,13 @@ const UserParentalControl: FunctionComponent = () => { }).catch(err => { console.error('[userparentalcontrol] failed to load data', err); }); - }, [loadUser]); + }, [loadUser, userId]); useEffect(() => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userparentalcontrol] Unexpected null page reference'); return; } @@ -344,8 +350,12 @@ const UserParentalControl: FunctionComponent = () => { const saveUser = handleSaveUser(page, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete); const onSubmit = (e: Event) => { + if (!userId) { + console.error('[userparentalcontrol.onSubmit] missing user id'); + return; + } + loading.show(); - const userId = getParameterByName('userId'); window.ApiClient.getUser(userId).then(function (result) { saveUser(result); }).catch(err => { diff --git a/src/apps/dashboard/routes/users/password.tsx b/src/apps/dashboard/routes/users/password.tsx index 544e8c6dec8..ff01ad2b4f8 100644 --- a/src/apps/dashboard/routes/users/password.tsx +++ b/src/apps/dashboard/routes/users/password.tsx @@ -1,17 +1,23 @@ import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm'; -import { getParameterByName } from '../../../../utils/url'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; import Page from '../../../../components/Page'; import loading from '../../../../components/loading/loading'; const UserPassword: FunctionComponent = () => { - const userId = getParameterByName('userId'); + const [ searchParams ] = useSearchParams(); + const userId = searchParams.get('userId'); const [ userName, setUserName ] = useState(''); const loadUser = useCallback(() => { + if (!userId) { + console.error('[userpassword] missing user id'); + return; + } + loading.show(); window.ApiClient.getUser(userId).then(function (user) { if (!user.Name) { diff --git a/src/apps/dashboard/routes/users/profile.tsx b/src/apps/dashboard/routes/users/profile.tsx index 7ef62f8261b..13170e48f68 100644 --- a/src/apps/dashboard/routes/users/profile.tsx +++ b/src/apps/dashboard/routes/users/profile.tsx @@ -1,6 +1,7 @@ import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client'; -import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; import escapeHTML from 'escape-html'; +import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; +import { useSearchParams } from 'react-router-dom'; import Dashboard from '../../../../utils/dashboard'; import globalize from '../../../../scripts/globalize'; @@ -13,7 +14,6 @@ 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 { getParameterByName } from '../../../../utils/url'; import SelectElement from '../../../../elements/SelectElement'; import Page from '../../../../components/Page'; @@ -41,6 +41,8 @@ function onSaveComplete() { } const UserEdit: FunctionComponent = () => { + const [ searchParams ] = useSearchParams(); + const userId = searchParams.get('userId'); const [ userName, setUserName ] = useState(''); const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState([]); const [ authProviders, setAuthProviders ] = useState([]); @@ -57,7 +59,7 @@ const UserEdit: FunctionComponent = () => { }; const getUser = () => { - const userId = getParameterByName('userId'); + if (!userId) throw new Error('missing user id'); return window.ApiClient.getUser(userId); }; @@ -144,7 +146,7 @@ const UserEdit: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[useredit] Unexpected null page reference'); return; } @@ -217,7 +219,7 @@ const UserEdit: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[useredit] Unexpected null page reference'); return; } diff --git a/src/apps/stable/routes/user/userprofile.tsx b/src/apps/stable/routes/user/userprofile.tsx index bb0500da0f4..6ba8fee49bc 100644 --- a/src/apps/stable/routes/user/userprofile.tsx +++ b/src/apps/stable/routes/user/userprofile.tsx @@ -1,6 +1,7 @@ import type { UserDto } from '@jellyfin/sdk/lib/generated-client'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react'; +import { useSearchParams } from 'react-router-dom'; import Dashboard from '../../../../utils/dashboard'; import globalize from '../../../../scripts/globalize'; @@ -11,11 +12,11 @@ import ButtonElement from '../../../../elements/ButtonElement'; import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm'; import loading from '../../../../components/loading/loading'; import toast from '../../../../components/toast/toast'; -import { getParameterByName } from '../../../../utils/url'; import Page from '../../../../components/Page'; const UserProfile: FunctionComponent = () => { - const userId = getParameterByName('userId'); + const [ searchParams ] = useSearchParams(); + const userId = searchParams.get('userId'); const [ userName, setUserName ] = useState(''); const element = useRef(null); @@ -24,7 +25,12 @@ const UserProfile: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userprofile] Unexpected null page reference'); + return; + } + + if (!userId) { + console.error('[userprofile] missing user id'); return; } @@ -72,7 +78,7 @@ const UserProfile: FunctionComponent = () => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[userprofile] Unexpected null page reference'); return; } @@ -110,6 +116,11 @@ const UserProfile: FunctionComponent = () => { reader.onerror = onFileReaderError; reader.onabort = onFileReaderAbort; reader.onload = () => { + if (!userId) { + console.error('[userprofile] missing user id'); + return; + } + userImage.style.backgroundImage = 'url(' + reader.result + ')'; window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () { loading.hide(); @@ -123,6 +134,11 @@ const UserProfile: FunctionComponent = () => { }; (page.querySelector('#btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () { + if (!userId) { + console.error('[userprofile] missing user id'); + return; + } + confirm( globalize.translate('DeleteImageConfirmation'), globalize.translate('DeleteImage') diff --git a/src/components/dashboard/users/UserPasswordForm.tsx b/src/components/dashboard/users/UserPasswordForm.tsx index 9b7cac235ea..18f057126f8 100644 --- a/src/components/dashboard/users/UserPasswordForm.tsx +++ b/src/components/dashboard/users/UserPasswordForm.tsx @@ -9,7 +9,7 @@ import ButtonElement from '../../../elements/ButtonElement'; import InputElement from '../../../elements/InputElement'; type IProps = { - userId: string; + userId: string | null; }; const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { @@ -19,7 +19,12 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[UserPasswordForm] Unexpected null page reference'); + return; + } + + if (!userId) { + console.error('[UserPasswordForm] missing user id'); return; } @@ -58,7 +63,7 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { const page = element.current; if (!page) { - console.error('Unexpected null reference'); + console.error('[UserPasswordForm] Unexpected null page reference'); return; } @@ -79,6 +84,11 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { }; const savePassword = () => { + if (!userId) { + console.error('[UserPasswordForm.savePassword] missing user id'); + return; + } + let currentPassword = (page.querySelector('#txtCurrentPassword') as HTMLInputElement).value; const newPassword = (page.querySelector('#txtNewPassword') as HTMLInputElement).value; @@ -105,6 +115,11 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { }; const resetPassword = () => { + if (!userId) { + console.error('[UserPasswordForm.resetPassword] missing user id'); + return; + } + const msg = globalize.translate('PasswordResetConfirmation'); confirm(msg, globalize.translate('ResetPassword')).then(function () { loading.show(); diff --git a/src/utils/url.test.ts b/src/utils/url.test.ts new file mode 100644 index 00000000000..234690dc405 --- /dev/null +++ b/src/utils/url.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { getLocationSearch } from './url'; + +const mockLocation = (urlString: string) => { + // eslint-disable-next-line compat/compat + const url = new URL(urlString); + vi.spyOn(window, 'location', 'get') + .mockReturnValue({ + ...window.location, + hash: url.hash, + host: url.host, + hostname: url.hostname, + href: url.href, + origin: url.origin, + pathname: url.pathname, + port: url.port, + protocol: url.protocol, + search: url.search + }); +}; + +describe('getLocationSearch', () => { + it('Should work with standard url search', () => { + mockLocation('https://example.com/path?foo#bar'); + expect(getLocationSearch()).toBe('?foo'); + }); + + it('Should work with search in the url hash', () => { + mockLocation('https://example.com/path#bar?foo'); + expect(getLocationSearch()).toBe('?foo'); + }); + + it('Should work with search in the url hash and standard url search', () => { + mockLocation('https://example.com/path?baz#bar?foo'); + expect(getLocationSearch()).toBe('?foo'); + }); + + it('Should return an empty string if there is no search', () => { + mockLocation('https://example.com'); + expect(getLocationSearch()).toBe(''); + }); + + it('Should fallback to the href if there is no hash or search', () => { + vi.spyOn(window, 'location', 'get') + .mockReturnValue({ + ...window.location, + hash: '', + host: '', + hostname: '', + href: 'https://example.com/path#bar?foo', + origin: '', + pathname: '', + port: '', + protocol: '', + search: '' + }); + expect(getLocationSearch()).toBe('?foo'); + }); +}); diff --git a/src/utils/url.ts b/src/utils/url.ts index 1c65b2343ac..1c88b3a115e 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -5,13 +5,19 @@ * @returns The url search string. */ export const getLocationSearch = () => { + // Check location.hash for a search string (this should be the case for our routing library) + let index = window.location.hash.indexOf('?'); + if (index !== -1) { + return window.location.hash.substring(index); + } + // Return location.search if it exists if (window.location.search) { return window.location.search; } - // Check the entire url in case the search string is in the hash - const index = window.location.href.indexOf('?'); + // Fallback to checking the entire url + index = window.location.href.indexOf('?'); if (index !== -1) { return window.location.href.substring(index); }