0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

feat(console): configure cors-allowed-origins (#695)

This commit is contained in:
Xiao Yijun 2022-04-29 19:01:36 +08:00 committed by GitHub
parent da49812164
commit 4a0577accd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 231 additions and 150 deletions

View file

@ -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 [''];
}

View file

@ -6,7 +6,7 @@ export type MultiTextInputError = {
export type MultiTextInputRule = {
required?: string;
pattern?: {
regex: RegExp;
verify: (value: string) => boolean;
message: string;
};
};

View file

@ -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) {

View file

@ -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 (
<>
<FormField title="admin_console.application_details.token_endpoint">
<CopyToClipboard
className={styles.textField}
value={oidcConfig.token_endpoint}
variant="border"
/>
</FormField>
<FormField title="admin_console.application_details.user_info_endpoint">
<CopyToClipboard
className={styles.textField}
value={oidcConfig.userinfo_endpoint}
variant="border"
/>
</FormField>
</>
);
};
export default AdvancedSettings;

View file

@ -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<Application>();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const uriPatternRules: MultiTextInputRule = {
pattern: {
verify: uriValidator(false),
message: t('errors.invalid_uri_format'),
},
};
return (
<>
<FormField
isRequired
title="admin_console.application_details.application_name"
className={styles.textField}
>
<TextInput {...register('name', { required: true })} />
</FormField>
<FormField title="admin_console.application_details.description" className={styles.textField}>
<TextInput {...register('description')} />
</FormField>
<FormField
title="admin_console.application_details.authorization_endpoint"
className={styles.textField}
>
<CopyToClipboard
className={styles.textField}
value={oidcConfig.authorization_endpoint}
variant="border"
/>
</FormField>
<FormField
isRequired
title="admin_console.application_details.redirect_uri"
className={styles.textField}
>
<Controller
name="oidcClientMetadata.redirectUris"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf({
...uriPatternRules,
required: t('application_details.redirect_uri_required'),
}),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
value={value}
error={convertRhfErrorMessage(error?.message)}
onChange={onChange}
/>
)}
/>
</FormField>
<FormField
title="admin_console.application_details.post_sign_out_redirect_uri"
className={styles.textField}
>
<Controller
name="oidcClientMetadata.postLogoutRedirectUris"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf(uriPatternRules),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
value={value}
error={convertRhfErrorMessage(error?.message)}
onChange={onChange}
/>
)}
/>
</FormField>
<FormField
title="admin_console.application_details.cors_allowed_origins"
className={styles.textField}
>
<Controller
name="customClientMetadata.corsAllowedOrigins"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf(uriPatternRules),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
value={value}
error={convertRhfErrorMessage(error?.message)}
onChange={onChange}
/>
)}
/>
</FormField>
</>
);
};
export default Settings;

View file

@ -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<Application, RequestError>(
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<Application>();
const {
control,
handleSubmit,
register,
reset,
formState: { isSubmitting },
} = useForm<Application>();
} = 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 && (
<>
<FormField
isRequired
title="admin_console.application_details.application_name"
className={styles.textField}
>
<TextInput {...register('name', { required: true })} />
</FormField>
<FormField title="admin_console.application_details.description" className={styles.textField}>
<TextInput {...register('description')} />
</FormField>
<FormField
title="admin_console.application_details.authorization_endpoint"
className={styles.textField}
>
<CopyToClipboard
className={styles.textField}
value={oidcConfig.authorization_endpoint}
variant="border"
/>
</FormField>
<FormField
isRequired
title="admin_console.application_details.redirect_uri"
className={styles.textField}
>
<Controller
name="oidcClientMetadata.redirectUris"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf({
required: t('application_details.redirect_uri_required'),
pattern: {
regex: noSpaceRegex,
message: t('errors.no_space_in_uri'),
},
}),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
value={value}
error={convertRhfErrorMessage(error?.message)}
onChange={onChange}
/>
)}
/>
</FormField>
<FormField
title="admin_console.application_details.post_sign_out_redirect_uri"
className={styles.textField}
>
<Controller
name="oidcClientMetadata.postLogoutRedirectUris"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf({
pattern: {
regex: noSpaceRegex,
message: t('errors.no_space_in_uri'),
},
}),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
value={value}
error={convertRhfErrorMessage(error?.message)}
onChange={onChange}
/>
)}
/>
</FormField>
</>
);
const AdvancedSettingsPage = oidcConfig && (
<>
<FormField title="admin_console.application_details.token_endpoint">
<CopyToClipboard
className={styles.textField}
value={oidcConfig.token_endpoint}
variant="border"
/>
</FormField>
<FormField title="admin_console.application_details.user_info_endpoint">
<CopyToClipboard
className={styles.textField}
value={oidcConfig.userinfo_endpoint}
variant="border"
/>
</FormField>
</>
);
return (
<div className={detailsStyles.container}>
<LinkButton
@ -197,7 +103,6 @@ const ApplicationDetails = () => {
className={styles.backLink}
/>
{isLoading && <div>loading</div>}
{error && <div>{`error occurred: ${error.body.message}`}</div>}
{data && oidcConfig && (
<>
<Card className={styles.header}>
@ -268,22 +173,28 @@ const ApplicationDetails = () => {
{t('application_details.advanced_settings')}
</TabNavLink>
</TabNav>
<form className={styles.form} onSubmit={onSubmit}>
<div className={styles.fields}>
{isAdvancedSettings ? AdvancedSettingsPage : SettingsPage}
</div>
<div className={detailsStyles.footer}>
<div className={detailsStyles.footerMain}>
<Button
isLoading={isSubmitting}
htmlType="submit"
type="primary"
size="large"
title="admin_console.application_details.save_changes"
/>
<FormProvider {...formMethods}>
<form className={styles.form} onSubmit={onSubmit}>
<div className={styles.fields}>
{isAdvancedSettings ? (
<AdvancedSettings oidcConfig={oidcConfig} />
) : (
<Settings oidcConfig={oidcConfig} />
)}
</div>
</div>
</form>
<div className={detailsStyles.footer}>
<div className={detailsStyles.footerMain}>
<Button
isLoading={isSubmitting}
htmlType="submit"
type="primary"
size="large"
title="admin_console.application_details.save_changes"
/>
</div>
</div>
</form>
</FormProvider>
</Card>
</>
)}

View file

@ -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'),
},
}),
}}

View file

@ -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*$/;

View file

@ -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;
};
};

View file

@ -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',

View file

@ -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',

View file

@ -29,11 +29,9 @@ export const oidcModelInstancePayloadGuard = z
export type OidcModelInstancePayload = z.infer<typeof oidcModelInstancePayloadGuard>;
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(),
});