0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

feat(core): add socialConnectors details for get sign-in-settings (#804)

* feat(core,ui): add social connector info to get-settings api

* refactor(connectors): rename api
This commit is contained in:
Darcy Ye 2022-05-13 15:59:07 +08:00 committed by GitHub
parent 357fb8dc40
commit 7a922cbd33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 116 additions and 32 deletions

View file

@ -2,7 +2,15 @@
import { User } from '@logto/schemas';
import { Provider } from 'oidc-provider';
import { mockSignInExperience, mockUser } from '@/__mocks__';
import {
mockSignInExperience,
mockUser,
mockAliyunDmConnectorInstance,
mockAliyunSmsConnectorInstance,
mockFacebookConnectorInstance,
mockGithubConnectorInstance,
mockGoogleConnectorInstance,
} from '@/__mocks__';
import { ConnectorType } from '@/connectors/types';
import RequestError from '@/errors/RequestError';
import * as signInExperienceQueries from '@/queries/sign-in-experience';
@ -99,6 +107,13 @@ const getConnectorInstanceById = jest.fn(async (connectorId: string) => {
return { connector, metadata, getAuthorizationUri };
});
const getConnectorInstances = jest.fn(async () => [
mockAliyunDmConnectorInstance,
mockAliyunSmsConnectorInstance,
mockFacebookConnectorInstance,
mockGithubConnectorInstance,
mockGoogleConnectorInstance,
]);
jest.mock('@/connectors', () => ({
getSocialConnectorInstanceById: async (connectorId: string) => {
const connectorInstance = await getConnectorInstanceById(connectorId);
@ -112,6 +127,7 @@ jest.mock('@/connectors', () => ({
return connectorInstance;
},
getConnectorInstances: async () => getConnectorInstances(),
}));
const grantSave = jest.fn(async () => 'finalGrantId');
@ -431,11 +447,11 @@ describe('sessionRoutes', () => {
});
});
describe('POST /session/bind-social-related-user', () => {
describe('POST /session/sign-in/bind-social-related-user', () => {
it('throw if session is not authorized', async () => {
await expect(
sessionRequest
.post('/session/bind-social-related-user')
.post('/session/sign-in/bind-social-related-user')
.send({ connectorId: 'connectorId' })
).resolves.toHaveProperty('statusCode', 400);
});
@ -445,7 +461,7 @@ describe('sessionRoutes', () => {
});
await expect(
sessionRequest
.post('/session/bind-social-related-user')
.post('/session/sign-in/bind-social-related-user')
.send({ connectorId: 'connectorId' })
).resolves.toHaveProperty('statusCode', 400);
});
@ -459,7 +475,7 @@ describe('sessionRoutes', () => {
},
},
});
const response = await sessionRequest.post('/session/bind-social-related-user').send({
const response = await sessionRequest.post('/session/sign-in/bind-social-related-user').send({
connectorId: 'connectorId',
});
expect(response.statusCode).toEqual(200);
@ -890,15 +906,29 @@ describe('sessionRoutes', () => {
});
describe('GET /sign-in-settings', () => {
const signInExperienceQuerySpon = jest
const signInExperienceQuerySpyOn = jest
.spyOn(signInExperienceQueries, 'findDefaultSignInExperience')
.mockResolvedValue(mockSignInExperience);
it('should call findDefaultSignInExperience', async () => {
it('should return github and facebook connector instances', async () => {
const response = await sessionRequest.get('/sign-in-settings');
expect(signInExperienceQuerySpon).toHaveBeenCalledTimes(1);
expect(signInExperienceQuerySpyOn).toHaveBeenCalledTimes(1);
expect(response.status).toEqual(200);
expect(response.body).toEqual(mockSignInExperience);
expect(response.body).toMatchObject(
expect.objectContaining({
...mockSignInExperience,
socialConnectors: [
{
...mockGithubConnectorInstance.metadata,
id: mockGithubConnectorInstance.connector.id,
},
{
...mockFacebookConnectorInstance.metadata,
id: mockFacebookConnectorInstance.connector.id,
},
],
})
);
});
});

View file

@ -8,7 +8,7 @@ import pick from 'lodash.pick';
import { Provider } from 'oidc-provider';
import { object, string } from 'zod';
import { getSocialConnectorInstanceById } from '@/connectors';
import { getConnectorInstances, getSocialConnectorInstanceById } from '@/connectors';
import RequestError from '@/errors/RequestError';
import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode';
import { assignInteractionResults, saveUserFirstConsentedAppId } from '@/lib/session';
@ -259,7 +259,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
);
router.post(
'/session/bind-social-related-user',
'/session/sign-in/bind-social-related-user',
koaGuard({
body: object({ connectorId: string() }),
}),
@ -577,9 +577,15 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
});
router.get('/sign-in-settings', async (ctx, next) => {
// TODO: Social Connector Details
const signInExperience = await findDefaultSignInExperience();
ctx.body = signInExperience;
const connectorInstances = await getConnectorInstances();
const instanceMap = new Map(
connectorInstances.map((instance) => [instance.connector.id, instance])
);
const socialConnectors = signInExperience.socialSignInConnectorIds.map((id) => {
return { ...instanceMap.get(id)?.metadata, id };
});
ctx.body = { ...signInExperience, socialConnectors };
return next();
});

View file

@ -1,5 +1,11 @@
import { Language } from '@logto/phrases';
import { BrandingStyle, SignInExperience, SignInMethodState } from '@logto/schemas';
import {
BrandingStyle,
ConnectorPlatform,
ConnectorType,
SignInExperience,
SignInMethodState,
} from '@logto/schemas';
import { SignInExperienceSettings } from '@/types';
@ -7,44 +13,86 @@ export const appLogo = 'https://avatars.githubusercontent.com/u/88327661?s=200&v
export const appHeadline = 'Build user identity in a modern way';
export const socialConnectors = [
{
id: 'BE8QXN0VsrOH7xdWFDJZ9',
target: 'github',
platform: ConnectorPlatform.Web,
type: ConnectorType.Social,
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
name: {
en: 'Sign in with GitHub',
'zh-CN': '使用 GitHub 登录',
},
description: {
en: 'Sign in with GitHub',
'zh-CN': '使用 GitHub 登录',
},
readme: '',
configTemplate: '',
},
{
id: '24yt_xIUl5btN4UwvFokt',
target: 'alipay',
platform: ConnectorPlatform.Web,
type: ConnectorType.Social,
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
name: {
en: 'Sign in with Alipay',
'zh-CN': '使用 Alipay 登录',
},
description: {
en: 'Sign in with Alipay',
'zh-CN': '使用 Alipay 登录',
},
readme: '',
configTemplate: '',
},
{
id: 'E5kb2gdq769qOEYaLg1V5',
target: 'wechat',
platform: ConnectorPlatform.Web,
type: ConnectorType.Social,
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
name: {
en: 'Sign in with WeChat',
'zh-CN': '使用 WeChat 登录',
},
description: {
en: 'Sign in with WeChat',
'zh-CN': '使用 WeChat 登录',
},
readme: '',
configTemplate: '',
},
{
id: 'xY2YZEweMFPKxphngGHhy',
target: 'google',
platform: ConnectorPlatform.Web,
type: ConnectorType.Social,
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
name: {
en: 'Sign in with Google',
'zh-CN': '使用 Google 登录',
},
description: { en: 'Sign in with Google', 'zh-CN': '使用 Google 登录' },
readme: '',
configTemplate: '',
},
{
id: 'lcXT4o2GSjbV9kg2shZC7',
target: 'facebook',
platform: ConnectorPlatform.Web,
type: ConnectorType.Social,
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
name: {
en: 'Sign in with Meta',
'zh-CN': '使用 Meta 登录',
},
description: {
en: 'Sign in with Meta',
'zh-CN': '使用 Meta 登录',
},
readme: '',
configTemplate: '',
},
];
@ -73,7 +121,7 @@ export const mockSignInExperience: SignInExperience = {
sms: SignInMethodState.Secondary,
social: SignInMethodState.Secondary,
},
socialSignInConnectorIds: ['github', 'facebook'],
socialSignInConnectorIds: ['BE8QXN0VsrOH7xdWFDJZ9', 'lcXT4o2GSjbV9kg2shZC7'],
};
export const mockSignInExperienceSettings: SignInExperienceSettings = {

View file

@ -6,6 +6,6 @@
import { SignInExperience } from '@logto/schemas';
import ky from 'ky';
export const getSignInExperience = async () => {
return ky.get('/api/sign-in-settings').json<SignInExperience>();
export const getSignInExperience = async <T extends SignInExperience>(): Promise<T> => {
return ky.get('/api/sign-in-settings').json<T>();
};

View file

@ -32,11 +32,11 @@ const SecondarySocialSignIn = ({ className }: Props) => {
<div className={classNames(styles.socialIconList, className)}>
{displayConnectors.map((connector) => (
<SocialIconButton
key={connector.target}
key={connector.id}
className={styles.socialButton}
connector={connector}
onClick={() => {
void invokeSocialSignIn(connector.target);
void invokeSocialSignIn(connector.id);
}}
/>
))}

View file

@ -90,10 +90,8 @@ const useSocial = () => {
// Filter native supported social connectors
const socialConnectors = useMemo(
() =>
(experienceSettings?.socialConnectors ?? []).filter(({ target }) => {
return (
!isNativeWebview() || getLogtoNativeSdk()?.supportedSocialConnectorIds.includes(target)
);
(experienceSettings?.socialConnectors ?? []).filter(({ id }) => {
return !isNativeWebview() || getLogtoNativeSdk()?.supportedSocialConnectorIds.includes(id);
}),
[experienceSettings?.socialConnectors]
);

View file

@ -19,9 +19,7 @@ const Callback = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const connectorLabel = useMemo(() => {
const connector = experienceSettings?.socialConnectors.find(
({ target }) => target === connectorId
);
const connector = experienceSettings?.socialConnectors.find(({ id }) => id === connectorId);
if (connector) {
return (

View file

@ -8,7 +8,9 @@ export enum SearchParameters {
bindWithSocial = 'bw',
}
type ConnectorData = Pick<ConnectorMetadata, 'target' | 'logo' | 'name'>;
export interface ConnectorData extends ConnectorMetadata {
id: string;
}
export type SignInExperienceSettings = {
branding: Branding;

View file

@ -3,11 +3,10 @@
* TODO: Remove this once we have a better way to get the sign in experience through SSR
*/
import { SignInMethods } from '@logto/schemas';
import { SignInMethods, SignInExperience } from '@logto/schemas';
import { socialConnectors } from '@/__mocks__/logto';
import { getSignInExperience } from '@/apis/settings';
import { SignInMethod, SignInExperienceSettings } from '@/types';
import { ConnectorData, SignInMethod, SignInExperienceSettings } from '@/types';
const getPrimarySignInMethod = (signInMethods: SignInMethods) => {
for (const [key, value] of Object.entries(signInMethods)) {
@ -28,8 +27,11 @@ const getSecondarySignInMethods = (signInMethods: SignInMethods) =>
return methods;
}, []);
const getSignInExperienceSettings = async (): Promise<SignInExperienceSettings> => {
const { branding, languageInfo, termsOfUse, signInMethods } = await getSignInExperience();
const getSignInExperienceSettings = async <
T extends SignInExperience & { socialConnectors: ConnectorData[] }
>(): Promise<SignInExperienceSettings> => {
const { branding, languageInfo, termsOfUse, signInMethods, socialConnectors } =
await getSignInExperience<T>();
return {
branding,