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:
parent
b451f09cb9
commit
9f23e13e2d
11 changed files with 134 additions and 17 deletions
|
@ -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 />}
|
||||
|
|
|
@ -11,5 +11,6 @@ export const useSieMethods = () => {
|
|||
signUpSettings: { password, verify },
|
||||
signInMethods: experienceSettings?.signIn.methods ?? [],
|
||||
socialConnectors: experienceSettings?.socialConnectors ?? [],
|
||||
signInMode: experienceSettings?.signInMode,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}>
|
||||
|
|
Loading…
Add table
Reference in a new issue