mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
feat(core,experience): add sso link and email form (#4850)
* feat(core,experience): add sso link and email form add sso link and email form * chore(phrases): update phrases phdate phrases * fix(core): fix ut fix ut * test(experience): add single sign on link test add single sign on link test Please enter the commit message for your changes. Lines starting
This commit is contained in:
parent
c5809e4722
commit
d07a673a99
71 changed files with 429 additions and 32 deletions
|
@ -160,6 +160,7 @@ describe('getFullSignInExperience()', () => {
|
|||
{
|
||||
id: wellConfiguredSsoConnector.id,
|
||||
connectorName: wellConfiguredSsoConnector.connectorName,
|
||||
ssoOnly: wellConfiguredSsoConnector.ssoOnly,
|
||||
logo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logo,
|
||||
darkLogo: undefined,
|
||||
},
|
||||
|
@ -184,6 +185,7 @@ describe('get sso connectors', () => {
|
|||
{
|
||||
id: wellConfiguredSsoConnector.id,
|
||||
connectorName: wellConfiguredSsoConnector.connectorName,
|
||||
ssoOnly: wellConfiguredSsoConnector.ssoOnly,
|
||||
logo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logo,
|
||||
darkLogo: undefined,
|
||||
},
|
||||
|
|
|
@ -72,12 +72,13 @@ export const createSignInExperienceLibrary = (
|
|||
const ssoConnectors = await getAvailableSsoConnectors();
|
||||
|
||||
return ssoConnectors.map(
|
||||
({ providerName, connectorName, id, branding }): SsoConnectorMetadata => {
|
||||
({ providerName, connectorName, id, branding, ssoOnly }): SsoConnectorMetadata => {
|
||||
const factory = ssoConnectorFactories[providerName];
|
||||
|
||||
return {
|
||||
id,
|
||||
connectorName,
|
||||
ssoOnly,
|
||||
logo: branding.logo ?? factory.logo,
|
||||
darkLogo: branding.darkLogo,
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ export const sessionNotFoundPath = '/unknown-session';
|
|||
export const guardedPath = [
|
||||
'/sign-in',
|
||||
'/register',
|
||||
'/single-sign-on',
|
||||
'/social/register',
|
||||
'/reset-password',
|
||||
'/forgot-password',
|
||||
|
|
|
@ -124,12 +124,7 @@ export default function singleSignOnRoutes<T extends IRouterParamContext>(
|
|||
email: z.string().email(),
|
||||
}),
|
||||
status: [200, 400],
|
||||
response: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
ssoOnly: z.boolean(),
|
||||
})
|
||||
),
|
||||
response: z.string().array(),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { email } = ctx.guard.query;
|
||||
|
@ -141,7 +136,7 @@ export default function singleSignOnRoutes<T extends IRouterParamContext>(
|
|||
|
||||
const availableConnectors = connectors.filter(({ domains }) => domains.includes(domain));
|
||||
|
||||
ctx.body = availableConnectors.map(({ id, ssoOnly }) => ({ id, ssoOnly }));
|
||||
ctx.body = availableConnectors.map(({ id }) => id);
|
||||
|
||||
return next();
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@ import AppBoundary from './Providers/AppBoundary';
|
|||
import LoadingLayerProvider from './Providers/LoadingLayerProvider';
|
||||
import PageContextProvider from './Providers/PageContextProvider';
|
||||
import SettingsProvider from './Providers/SettingsProvider';
|
||||
import { isDevFeaturesEnabled as isDevelopmentFeaturesEnabled } from './constants/env';
|
||||
import SingleSignOnContextProvider from './Providers/SingleSignOnContextProvider';
|
||||
import {
|
||||
isDevFeaturesEnabled as isDevelopmentFeaturesEnabled,
|
||||
singleSignOnPath,
|
||||
} from './constants/env';
|
||||
import Callback from './pages/Callback';
|
||||
import Consent from './pages/Consent';
|
||||
import Continue from './pages/Continue';
|
||||
|
@ -26,6 +30,7 @@ import RegisterPassword from './pages/RegisterPassword';
|
|||
import ResetPassword from './pages/ResetPassword';
|
||||
import SignIn from './pages/SignIn';
|
||||
import SignInPassword from './pages/SignInPassword';
|
||||
import SingleSignOnEmail from './pages/SingleSignOnEmail';
|
||||
import SocialLanding from './pages/SocialLanding';
|
||||
import SocialLinkAccount from './pages/SocialLinkAccount';
|
||||
import SocialSignIn from './pages/SocialSignInCallback';
|
||||
|
@ -110,6 +115,13 @@ const App = () => {
|
|||
<Route path="callback/:connectorId" element={<Callback />} />
|
||||
</Route>
|
||||
|
||||
{/* Single sign on */}
|
||||
{isDevelopmentFeaturesEnabled && (
|
||||
<Route path={singleSignOnPath} element={<SingleSignOnContextProvider />}>
|
||||
<Route path="email" element={<SingleSignOnEmail />} />
|
||||
</Route>
|
||||
)}
|
||||
|
||||
<Route path="*" element={<ErrorPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useSignInExperience } from '@/hooks/use-sie';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CustomContent = ({ className }: Props) => {
|
||||
const signInExperience = useSignInExperience();
|
||||
const { customContent } = useSieMethods();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const customHtml = signInExperience?.customContent[pathname];
|
||||
const customHtml = customContent?.[pathname];
|
||||
|
||||
if (!customHtml) {
|
||||
return null;
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { type SsoConnectorMetadata } from '@logto/schemas';
|
||||
import { noop } from '@silverhand/essentials';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type SingleSignOnContextType = {
|
||||
// All the enabled sso connectors
|
||||
availableSsoConnectorsMap: Map<string, SsoConnectorMetadata>;
|
||||
email?: string;
|
||||
setEmail: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
// The sso connectors that are enabled for the current domain
|
||||
ssoConnectors: SsoConnectorMetadata[];
|
||||
setSsoConnectors: React.Dispatch<React.SetStateAction<SsoConnectorMetadata[]>>;
|
||||
};
|
||||
|
||||
export default createContext<SingleSignOnContextType>({
|
||||
email: undefined,
|
||||
availableSsoConnectorsMap: new Map(),
|
||||
ssoConnectors: [],
|
||||
setEmail: noop,
|
||||
setSsoConnectors: noop,
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { type SsoConnectorMetadata } from '@logto/schemas';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
|
||||
import SingleSignOnContext, { type SingleSignOnContextType } from './SingleSignOnContext';
|
||||
|
||||
const SingleSignOnContextProvider = () => {
|
||||
const { ssoConnectors } = useSieMethods();
|
||||
const [email, setEmail] = useState<string | undefined>();
|
||||
const [domainFilteredConnectors, setDomainFilteredConnectors] = useState<SsoConnectorMetadata[]>(
|
||||
[]
|
||||
);
|
||||
const ssoConnectorsMap = useMemo(
|
||||
() => new Map(ssoConnectors.map((connector) => [connector.id, connector])),
|
||||
[ssoConnectors]
|
||||
);
|
||||
|
||||
const singleSignOnContext = useMemo<SingleSignOnContextType>(
|
||||
() => ({
|
||||
email,
|
||||
setEmail,
|
||||
availableSsoConnectorsMap: ssoConnectorsMap,
|
||||
ssoConnectors: domainFilteredConnectors,
|
||||
setSsoConnectors: setDomainFilteredConnectors,
|
||||
}),
|
||||
[domainFilteredConnectors, email, ssoConnectorsMap]
|
||||
);
|
||||
|
||||
return (
|
||||
<SingleSignOnContext.Provider value={singleSignOnContext}>
|
||||
<Outlet />
|
||||
</SingleSignOnContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleSignOnContextProvider;
|
|
@ -1,4 +1,4 @@
|
|||
import type { SignInExperience, SignIn } from '@logto/schemas';
|
||||
import type { SignInExperience, SignIn, SsoConnectorMetadata } from '@logto/schemas';
|
||||
import {
|
||||
ConnectorPlatform,
|
||||
ConnectorType,
|
||||
|
@ -41,6 +41,15 @@ export const mockSocialConnectorData = {
|
|||
configTemplate: '',
|
||||
};
|
||||
|
||||
export const mockSsoConnectors: SsoConnectorMetadata[] = [
|
||||
{
|
||||
id: 'arbitrary-sso-connector',
|
||||
connectorName: 'AzureAD',
|
||||
ssoOnly: true,
|
||||
logo: 'http://logto.dev/logto.png',
|
||||
},
|
||||
];
|
||||
|
||||
export const emailSignInMethod = {
|
||||
identifier: SignInIdentifier.Email,
|
||||
password: true,
|
||||
|
@ -113,6 +122,7 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = {
|
|||
verify: true,
|
||||
},
|
||||
socialConnectors,
|
||||
ssoConnectors: [],
|
||||
signInMode: SignInMode.SignInAndRegister,
|
||||
forgotPassword: {
|
||||
email: true,
|
||||
|
|
12
packages/experience/src/apis/single-sign-on.ts
Normal file
12
packages/experience/src/apis/single-sign-on.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import api from './api';
|
||||
|
||||
const ssoPrefix = '/api/interaction/single-sign-on';
|
||||
|
||||
export const getSingleSignOnConnectors = async (email: string) =>
|
||||
api
|
||||
.get(`${ssoPrefix}/connectors`, {
|
||||
searchParams: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
.json<string[]>();
|
|
@ -5,3 +5,5 @@ export const isDevFeaturesEnabled =
|
|||
process.env.NODE_ENV !== 'production' ||
|
||||
yes(process.env.DEV_FEATURES_ENABLED) ||
|
||||
yes(process.env.INTEGRATION_TEST);
|
||||
|
||||
export const singleSignOnPath = 'single-sign-on';
|
||||
|
|
|
@ -20,17 +20,13 @@ export const useSieMethods = () => {
|
|||
({ password, verificationCode }) => password || verificationCode
|
||||
) ?? [],
|
||||
socialConnectors: experienceSettings?.socialConnectors ?? [],
|
||||
ssoConnectors: experienceSettings?.ssoConnectors ?? [],
|
||||
signInMode: experienceSettings?.signInMode,
|
||||
forgotPassword: experienceSettings?.forgotPassword,
|
||||
customContent: experienceSettings?.customContent,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSignInExperience = () => {
|
||||
const { experienceSettings } = useContext(PageContext);
|
||||
|
||||
return experienceSettings;
|
||||
};
|
||||
|
||||
export const usePasswordPolicy = () => {
|
||||
const { t } = useTranslation();
|
||||
const { experienceSettings } = useContext(PageContext);
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
.createAccount {
|
||||
margin-top: _.unit(2);
|
||||
.createAccount,
|
||||
.singleSignOn {
|
||||
text-align: center;
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Route, Routes } from 'react-router-dom';
|
|||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { mockSignInExperienceSettings } from '@/__mocks__/logto';
|
||||
import { mockSignInExperienceSettings, mockSsoConnectors } from '@/__mocks__/logto';
|
||||
import Register from '@/pages/Register';
|
||||
import type { SignInExperienceResponse } from '@/types';
|
||||
|
||||
|
@ -73,4 +73,12 @@ describe('<Register />', () => {
|
|||
);
|
||||
expect(queryByText('sign-in')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('render single sign on link', () => {
|
||||
const { queryByText } = renderRegisterPage({
|
||||
ssoConnectors: mockSsoConnectors,
|
||||
});
|
||||
|
||||
expect(queryByText('action.single_sign_on')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Navigate } from 'react-router-dom';
|
|||
import LandingPageLayout from '@/Layout/LandingPageLayout';
|
||||
import Divider from '@/components/Divider';
|
||||
import TextLink from '@/components/TextLink';
|
||||
import { isDevFeaturesEnabled } from '@/constants/env';
|
||||
import SocialSignInList from '@/containers/SocialSignInList';
|
||||
import TermsAndPrivacy from '@/containers/TermsAndPrivacy';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
|
@ -15,7 +16,8 @@ import IdentifierRegisterForm from './IdentifierRegisterForm';
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
const Register = () => {
|
||||
const { signUpMethods, socialConnectors, signInMode, signInMethods } = useSieMethods();
|
||||
const { signUpMethods, socialConnectors, signInMode, signInMethods, ssoConnectors } =
|
||||
useSieMethods();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!signInMode) {
|
||||
|
@ -37,6 +39,15 @@ const Register = () => {
|
|||
<SocialSignInList className={styles.main} socialConnectors={socialConnectors} />
|
||||
</>
|
||||
)}
|
||||
{
|
||||
// Single Sign On footer TODO: remove the dev feature check once SSO is ready
|
||||
isDevFeaturesEnabled && ssoConnectors.length > 0 && (
|
||||
<div className={styles.singleSignOn}>
|
||||
{t('description.use')}{' '}
|
||||
<TextLink to="/single-sign-on/email" text="action.single_sign_on" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
// SignIn footer
|
||||
signInMode === SignInMode.SignInAndRegister && signInMethods.length > 0 && (
|
||||
|
|
|
@ -12,8 +12,9 @@
|
|||
font: var(--font-body-3);
|
||||
}
|
||||
|
||||
.createAccount {
|
||||
margin-top: _.unit(2);
|
||||
|
||||
.createAccount,
|
||||
.singleSignOn {
|
||||
text-align: center;
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,11 @@ import { Route, Routes } from 'react-router-dom';
|
|||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { mockSignInExperienceSettings, mockSignInMethodSettingsTestCases } from '@/__mocks__/logto';
|
||||
import {
|
||||
mockSignInExperienceSettings,
|
||||
mockSignInMethodSettingsTestCases,
|
||||
mockSsoConnectors,
|
||||
} from '@/__mocks__/logto';
|
||||
import SignIn from '@/pages/SignIn';
|
||||
|
||||
jest.mock('i18next', () => ({
|
||||
|
@ -108,4 +112,12 @@ describe('<SignIn />', () => {
|
|||
|
||||
expect(queryByText('Register')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('render single sign on link', () => {
|
||||
const { queryByText } = renderSignIn({
|
||||
ssoConnectors: mockSsoConnectors,
|
||||
});
|
||||
|
||||
expect(queryByText('action.single_sign_on')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Navigate } from 'react-router-dom';
|
|||
import LandingPageLayout from '@/Layout/LandingPageLayout';
|
||||
import Divider from '@/components/Divider';
|
||||
import TextLink from '@/components/TextLink';
|
||||
import { isDevFeaturesEnabled } from '@/constants/env';
|
||||
import SocialSignInList from '@/containers/SocialSignInList';
|
||||
import TermsAndPrivacyLinks from '@/containers/TermsAndPrivacyLinks';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
|
@ -15,7 +16,8 @@ import Main from './Main';
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
const SignIn = () => {
|
||||
const { signInMethods, signUpMethods, socialConnectors, signInMode } = useSieMethods();
|
||||
const { signInMethods, signUpMethods, socialConnectors, signInMode, ssoConnectors } =
|
||||
useSieMethods();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!signInMode) {
|
||||
|
@ -29,6 +31,15 @@ const SignIn = () => {
|
|||
return (
|
||||
<LandingPageLayout title="description.sign_in_to_your_account">
|
||||
<Main signInMethods={signInMethods} socialConnectors={socialConnectors} />
|
||||
{
|
||||
// Single Sign On footer TODO: remove the dev feature check once SSO is ready
|
||||
isDevFeaturesEnabled && ssoConnectors.length > 0 && (
|
||||
<div className={styles.singleSignOn}>
|
||||
{t('description.use')}{' '}
|
||||
<TextLink to="/single-sign-on/email" text="action.single_sign_on" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
// Create Account footer
|
||||
signInMode === SignInMode.SignInAndRegister && signUpMethods.length > 0 && (
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.form {
|
||||
@include _.flex-column;
|
||||
|
||||
> * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputField,
|
||||
.formErrors {
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
.formErrors {
|
||||
margin-left: _.unit(0.5);
|
||||
margin-top: _.unit(-3);
|
||||
}
|
||||
}
|
89
packages/experience/src/pages/SingleSignOnEmail/index.tsx
Normal file
89
packages/experience/src/pages/SingleSignOnEmail/index.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
|
||||
import Button from '@/components/Button';
|
||||
import ErrorMessage from '@/components/ErrorMessage';
|
||||
import SmartInputField, {
|
||||
type IdentifierInputValue,
|
||||
} from '@/components/InputFields/SmartInputField';
|
||||
import { getGeneralIdentifierErrorMessage, validateIdentifierField } from '@/utils/form';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
import useOnSubmit from './use-on-submit';
|
||||
|
||||
type FormState = {
|
||||
identifier: IdentifierInputValue;
|
||||
};
|
||||
|
||||
const SingleSignOnEmail = () => {
|
||||
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors, isValid },
|
||||
} = useForm<FormState>({
|
||||
reValidateMode: 'onBlur',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isValid) {
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [clearErrorMessage, isValid]);
|
||||
|
||||
const onSubmitHandler = useCallback(
|
||||
async (event?: React.FormEvent<HTMLFormElement>) => {
|
||||
clearErrorMessage();
|
||||
await handleSubmit(async ({ identifier: { value } }) => onSubmit(value))(event);
|
||||
},
|
||||
[clearErrorMessage, handleSubmit, onSubmit]
|
||||
);
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout
|
||||
title="action.single_sign_on"
|
||||
description="description.single_sign_on_email_form"
|
||||
>
|
||||
<form className={styles.form} onSubmit={onSubmitHandler}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="identifier"
|
||||
rules={{
|
||||
validate: ({ value }) => {
|
||||
if (!value) {
|
||||
return getGeneralIdentifierErrorMessage([SignInIdentifier.Email], 'required');
|
||||
}
|
||||
|
||||
const errorMessage = validateIdentifierField(SignInIdentifier.Email, value);
|
||||
|
||||
return errorMessage
|
||||
? getGeneralIdentifierErrorMessage([SignInIdentifier.Email], 'invalid')
|
||||
: true;
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<SmartInputField
|
||||
autoFocus
|
||||
className={styles.inputField}
|
||||
{...field}
|
||||
isDanger={!!errors.identifier}
|
||||
errorMessage={errors.identifier?.message}
|
||||
enabledTypes={[SignInIdentifier.Email]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
|
||||
|
||||
<Button title="action.single_sign_on" htmlType="submit" />
|
||||
|
||||
<input hidden type="submit" />
|
||||
</form>
|
||||
</SecondaryPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleSignOnEmail;
|
|
@ -0,0 +1,62 @@
|
|||
import { type SsoConnectorMetadata } from '@logto/schemas';
|
||||
import { useCallback, useState, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import SingleSignOnContext from '@/Providers/SingleSignOnContextProvider/SingleSignOnContext';
|
||||
import { getSingleSignOnConnectors } from '@/apis/single-sign-on';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useErrorHandler from '@/hooks/use-error-handler';
|
||||
|
||||
const useOnSubmit = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const request = useApi(getSingleSignOnConnectors);
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||
const { setEmail, setSsoConnectors, availableSsoConnectorsMap } = useContext(SingleSignOnContext);
|
||||
|
||||
const handleError = useErrorHandler();
|
||||
const clearErrorMessage = useCallback(() => {
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
setErrorMessage(undefined);
|
||||
}, [setErrorMessage]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (email: string) => {
|
||||
const [error, result] = await request(email);
|
||||
|
||||
if (error) {
|
||||
await handleError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const connectors = result
|
||||
?.map((connectorId) =>
|
||||
availableSsoConnectorsMap.has(connectorId)
|
||||
? availableSsoConnectorsMap.get(connectorId)
|
||||
: undefined
|
||||
)
|
||||
// eslint-disable-next-line unicorn/prefer-native-coercion-functions -- make the type more specific
|
||||
.filter((connector): connector is SsoConnectorMetadata => Boolean(connector));
|
||||
|
||||
if (!connectors || connectors.length === 0) {
|
||||
setErrorMessage(t('error.sso_not_enabled'));
|
||||
return;
|
||||
}
|
||||
|
||||
setSsoConnectors(connectors);
|
||||
setEmail(email);
|
||||
|
||||
navigate('/connectors');
|
||||
},
|
||||
[availableSsoConnectorsMap, handleError, navigate, request, setEmail, setSsoConnectors, t]
|
||||
);
|
||||
|
||||
return {
|
||||
onSubmit,
|
||||
errorMessage,
|
||||
clearErrorMessage,
|
||||
};
|
||||
};
|
||||
|
||||
export default useOnSubmit;
|
|
@ -5,6 +5,7 @@ import type {
|
|||
Theme,
|
||||
WebAuthnRegistrationOptions,
|
||||
WebAuthnAuthenticationOptions,
|
||||
SsoConnectorMetadata,
|
||||
} from '@logto/schemas';
|
||||
|
||||
export enum UserFlow {
|
||||
|
@ -32,6 +33,7 @@ export type VerificationCodeIdentifier = SignInIdentifier.Email | SignInIdentifi
|
|||
// Omit socialSignInConnectorTargets since it is being translated into socialConnectors
|
||||
export type SignInExperienceResponse = Omit<SignInExperience, 'socialSignInConnectorTargets'> & {
|
||||
socialConnectors: ConnectorMetadata[];
|
||||
ssoConnectors: SsoConnectorMetadata[];
|
||||
notification?: string;
|
||||
forgotPassword: {
|
||||
phone: boolean;
|
||||
|
|
|
@ -33,5 +33,5 @@ export const getSsoConnectorsByEmail = async (
|
|||
email: data.email,
|
||||
},
|
||||
})
|
||||
.json<Array<{ id: string; ssoOnly: boolean }>>();
|
||||
.json<string[]>();
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@ import { ProviderName, logtoUrl } from '#src/constants.js';
|
|||
import { initClient } from '#src/helpers/client.js';
|
||||
|
||||
describe('Single Sign On Happy Path', () => {
|
||||
const connectorIdMap = new Map<string, SsoConnectorMetadata & { ssoOnly: boolean }>();
|
||||
const connectorIdMap = new Map<string, SsoConnectorMetadata>();
|
||||
|
||||
const state = 'foo_state';
|
||||
const redirectUri = 'http://foo.dev/callback';
|
||||
|
@ -59,9 +59,8 @@ describe('Single Sign On Happy Path', () => {
|
|||
|
||||
expect(response.length).toBeGreaterThan(0);
|
||||
|
||||
for (const connector of response) {
|
||||
expect(connectorIdMap.has(connector.id)).toBe(true);
|
||||
expect(connector.ssoOnly).toEqual(connectorIdMap.get(connector.id)!.ssoOnly);
|
||||
for (const connectorId of response) {
|
||||
expect(connectorIdMap.has(connectorId)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ describe('.well-known api', () => {
|
|||
expect(newCreatedConnector).toMatchObject({
|
||||
id,
|
||||
connectorName,
|
||||
ssoOnly: newOidcSsoConnectorPayload.ssoOnly,
|
||||
logo: newOidcSsoConnectorPayload.branding.logo,
|
||||
darkLogo: newOidcSsoConnectorPayload.branding.darkLogo,
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Kopieren',
|
||||
verify_via_passkey: 'Überprüfen über Passkey',
|
||||
download: 'Herunterladen',
|
||||
single_sign_on: 'Single Sign-On',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -71,6 +71,10 @@ const description = {
|
|||
character_types_other:
|
||||
'sollte mindestens {{count}} Kategorien der folgenden Zeichenarten enthalten: Großbuchstaben, Kleinbuchstaben, Zahlen und Symbole',
|
||||
},
|
||||
use: 'Verwenden',
|
||||
single_sign_on_email_form: 'Gib deine Unternehmens-E-Mail-Adresse ein.',
|
||||
single_sign_on_connectors_list:
|
||||
'Ihr Unternehmen hat Single Sign-On für das E-Mail-Konto {{email}} aktiviert. Sie können sich weiterhin mit den folgenden SSO-Anbietern anmelden.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -18,6 +18,7 @@ const error = {
|
|||
invalid_session: 'Die Sitzung ist ungültig. Bitte melde dich erneut an.',
|
||||
timeout: 'Zeitüberschreitung. Bitte melde dich erneut an.',
|
||||
password_rejected,
|
||||
sso_not_enabled: 'Single Sign-On ist für dieses E-Mail-Konto nicht aktiviert.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Copy',
|
||||
verify_via_passkey: 'Verify via passkey',
|
||||
download: 'Download',
|
||||
single_sign_on: 'Single Sign-On',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -68,6 +68,10 @@ const description = {
|
|||
character_types_other:
|
||||
'should contain at least {{count}} types of uppercase letters, lowercase letters, digits, and symbols',
|
||||
},
|
||||
use: 'Use',
|
||||
single_sign_on_email_form: 'Enter your enterprise email address',
|
||||
single_sign_on_connectors_list:
|
||||
'Your enterprise has enabled Single Sign-On for the email account {{email}}. You can continue to sign in with the following SSO providers.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -18,6 +18,7 @@ const error = {
|
|||
invalid_session: 'Session not found. Please go back and sign in again.',
|
||||
timeout: 'Request timeout. Please try again later.',
|
||||
password_rejected,
|
||||
sso_not_enabled: 'Single Sign-On is not enabled for this email account.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Copiar',
|
||||
verify_via_passkey: 'Verificar mediante clave de acceso',
|
||||
download: 'Descargar',
|
||||
single_sign_on: 'Inicio de sesión único',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -69,6 +69,10 @@ const description = {
|
|||
character_types_other:
|
||||
'debe contener al menos {{count}} tipos de letras mayúsculas, letras minúsculas, dígitos y símbolos',
|
||||
},
|
||||
use: 'Usar',
|
||||
single_sign_on_email_form: 'Ingrese su dirección de correo electrónico corporativo',
|
||||
single_sign_on_connectors_list:
|
||||
'Su empresa ha habilitado el inicio de sesión único (Single Sign-On) para la cuenta de correo electrónico {{email}}. Puede continuar iniciando sesión con los siguientes proveedores de SSO.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -19,6 +19,8 @@ const error = {
|
|||
invalid_session: 'No se encontró la sesión. Por favor regrese e inicie sesión nuevamente.',
|
||||
timeout: 'Tiempo de espera de solicitud agotado. Por favor intente de nuevo más tarde.',
|
||||
password_rejected,
|
||||
sso_not_enabled:
|
||||
'El inicio de sesión único no está habilitado para esta cuenta de correo electrónico.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Copier',
|
||||
verify_via_passkey: "Vérifier via la clé d'accès",
|
||||
download: 'Télécharger',
|
||||
single_sign_on: 'Connexion unique',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -71,6 +71,10 @@ const description = {
|
|||
character_types_other:
|
||||
'doit contenir au moins {{count}} types de lettres majuscules, lettres minuscules, chiffres et symboles',
|
||||
},
|
||||
use: 'Utiliser',
|
||||
single_sign_on_email_form: "Entrez votre adresse e-mail d'entreprise",
|
||||
single_sign_on_connectors_list:
|
||||
'Votre entreprise a activé la connexion unique (Single Sign-On) pour le compte email {{email}}. Vous pouvez continuer à vous connecter avec les fournisseurs SSO suivants.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -20,6 +20,7 @@ const error = {
|
|||
invalid_session: 'Session non trouvée. Veuillez revenir en arrière et vous connecter à nouveau.',
|
||||
timeout: "Délai d'attente de la requête dépassé. Veuillez réessayer plus tard.",
|
||||
password_rejected,
|
||||
sso_not_enabled: "La authentification unique n'est pas activée pour ce compte de messagerie.",
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Copia',
|
||||
verify_via_passkey: 'Verifica tramite passkey',
|
||||
download: 'Scarica',
|
||||
single_sign_on: 'Single Sign-On',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -67,6 +67,10 @@ const description = {
|
|||
character_types_other:
|
||||
'dovrebbe contenere almeno {{count}} tipi di lettere maiuscole, lettere minuscole, numeri e simboli',
|
||||
},
|
||||
use: 'Utilizzare',
|
||||
single_sign_on_email_form: 'Inserisci il tuo indirizzo email aziendale',
|
||||
single_sign_on_connectors_list:
|
||||
"La tua azienda ha abilitato il Single Sign-On per l'account email {{email}}. Puoi continuare ad accedere con i seguenti fornitori di SSO.",
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -18,6 +18,7 @@ const error = {
|
|||
invalid_session: 'Sessione non trovata. Si prega di tornare indietro e accedere di nuovo.',
|
||||
timeout: 'Timeout della richiesta. Si prega di riprovare più tardi.',
|
||||
password_rejected,
|
||||
sso_not_enabled: 'Single sign-on non abilitato per questo account email.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'コピー',
|
||||
verify_via_passkey: 'パスキー経由で確認',
|
||||
download: 'ダウンロード',
|
||||
single_sign_on: 'シングルサインオン',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -67,6 +67,10 @@ const description = {
|
|||
character_types_one: '大文字、小文字、数字、記号のうち {{count}} 種類を含む必要があります',
|
||||
character_types_other: '大文字、小文字、数字、記号のうち {{count}} 種類を含む必要があります',
|
||||
},
|
||||
use: '使用する',
|
||||
single_sign_on_email_form: '企業のメールアドレスを入力してください',
|
||||
single_sign_on_connectors_list:
|
||||
'あなたの企業は、メールアカウント{{email}}に対してシングルサインオンを有効にしました。以下のSSOプロバイダーを使用してサインインを続けることができます。',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -19,6 +19,7 @@ const error = {
|
|||
invalid_session: 'セッションが見つかりません。もう一度サインインしてください。',
|
||||
timeout: 'リクエストタイムアウト。後でもう一度お試しください。',
|
||||
password_rejected,
|
||||
sso_not_enabled: 'このメールアカウントではシングルサインオンが有効になっていません。',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: '복사',
|
||||
verify_via_passkey: '패스키로 확인',
|
||||
download: '다운로드',
|
||||
single_sign_on: '단일 로그인',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -62,6 +62,10 @@ const description = {
|
|||
character_types_one: '최소 {{count}} 개의 대문자, 소문자, 숫자, 특수 기호를 포함해야 함',
|
||||
character_types_other: '최소 {{count}} 개의 대문자, 소문자, 숫자, 특수 기호를 포함해야 함',
|
||||
},
|
||||
use: '사용',
|
||||
single_sign_on_email_form: '기업 이메일 주소를 입력하세요',
|
||||
single_sign_on_connectors_list:
|
||||
'귀하의 기업은 {{email}} 이메일 계정에 대해 Single Sign-On을 활성화했습니다. 다음 SSO 제공업체를 사용하여 로그인을 계속할 수 있습니다.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -18,6 +18,7 @@ const error = {
|
|||
invalid_session: '세션을 찾을 수 없어요. 다시 로그인해 주세요.',
|
||||
timeout: '요청 시간이 초과되었어요. 잠시 후에 다시 시도해 주세요.',
|
||||
password_rejected,
|
||||
sso_not_enabled: '이 이메일 계정에 대해 단일 로그인이 활성화되지 않았어요.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Kopiuj',
|
||||
verify_via_passkey: 'Weryfikacja za pomocą klucza dostępu',
|
||||
download: 'Pobierz',
|
||||
single_sign_on: 'Pojedyncze logowanie',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -67,6 +67,10 @@ const description = {
|
|||
character_types_other:
|
||||
'powinno zawierać co najmniej {{count}} rodzaje liter wielkich, małych liter, cyfr i symboli',
|
||||
},
|
||||
use: 'Użyj',
|
||||
single_sign_on_email_form: 'Wpisz swój służbowy adres email',
|
||||
single_sign_on_connectors_list:
|
||||
'Twoja firma włączyła jednokrotne logowanie dla konta e-mail {{email}}. Możesz kontynuować logowanie za pomocą następujących dostawców SSO.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -19,6 +19,7 @@ const error = {
|
|||
invalid_session: 'Sesja nie znaleziona. Proszę wróć i zaloguj się ponownie.',
|
||||
timeout: 'Czas żądania upłynął. Proszę spróbuj ponownie później.',
|
||||
password_rejected,
|
||||
sso_not_enabled: 'Pojedyncze logowanie nie jest włączony dla tego konta e-mail.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Copiar',
|
||||
verify_via_passkey: 'Verificar via chave de acesso',
|
||||
download: 'Baixar',
|
||||
single_sign_on: 'Single Sign-On',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -66,6 +66,10 @@ const description = {
|
|||
character_types_other:
|
||||
'deve conter pelo menos {{count}} tipos de letra maiúscula, letra minúscula, dígito e símbolo.',
|
||||
},
|
||||
use: 'Usar',
|
||||
single_sign_on_email_form: 'Insira o endereço de e-mail corporativo',
|
||||
single_sign_on_connectors_list:
|
||||
'Sua empresa ativou o Single Sign-On para a conta de email {{email}}. Você pode continuar a fazer login com os seguintes provedores de SSO.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -18,6 +18,7 @@ const error = {
|
|||
invalid_session: 'Sessão não encontrada. Volte e faça login novamente.',
|
||||
timeout: 'Tempo limite excedido. Por favor, tente novamente mais tarde.',
|
||||
password_rejected,
|
||||
sso_not_enabled: 'O Single Sign-On não está habilitado para esta conta de e-mail.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Copiar',
|
||||
verify_via_passkey: 'Verificar através de chave de acesso',
|
||||
download: 'Transferir',
|
||||
single_sign_on: 'Logon Único',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -66,6 +66,10 @@ const description = {
|
|||
character_types_other:
|
||||
'deve conter pelo menos {{count}} tipos de letras maiúsculas, letras minúsculas, dígitos e símbolos',
|
||||
},
|
||||
use: 'Usar',
|
||||
single_sign_on_email_form: 'Insira o endereço de email corporativo',
|
||||
single_sign_on_connectors_list:
|
||||
'A sua empresa ativou o Single Sign-On para a conta de email {{email}}. Pode continuar a iniciar sessão com os seguintes fornecedores de SSO.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -19,6 +19,7 @@ const error = {
|
|||
invalid_session: 'Sessão não encontrada. Volte e faça login novamente.',
|
||||
timeout: 'Tempo limite de sessão. Volte e faça login novamente.',
|
||||
password_rejected,
|
||||
sso_not_enabled: 'O Single Sign-On não está habilitado para esta conta de e-mail.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Копировать',
|
||||
verify_via_passkey: 'Проверить с помощью ключа доступа',
|
||||
download: 'Скачать',
|
||||
single_sign_on: 'Единый вход',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -70,6 +70,10 @@ const description = {
|
|||
character_types_other:
|
||||
'должен содержать по крайней мере {{count}} типа прописных букв, строчных букв, цифр и символов',
|
||||
},
|
||||
use: 'Использовать',
|
||||
single_sign_on_email_form: 'Введите корпоративный адрес электронной почты',
|
||||
single_sign_on_connectors_list:
|
||||
'Ваше предприятие включило функцию единого входа для электронной почты {{email}}. Вы можете продолжить вход в систему с помощью следующих провайдеров SSO.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -19,6 +19,7 @@ const error = {
|
|||
invalid_session: 'Сессия не найдена. Пожалуйста, войдите снова.',
|
||||
timeout: 'Время ожидания истекло. Пожалуйста, повторите попытку позднее.',
|
||||
password_rejected,
|
||||
sso_not_enabled: 'Односторонняя авторизация не включена для этого аккаунта электронной почты.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: 'Kopyala',
|
||||
verify_via_passkey: 'Parola ile doğrula',
|
||||
download: 'İndir',
|
||||
single_sign_on: 'Tek oturum açma',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -66,6 +66,10 @@ const description = {
|
|||
character_types_other:
|
||||
'en az {{count}} tane büyük harf, küçük harf, rakam ve sembol içermelidir',
|
||||
},
|
||||
use: 'Kullan',
|
||||
single_sign_on_email_form: 'Kurumsal e-posta adresinizi girin',
|
||||
single_sign_on_connectors_list:
|
||||
'Şirketiniz, {{email}} e-posta hesabı için Tekli Oturum Açmayı (Single Sign-On) etkinleştirdi. Aşağıdaki SSO sağlayıcıları ile oturum açmaya devam edebilirsiniz.',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -18,6 +18,7 @@ const error = {
|
|||
invalid_session: 'Oturum bulunamadı. Lütfen geri dönüp tekrar giriş yapınız.',
|
||||
timeout: 'Oturum zaman aşımına uğradı. Lütfen geri dönüp tekrar giriş yapınız.',
|
||||
password_rejected,
|
||||
sso_not_enabled: 'Bu e-posta hesabı için tek oturum açma etkin değil.',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: '复制',
|
||||
verify_via_passkey: '通过 Passkey 验证',
|
||||
download: '下载',
|
||||
single_sign_on: '单点登录',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -58,6 +58,10 @@ const description = {
|
|||
character_types_one: '应包含至少 {{count}} 种大写字母、小写字母、数字和符号',
|
||||
character_types_other: '应包含至少 {{count}} 种大写字母、小写字母、数字和符号',
|
||||
},
|
||||
use: '使用',
|
||||
single_sign_on_email_form: '输入你的企业电子邮件地址',
|
||||
single_sign_on_connectors_list:
|
||||
'你的企业已为电子邮件账户{{email}}启用了单点登录。你可以继续使用以下SSO提供商进行登录。',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -18,6 +18,7 @@ const error = {
|
|||
invalid_session: '未找到会话,请返回并重新登录。',
|
||||
timeout: '请求超时,请稍后重试。',
|
||||
password_rejected,
|
||||
sso_not_enabled: '此邮箱账户未启用单点登录。',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: '複製',
|
||||
verify_via_passkey: '透過 Passkey 驗證',
|
||||
download: '下載',
|
||||
single_sign_on: '單點登錄',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -58,6 +58,10 @@ const description = {
|
|||
character_types_one: '要求包含至少 {{count}} 類型的大寫字母,小寫字母,數字和符號',
|
||||
character_types_other: '要求包含至少 {{count}} 類型的大寫字母,小寫字母,數字和符號',
|
||||
},
|
||||
use: '使用',
|
||||
single_sign_on_email_form: '輸入你的企業電子郵件地址',
|
||||
single_sign_on_connectors_list:
|
||||
'您的企業已為電郵賬戶{{email}}啟用單一登入。您可以繼續使用以下的SSO供應商登入。',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -18,6 +18,7 @@ const error = {
|
|||
invalid_session: '未找到會話,請返回並重新登錄。',
|
||||
timeout: '請求超時,請稍後重試。',
|
||||
password_rejected,
|
||||
sso_not_enabled: '此電子郵件帳戶未啟用單一登錄。',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -28,6 +28,7 @@ const action = {
|
|||
copy: '複製',
|
||||
verify_via_passkey: '透過 Passkey 驗證',
|
||||
download: '下載',
|
||||
single_sign_on: '單點登錄',
|
||||
};
|
||||
|
||||
export default Object.freeze(action);
|
||||
|
|
|
@ -58,6 +58,10 @@ const description = {
|
|||
character_types_one: '需包含 {{count}} 類型的大寫字母、小寫字母、數字和符號',
|
||||
character_types_other: '需包含 {{count}} 類型的大寫字母、小寫字母、數字和符號',
|
||||
},
|
||||
use: '使用',
|
||||
single_sign_on_email_form: '輸入你的企業電子郵件地址',
|
||||
single_sign_on_connectors_list:
|
||||
'您的企業已為電子郵件帳戶{{email}}啟用單一登入。您可以繼續使用以下的SSO供應商登入。',
|
||||
};
|
||||
|
||||
export default Object.freeze(description);
|
||||
|
|
|
@ -18,6 +18,7 @@ const error = {
|
|||
invalid_session: '未找到會話,請返回並重新登錄。',
|
||||
timeout: '請求超時,請稍後重試。',
|
||||
password_rejected,
|
||||
sso_not_enabled: '此郵箱帳戶未啟用單一登錄。',
|
||||
};
|
||||
|
||||
export default Object.freeze(error);
|
||||
|
|
|
@ -7,6 +7,7 @@ export const ssoConnectorMetadataGuard = z.object({
|
|||
id: z.string(),
|
||||
connectorName: z.string(),
|
||||
logo: z.string(),
|
||||
ssoOnly: z.boolean(),
|
||||
darkLogo: z.string().optional(),
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue