0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

feat(core): add sign-in-mode (#1132)

* feat(core): add sign-in-mode
add sign-in-mode

* fix(core): remove console.log

remove console.log

* fix(ui): hide secondary signin section
hide secondary signin section if is under register mode

* fix(core): ci fix

ci fix
This commit is contained in:
simeng-li 2022-06-17 10:36:07 +08:00 committed by GitHub
parent 20f7ad9863
commit f640dad52f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 124 additions and 16 deletions

View file

@ -7,6 +7,7 @@ import {
SignInMethods,
SignInMethodState,
TermsOfUse,
SignInMode,
} from '@logto/schemas';
export const mockSignInExperience: SignInExperience = {
@ -34,6 +35,7 @@ export const mockSignInExperience: SignInExperience = {
social: SignInMethodState.Secondary,
},
socialSignInConnectorTargets: ['github', 'facebook', 'wechat'],
signInMode: SignInMode.SignInAndRegister,
};
export const mockBranding: Branding = {

View file

@ -31,7 +31,7 @@ describe('sign-in-experience query', () => {
it('findDefaultSignInExperience', async () => {
/* eslint-disable sql/no-unsafe-query */
const expectSql = `
select "id", "branding", "language_info", "terms_of_use", "sign_in_methods", "social_sign_in_connector_targets"
select "id", "branding", "language_info", "terms_of_use", "sign_in_methods", "social_sign_in_connector_targets", "sign_in_mode"
from "sign_in_experiences"
where "id" = $1
`;

View file

@ -49,7 +49,7 @@ const createRouters = (provider: Provider) => {
meRoutes(meRouter);
const anonymousRouter: AnonymousRouter = new Router();
signInSettingsRoutes(anonymousRouter);
signInSettingsRoutes(anonymousRouter, provider);
statusRoutes(anonymousRouter);
// The swagger.json should contain all API routers.
swaggerRoutes(anonymousRouter, [sessionRouter, managementRouter, meRouter, anonymousRouter]);

View file

@ -1,4 +1,6 @@
import { ConnectorType } from '@logto/schemas';
import { ConnectorType, SignInMode } from '@logto/schemas';
import { adminConsoleApplicationId, adminConsoleSignInMethods } from '@logto/schemas/lib/seeds';
import { Provider } from 'oidc-provider';
import {
mockAliyunDmConnectorInstance,
@ -25,6 +27,7 @@ const getConnectorInstances = jest.fn(async () => [
mockWechatConnectorInstance,
mockWechatNativeConnectorInstance,
]);
jest.mock('@/connectors', () => ({
getSocialConnectorInstanceById: async (connectorId: string) => {
const connectorInstance = await getConnectorInstanceById(connectorId);
@ -41,9 +44,28 @@ jest.mock('@/connectors', () => ({
getConnectorInstances: async () => getConnectorInstances(),
}));
jest.mock('@/queries/user', () => ({
hasActiveUsers: jest.fn().mockResolvedValue(true),
}));
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({
params: {},
}));
jest.mock('oidc-provider', () => ({
Provider: jest.fn(() => ({
interactionDetails,
})),
}));
describe('GET /sign-in-settings', () => {
afterEach(() => {
jest.clearAllMocks();
});
const sessionRequest = createRequester({
anonymousRoutes: signInSettingsRoutes,
provider: new Provider(''),
middlewares: [
async (ctx, next) => {
ctx.addLogContext = jest.fn();
@ -86,4 +108,20 @@ describe('GET /sign-in-settings', () => {
})
);
});
it('should return admin console settings', async () => {
interactionDetails.mockResolvedValue({ params: { client_id: adminConsoleApplicationId } });
const response = await sessionRequest.get('/sign-in-settings');
expect(signInExperienceQuerySpyOn).toHaveBeenCalledTimes(1);
expect(response.status).toEqual(200);
expect(response.body).toMatchObject(
expect.objectContaining({
...mockSignInExperience,
signInMethods: adminConsoleSignInMethods,
socialConnectors: [],
signInMode: SignInMode.SignIn,
})
);
});
});

View file

@ -1,19 +1,28 @@
import { ConnectorMetadata } from '@logto/connector-types';
import { SignInMode } from '@logto/schemas';
import { adminConsoleApplicationId, adminConsoleSignInMethods } from '@logto/schemas/lib/seeds';
import etag from 'etag';
import { Provider } from 'oidc-provider';
import { getConnectorInstances } from '@/connectors';
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
import { hasActiveUsers } from '@/queries/user';
import { AnonymousRouter } from './types';
export default function signInSettingsRoutes<T extends AnonymousRouter>(router: T) {
export default function signInSettingsRoutes<T extends AnonymousRouter>(
router: T,
provider: Provider
) {
router.get(
'/sign-in-settings',
async (ctx, next) => {
const [signInExperience, connectorInstances] = await Promise.all([
const [signInExperience, connectorInstances, interaction] = await Promise.all([
findDefaultSignInExperience(),
getConnectorInstances(),
provider.interactionDetails(ctx.req, ctx.res),
]);
const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
Array<ConnectorMetadata & { id: string }>
>((previous, connectorTarget) => {
@ -27,6 +36,23 @@ export default function signInSettingsRoutes<T extends AnonymousRouter>(router:
...connectors.map(({ metadata, connector: { id } }) => ({ ...metadata, id })),
];
}, []);
const {
params: { client_id },
} = interaction;
// Hard code AdminConsole sign-in methods settings.
if (client_id === adminConsoleApplicationId) {
ctx.body = {
...signInExperience,
signInMethods: adminConsoleSignInMethods,
signInMode: (await hasActiveUsers()) ? SignInMode.SignIn : SignInMode.Register,
socialConnectors: [],
};
return next();
}
ctx.body = { ...signInExperience, socialConnectors };
return next();

View file

@ -1,6 +1,6 @@
import { Language } from '@logto/phrases';
import { CreateSignInExperience } from '../db-entries';
import { CreateSignInExperience, SignInMode } from '../db-entries';
import { BrandingStyle, SignInMethodState } from '../foundations';
export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
@ -28,4 +28,12 @@ export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
social: SignInMethodState.Disabled,
},
socialSignInConnectorTargets: [],
signInMode: SignInMode.SignInAndRegister,
};
export const adminConsoleSignInMethods = {
username: SignInMethodState.Primary,
email: SignInMethodState.Disabled,
sms: SignInMethodState.Disabled,
social: SignInMethodState.Disabled,
};

View file

@ -1,3 +1,5 @@
create type sign_in_mode as enum ('SignIn', 'Register', 'SignInAndRegister');
create table sign_in_experiences (
id varchar(21) not null,
branding jsonb /* @use Branding */ not null,
@ -5,5 +7,6 @@ create table sign_in_experiences (
terms_of_use jsonb /* @use TermsOfUse */ not null,
sign_in_methods jsonb /* @use SignInMethods */ not null,
social_sign_in_connector_targets jsonb /* @use ConnectorTargets */ not null default '[]'::jsonb,
sign_in_mode sign_in_mode not null default 'SignInAndRegister',
primary key (id)
);

View file

@ -5,6 +5,7 @@ import {
ConnectorType,
SignInExperience,
SignInMethodState,
SignInMode,
} from '@logto/schemas';
import { SignInExperienceSettings } from '@/types';
@ -140,6 +141,7 @@ export const mockSignInExperience: SignInExperience = {
social: SignInMethodState.Secondary,
},
socialSignInConnectorTargets: ['BE8QXN0VsrOH7xdWFDJZ9', 'lcXT4o2GSjbV9kg2shZC7'],
signInMode: SignInMode.SignInAndRegister,
};
export const mockSignInExperienceSettings: SignInExperienceSettings = {
@ -149,4 +151,5 @@ export const mockSignInExperienceSettings: SignInExperienceSettings = {
primarySignInMethod: 'username',
secondarySignInMethods: ['email', 'sms', 'social'],
socialConnectors,
signInMode: SignInMode.SignInAndRegister,
};

View file

@ -1,4 +1,4 @@
import { BrandingStyle } from '@logto/schemas';
import { BrandingStyle, SignInMode } from '@logto/schemas';
import classNames from 'classnames';
import React, { useContext } from 'react';
@ -29,13 +29,21 @@ const SignIn = () => {
<PrimarySection
signInMethod={experienceSettings.primarySignInMethod}
socialConnectors={experienceSettings.socialConnectors}
signInMode={experienceSettings.signInMode}
/>
<SecondarySection
primarySignInMethod={experienceSettings.primarySignInMethod}
secondarySignInMethods={experienceSettings.secondarySignInMethods}
socialConnectors={experienceSettings.socialConnectors}
/>
<CreateAccountLink primarySignInMethod={experienceSettings.primarySignInMethod} />
{experienceSettings.signInMode !== SignInMode.Register && (
<SecondarySection
primarySignInMethod={experienceSettings.primarySignInMethod}
secondarySignInMethods={experienceSettings.secondarySignInMethods}
socialConnectors={experienceSettings.socialConnectors}
/>
)}
{experienceSettings.signInMode === SignInMode.SignInAndRegister && (
<CreateAccountLink primarySignInMethod={experienceSettings.primarySignInMethod} />
)}
<AppNotification />
</div>
);

View file

@ -1,7 +1,9 @@
import { SignInMode } from '@logto/schemas';
import React from 'react';
import Divider from '@/components/Divider';
import TextLink from '@/components/TextLink';
import CreateAccount from '@/containers/CreateAccount';
import { EmailPasswordless, PhonePasswordless } from '@/containers/Passwordless';
import SignInMethodsLink from '@/containers/SignInMethodsLink';
import { PrimarySocialSignIn, SecondarySocialSignIn } from '@/containers/SocialSignIn';
@ -14,17 +16,33 @@ import * as styles from './index.module.scss';
export const PrimarySection = ({
signInMethod,
socialConnectors = [],
signInMode,
}: {
signInMethod?: SignInMethod;
socialConnectors?: ConnectorData[];
signInMode?: SignInMode;
}) => {
switch (signInMethod) {
case 'email':
return <EmailPasswordless type="sign-in" className={styles.primarySignIn} />;
return (
<EmailPasswordless
type={signInMode === SignInMode.Register ? 'register' : 'sign-in'}
className={styles.primarySignIn}
/>
);
case 'sms':
return <PhonePasswordless type="sign-in" className={styles.primarySignIn} />;
return (
<PhonePasswordless
type={signInMode === SignInMode.Register ? 'register' : 'sign-in'}
className={styles.primarySignIn}
/>
);
case 'username':
return <UsernameSignin className={styles.primarySignIn} />;
return signInMode === SignInMode.Register ? (
<CreateAccount />
) : (
<UsernameSignin className={styles.primarySignIn} />
);
case 'social':
return socialConnectors.length > 0 ? (
<>

View file

@ -4,6 +4,7 @@ import {
TermsOfUse,
SignInExperience,
ConnectorMetadata,
SignInMode,
} from '@logto/schemas';
export type UserFlow = 'sign-in' | 'register';
@ -35,4 +36,5 @@ export type SignInExperienceSettings = {
primarySignInMethod: SignInMethod;
secondarySignInMethods: SignInMethod[];
socialConnectors: ConnectorData[];
signInMode: SignInMode;
};