From 4a0577accdb36e2b916b0e520b3352f6426b64c7 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 29 Apr 2022 19:01:36 +0800 Subject: [PATCH] feat(console): configure cors-allowed-origins (#695) --- .../src/components/MultiTextInput/index.tsx | 4 +- .../src/components/MultiTextInput/types.ts | 2 +- .../src/components/MultiTextInput/utils.ts | 18 +- .../components/AdvancedSettings.tsx | 34 ++++ .../components/Settings.tsx | 121 +++++++++++++ .../src/pages/ApplicationDetails/index.tsx | 167 ++++-------------- .../components/MultiTextInputField/index.tsx | 6 +- packages/console/src/utilities/regex.ts | 1 - packages/console/src/utilities/validator.ts | 16 ++ packages/phrases/src/locales/en.ts | 3 +- packages/phrases/src/locales/zh-cn.ts | 3 +- .../schemas/src/foundations/jsonb-types.ts | 6 +- 12 files changed, 231 insertions(+), 150 deletions(-) create mode 100644 packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx create mode 100644 packages/console/src/pages/ApplicationDetails/components/Settings.tsx create mode 100644 packages/console/src/utilities/validator.ts diff --git a/packages/console/src/components/MultiTextInput/index.tsx b/packages/console/src/components/MultiTextInput/index.tsx index 87c4dd6d7..18af67f64 100644 --- a/packages/console/src/components/MultiTextInput/index.tsx +++ b/packages/console/src/components/MultiTextInput/index.tsx @@ -10,7 +10,7 @@ import * as styles from './index.module.scss'; import { MultiTextInputError } from './types'; type Props = { - value: string[]; + value?: string[]; onChange: (value: string[]) => void; error?: MultiTextInputError; }; @@ -21,7 +21,7 @@ const MultiTextInput = ({ value, onChange, error }: Props) => { }); const fields = useMemo(() => { - if (value.length === 0) { + if (!value?.length) { return ['']; } diff --git a/packages/console/src/components/MultiTextInput/types.ts b/packages/console/src/components/MultiTextInput/types.ts index 1390ed93e..9e40c3c5a 100644 --- a/packages/console/src/components/MultiTextInput/types.ts +++ b/packages/console/src/components/MultiTextInput/types.ts @@ -6,7 +6,7 @@ export type MultiTextInputError = { export type MultiTextInputRule = { required?: string; pattern?: { - regex: RegExp; + verify: (value: string) => boolean; message: string; }; }; diff --git a/packages/console/src/components/MultiTextInput/utils.ts b/packages/console/src/components/MultiTextInput/utils.ts index cffcc6630..ba3187e3f 100644 --- a/packages/console/src/components/MultiTextInput/utils.ts +++ b/packages/console/src/components/MultiTextInput/utils.ts @@ -1,29 +1,29 @@ -import { conditional } from '@silverhand/essentials'; - import { MultiTextInputError, MultiTextInputRule } from './types'; export const validate = ( - value: string[], + value?: string[], rule?: MultiTextInputRule ): MultiTextInputError | undefined => { if (!rule) { return; } - const requiredError = conditional(value.filter(Boolean).length === 0 && rule.required); + if (!value?.filter(Boolean).length) { + if (!rule.required) { + return; + } - if (requiredError) { return { - required: requiredError, + required: rule.required, }; } if (rule.pattern) { - const { regex, message } = rule.pattern; + const { verify, message } = rule.pattern; const inputErrors = Object.fromEntries( value.map((element, index) => { - return [index, regex.test(element) ? undefined : message]; + return [index, verify(element) ? undefined : message]; }) ); @@ -40,7 +40,7 @@ export const validate = ( */ export const createValidatorForRhf = (rule: MultiTextInputRule) => - (value: string[]): boolean | string => { + (value?: string[]): boolean | string => { const error = validate(value, rule); if (error) { diff --git a/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx b/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx new file mode 100644 index 000000000..4cb25a8f2 --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/components/AdvancedSettings.tsx @@ -0,0 +1,34 @@ +import { SnakeCaseOidcConfig } from '@logto/schemas'; +import React from 'react'; + +import CopyToClipboard from '@/components/CopyToClipboard'; +import FormField from '@/components/FormField'; + +import * as styles from '../index.module.scss'; + +type Props = { + oidcConfig: SnakeCaseOidcConfig; +}; + +const AdvancedSettings = ({ oidcConfig }: Props) => { + return ( + <> + + + + + + + + ); +}; + +export default AdvancedSettings; diff --git a/packages/console/src/pages/ApplicationDetails/components/Settings.tsx b/packages/console/src/pages/ApplicationDetails/components/Settings.tsx new file mode 100644 index 000000000..d663fe893 --- /dev/null +++ b/packages/console/src/pages/ApplicationDetails/components/Settings.tsx @@ -0,0 +1,121 @@ +import { Application, SnakeCaseOidcConfig } from '@logto/schemas'; +import React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import CopyToClipboard from '@/components/CopyToClipboard'; +import FormField from '@/components/FormField'; +import MultiTextInput from '@/components/MultiTextInput'; +import { MultiTextInputRule } from '@/components/MultiTextInput/types'; +import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils'; +import TextInput from '@/components/TextInput'; +import { uriValidator } from '@/utilities/validator'; + +import * as styles from '../index.module.scss'; + +type Props = { + oidcConfig: SnakeCaseOidcConfig; +}; + +const Settings = ({ oidcConfig }: Props) => { + const { control, register } = useFormContext(); + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const uriPatternRules: MultiTextInputRule = { + pattern: { + verify: uriValidator(false), + message: t('errors.invalid_uri_format'), + }, + }; + + return ( + <> + + + + + + + + + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + ); +}; + +export default Settings; diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index 625b14d0a..8bdffb50d 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -1,7 +1,7 @@ import { Application, SnakeCaseOidcConfig } from '@logto/schemas'; import classNames from 'classnames'; import React, { useEffect, useState } from 'react'; -import { Controller, useForm } from 'react-hook-form'; +import { FormProvider, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import Modal from 'react-modal'; @@ -13,13 +13,9 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import CopyToClipboard from '@/components/CopyToClipboard'; import Drawer from '@/components/Drawer'; -import FormField from '@/components/FormField'; import ImagePlaceholder from '@/components/ImagePlaceholder'; import LinkButton from '@/components/LinkButton'; -import MultiTextInput from '@/components/MultiTextInput'; -import { convertRhfErrorMessage, createValidatorForRhf } from '@/components/MultiTextInput/utils'; import TabNav, { TabNavLink } from '@/components/TabNav'; -import TextInput from '@/components/TextInput'; import useApi, { RequestError } from '@/hooks/use-api'; import Back from '@/icons/Back'; import Delete from '@/icons/Delete'; @@ -27,38 +23,37 @@ import More from '@/icons/More'; import * as detailsStyles from '@/scss/details.module.scss'; import * as modalStyles from '@/scss/modal.module.scss'; import { applicationTypeI18nKey } from '@/types/applications'; -import { noSpaceRegex } from '@/utilities/regex'; +import AdvancedSettings from './components/AdvancedSettings'; import DeleteForm from './components/DeleteForm'; +import Settings from './components/Settings'; import * as styles from './index.module.scss'; +const mapToUriFormatArrays = (value?: string[]) => + value?.filter(Boolean).map((uri) => decodeURIComponent(new URL(uri).toString())); + const ApplicationDetails = () => { const { id } = useParams(); const location = useLocation(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { data, error, mutate } = useSWR( id && `/api/applications/${id}` ); - const { data: oidcConfig, error: fetchOidcConfigError } = useSWR< SnakeCaseOidcConfig, RequestError >('/oidc/.well-known/openid-configuration'); const isLoading = !data && !error && !oidcConfig && !fetchOidcConfigError; - const [isReadmeOpen, setIsReadmeOpen] = useState(false); - const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false); const api = useApi(); + const formMethods = useForm(); const { - control, handleSubmit, - register, reset, formState: { isSubmitting }, - } = useForm(); + } = formMethods; useEffect(() => { if (!data) { @@ -79,9 +74,16 @@ const ApplicationDetails = () => { ...formData, oidcClientMetadata: { ...formData.oidcClientMetadata, - redirectUris: formData.oidcClientMetadata.redirectUris.filter(Boolean), - postLogoutRedirectUris: - formData.oidcClientMetadata.postLogoutRedirectUris.filter(Boolean), + redirectUris: mapToUriFormatArrays(formData.oidcClientMetadata.redirectUris), + postLogoutRedirectUris: mapToUriFormatArrays( + formData.oidcClientMetadata.postLogoutRedirectUris + ), + }, + customClientMetadata: { + ...formData.customClientMetadata, + corsAllowedOrigins: mapToUriFormatArrays( + formData.customClientMetadata.corsAllowedOrigins + ), }, }, }) @@ -92,102 +94,6 @@ const ApplicationDetails = () => { const isAdvancedSettings = location.pathname.includes('advanced-settings'); - const SettingsPage = oidcConfig && ( - <> - - - - - - - - - - - ( - - )} - /> - - - ( - - )} - /> - - - ); - - const AdvancedSettingsPage = oidcConfig && ( - <> - - - - - - - - ); - return (
{ className={styles.backLink} /> {isLoading &&
loading
} - {error &&
{`error occurred: ${error.body.message}`}
} {data && oidcConfig && ( <> @@ -268,22 +173,28 @@ const ApplicationDetails = () => { {t('application_details.advanced_settings')} -
-
- {isAdvancedSettings ? AdvancedSettingsPage : SettingsPage} -
-
-
-
- +
+
+
+
+ + )} diff --git a/packages/console/src/pages/Guide/components/MultiTextInputField/index.tsx b/packages/console/src/pages/Guide/components/MultiTextInputField/index.tsx index 00d0f3364..03b16a04c 100644 --- a/packages/console/src/pages/Guide/components/MultiTextInputField/index.tsx +++ b/packages/console/src/pages/Guide/components/MultiTextInputField/index.tsx @@ -7,7 +7,7 @@ import FormField from '@/components/FormField'; import MultiTextInput from '@/components/MultiTextInput'; import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils'; import { GuideForm } from '@/types/guide'; -import { noSpaceRegex } from '@/utilities/regex'; +import { uriValidator } from '@/utilities/validator'; type Props = { name: 'redirectUris' | 'postLogoutRedirectUris'; @@ -42,8 +42,8 @@ const MultiTextInputField = ({ name, title, onError }: Props) => { validate: createValidatorForRhf({ required: t('errors.required_field_missing_plural', { field: title }), pattern: { - regex: noSpaceRegex, - message: t('errors.no_space_in_uri'), + verify: uriValidator(false), + message: t('errors.invalid_uri_format'), }, }), }} diff --git a/packages/console/src/utilities/regex.ts b/packages/console/src/utilities/regex.ts index 5922e47fc..01e9acb72 100644 --- a/packages/console/src/utilities/regex.ts +++ b/packages/console/src/utilities/regex.ts @@ -1,4 +1,3 @@ // TODO - LOG-1876: Share Regex Between Logto Core and Front-End export const emailRegEx = /^\S+@\S+\.\S+$/; export const phoneRegEx = /^[1-9]\d{10}$/; -export const noSpaceRegex = /^\S*$/; diff --git a/packages/console/src/utilities/validator.ts b/packages/console/src/utilities/validator.ts new file mode 100644 index 000000000..8739a0229 --- /dev/null +++ b/packages/console/src/utilities/validator.ts @@ -0,0 +1,16 @@ +export const uriValidator = (verifyBlank = true) => { + return (value: string) => { + if (!verifyBlank && value.trim().length === 0) { + return true; + } + + try { + // eslint-disable-next-line no-new + new URL(value); + } catch { + return false; + } + + return true; + }; +}; diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 34812c2d8..771f0be30 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -94,7 +94,7 @@ const translation = { unknown_server_error: 'Unknown server error occurred.', empty: 'No Data', missing_total_number: 'Unable to find Total-Number in response headers.', - no_space_in_uri: 'Space is not allowed in URI', + invalid_uri_format: 'Invalid URI format', required_field_missing: 'Please enter {{field}}', required_field_missing_plural: 'You have to enter at least one {{field}}', }, @@ -166,6 +166,7 @@ const translation = { authorization_endpoint: 'Authorization Endpoint', redirect_uri: 'Redirect URI', post_sign_out_redirect_uri: 'Post Sign Out Redirect URI', + cors_allowed_origins: 'CORS Allowed Origins', add_another: 'Add another', id_token_expiration: 'ID Token Expiration', refresh_token_expiration: 'Refresh Token Expiration', diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index 37e19fb8b..22b632f76 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -94,7 +94,7 @@ const translation = { unknown_server_error: '服务器发生未知错误。', empty: '没有数据', missing_total_number: '无法从返回的头部信息中找到 Total-Number。', - no_space_in_uri: 'URI 中不能包含空格', + invalid_uri_format: '无效的 URI 格式', required_field_missing: '请输入{{field}}', required_field_missing_plural: '{{field}}不能全部为空', }, @@ -164,6 +164,7 @@ const translation = { authorization_endpoint: 'Authorization Endpoint', redirect_uri: 'Redirect URI', post_sign_out_redirect_uri: 'Post Sign Out Redirect URI', + cors_allowed_origins: 'CORS Allowed Origins', add_another: 'Add another', id_token_expiration: 'ID Token Expiration', refresh_token_expiration: 'Refresh Token Expiration', diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 5802298f7..89146e7c3 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -29,11 +29,9 @@ export const oidcModelInstancePayloadGuard = z export type OidcModelInstancePayload = z.infer; -const noSpaceRegex = /^\S*$/; - export const oidcClientMetadataGuard = z.object({ - redirectUris: z.string().regex(noSpaceRegex).array(), - postLogoutRedirectUris: z.string().regex(noSpaceRegex).array(), + redirectUris: z.string().url().array(), + postLogoutRedirectUris: z.string().url().array(), logoUri: z.string().optional(), });