mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
fix(console,phrases): should check duplicated emails when inviting members (#5581)
fix(console,phrases): check duplicated emails when inviting members
This commit is contained in:
parent
7e33eae6d9
commit
03144ae598
19 changed files with 118 additions and 87 deletions
|
@ -0,0 +1,97 @@
|
|||
import { emailRegEx } from '@logto/core-kit';
|
||||
import { conditional, conditionalArray, conditionalString } from '@silverhand/essentials';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type TenantMemberResponse } from '@/cloud/types/router';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import { type RequestError } from '@/hooks/use-api';
|
||||
|
||||
import { type InviteeEmailItem } from '../types';
|
||||
|
||||
const useEmailInputUtils = () => {
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const { data: existingMembers = [] } = useSWR<TenantMemberResponse[], RequestError>(
|
||||
`api/tenant/${currentTenantId}/members`,
|
||||
async () =>
|
||||
cloudApi.get('/api/tenants/:tenantId/members', { params: { tenantId: currentTenantId } })
|
||||
);
|
||||
|
||||
/**
|
||||
* Find duplicated and invalid formatted email addresses.
|
||||
*
|
||||
* @param emails Array of email emails.
|
||||
* @returns
|
||||
*/
|
||||
const findDuplicatedOrInvalidEmails = useCallback(
|
||||
(emails: string[] = []) => {
|
||||
const duplicatedEmails = new Set<string>();
|
||||
const invalidEmails = new Set<string>();
|
||||
const validEmails = new Set<string>(
|
||||
existingMembers.map(({ primaryEmail }) => primaryEmail ?? '').filter(Boolean)
|
||||
);
|
||||
|
||||
for (const email of emails) {
|
||||
if (!emailRegEx.test(email)) {
|
||||
invalidEmails.add(email);
|
||||
}
|
||||
|
||||
if (validEmails.has(email)) {
|
||||
duplicatedEmails.add(email);
|
||||
} else {
|
||||
validEmails.add(email);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
duplicatedEmails,
|
||||
invalidEmails,
|
||||
};
|
||||
},
|
||||
[existingMembers]
|
||||
);
|
||||
|
||||
const parseEmailOptions = useCallback(
|
||||
(
|
||||
inputValues: InviteeEmailItem[]
|
||||
): {
|
||||
values: InviteeEmailItem[];
|
||||
errorMessage?: string;
|
||||
} => {
|
||||
const { duplicatedEmails, invalidEmails } = findDuplicatedOrInvalidEmails(
|
||||
inputValues.map((email) => email.value)
|
||||
);
|
||||
// Show error message and update the inputs' status for error display.
|
||||
if (duplicatedEmails.size > 0 || invalidEmails.size > 0) {
|
||||
return {
|
||||
values: inputValues.map(({ status, ...rest }) => ({
|
||||
...rest,
|
||||
...conditional(
|
||||
(duplicatedEmails.has(rest.value) || invalidEmails.has(rest.value)) && {
|
||||
status: 'info',
|
||||
}
|
||||
),
|
||||
})),
|
||||
errorMessage: conditionalArray(
|
||||
conditionalString(duplicatedEmails.size > 0 && t('tenant_members.errors.user_exists')),
|
||||
conditionalString(invalidEmails.size > 0 && t('tenant_members.errors.invalid_email'))
|
||||
).join(' '),
|
||||
};
|
||||
}
|
||||
|
||||
return { values: inputValues };
|
||||
},
|
||||
[findDuplicatedOrInvalidEmails]
|
||||
);
|
||||
|
||||
return {
|
||||
parseEmailOptions,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEmailInputUtils;
|
|
@ -12,8 +12,8 @@ import { onKeyDownHandler } from '@/utils/a11y';
|
|||
|
||||
import type { InviteeEmailItem, InviteMemberForm } from '../types';
|
||||
|
||||
import useEmailInputUtils from './hooks';
|
||||
import * as styles from './index.module.scss';
|
||||
import { emailOptionsParser } from './utils';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
|
@ -34,9 +34,10 @@ function InviteEmailsInput({
|
|||
const [focusedValueId, setFocusedValueId] = useState<Nullable<string>>(null);
|
||||
const [currentValue, setCurrentValue] = useState('');
|
||||
const { setError, clearErrors } = useFormContext<InviteMemberForm>();
|
||||
const { parseEmailOptions } = useEmailInputUtils();
|
||||
|
||||
const onChange = (values: InviteeEmailItem[]) => {
|
||||
const { values: parsedValues, errorMessage } = emailOptionsParser(values);
|
||||
const { values: parsedValues, errorMessage } = parseEmailOptions(values);
|
||||
if (errorMessage) {
|
||||
setError('emails', { type: 'custom', message: errorMessage });
|
||||
} else {
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
import { emailRegEx } from '@logto/core-kit';
|
||||
import { conditional, conditionalArray, conditionalString } from '@silverhand/essentials';
|
||||
import { t as globalTranslate } from 'i18next';
|
||||
|
||||
import { type InviteeEmailItem } from '../types';
|
||||
|
||||
export const emailOptionsParser = (
|
||||
inputValues: InviteeEmailItem[]
|
||||
): {
|
||||
values: InviteeEmailItem[];
|
||||
errorMessage?: string;
|
||||
} => {
|
||||
const { duplicatedEmails, invalidEmails } = findDuplicatedOrInvalidEmails(
|
||||
inputValues.map((email) => email.value)
|
||||
);
|
||||
// Show error message and update the inputs' status for error display.
|
||||
if (duplicatedEmails.size > 0 || invalidEmails.size > 0) {
|
||||
return {
|
||||
values: inputValues.map(({ status, ...rest }) => ({
|
||||
...rest,
|
||||
...conditional(
|
||||
(duplicatedEmails.has(rest.value) || invalidEmails.has(rest.value)) && {
|
||||
status: 'info',
|
||||
}
|
||||
),
|
||||
})),
|
||||
errorMessage: conditionalArray(
|
||||
conditionalString(
|
||||
duplicatedEmails.size > 0 &&
|
||||
globalTranslate('admin_console.tenant_members.errors.user_exists')
|
||||
),
|
||||
conditionalString(
|
||||
invalidEmails.size > 0 &&
|
||||
globalTranslate('admin_console.tenant_members.errors.invalid_email')
|
||||
)
|
||||
).join(' '),
|
||||
};
|
||||
}
|
||||
|
||||
return { values: inputValues };
|
||||
};
|
||||
|
||||
/**
|
||||
* Find duplicated and invalid formatted email addresses.
|
||||
*
|
||||
* @param emails Array of email emails.
|
||||
* @returns
|
||||
*/
|
||||
const findDuplicatedOrInvalidEmails = (emails: string[] = []) => {
|
||||
const duplicatedEmails = new Set<string>();
|
||||
const invalidEmails = new Set<string>();
|
||||
const validEmails = new Set<string>();
|
||||
|
||||
for (const email of emails) {
|
||||
if (!emailRegEx.test(email)) {
|
||||
invalidEmails.add(email);
|
||||
}
|
||||
|
||||
if (validEmails.has(email)) {
|
||||
duplicatedEmails.add(email);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
duplicatedEmails,
|
||||
invalidEmails,
|
||||
};
|
||||
};
|
|
@ -15,7 +15,7 @@ import Select, { type Option } from '@/ds-components/Select';
|
|||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import InviteEmailsInput from '../InviteEmailsInput';
|
||||
import { emailOptionsParser } from '../InviteEmailsInput/utils';
|
||||
import useEmailInputUtils from '../InviteEmailsInput/hooks';
|
||||
import { type InviteMemberForm } from '../types';
|
||||
|
||||
type Props = {
|
||||
|
@ -37,6 +37,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
const { parseEmailOptions } = useEmailInputUtils();
|
||||
|
||||
const formMethods = useForm<InviteMemberForm>({
|
||||
defaultValues: {
|
||||
|
@ -123,7 +124,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
if (value.length === 0) {
|
||||
return t('errors.email_required');
|
||||
}
|
||||
const { errorMessage } = emailOptionsParser(value);
|
||||
const { errorMessage } = parseEmailOptions(value);
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
return errorMessage || true;
|
||||
},
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -50,7 +50,7 @@ const tenant_members = {
|
|||
},
|
||||
errors: {
|
||||
email_required: 'Invitee email is required.',
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
max_member_limit: 'You have reached the maximum number of members ({{limit}}) for this tenant.',
|
||||
},
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -87,7 +87,7 @@ const tenant_members = {
|
|||
/** UNTRANSLATED */
|
||||
email_required: 'Invitee email is required.',
|
||||
/** UNTRANSLATED */
|
||||
user_exists: 'This user is already in this organization.',
|
||||
user_exists: 'This user is already invited to this organization.',
|
||||
/** UNTRANSLATED */
|
||||
invalid_email: 'Email address is invalid. Please make sure it is in the right format.',
|
||||
/** UNTRANSLATED */
|
||||
|
|
Loading…
Reference in a new issue