diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index c2e1e4a8d2c..c92bdc2977c 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -10,5 +10,6 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'users/add', type: AsyncRouteType.Dashboard }, { path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard }, { path: 'users/password', type: AsyncRouteType.Dashboard }, - { path: 'users/profile', type: AsyncRouteType.Dashboard } + { path: 'users/profile', type: AsyncRouteType.Dashboard }, + { path: 'keys', type: AsyncRouteType.Dashboard } ]; diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts index ae82c7e5f74..d8784cbc8a4 100644 --- a/src/apps/dashboard/routes/_legacyRoutes.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -127,12 +127,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'dashboard/scheduledtasks/scheduledtasks', view: 'dashboard/scheduledtasks/scheduledtasks.html' } - }, { - path: 'keys', - pageProps: { - controller: 'dashboard/apikeys', - view: 'dashboard/apikeys.html' - } }, { path: 'playback/streaming', pageProps: { diff --git a/src/apps/dashboard/routes/keys.tsx b/src/apps/dashboard/routes/keys.tsx new file mode 100644 index 00000000000..eaddfce2509 --- /dev/null +++ b/src/apps/dashboard/routes/keys.tsx @@ -0,0 +1,141 @@ +import Page from 'components/Page'; +import SectionTitleContainer from 'elements/SectionTitleContainer'; +import { useApi } from 'hooks/useApi'; +import globalize from 'lib/globalize'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api'; +import type { AuthenticationInfo } from '@jellyfin/sdk/lib/generated-client/models/authentication-info'; +import Loading from 'components/loading/LoadingComponent'; +import { Api } from '@jellyfin/sdk'; +import confirm from 'components/confirm/confirm'; +import ApiKeyCell from 'components/dashboard/apikeys/ApiKeyCell'; + +const ApiKeys = () => { + const { api } = useApi(); + const [ keys, setKeys ] = useState([]); + const [ loading, setLoading ] = useState(true); + const element = useRef(null); + + const loadKeys = (currentApi: Api) => { + return getApiKeyApi(currentApi) + .getKeys() + .then(({ data }) => { + if (data.Items) { + setKeys(data.Items); + } + }) + .catch((err) => { + console.error('[apikeys] failed to load api keys', err); + }); + }; + + const revokeKey = useCallback((accessToken: string) => { + if (api) { + confirm(globalize.translate('MessageConfirmRevokeApiKey'), globalize.translate('HeaderConfirmRevokeApiKey')).then(function () { + setLoading(true); + getApiKeyApi(api) + .revokeKey({ key: accessToken }) + .then(() => loadKeys(api)) + .then(() => setLoading(false)) + .catch(err => { + console.error('[apikeys] failed to revoke key', err); + }); + }).catch(err => { + console.error('[apikeys] failed to show confirmation dialog', err); + }); + } + }, [api]); + + useEffect(() => { + if (!api) { + return; + } + + loadKeys(api).then(() => { + setLoading(false); + }).catch(err => { + console.error('[apikeys] failed to load api keys', err); + }); + + if (loading) { + return; + } + + const page = element.current; + + if (!page) { + console.error('[apikeys] Unexpected null page reference'); + return; + } + + const showNewKeyPopup = () => { + import('../../../components/prompt/prompt').then(({ default: prompt }) => { + prompt({ + title: globalize.translate('HeaderNewApiKey'), + label: globalize.translate('LabelAppName'), + description: globalize.translate('LabelAppNameExample') + }).then((value) => { + getApiKeyApi(api) + .createKey({ app: value }) + .then(() => loadKeys(api)) + .catch(err => { + console.error('[apikeys] failed to create api key', err); + }); + }).catch(() => { + // popup closed + }); + }).catch(err => { + console.error('[apikeys] failed to load api key popup', err); + }); + }; + + (page.querySelector('.btnNewKey') as HTMLButtonElement).addEventListener('click', showNewKeyPopup); + + return () => { + (page.querySelector('.btnNewKey') as HTMLButtonElement).removeEventListener('click', showNewKeyPopup); + }; + }, [api, loading]); + + if (loading) { + return ; + } + + return ( + +
+ +

{globalize.translate('HeaderApiKeysHelp')}

+
+ + + + + + + + + + + + {keys.map(key => { + return ; + })} + +
{globalize.translate('ApiKeysCaption')}
{globalize.translate('HeaderApiKey')}{globalize.translate('HeaderApp')}{globalize.translate('HeaderDateIssued')}
+
+
+ ); +}; + +export default ApiKeys; diff --git a/src/components/dashboard/apikeys/ApiKeyCell.tsx b/src/components/dashboard/apikeys/ApiKeyCell.tsx new file mode 100644 index 00000000000..6604b0dd1f1 --- /dev/null +++ b/src/components/dashboard/apikeys/ApiKeyCell.tsx @@ -0,0 +1,46 @@ +import React, { FunctionComponent, useCallback } from 'react'; +import type { AuthenticationInfo } from '@jellyfin/sdk/lib/generated-client/models/authentication-info'; +import ButtonElement from 'elements/ButtonElement'; +import datetime from 'scripts/datetime'; +import globalize from 'lib/globalize'; + +type ApiKeyCellProps = { + apiKey: AuthenticationInfo; + revokeKey?: (accessToken: string) => void; +}; + +const ApiKeyCell: FunctionComponent = ({ apiKey, revokeKey }: ApiKeyCellProps) => { + const getDate = (dateCreated: string | undefined) => { + const date = datetime.parseISO8601Date(dateCreated, true); + return datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date); + }; + + const onClick = useCallback(() => { + if (apiKey?.AccessToken && revokeKey !== undefined) { + revokeKey(apiKey.AccessToken); + } + }, [apiKey, revokeKey]); + + return ( + + + + + + {apiKey.AccessToken} + + + {apiKey.AppName} + + + {getDate(apiKey.DateCreated)} + + + ); +}; + +export default ApiKeyCell; diff --git a/src/controllers/dashboard/apikeys.html b/src/controllers/dashboard/apikeys.html deleted file mode 100644 index fd8ade8bba6..00000000000 --- a/src/controllers/dashboard/apikeys.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
-
-
-

${HeaderApiKeys}

- -
-

${HeaderApiKeysHelp}

-
- - - - - - - - - - - -
${ApiKeysCaption}
${HeaderApiKey}${HeaderApp}${HeaderDateIssued}
-
-
-
diff --git a/src/controllers/dashboard/apikeys.js b/src/controllers/dashboard/apikeys.js deleted file mode 100644 index 3fc7e5fe7d1..00000000000 --- a/src/controllers/dashboard/apikeys.js +++ /dev/null @@ -1,89 +0,0 @@ -import escapeHTML from 'escape-html'; - -import datetime from '../../scripts/datetime'; -import loading from '../../components/loading/loading'; -import dom from '../../scripts/dom'; -import globalize from '../../lib/globalize'; -import '../../elements/emby-button/emby-button'; -import confirm from '../../components/confirm/confirm'; -import { pageIdOn } from '../../utils/dashboard'; - -function revoke(page, key) { - confirm(globalize.translate('MessageConfirmRevokeApiKey'), globalize.translate('HeaderConfirmRevokeApiKey')).then(function () { - loading.show(); - ApiClient.ajax({ - type: 'DELETE', - url: ApiClient.getUrl('Auth/Keys/' + key) - }).then(function () { - loadData(page); - }); - }); -} - -function renderKeys(page, keys) { - const rows = keys.map(function (item) { - let html = ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += escapeHTML(item.AccessToken); - html += ''; - html += ''; - html += escapeHTML(item.AppName) || ''; - html += ''; - html += ''; - const date = datetime.parseISO8601Date(item.DateCreated, true); - html += datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date); - html += ''; - html += ''; - return html; - }).join(''); - page.querySelector('.resultBody').innerHTML = rows; - loading.hide(); -} - -function loadData(page) { - loading.show(); - ApiClient.getJSON(ApiClient.getUrl('Auth/Keys')).then(function (result) { - renderKeys(page, result.Items); - }); -} - -function showNewKeyPrompt(page) { - import('../../components/prompt/prompt').then(({ default: prompt }) => { - prompt({ - title: globalize.translate('HeaderNewApiKey'), - label: globalize.translate('LabelAppName'), - description: globalize.translate('LabelAppNameExample') - }).then(function (value) { - ApiClient.ajax({ - type: 'POST', - url: ApiClient.getUrl('Auth/Keys', { - App: value - }) - }).then(function () { - loadData(page); - }); - }); - }); -} - -pageIdOn('pageinit', 'apiKeysPage', function () { - const page = this; - page.querySelector('.btnNewKey').addEventListener('click', function () { - showNewKeyPrompt(page); - }); - page.querySelector('.tblApiKeys').addEventListener('click', function (e) { - const btnRevoke = dom.parentWithClass(e.target, 'btnRevoke'); - - if (btnRevoke) { - revoke(page, btnRevoke.getAttribute('data-token')); - } - }); -}); -pageIdOn('pagebeforeshow', 'apiKeysPage', function () { - loadData(this); -}); - diff --git a/src/elements/ButtonElement.tsx b/src/elements/ButtonElement.tsx index 379f4f7995c..ca6e92cbfd0 100644 --- a/src/elements/ButtonElement.tsx +++ b/src/elements/ButtonElement.tsx @@ -22,19 +22,32 @@ type IProps = { title?: string; leftIcon?: string; rightIcon?: string; + onClick?: () => void; }; -const ButtonElement: FunctionComponent = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => { +const ButtonElement: FunctionComponent = ({ type, id, className, title, leftIcon, rightIcon, onClick }: IProps) => { + const button = createButtonElement({ + type: type, + id: id ? `id="${id}"` : '', + className: className, + title: globalize.translate(title), + leftIcon: leftIcon ? `` : '', + rightIcon: rightIcon ? `` : '' + }); + + if (onClick !== undefined) { + return ( +