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

refactor(console): improve protected app creation form validation (#5293)

This commit is contained in:
Charles Zhao 2024-01-25 16:03:27 +08:00 committed by GitHub
parent e8e57a410f
commit f3c69ce3f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 68 additions and 16 deletions

View file

@ -44,7 +44,6 @@ form {
.domainFieldWrapper {
display: flex;
align-items: center;
width: 100%;
.subdomain {

View file

@ -1,6 +1,9 @@
import { ApplicationType, type Application } from '@logto/schemas';
import { isValidUrl } from '@logto/core-kit';
import { ApplicationType, type Application, type RequestErrorBody } from '@logto/schemas';
import { isValidSubdomain } from '@logto/shared/universal';
import { conditional } from '@silverhand/essentials';
import classNames from 'classnames';
import { HTTPError } from 'ky';
import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
@ -10,7 +13,6 @@ import Button, { type Props as ButtonProps } from '@/ds-components/Button';
import FormField from '@/ds-components/FormField';
import TextInput from '@/ds-components/TextInput';
import useApi from '@/hooks/use-api';
import { trySubmitSafe } from '@/utils/form';
import * as styles from './index.module.scss';
@ -39,17 +41,18 @@ function ProtectedAppForm({
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<ProtectedAppForm>();
const api = useApi();
const api = useApi({ hideErrorToast: true });
const onSubmit = handleSubmit(
trySubmitSafe(async (data) => {
if (isSubmitting) {
return;
}
const onSubmit = handleSubmit(async (data) => {
if (isSubmitting) {
return;
}
try {
const createdApp = await api
.post('api/applications', {
json: {
@ -62,8 +65,16 @@ function ProtectedAppForm({
.json<Application>();
toast.success(t('applications.application_created'));
onCreateSuccess?.(createdApp);
})
);
} catch (error: unknown) {
if (error instanceof HTTPError) {
const { code, message } = await error.response.json<RequestErrorBody>();
if (code === 'application.protected_application_subdomain_exists') {
setError('subDomain', { type: 'custom', message });
}
}
}
});
return (
<form className={className}>
@ -88,9 +99,18 @@ function ProtectedAppForm({
<div className={styles.domainFieldWrapper}>
<TextInput
className={styles.subdomain}
{...register('subDomain', { required: true })}
{...register('subDomain', {
required: true,
validate: (value) =>
isValidSubdomain(value) || t('protected_app.form.errors.invalid_domain_format'),
})}
placeholder={t('protected_app.form.domain_field_placeholder')}
error={Boolean(errors.subDomain)}
error={
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
errors.subDomain?.message ||
(errors.subDomain?.type === 'required' &&
t('protected_app.form.errors.domain_required'))
}
/>
{defaultDomain && <div className={styles.domain}>{defaultDomain}</div>}
</div>
@ -108,9 +128,16 @@ function ProtectedAppForm({
tip={conditional(!hasDetailedInstructions && t('protected_app.form.url_field_tooltip'))}
>
<TextInput
{...register('origin', { required: true })}
{...register('origin', {
required: true,
validate: (value) => isValidUrl(value) || t('protected_app.form.errors.invalid_url'),
})}
placeholder={t('protected_app.form.url_field_placeholder')}
error={Boolean(errors.origin)}
error={
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
errors.origin?.message ||
(errors.origin?.type === 'required' && t('protected_app.form.errors.url_required'))
}
/>
</FormField>
</div>

View file

@ -1,4 +1,4 @@
import { validateRedirectUrl } from './url.js';
import { isValidUrl, validateRedirectUrl } from './url.js';
describe('url utilities', () => {
it('should allow valid redirect URIs', () => {
@ -22,4 +22,22 @@ describe('url utilities', () => {
expect(validateRedirectUrl('https://logto.dev/callback', 'mobile')).toBeFalsy();
expect(validateRedirectUrl('demoApp/callback', 'mobile')).toBeFalsy();
});
it('should allow valid URIs', () => {
expect(isValidUrl('http://localhost:3001')).toBeTruthy();
expect(isValidUrl('https://google.com')).toBeTruthy();
expect(isValidUrl('https://logto.dev/callback')).toBeTruthy();
expect(isValidUrl('https://my-company.com/callback?test=123')).toBeTruthy();
expect(isValidUrl('https://abc.com/callback?test=123#param=hash')).toBeTruthy();
expect(isValidUrl('io.logto://my-app/callback')).toBeTruthy();
expect(isValidUrl('io.logto.SwiftUI-Demo://callback')).toBeTruthy();
});
it('should detect invalid URIs', () => {
expect(isValidUrl('invalid_url')).toBeFalsy();
expect(isValidUrl('abc.com')).toBeFalsy();
expect(isValidUrl('abc.com/callback')).toBeFalsy();
expect(isValidUrl('abc.com/callback?test=123')).toBeFalsy();
expect(isValidUrl('abc.com/callback#test=123')).toBeFalsy();
});
});

View file

@ -19,3 +19,11 @@ export const validateUriOrigin = (url: string) => {
return false;
}
};
export const isValidUrl = (url?: string) => {
try {
return Boolean(url && new URL(url));
} catch {
return false;
}
};