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:
parent
20f7ad9863
commit
f640dad52f
11 changed files with 124 additions and 16 deletions
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
`;
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 ? (
|
||||
<>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue