diff --git a/packages/console/src/assets/images/machine-to-machine-dark.svg b/packages/console/src/assets/images/machine-to-machine-dark.svg new file mode 100644 index 000000000..cfdad93bd --- /dev/null +++ b/packages/console/src/assets/images/machine-to-machine-dark.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/console/src/assets/images/machine-to-machine.svg b/packages/console/src/assets/images/machine-to-machine.svg new file mode 100644 index 000000000..506951028 --- /dev/null +++ b/packages/console/src/assets/images/machine-to-machine.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/console/src/assets/images/native-app-dark.svg b/packages/console/src/assets/images/native-app-dark.svg index 3d3cc729f..7cf222605 100644 --- a/packages/console/src/assets/images/native-app-dark.svg +++ b/packages/console/src/assets/images/native-app-dark.svg @@ -1,14 +1,14 @@ - - - - + - + + + + - + diff --git a/packages/console/src/assets/images/native-app.svg b/packages/console/src/assets/images/native-app.svg index 77920a70b..643514689 100644 --- a/packages/console/src/assets/images/native-app.svg +++ b/packages/console/src/assets/images/native-app.svg @@ -1,14 +1,14 @@ - - + + - + - + diff --git a/packages/console/src/assets/images/single-page-app-dark.svg b/packages/console/src/assets/images/single-page-app-dark.svg index 58acd1640..54f859e01 100644 --- a/packages/console/src/assets/images/single-page-app-dark.svg +++ b/packages/console/src/assets/images/single-page-app-dark.svg @@ -4,21 +4,21 @@ - + - - + + - + - + - + diff --git a/packages/console/src/assets/images/single-page-app.svg b/packages/console/src/assets/images/single-page-app.svg index 59e596fe2..d8239af73 100644 --- a/packages/console/src/assets/images/single-page-app.svg +++ b/packages/console/src/assets/images/single-page-app.svg @@ -1,24 +1,24 @@ - + - - + + - + - - + + - + - + - + diff --git a/packages/console/src/assets/images/traditional-web-app-dark.svg b/packages/console/src/assets/images/traditional-web-app-dark.svg index 8a4b68646..3befdf282 100644 --- a/packages/console/src/assets/images/traditional-web-app-dark.svg +++ b/packages/console/src/assets/images/traditional-web-app-dark.svg @@ -1,33 +1,17 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - + + + diff --git a/packages/console/src/assets/images/traditional-web-app.svg b/packages/console/src/assets/images/traditional-web-app.svg index cb67ce6d9..d69f0f9c8 100644 --- a/packages/console/src/assets/images/traditional-web-app.svg +++ b/packages/console/src/assets/images/traditional-web-app.svg @@ -1,35 +1,15 @@ - - - - - - - - - - - - - - - - - - - + + + + + + - - - - + - - - - - + diff --git a/packages/console/src/components/RadioGroup/index.module.scss b/packages/console/src/components/RadioGroup/index.module.scss index 389c3cdad..b712f4cfd 100644 --- a/packages/console/src/components/RadioGroup/index.module.scss +++ b/packages/console/src/components/RadioGroup/index.module.scss @@ -13,12 +13,12 @@ .card { display: flex; flex-wrap: wrap; - margin: 0 _.unit(-8) _.unit(-8) 0; + gap: _.unit(3); > .radio { position: relative; flex: 1; - min-width: 180px; + min-width: 170px; max-width: 230px; padding: _.unit(5); display: flex; @@ -28,7 +28,6 @@ outline: 1px solid var(--color-neutral-90); user-select: none; cursor: pointer; - margin: 0 _.unit(8) _.unit(8) 0; &.disabled { cursor: not-allowed; diff --git a/packages/console/src/consts/applications.ts b/packages/console/src/consts/applications.ts index dbf80ec03..e83f653ba 100644 --- a/packages/console/src/consts/applications.ts +++ b/packages/console/src/consts/applications.ts @@ -1,5 +1,7 @@ import { ApplicationType } from '@logto/schemas'; +import MachineToMachineDark from '@/assets/images/machine-to-machine-dark.svg'; +import MachineToMachine from '@/assets/images/machine-to-machine.svg'; import NativeAppDark from '@/assets/images/native-app-dark.svg'; import NativeApp from '@/assets/images/native-app.svg'; import SinglePageAppDark from '@/assets/images/single-page-app-dark.svg'; @@ -15,12 +17,12 @@ export const lightModeApplicationIconMap: ApplicationIconMap = Object.freeze({ [ApplicationType.Native]: NativeApp, [ApplicationType.SPA]: SinglePageApp, [ApplicationType.Traditional]: TraditionalWebApp, - [ApplicationType.MachineToMachine]: TraditionalWebApp, + [ApplicationType.MachineToMachine]: MachineToMachine, } as const); export const darkModeApplicationIconMap: ApplicationIconMap = Object.freeze({ [ApplicationType.Native]: NativeAppDark, [ApplicationType.SPA]: SinglePageAppDark, [ApplicationType.Traditional]: TraditionalWebAppDark, - [ApplicationType.MachineToMachine]: TraditionalWebAppDark, + [ApplicationType.MachineToMachine]: MachineToMachineDark, } as const); diff --git a/packages/console/src/hooks/use-logto-user-id.ts b/packages/console/src/hooks/use-logto-user-id.ts new file mode 100644 index 000000000..e2086c4ca --- /dev/null +++ b/packages/console/src/hooks/use-logto-user-id.ts @@ -0,0 +1,25 @@ +import { useLogto } from '@logto/react'; +import { useEffect, useState } from 'react'; + +const useLogtoUserId = () => { + const { getIdTokenClaims, isAuthenticated } = useLogto(); + const [userId, setUserId] = useState(); + + useEffect(() => { + const fetch = async () => { + const claims = await getIdTokenClaims(); + setUserId(claims?.sub); + }; + + if (isAuthenticated) { + void fetch(); + } else { + // eslint-disable-next-line unicorn/no-useless-undefined + setUserId(undefined); + } + }, [getIdTokenClaims, isAuthenticated]); + + return userId; +}; + +export default useLogtoUserId; diff --git a/packages/console/src/hooks/use-user-preferences.ts b/packages/console/src/hooks/use-user-preferences.ts index 7404e6b65..f7f680578 100644 --- a/packages/console/src/hooks/use-user-preferences.ts +++ b/packages/console/src/hooks/use-user-preferences.ts @@ -2,13 +2,16 @@ import { languageKeys } from '@logto/core-kit'; import { useLogto } from '@logto/react'; import { AppearanceMode } from '@logto/schemas'; import { Nullable, Optional } from '@silverhand/essentials'; +import { t } from 'i18next'; import { useCallback, useEffect, useMemo } from 'react'; +import { toast } from 'react-hot-toast'; import useSWR from 'swr'; import { z } from 'zod'; import { themeStorageKey } from '@/consts'; import useApi, { RequestError } from './use-api'; +import useLogtoUserId from './use-logto-user-id'; const userPreferencesGuard = z.object({ language: z.enum(languageKeys).optional(), @@ -28,9 +31,10 @@ const getEnumFromArray = ( const useUserPreferences = () => { const { isAuthenticated, error: authError } = useLogto(); - const shouldFetch = isAuthenticated && !authError; + const userId = useLogtoUserId(); + const shouldFetch = isAuthenticated && !authError && userId; const { data, mutate, error } = useSWR( - shouldFetch && '/api/users/me/custom-data' + shouldFetch && `/api/users/${userId}/custom-data` ); const api = useApi(); @@ -49,8 +53,14 @@ const useUserPreferences = () => { const userPreferences = useMemo(() => parseData(), [parseData]); const update = async (data: Partial) => { + if (!userId) { + toast.error(t('errors.unexpected_error')); + + return; + } + const updated = await api - .patch('/api/users/me/custom-data', { + .patch(`/api/users/${userId}/custom-data`, { json: { customData: { [key]: { diff --git a/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx b/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx index e24ce1695..0e8c6538d 100644 --- a/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx +++ b/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx @@ -1,24 +1,29 @@ -import { Application, SnakeCaseOidcConfig } from '@logto/schemas'; +import { Application, ApplicationType, SnakeCaseOidcConfig, UserRole } from '@logto/schemas'; import { useEffect } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { Controller, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import CopyToClipboard from '@/components/CopyToClipboard'; import FormField from '@/components/FormField'; +import Switch from '@/components/Switch'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import * as styles from '../index.module.scss'; type Props = { + applicationType: ApplicationType; oidcConfig: SnakeCaseOidcConfig; defaultData: Application; isDeleted: boolean; }; -const AdvancedSettings = ({ oidcConfig, defaultData, isDeleted }: Props) => { +const AdvancedSettings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props) => { const { + control, reset, formState: { isDirty }, } = useFormContext(); + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); useEffect(() => { reset(defaultData); @@ -48,6 +53,28 @@ const AdvancedSettings = ({ oidcConfig, defaultData, isDeleted }: Props) => { variant="border" /> + {applicationType === ApplicationType.MachineToMachine && ( + + ( + { + if (checked) { + onChange([...new Set(value.concat(UserRole.Admin))]); + } else { + onChange(value.filter((value) => value !== UserRole.Admin)); + } + }} + /> + )} + /> + + )} ); diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index 41eaa2593..a8db900e3 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -204,6 +204,7 @@ const ApplicationDetails = () => {
{isAdvancedSettings && ( { const [isOpen, setIsOpen] = useState(false); const { watch, register, reset } = useForm(); const [isLoading, setIsLoading] = useState(false); + const userId = useLogtoUserId(); const api = useApi(); const password = watch('password'); const confirmPassword = watch('confirmPassword'); const isDisabled = !password || password !== confirmPassword; const onSubmit = async () => { + if (!userId) { + toast.error(t('errors.unexpected_error')); + + return; + } + setIsLoading(true); - await api.patch(`/api/users/me/password`, { json: { password } }).json(); + await api.patch(`/api/users/${userId}/password`, { json: { password } }).json(); setIsLoading(false); setIsOpen(false); toast.success(t('settings.password_changed')); diff --git a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts index ea9356adc..d22a6c07b 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/application-details.ts @@ -31,6 +31,9 @@ const application_details = { refresh_token_expiration: 'Refresh Token expiration', token_endpoint: 'Token Endpoint', user_info_endpoint: 'Userinfo endpoint', + enable_admin_access: 'Enable admin access', + enable_admin_access_label: + 'Enable or disable the access to Management API. Once enabled, you can use access tokens to call Management API on behalf on this application.', delete_description: 'This action cannot be undone. It will permanently delete the application. Please enter the application name {{name}} to confirm.', enter_your_application_name: 'Enter your application name', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts b/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts index dd3bf7922..a9151bbb1 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/application-details.ts @@ -30,7 +30,10 @@ const application_details = { id_token_expiration: "Expiration du jeton d'identification", refresh_token_expiration: "Rafraîchir l'expiration du jeton", token_endpoint: 'Token Endpoint', - user_info_endpoint: 'Userinfo endpoint', + user_info_endpoint: 'Userinfo Endpoint', + enable_admin_access: 'Enable admin access', // UNTRANSLATED + enable_admin_access_label: + 'Enable or disable the access to Management API. Once enabled, you can use access tokens to call Management API on behalf on this application.', // UNTRANSLATED delete_description: "Cette action ne peut être annulée. Elle supprimera définitivement l'application. Veuillez entrer le nom de l'application {{nom}} pour confirmer.", enter_your_application_name: "Saisissez votre nom d'application", diff --git a/packages/phrases/src/locales/ko-kr/translation/admin-console/application-details.ts b/packages/phrases/src/locales/ko-kr/translation/admin-console/application-details.ts index f3295f5f3..1e6e2f5da 100644 --- a/packages/phrases/src/locales/ko-kr/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/ko-kr/translation/admin-console/application-details.ts @@ -31,6 +31,9 @@ const application_details = { refresh_token_expiration: 'Refresh 토큰 만료', token_endpoint: '토큰 End-Point', user_info_endpoint: '사용자 정보 End-Point', + enable_admin_access: 'Enable admin access', // UNTRANSLATED + enable_admin_access_label: + 'Enable or disable the access to Management API. Once enabled, you can use access tokens to call Management API on behalf on this application.', // UNTRANSLATED delete_description: '이 행동은 취소될 수 없어요. 어플리케이션을 영원히 삭제할 거에요. 삭제를 진행하기 위해 {{name}} 를 입력해주세요.', enter_your_application_name: '어플리케이션 이름을 입력해주세요.', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts index 1d521fcd5..648d1ff7b 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/application-details.ts @@ -31,6 +31,9 @@ const application_details = { refresh_token_expiration: 'Expiração do token de atualização', token_endpoint: 'Endpoint Token', user_info_endpoint: 'Enpoint Userinfo', + enable_admin_access: 'Enable admin access', // UNTRANSLATED + enable_admin_access_label: + 'Enable or disable the access to Management API. Once enabled, you can use access tokens to call Management API on behalf on this application.', // UNTRANSLATED delete_description: 'Esta ação não pode ser desfeita. Isso ira eliminar permanentemente a app. Insira o nome da aplicação {{name}} para confirmar.', enter_your_application_name: 'Digite o nome da aplicação', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts index 4fb33729c..dc5466041 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/application-details.ts @@ -31,6 +31,9 @@ const application_details = { refresh_token_expiration: 'Refresh Token sona erme süresi', token_endpoint: 'Token bitiş noktası', user_info_endpoint: 'Userinfo bitiş noktası', + enable_admin_access: 'Enable admin access', // UNTRANSLATED + enable_admin_access_label: + 'Enable or disable the access to Management API. Once enabled, you can use access tokens to call Management API on behalf on this application.', // UNTRANSLATED delete_description: 'Bu eylem geri alınamaz. Uygulama kalıcı olarak silinecektir. Lütfen onaylamak için uygulama adı {{name}} girin.', enter_your_application_name: 'Uygulama adı giriniz', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts index 28555bff3..93a297d74 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/application-details.ts @@ -29,7 +29,10 @@ const application_details = { id_token_expiration: 'ID Token 过期时间', refresh_token_expiration: 'Refresh Token 过期时间', token_endpoint: 'Token Endpoint', - user_info_endpoint: 'UserInfo endpoint', + user_info_endpoint: 'UserInfo Endpoint', + enable_admin_access: 'Enable admin access', // UNTRANSLATED + enable_admin_access_label: + 'Enable or disable the access to Management API. Once enabled, you can use access tokens to call Management API on behalf on this application.', // UNTRANSLATED delete_description: '本操作会永久性地删除该应用,且不可撤销。输入 {{name}} 确认。', enter_your_application_name: '输入你的应用名称', application_deleted: '应用 {{name}} 成功删除.',