mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge branch 'master' into gao-test-native-esm-2
This commit is contained in:
commit
7b87a3d2e6
42 changed files with 147 additions and 92 deletions
|
@ -1,5 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.00017 1.33325C6.68162 1.33325 5.39269 1.72425 4.29636 2.45679C3.20004 3.18933 2.34555 4.23052 1.84097 5.4487C1.33638 6.66687 1.20436 8.00731 1.4616 9.30052C1.71883 10.5937 2.35377 11.7816 3.28612 12.714C4.21847 13.6463 5.40636 14.2813 6.69956 14.5385C7.99277 14.7957 9.33321 14.6637 10.5514 14.1591C11.7696 13.6545 12.8108 12.8 13.5433 11.7037C14.2758 10.6074 14.6668 9.31846 14.6668 7.99992C14.6668 7.12444 14.4944 6.25753 14.1594 5.4487C13.8243 4.63986 13.3333 3.90493 12.7142 3.28587C12.0952 2.66682 11.3602 2.17575 10.5514 1.84072C9.74255 1.50569 8.87564 1.33325 8.00017 1.33325ZM8.00017 13.3333C6.94533 13.3333 5.91419 13.0205 5.03712 12.4344C4.16006 11.8484 3.47648 11.0154 3.07281 10.0409C2.66914 9.06636 2.56352 7.994 2.76931 6.95944C2.9751 5.92487 3.48305 4.97456 4.22893 4.22868C4.97481 3.4828 5.92512 2.97485 6.95968 2.76906C7.99425 2.56328 9.0666 2.66889 10.0411 3.07256C11.0157 3.47623 11.8486 4.15982 12.4347 5.03688C13.0207 5.91394 13.3335 6.94509 13.3335 7.99992C13.3335 9.41441 12.7716 10.771 11.7714 11.7712C10.7712 12.7713 9.41465 13.3333 8.00017 13.3333ZM10.6668 7.33325H8.66683V5.33325C8.66683 5.15644 8.59659 4.98687 8.47157 4.86185C8.34655 4.73682 8.17698 4.66659 8.00017 4.66659C7.82335 4.66659 7.65379 4.73682 7.52876 4.86185C7.40374 4.98687 7.3335 5.15644 7.3335 5.33325V7.33325H5.3335C5.15669 7.33325 4.98712 7.40349 4.86209 7.52851C4.73707 7.65354 4.66683 7.82311 4.66683 7.99992C4.66683 8.17673 4.73707 8.3463 4.86209 8.47132C4.98712 8.59635 5.15669 8.66659 5.3335 8.66659H7.3335V10.6666C7.3335 10.8434 7.40374 11.013 7.52876 11.138C7.65379 11.263 7.82335 11.3333 8.00017 11.3333C8.17698 11.3333 8.34655 11.263 8.47157 11.138C8.59659 11.013 8.66683 10.8434 8.66683 10.6666V8.66659H10.6668C10.8436 8.66659 11.0132 8.59635 11.1382 8.47132C11.2633 8.3463 11.3335 8.17673 11.3335 7.99992C11.3335 7.82311 11.2633 7.65354 11.1382 7.52851C11.0132 7.40349 10.8436 7.33325 10.6668 7.33325Z"
|
||||
fill="currentColor" />
|
||||
<path d="M7.99998 1.33325C6.68144 1.33325 5.39251 1.72425 4.29618 2.45679C3.19985 3.18933 2.34537 4.23052 1.84079 5.4487C1.3362 6.66687 1.20418 8.00731 1.46141 9.30052C1.71865 10.5937 2.35359 11.7816 3.28594 12.714C4.21829 13.6463 5.40617 14.2813 6.69938 14.5385C7.99259 14.7957 9.33303 14.6637 10.5512 14.1591C11.7694 13.6545 12.8106 12.8 13.5431 11.7037C14.2757 10.6074 14.6666 9.31846 14.6666 7.99992C14.6666 7.12444 14.4942 6.25753 14.1592 5.4487C13.8241 4.63986 13.3331 3.90493 12.714 3.28587C12.095 2.66682 11.36 2.17575 10.5512 1.84072C9.74237 1.50569 8.87546 1.33325 7.99998 1.33325V1.33325ZM7.99998 13.3333C6.94515 13.3333 5.914 13.0205 5.03694 12.4344C4.15988 11.8484 3.47629 11.0154 3.07263 10.0409C2.66896 9.06636 2.56334 7.994 2.76913 6.95944C2.97492 5.92487 3.48287 4.97456 4.22875 4.22868C4.97463 3.4828 5.92494 2.97485 6.9595 2.76906C7.99407 2.56328 9.06642 2.66889 10.041 3.07256C11.0155 3.47623 11.8485 4.15982 12.4345 5.03688C13.0205 5.91394 13.3333 6.94509 13.3333 7.99992C13.3333 9.41441 12.7714 10.771 11.7712 11.7712C10.771 12.7713 9.41447 13.3333 7.99998 13.3333V13.3333ZM10.6666 7.33325H8.66665V5.33325C8.66665 5.15644 8.59641 4.98687 8.47139 4.86185C8.34636 4.73682 8.17679 4.66659 7.99998 4.66659C7.82317 4.66659 7.6536 4.73682 7.52858 4.86185C7.40355 4.98687 7.33332 5.15644 7.33332 5.33325V7.33325H5.33332C5.1565 7.33325 4.98694 7.40349 4.86191 7.52851C4.73689 7.65354 4.66665 7.82311 4.66665 7.99992C4.66665 8.17673 4.73689 8.3463 4.86191 8.47132C4.98694 8.59635 5.1565 8.66659 5.33332 8.66659H7.33332V10.6666C7.33332 10.8434 7.40355 11.013 7.52858 11.138C7.6536 11.263 7.82317 11.3333 7.99998 11.3333C8.17679 11.3333 8.34636 11.263 8.47139 11.138C8.59641 11.013 8.66665 10.8434 8.66665 10.6666V8.66659H10.6666C10.8435 8.66659 11.013 8.59635 11.1381 8.47132C11.2631 8.3463 11.3333 8.17673 11.3333 7.99992C11.3333 7.82311 11.2631 7.65354 11.1381 7.52851C11.013 7.40349 10.8435 7.33325 10.6666 7.33325Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
@ -40,9 +40,8 @@
|
|||
}
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: _.unit(2);
|
||||
|
@ -62,6 +61,12 @@
|
|||
&.text {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
&:not(:last-child) {
|
||||
margin-right: _.unit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { KeyboardEvent } from 'react';
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CirclePlus from '@/assets/images/circle-plus.svg';
|
||||
import Minus from '@/assets/images/minus.svg';
|
||||
|
||||
import Button from '../Button';
|
||||
|
@ -106,6 +107,7 @@ const MultiTextInput = ({
|
|||
type="text"
|
||||
title="general.add_another"
|
||||
className={styles.addAnother}
|
||||
icon={<CirclePlus />}
|
||||
onClick={handleAdd}
|
||||
/>
|
||||
<ConfirmModal
|
||||
|
|
|
@ -67,10 +67,13 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
config: result.data,
|
||||
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
|
||||
};
|
||||
const payload =
|
||||
connectorData.type === ConnectorType.Social
|
||||
? {
|
||||
config: result.data,
|
||||
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
|
||||
}
|
||||
: { config: result.data };
|
||||
const standardConnectorPayload = {
|
||||
...payload,
|
||||
metadata: { ...metadata, name: { en: metadata.name } },
|
||||
|
|
|
@ -12,6 +12,7 @@ import FormField from '@/components/FormField';
|
|||
import TextInput from '@/components/TextInput';
|
||||
import { Tooltip } from '@/components/Tip';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
import { safeParseJson } from '@/utilities/json';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -77,7 +78,6 @@ const SenderTester = ({ connectorId, connectorType, config, className }: Props)
|
|||
<div className={className}>
|
||||
<div className={styles.fields}>
|
||||
<FormField
|
||||
isRequired
|
||||
title={
|
||||
isSms ? 'connector_details.test_sms_sender' : 'connector_details.test_email_sender'
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ const SenderTester = ({ connectorId, connectorType, config, className }: Props)
|
|||
? t('connector_details.test_sms_placeholder')
|
||||
: t('connector_details.test_email_placeholder')
|
||||
}
|
||||
onKeyDown={onKeyDownHandler({ Enter: onSubmit })}
|
||||
{...register('sendTo', {
|
||||
required: true,
|
||||
pattern: {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { ConnectorFactoryResponse } from '@logto/schemas';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -91,7 +92,7 @@ const ConnectorForm = ({ connector, isAllowEditTarget }: Props) => {
|
|||
</FormField>
|
||||
</>
|
||||
)}
|
||||
<FormField isRequired title="connectors.guide.config">
|
||||
<FormField title="connectors.guide.config">
|
||||
<Controller
|
||||
name="config"
|
||||
control={control}
|
||||
|
@ -102,16 +103,18 @@ const ConnectorForm = ({ connector, isAllowEditTarget }: Props) => {
|
|||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField isRequired title="connectors.guide.sync_profile">
|
||||
<Controller
|
||||
name="syncProfile"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select options={syncProfileOptions} value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
{connector.type === ConnectorType.Social && (
|
||||
<FormField title="connectors.guide.sync_profile">
|
||||
<Controller
|
||||
name="syncProfile"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select options={syncProfileOptions} value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import i18next from 'i18next';
|
|||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Close from '@/assets/images/close.svg';
|
||||
import Button from '@/components/Button';
|
||||
|
@ -30,6 +31,7 @@ type Props = {
|
|||
|
||||
const Guide = ({ connector, onClose }: Props) => {
|
||||
const api = useApi();
|
||||
const navigate = useNavigate();
|
||||
const { updateSettings } = useSettings();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { id: connectorId, type: connectorType, name, readme, isStandard } = connector;
|
||||
|
@ -65,19 +67,24 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
|
||||
const { id: connectorId } = connector;
|
||||
|
||||
await api
|
||||
const basePayload = {
|
||||
config: result.data,
|
||||
connectorId,
|
||||
metadata: conditional(
|
||||
isStandard && {
|
||||
...otherData,
|
||||
name: { en: name },
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
const payload = isSocialConnector
|
||||
? { ...basePayload, syncProfile: syncProfile === SyncProfileMode.EachSignIn }
|
||||
: basePayload;
|
||||
|
||||
const createdConnector = await api
|
||||
.post('/api/connectors', {
|
||||
json: {
|
||||
config: result.data,
|
||||
connectorId,
|
||||
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
|
||||
metadata: conditional(
|
||||
isStandard && {
|
||||
...otherData,
|
||||
name: { en: name },
|
||||
}
|
||||
),
|
||||
},
|
||||
json: payload,
|
||||
})
|
||||
.json<ConnectorResponse>();
|
||||
|
||||
|
@ -88,6 +95,7 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
|
||||
onClose();
|
||||
toast.success(t('general.saved'));
|
||||
navigate(`/connectors/${createdConnector.id}`);
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -194,7 +194,7 @@ const Preview = ({ signInExperience, className }: Props) => {
|
|||
style={conditional(
|
||||
platform === 'desktopWeb' && {
|
||||
// Set background color to match iframe's background color on both dark and light mode.
|
||||
backgroundColor: mode === AppearanceMode.DarkMode ? '#2A2C31' : '#e5e1ec',
|
||||
backgroundColor: mode === AppearanceMode.DarkMode ? '#000' : '#e5e1ec',
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -3,6 +3,7 @@ import classNames from 'classnames';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
import CirclePlus from '@/assets/images/circle-plus.svg';
|
||||
import Plus from '@/assets/images/plus.svg';
|
||||
import ActionMenu from '@/components/ActionMenu';
|
||||
import type { Props as ButtonProps } from '@/components/Button';
|
||||
|
@ -33,6 +34,7 @@ const AddButton = ({ options, onSelected, hasSelectedIdentifiers }: Props) => {
|
|||
type: 'text',
|
||||
size: 'small',
|
||||
title: 'general.add_another',
|
||||
icon: <CirclePlus />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
import CirclePlus from '@/assets/images/circle-plus.svg';
|
||||
import Plus from '@/assets/images/plus.svg';
|
||||
import ActionMenu from '@/components/ActionMenu';
|
||||
import type { Props as ButtonProps } from '@/components/Button';
|
||||
|
@ -32,6 +33,7 @@ const AddButton = ({ options, onSelected, hasSelectedConnectors }: Props) => {
|
|||
type: 'text',
|
||||
size: 'small',
|
||||
title: 'general.add_another',
|
||||
icon: <CirclePlus />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -15,7 +15,11 @@ export const onKeyDownHandler =
|
|||
}
|
||||
|
||||
if (typeof callback === 'object') {
|
||||
callback[key]?.(event);
|
||||
event.preventDefault();
|
||||
const handler = callback[key];
|
||||
|
||||
if (handler) {
|
||||
handler(event);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -307,6 +307,7 @@ describe('user query', () => {
|
|||
where ${fields.primaryEmail} ilike $1 or ${fields.primaryPhone} ilike $2 or ${
|
||||
fields.username
|
||||
} ilike $3 or ${fields.name} ilike $4
|
||||
order by "created_at" desc
|
||||
limit $5
|
||||
offset $6
|
||||
`;
|
||||
|
@ -339,6 +340,7 @@ describe('user query', () => {
|
|||
and (${fields.primaryEmail} ilike $2 or ${fields.primaryPhone} ilike $3 or ${
|
||||
fields.username
|
||||
} ilike $4 or ${fields.name} ilike $5)
|
||||
order by "created_at" desc
|
||||
limit $6
|
||||
offset $7
|
||||
`;
|
||||
|
@ -371,6 +373,7 @@ describe('user query', () => {
|
|||
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${
|
||||
fields.username
|
||||
} like $3 or ${fields.name} like $4
|
||||
order by "created_at" desc
|
||||
limit $5
|
||||
offset $6
|
||||
`;
|
||||
|
|
|
@ -145,6 +145,7 @@ export const findUsers = async (
|
|||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
${buildUserConditions(search, hideAdminUser, isCaseSensitive)}
|
||||
order by ${fields.createdAt} desc
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`
|
||||
|
|
|
@ -188,7 +188,6 @@ describe('connector route', () => {
|
|||
metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
|
||||
},
|
||||
]);
|
||||
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
const response = await connectorRequest.post('/connectors').send({
|
||||
connectorId: 'id0',
|
||||
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
||||
|
@ -255,7 +254,7 @@ describe('connector route', () => {
|
|||
{
|
||||
...mockConnectorFactory,
|
||||
type: ConnectorType.Sms,
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'id0', isStandard: true },
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'id1' },
|
||||
},
|
||||
]);
|
||||
getLogtoConnectors.mockResolvedValueOnce([
|
||||
|
@ -266,20 +265,19 @@ describe('connector route', () => {
|
|||
...mockLogtoConnector,
|
||||
},
|
||||
]);
|
||||
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
const response = await connectorRequest.post('/connectors').send({
|
||||
connectorId: 'id0',
|
||||
connectorId: 'id1',
|
||||
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
||||
metadata: { target: 'target', name: { en: '' }, logo: '', logoDark: null },
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 200);
|
||||
expect(response.body).toMatchObject(
|
||||
expect.objectContaining({
|
||||
connectorId: 'id0',
|
||||
connectorId: 'id1',
|
||||
config: {
|
||||
cliend_id: 'client_id',
|
||||
client_secret: 'client_secret',
|
||||
},
|
||||
metadata: { target: 'target' },
|
||||
})
|
||||
);
|
||||
expect(deleteConnectorByIds).toHaveBeenCalledWith(['id']);
|
||||
|
@ -347,6 +345,7 @@ describe('connector route', () => {
|
|||
]);
|
||||
const response = await connectorRequest.post('/connectors').send({
|
||||
connectorId: 'id0',
|
||||
metadata: { target: 'target' },
|
||||
});
|
||||
expect(response).toHaveProperty('statusCode', 422);
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@ import { emailRegEx, phoneRegEx } from '@logto/core-kit';
|
|||
import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas';
|
||||
import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas';
|
||||
import { buildIdGenerator } from '@logto/shared';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import cleanDeep from 'clean-deep';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
|
@ -114,9 +113,9 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
// eslint-disable-next-line complexity
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
body: { connectorId },
|
||||
body,
|
||||
body: { connectorId, metadata, config, syncProfile },
|
||||
} = ctx.guard;
|
||||
|
||||
const connectorFactories = await loadConnectorFactories();
|
||||
const connectorFactory = connectorFactories.find(
|
||||
({ metadata: { id } }) => id === connectorId
|
||||
|
@ -129,6 +128,15 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
});
|
||||
}
|
||||
|
||||
assertThat(
|
||||
connectorFactory.metadata.isStandard !== true || metadata?.target,
|
||||
'connector.can_not_modify_target'
|
||||
);
|
||||
assertThat(
|
||||
connectorFactory.metadata.isStandard === true || metadata === undefined,
|
||||
'connector.cannot_change_metadata_for_non_standard_connector'
|
||||
);
|
||||
|
||||
const { count } = await countConnectorByConnectorId(connectorId);
|
||||
assertThat(
|
||||
count === 0 || connectorFactory.metadata.isStandard === true,
|
||||
|
@ -140,25 +148,23 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
|
||||
if (connectorFactory.type === ConnectorType.Social) {
|
||||
const connectors = await getLogtoConnectors();
|
||||
const connectorTarget = body.metadata?.target ?? connectorFactory.metadata.target;
|
||||
assertThat(
|
||||
!connectors
|
||||
.filter(({ type }) => type === ConnectorType.Social)
|
||||
.some(
|
||||
({ metadata: { target, platform } }) =>
|
||||
target === connectorTarget && platform === connectorFactory.metadata.platform
|
||||
target === cleanDeep(metadata)?.target &&
|
||||
platform === connectorFactory.metadata.platform
|
||||
),
|
||||
new RequestError({ code: 'connector.multiple_target_with_same_platform', status: 422 })
|
||||
);
|
||||
}
|
||||
|
||||
const insertConnectorId = generateConnectorId();
|
||||
const { metadata, ...rest } = body;
|
||||
|
||||
ctx.body = await insertConnector({
|
||||
id: insertConnectorId,
|
||||
...conditional(metadata && { metadata: cleanDeep(metadata) }),
|
||||
...rest,
|
||||
connectorId,
|
||||
...cleanDeep({ syncProfile, config, metadata }),
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -194,17 +200,25 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
.pick({ config: true, metadata: true, syncProfile: true })
|
||||
.partial(),
|
||||
}),
|
||||
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
body: { config },
|
||||
body,
|
||||
body: { config, metadata, syncProfile },
|
||||
} = ctx.guard;
|
||||
|
||||
const { type, validateConfig } = await getLogtoConnectorById(id);
|
||||
const { type, validateConfig, metadata: originalMetadata } = await getLogtoConnectorById(id);
|
||||
|
||||
if (body.syncProfile) {
|
||||
assertThat(
|
||||
originalMetadata.isStandard !== true || metadata?.target === originalMetadata.target,
|
||||
'connector.can_not_modify_target'
|
||||
);
|
||||
|
||||
assertThat(
|
||||
originalMetadata.isStandard === true || metadata === undefined,
|
||||
'connector.cannot_change_metadata_for_non_standard_connector'
|
||||
);
|
||||
|
||||
if (syncProfile) {
|
||||
assertThat(
|
||||
type === ConnectorType.Social,
|
||||
new RequestError({ code: 'connector.invalid_type_for_syncing_profile', status: 422 })
|
||||
|
@ -214,12 +228,9 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
if (config) {
|
||||
validateConfig(config);
|
||||
}
|
||||
// Once created, target can not be modified.
|
||||
assertThat(body.metadata?.target === undefined, 'connector.can_not_modify_target');
|
||||
|
||||
const { metadata: databaseMetadata, ...rest } = body;
|
||||
await updateConnector({
|
||||
set: databaseMetadata ? { metadata: cleanDeep(databaseMetadata), ...rest } : rest,
|
||||
set: cleanDeep({ config, metadata, syncProfile }),
|
||||
where: { id },
|
||||
jsonbMode: 'replace',
|
||||
});
|
||||
|
|
|
@ -120,7 +120,7 @@ describe('connector PATCH routes', () => {
|
|||
updateConnector.mockResolvedValueOnce({
|
||||
...mockConnector,
|
||||
metadata: {
|
||||
target: 'target',
|
||||
target: 'connector',
|
||||
name: { en: 'connector_name', fr: 'connector_name' },
|
||||
logo: 'new_logo.png',
|
||||
},
|
||||
|
@ -131,6 +131,7 @@ describe('connector PATCH routes', () => {
|
|||
name: { en: 'connector_name', fr: 'connector_name' },
|
||||
logo: 'new_logo.png',
|
||||
logoDark: null,
|
||||
target: 'connector',
|
||||
},
|
||||
});
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
|
@ -141,6 +142,7 @@ describe('connector PATCH routes', () => {
|
|||
metadata: {
|
||||
name: { en: 'connector_name', fr: 'connector_name' },
|
||||
logo: 'new_logo.png',
|
||||
target: 'connector',
|
||||
},
|
||||
},
|
||||
jsonbMode: 'replace',
|
||||
|
@ -161,24 +163,20 @@ describe('connector PATCH routes', () => {
|
|||
updateConnector.mockResolvedValueOnce({
|
||||
...mockConnector,
|
||||
metadata: {
|
||||
target: '',
|
||||
target: 'connector',
|
||||
name: { en: '' },
|
||||
logo: '',
|
||||
logoDark: '',
|
||||
},
|
||||
});
|
||||
const response = await connectorRequest.patch('/connectors/id').send({
|
||||
metadata: {
|
||||
name: { en: '' },
|
||||
logo: '',
|
||||
logoDark: '',
|
||||
},
|
||||
metadata: { target: 'connector', name: { en: '' }, logo: '', logoDark: '' },
|
||||
});
|
||||
expect(updateConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'id' },
|
||||
set: {
|
||||
metadata: {},
|
||||
metadata: { target: 'connector' },
|
||||
},
|
||||
jsonbMode: 'replace',
|
||||
})
|
||||
|
|
|
@ -9,22 +9,26 @@ export const getConnector = async (connectorId: string) =>
|
|||
authedAdminApi.get(`connectors/${connectorId}`).json<ConnectorResponse>();
|
||||
|
||||
// FIXME @Darcy: correct use of `id` and `connectorId`.
|
||||
export const postConnector = async (connectorId: string) =>
|
||||
export const postConnector = async (connectorId: string, metadata?: Record<string, unknown>) =>
|
||||
authedAdminApi
|
||||
.post({
|
||||
url: `connectors`,
|
||||
json: { connectorId },
|
||||
json: { connectorId, metadata },
|
||||
})
|
||||
.json<Connector>();
|
||||
|
||||
export const deleteConnectorById = async (id: string) =>
|
||||
authedAdminApi.delete({ url: `connectors/${id}` }).json();
|
||||
|
||||
export const updateConnectorConfig = async (connectorId: string, config: Record<string, unknown>) =>
|
||||
export const updateConnectorConfig = async (
|
||||
connectorId: string,
|
||||
config: Record<string, unknown>,
|
||||
metadata?: Record<string, unknown>
|
||||
) =>
|
||||
authedAdminApi
|
||||
.patch({
|
||||
url: `connectors/${connectorId}`,
|
||||
json: { config },
|
||||
json: { config, metadata },
|
||||
})
|
||||
.json<ConnectorResponse>();
|
||||
|
||||
|
|
|
@ -71,13 +71,18 @@ test('connector set-up flow', async () => {
|
|||
/*
|
||||
* Change to another SMS/Email connector
|
||||
*/
|
||||
const { id } = await postConnector(mockStandardEmailConnectorId);
|
||||
await updateConnectorConfig(id, mockStandardEmailConnectorConfig);
|
||||
const { id } = await postConnector(mockStandardEmailConnectorId, {
|
||||
target: 'mock-standard-mail',
|
||||
}); // TODO [LOG-4862]: update mock connector
|
||||
await updateConnectorConfig(id, mockStandardEmailConnectorConfig, {
|
||||
target: 'mock-standard-mail',
|
||||
}); // TODO [LOG-4862]: update mock connector
|
||||
connectorIdMap.set(mockStandardEmailConnectorId, id);
|
||||
const currentConnectors = await listConnectors();
|
||||
expect(
|
||||
currentConnectors.some((connector) => connector.connectorId === mockEmailConnectorId)
|
||||
).toBeFalsy();
|
||||
connectorIdMap.delete(mockEmailConnectorId);
|
||||
expect(
|
||||
currentConnectors.some((connector) => connector.connectorId === mockStandardEmailConnectorId)
|
||||
).toBeTruthy();
|
||||
|
@ -85,7 +90,6 @@ test('connector set-up flow', async () => {
|
|||
currentConnectors.find((connector) => connector.connectorId === mockStandardEmailConnectorId)
|
||||
?.config
|
||||
).toEqual(mockStandardEmailConnectorConfig);
|
||||
connectorIdMap.delete(mockEmailConnectorId);
|
||||
|
||||
/*
|
||||
* Delete (i.e. disable) a connector
|
||||
|
|
|
@ -112,6 +112,8 @@ const errors = {
|
|||
can_not_modify_target: 'The connector target can not be modified.',
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.',
|
||||
cannot_change_metadata_for_non_standard_connector:
|
||||
"This connector's `metadata` cannot be changed.",
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'Telefonnummer oder E-Mail darf nicht leer sein.',
|
||||
|
|
|
@ -33,7 +33,6 @@ const application_details = {
|
|||
cors_allowed_origins_placeholder: 'https://your.website.de',
|
||||
cors_allowed_origins_tip:
|
||||
'Es sind standardmäßig alle Umleitungs-URI Origins erlaubt. Normalerweise ist dieses Feld nicht erforderlich. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
|
||||
add_another: 'Weitere hinzufügen',
|
||||
id_token_expiration: 'ID Token Ablaufzeit',
|
||||
refresh_token_expiration: 'Refresh Token Ablaufzeit',
|
||||
token_endpoint: 'Token Endpoint',
|
||||
|
|
|
@ -30,7 +30,7 @@ const general = {
|
|||
copying: 'Kopiere',
|
||||
copied: 'Kopiert',
|
||||
required: 'Erforderlich',
|
||||
add_another: '+ Weitere hinzufügen',
|
||||
add_another: 'Weitere hinzufügen',
|
||||
deletion_confirmation: 'Willst du {{title}} wirklich löschen?',
|
||||
settings_nav: 'Einstellungen',
|
||||
unsaved_changes_warning:
|
||||
|
|
|
@ -111,6 +111,8 @@ const errors = {
|
|||
can_not_modify_target: 'The connector target can not be modified.',
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.',
|
||||
cannot_change_metadata_for_non_standard_connector:
|
||||
"This connector's `metadata` cannot be changed.",
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'Both phone and email are empty.',
|
||||
|
|
|
@ -33,7 +33,6 @@ const application_details = {
|
|||
cors_allowed_origins_placeholder: 'https://your.website.com',
|
||||
cors_allowed_origins_tip:
|
||||
'By default, all the origins of Redirect URIs will be allowed. Usually no action is required for this field. See the <a>MDN doc</a> for detailed info.',
|
||||
add_another: 'Add Another',
|
||||
id_token_expiration: 'ID Token expiration',
|
||||
refresh_token_expiration: 'Refresh Token expiration',
|
||||
token_endpoint: 'Token Endpoint',
|
||||
|
|
|
@ -30,7 +30,7 @@ const general = {
|
|||
copying: 'Copying',
|
||||
copied: 'Copied',
|
||||
required: 'Required',
|
||||
add_another: '+ Add Another',
|
||||
add_another: 'Add Another',
|
||||
deletion_confirmation: 'Are you sure you want to delete this {{title}}?',
|
||||
settings_nav: 'Settings',
|
||||
unsaved_changes_warning: 'You have made some changes. Are you sure you want to leave this page?',
|
||||
|
|
|
@ -118,6 +118,8 @@ const errors = {
|
|||
can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
|
||||
cannot_change_metadata_for_non_standard_connector:
|
||||
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: "Le téléphone et l'email sont vides.",
|
||||
|
|
|
@ -33,7 +33,6 @@ const application_details = {
|
|||
cors_allowed_origins_placeholder: 'https://votre.site.com',
|
||||
cors_allowed_origins_tip:
|
||||
"Par défaut, toutes les origines des URI de redirection seront autorisées. En général, aucune action n'est requise pour ce champ. See the <a>MDN doc</a> for detailed info.", // UNTRANSLATED
|
||||
add_another: 'Ajouter un autre',
|
||||
id_token_expiration: "Expiration du jeton d'identification",
|
||||
refresh_token_expiration: "Rafraîchir l'expiration du jeton",
|
||||
token_endpoint: 'Token Endpoint',
|
||||
|
|
|
@ -30,7 +30,7 @@ const general = {
|
|||
copying: 'Copie',
|
||||
copied: 'Copié',
|
||||
required: 'Requis',
|
||||
add_another: '+ Ajouter un autre',
|
||||
add_another: 'Ajouter un autre',
|
||||
deletion_confirmation: 'Êtes-vous sûr de vouloir supprimer ce {{title}} ?',
|
||||
settings_nav: 'Paramètres',
|
||||
unsaved_changes_warning:
|
||||
|
|
|
@ -110,6 +110,8 @@ const errors = {
|
|||
can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
|
||||
cannot_change_metadata_for_non_standard_connector:
|
||||
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: '휴대전화번호 그리고 이메일이 비어있어요.',
|
||||
|
|
|
@ -33,7 +33,6 @@ const application_details = {
|
|||
cors_allowed_origins_placeholder: 'https://your.website.com',
|
||||
cors_allowed_origins_tip:
|
||||
'기본으로 모든 리다이렉트의 오리진들은 허용되요. 대체적으로 이 값을 건들 필요는 없어요. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
|
||||
add_another: '새로 추가',
|
||||
id_token_expiration: 'ID 토큰 만료',
|
||||
refresh_token_expiration: 'Refresh 토큰 만료',
|
||||
token_endpoint: '토큰 End-Point',
|
||||
|
|
|
@ -30,7 +30,7 @@ const general = {
|
|||
copying: '복사 중',
|
||||
copied: '복사됨',
|
||||
required: '필수',
|
||||
add_another: '+ 새로 추가',
|
||||
add_another: '새로 추가',
|
||||
deletion_confirmation: '정말로 {{title}}을/를 삭제할까요?',
|
||||
settings_nav: '설정',
|
||||
unsaved_changes_warning: '수정된 내용이 있어요. 정말로 현재 페이지를 벗어날까요?',
|
||||
|
|
|
@ -115,6 +115,8 @@ const errors = {
|
|||
can_not_modify_target: 'O destino do conector não pode ser modificado.',
|
||||
multiple_target_with_same_platform:
|
||||
'Você não pode ter vários conectores sociais com o mesmo destino e plataforma.',
|
||||
cannot_change_metadata_for_non_standard_connector:
|
||||
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'Telefone e e-mail estão vazios.',
|
||||
|
|
|
@ -33,7 +33,6 @@ const application_details = {
|
|||
cors_allowed_origins_placeholder: 'https://your.website.com',
|
||||
cors_allowed_origins_tip:
|
||||
'Por padrão, todas as origens de URIs de redirecionamento serão permitidas. Normalmente, nenhuma ação é necessária para este campo. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
|
||||
add_another: 'Adicionar outro',
|
||||
id_token_expiration: 'Expiração do token de ID',
|
||||
refresh_token_expiration: 'Expiração Refresh Token',
|
||||
token_endpoint: 'Token Endpoint',
|
||||
|
|
|
@ -30,7 +30,7 @@ const general = {
|
|||
copying: 'Copiando',
|
||||
copied: 'Copiado',
|
||||
required: 'Obrigatório',
|
||||
add_another: '+ Adicionar outro',
|
||||
add_another: 'Adicionar outro',
|
||||
deletion_confirmation: 'Tem certeza de que deseja excluir este {{title}}?',
|
||||
settings_nav: 'Configurações',
|
||||
unsaved_changes_warning:
|
||||
|
|
|
@ -113,6 +113,8 @@ const errors = {
|
|||
can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
|
||||
cannot_change_metadata_for_non_standard_connector:
|
||||
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'O campos telefone e email estão vazios.',
|
||||
|
|
|
@ -33,7 +33,6 @@ const application_details = {
|
|||
cors_allowed_origins_placeholder: 'https://your.website.com',
|
||||
cors_allowed_origins_tip:
|
||||
'Por padrão, todas as origens de redirecionamento serão permitidas. Recomenda-se restringir isto. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
|
||||
add_another: 'Adicionar outro',
|
||||
id_token_expiration: 'Expiração do token de ID',
|
||||
refresh_token_expiration: 'Expiração do token de atualização',
|
||||
token_endpoint: 'Endpoint Token',
|
||||
|
|
|
@ -30,7 +30,7 @@ const general = {
|
|||
copying: 'Copiando',
|
||||
copied: 'Copiado',
|
||||
required: 'Necessário',
|
||||
add_another: '+ Adicionar outro',
|
||||
add_another: 'Adicionar outro',
|
||||
deletion_confirmation: 'Tem a certeza que deseja eliminar isso {{title}}?',
|
||||
settings_nav: 'Definições',
|
||||
unsaved_changes_warning: 'Fez algumas alterações. Tem a certeza que deseja sair desta página?',
|
||||
|
|
|
@ -112,6 +112,8 @@ const errors = {
|
|||
can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED
|
||||
multiple_target_with_same_platform:
|
||||
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
|
||||
cannot_change_metadata_for_non_standard_connector:
|
||||
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: 'Hem telefon hem de e-posta adresi yok.',
|
||||
|
|
|
@ -33,7 +33,6 @@ const application_details = {
|
|||
cors_allowed_origins_placeholder: 'https://your.website.com',
|
||||
cors_allowed_origins_tip:
|
||||
'Varsayılan olarak, Yönlendirme URIlerinin tüm originlerine izin verilir. Genellikle bu alan için herhangi bir işlem gerekmez. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
|
||||
add_another: 'Bir tane daha ekle',
|
||||
id_token_expiration: 'ID Token sona erme süresi',
|
||||
refresh_token_expiration: 'Refresh Token sona erme süresi',
|
||||
token_endpoint: 'Token bitiş noktası',
|
||||
|
|
|
@ -30,7 +30,7 @@ const general = {
|
|||
copying: 'Kopyalanıyor',
|
||||
copied: 'Kopyalandı',
|
||||
required: 'Gerekli',
|
||||
add_another: '+ Bir tane daha ekle',
|
||||
add_another: 'Bir tane daha ekle',
|
||||
deletion_confirmation: 'Bu dosyayı silmek istediğinize emin misiniz: {{title}}?',
|
||||
settings_nav: 'Ayarlar',
|
||||
unsaved_changes_warning:
|
||||
|
|
|
@ -103,6 +103,7 @@ const errors = {
|
|||
invalid_type_for_syncing_profile: '只有社交连接器可以开启用户档案同步。',
|
||||
can_not_modify_target: '不可修改连接器 target。',
|
||||
multiple_target_with_same_platform: '不能同时存在多个有相同 target 和平台类型的社交连接器。',
|
||||
cannot_change_metadata_for_non_standard_connector: '不可配置该连接器的 metadata 参数。',
|
||||
},
|
||||
passcode: {
|
||||
phone_email_empty: '手机号与邮箱地址均为空',
|
||||
|
|
|
@ -33,7 +33,6 @@ const application_details = {
|
|||
cors_allowed_origins_placeholder: 'https://your.website.com',
|
||||
cors_allowed_origins_tip:
|
||||
'所有 Redirect URI 的 origin 将默认被允许。通常不需要对此字段进行操作。参见 <a>MDN 文档</a>以了解更多',
|
||||
add_another: '新增',
|
||||
id_token_expiration: 'ID Token 过期时间',
|
||||
refresh_token_expiration: 'Refresh Token 过期时间',
|
||||
token_endpoint: 'Token Endpoint',
|
||||
|
|
|
@ -30,7 +30,7 @@ const general = {
|
|||
copying: '复制中',
|
||||
copied: '已复制',
|
||||
required: '必填',
|
||||
add_another: '+ 新增',
|
||||
add_another: '新增',
|
||||
deletion_confirmation: '你确定要删除这个 {{title}} 吗?',
|
||||
settings_nav: '设置',
|
||||
unsaved_changes_warning: '还有未保存的变更, 确定要离开吗?',
|
||||
|
|
Loading…
Reference in a new issue