0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

feat(ui): add signInMode guard (#2317)

This commit is contained in:
simeng-li 2022-11-04 16:59:52 +08:00 committed by GitHub
parent b451f09cb9
commit 9f23e13e2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 134 additions and 17 deletions

View file

@ -1,3 +1,4 @@
import { SignInMode } from '@logto/schemas';
import { useEffect } from 'react';
import { Route, Routes, BrowserRouter, Navigate } from 'react-router-dom';
@ -50,6 +51,9 @@ const App = () => {
return null;
}
const isRegisterOnly = experienceSettings.signInMode === SignInMode.Register;
const isSignInOnly = experienceSettings.signInMode === SignInMode.SignIn;
return (
<Provider value={context}>
<AppContent>
@ -64,13 +68,19 @@ const App = () => {
<Route element={<LoadingLayerProvider />}>
{/* sign-in */}
<Route path="/sign-in" element={<SignIn />} />
<Route
path="/sign-in"
element={isRegisterOnly ? <Navigate replace to="/register" /> : <SignIn />}
/>
<Route path="/sign-in/social/:connector" element={<SocialSignIn />} />
<Route path="/sign-in/:method" element={<SecondarySignIn />} />
<Route path="/sign-in/:method/password" element={<SignInPassword />} />
{/* register */}
<Route path="/register" element={<Register />} />
<Route
path="/register"
element={isSignInOnly ? <Navigate replace to="/sign-in" /> : <Register />}
/>
<Route
path="/register/username/password"
element={<PasswordRegisterWithUsername />}

View file

@ -11,5 +11,6 @@ export const useSieMethods = () => {
signUpSettings: { password, verify },
signInMethods: experienceSettings?.signIn.methods ?? [],
socialConnectors: experienceSettings?.socialConnectors ?? [],
signInMode: experienceSettings?.signInMode,
};
};

View file

@ -1,3 +1,4 @@
import { SignInMode } from '@logto/schemas';
import { useEffect, useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useNavigate } from 'react-router-dom';
@ -11,7 +12,8 @@ import useApi from './use-api';
import { PageContext } from './use-page-context';
const useSocialSignInListener = () => {
const { setToast } = useContext(PageContext);
const { setToast, experienceSettings } = useContext(PageContext);
const { t } = useTranslation();
const parameters = useParams();
const navigate = useNavigate();
@ -19,6 +21,13 @@ const useSocialSignInListener = () => {
const signInWithSocialErrorHandlers: ErrorHandlers = useMemo(
() => ({
'user.identity_not_exists': (error) => {
// Should not let user register under sign-in only mode
if (experienceSettings?.signInMode === SignInMode.SignIn) {
setToast(error.message);
return;
}
if (parameters.connector) {
navigate(`/social/register/${parameters.connector}`, {
replace: true,
@ -27,7 +36,7 @@ const useSocialSignInListener = () => {
}
},
}),
[navigate, parameters.connector]
[experienceSettings?.signInMode, navigate, parameters.connector, setToast]
);
const { result, run: asyncSignInWithSocial } = useApi(

View file

@ -1,4 +1,4 @@
import { SignInIdentifier } from '@logto/schemas';
import { SignInMode, SignInIdentifier } from '@logto/schemas';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
@ -99,4 +99,18 @@ describe('<Register />', () => {
mockSignInExperienceSettings.socialConnectors.length
);
});
test('render with sign-in only mode should return ErrorPage', () => {
const { queryByText } = renderWithPageContext(
<SettingsProvider
settings={{ ...mockSignInExperienceSettings, signInMode: SignInMode.SignIn }}
>
<MemoryRouter>
<Register />
</MemoryRouter>
</SettingsProvider>
);
expect(queryByText('description.not_found')).not.toBeNull();
});
});

View file

@ -1,3 +1,4 @@
import { SignInMode } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
import Divider from '@/components/Divider';
@ -8,14 +9,19 @@ import { SocialSignInList } from '@/containers/SocialSignIn';
import { useSieMethods } from '@/hooks/use-sie';
import { UserFlow } from '@/types';
import ErrorPage from '../ErrorPage';
import Main from './Main';
import * as styles from './index.module.scss';
const Register = () => {
const { signUpMethods, socialConnectors } = useSieMethods();
const { signUpMethods, socialConnectors, signInMode } = useSieMethods();
const otherMethods = signUpMethods.slice(1);
const { t } = useTranslation();
if (!signInMode || signInMode === SignInMode.SignIn) {
return <ErrorPage />;
}
return (
<LandingPageContainer>
<Main signUpMethod={signUpMethods[0]} socialConnectors={socialConnectors} />
@ -40,7 +46,7 @@ const Register = () => {
}
{
// SignIn footer
signUpMethods.length > 0 && (
signInMode === SignInMode.SignInAndRegister && signUpMethods.length > 0 && (
<>
<div className={styles.placeHolder} />
<div className={styles.createAccount}>

View file

@ -1,4 +1,4 @@
import { SignInIdentifier } from '@logto/schemas';
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { Routes, Route, MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
@ -86,7 +86,7 @@ describe('<SecondaryRegister />', () => {
});
test('renders non-supported signUp methods should return error page', () => {
const { queryByText, container } = renderWithPageContext(
const { queryByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/register/email']}>
<Routes>
<Route
@ -105,7 +105,7 @@ describe('<SecondaryRegister />', () => {
});
test('render non-verified passwordless methods should return error page', () => {
const { queryByText, container } = renderWithPageContext(
const { queryByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/register/email']}>
<Routes>
<Route
@ -131,4 +131,28 @@ describe('<SecondaryRegister />', () => {
expect(queryByText('action.create_account')).toBeNull();
expect(queryByText('description.not_found')).not.toBeNull();
});
test('render with sign-in only mode', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/register/email']}>
<Routes>
<Route
path="/register/:method"
element={
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
signInMode: SignInMode.SignIn,
}}
>
<SecondaryRegister />
</SettingsProvider>
}
/>
</Routes>
</MemoryRouter>
);
expect(queryByText('action.create_account')).toBeNull();
expect(queryByText('description.not_found')).not.toBeNull();
});
});

View file

@ -1,4 +1,4 @@
import { SignInIdentifier } from '@logto/schemas';
import { SignInMode, SignInIdentifier } from '@logto/schemas';
import { useParams } from 'react-router-dom';
import { is } from 'superstruct';
@ -16,7 +16,11 @@ type Parameters = {
const SecondaryRegister = () => {
const { method = '' } = useParams<Parameters>();
const { signUpMethods, signUpSettings } = useSieMethods();
const { signUpMethods, signUpSettings, signInMode } = useSieMethods();
if (!signInMode || signInMode === SignInMode.SignIn) {
return <ErrorPage />;
}
// Validate the signUp method
if (!is(method, SignInMethodGuard) || !signUpMethods.includes(method)) {

View file

@ -1,4 +1,4 @@
import { SignInIdentifier } from '@logto/schemas';
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { Routes, Route, MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
@ -114,4 +114,28 @@ describe('<SecondarySignIn />', () => {
expect(queryByText('action.sign_in')).toBeNull();
expect(queryByText('description.not_found')).not.toBeNull();
});
test('render with register only mode', async () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/sign-in/email']}>
<Routes>
<Route
path="/sign-in/:method"
element={
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
signInMode: SignInMode.Register,
}}
>
<SecondarySignIn />
</SettingsProvider>
}
/>
</Routes>
</MemoryRouter>
);
expect(queryByText('action.sign_in')).toBeNull();
expect(queryByText('description.not_found')).not.toBeNull();
});
});

View file

@ -1,4 +1,4 @@
import { SignInIdentifier } from '@logto/schemas';
import { SignInMode, SignInIdentifier } from '@logto/schemas';
import { useParams } from 'react-router-dom';
import SecondaryPageWrapper from '@/components/SecondaryPageWrapper';
@ -14,9 +14,13 @@ type Props = {
const SecondarySignIn = () => {
const { method = '' } = useParams<Props>();
const { signInMethods } = useSieMethods();
const { signInMethods, signInMode } = useSieMethods();
const signInMethod = signInMethods.find(({ identifier }) => identifier === method);
if (!signInMode || signInMode === SignInMode.Register) {
return <ErrorPage />;
}
if (!signInMethod) {
return <ErrorPage />;
}

View file

@ -1,3 +1,4 @@
import { SignInMode } from '@logto/schemas';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
@ -130,4 +131,18 @@ describe('<SignIn />', () => {
mockSignInExperienceSettings.socialConnectors.length
);
});
test('render with register only mode should return ErrorPage', () => {
const { queryByText } = renderWithPageContext(
<SettingsProvider
settings={{ ...mockSignInExperienceSettings, signInMode: SignInMode.Register }}
>
<MemoryRouter>
<SignIn />
</MemoryRouter>
</SettingsProvider>
);
expect(queryByText('description.not_found')).not.toBeNull();
});
});

View file

@ -1,3 +1,4 @@
import { SignInMode } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
import Divider from '@/components/Divider';
@ -8,14 +9,19 @@ import { SocialSignInList } from '@/containers/SocialSignIn';
import { useSieMethods } from '@/hooks/use-sie';
import { UserFlow } from '@/types';
import ErrorPage from '../ErrorPage';
import Main from './Main';
import * as styles from './index.module.scss';
const SignIn = () => {
const { signInMethods, signUpMethods, socialConnectors } = useSieMethods();
const { signInMethods, signUpMethods, socialConnectors, signInMode } = useSieMethods();
const otherMethods = signInMethods.slice(1).map(({ identifier }) => identifier);
const { t } = useTranslation();
if (!signInMode || signInMode === SignInMode.Register) {
return <ErrorPage />;
}
return (
<LandingPageContainer>
<Main signInMethod={signInMethods[0]} socialConnectors={socialConnectors} />
@ -41,7 +47,7 @@ const SignIn = () => {
}
{
// Create Account footer
signUpMethods.length > 0 && (
signInMode === SignInMode.SignInAndRegister && signUpMethods.length > 0 && (
<>
<div className={styles.placeHolder} />
<div className={styles.createAccount}>