0
Fork 0
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:
simeng-li 2023-11-10 14:41:59 +08:00 committed by GitHub
parent c5809e4722
commit d07a673a99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 429 additions and 32 deletions

View file

@ -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,
},

View file

@ -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,
};

View file

@ -14,6 +14,7 @@ export const sessionNotFoundPath = '/unknown-session';
export const guardedPath = [
'/sign-in',
'/register',
'/single-sign-on',
'/social/register',
'/reset-password',
'/forgot-password',

View file

@ -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();
}

View file

@ -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>

View file

@ -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;

View file

@ -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,
});

View file

@ -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;

View file

@ -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,

View 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[]>();

View file

@ -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';

View file

@ -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);

View file

@ -10,8 +10,8 @@
margin-bottom: _.unit(4);
}
.createAccount {
margin-top: _.unit(2);
.createAccount,
.singleSignOn {
text-align: center;
margin-bottom: _.unit(4);
}

View file

@ -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();
});
});

View file

@ -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 && (

View file

@ -12,8 +12,9 @@
font: var(--font-body-3);
}
.createAccount {
margin-top: _.unit(2);
.createAccount,
.singleSignOn {
text-align: center;
margin-bottom: _.unit(4);
}

View file

@ -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();
});
});

View file

@ -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 && (

View file

@ -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);
}
}

View 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;

View file

@ -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;

View file

@ -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;

View file

@ -33,5 +33,5 @@ export const getSsoConnectorsByEmail = async (
email: data.email,
},
})
.json<Array<{ id: string; ssoOnly: boolean }>>();
.json<string[]>();
};

View file

@ -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);
}
});

View file

@ -72,6 +72,7 @@ describe('.well-known api', () => {
expect(newCreatedConnector).toMatchObject({
id,
connectorName,
ssoOnly: newOidcSsoConnectorPayload.ssoOnly,
logo: newOidcSsoConnectorPayload.branding.logo,
darkLogo: newOidcSsoConnectorPayload.branding.darkLogo,
});

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -28,6 +28,7 @@ const action = {
copy: 'コピー',
verify_via_passkey: 'パスキー経由で確認',
download: 'ダウンロード',
single_sign_on: 'シングルサインオン',
};
export default Object.freeze(action);

View file

@ -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);

View file

@ -19,6 +19,7 @@ const error = {
invalid_session: 'セッションが見つかりません。もう一度サインインしてください。',
timeout: 'リクエストタイムアウト。後でもう一度お試しください。',
password_rejected,
sso_not_enabled: 'このメールアカウントではシングルサインオンが有効になっていません。',
};
export default Object.freeze(error);

View file

@ -28,6 +28,7 @@ const action = {
copy: '복사',
verify_via_passkey: '패스키로 확인',
download: '다운로드',
single_sign_on: '단일 로그인',
};
export default Object.freeze(action);

View file

@ -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);

View file

@ -18,6 +18,7 @@ const error = {
invalid_session: '세션을 찾을 수 없어요. 다시 로그인해 주세요.',
timeout: '요청 시간이 초과되었어요. 잠시 후에 다시 시도해 주세요.',
password_rejected,
sso_not_enabled: '이 이메일 계정에 대해 단일 로그인이 활성화되지 않았어요.',
};
export default Object.freeze(error);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -28,6 +28,7 @@ const action = {
copy: 'Копировать',
verify_via_passkey: 'Проверить с помощью ключа доступа',
download: 'Скачать',
single_sign_on: 'Единый вход',
};
export default Object.freeze(action);

View file

@ -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);

View file

@ -19,6 +19,7 @@ const error = {
invalid_session: 'Сессия не найдена. Пожалуйста, войдите снова.',
timeout: 'Время ожидания истекло. Пожалуйста, повторите попытку позднее.',
password_rejected,
sso_not_enabled: 'Односторонняя авторизация не включена для этого аккаунта электронной почты.',
};
export default Object.freeze(error);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -28,6 +28,7 @@ const action = {
copy: '复制',
verify_via_passkey: '通过 Passkey 验证',
download: '下载',
single_sign_on: '单点登录',
};
export default Object.freeze(action);

View file

@ -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);

View file

@ -18,6 +18,7 @@ const error = {
invalid_session: '未找到会话,请返回并重新登录。',
timeout: '请求超时,请稍后重试。',
password_rejected,
sso_not_enabled: '此邮箱账户未启用单点登录。',
};
export default Object.freeze(error);

View file

@ -28,6 +28,7 @@ const action = {
copy: '複製',
verify_via_passkey: '透過 Passkey 驗證',
download: '下載',
single_sign_on: '單點登錄',
};
export default Object.freeze(action);

View file

@ -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);

View file

@ -18,6 +18,7 @@ const error = {
invalid_session: '未找到會話,請返回並重新登錄。',
timeout: '請求超時,請稍後重試。',
password_rejected,
sso_not_enabled: '此電子郵件帳戶未啟用單一登錄。',
};
export default Object.freeze(error);

View file

@ -28,6 +28,7 @@ const action = {
copy: '複製',
verify_via_passkey: '透過 Passkey 驗證',
download: '下載',
single_sign_on: '單點登錄',
};
export default Object.freeze(action);

View file

@ -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);

View file

@ -18,6 +18,7 @@ const error = {
invalid_session: '未找到會話,請返回並重新登錄。',
timeout: '請求超時,請稍後重試。',
password_rejected,
sso_not_enabled: '此郵箱帳戶未啟用單一登錄。',
};
export default Object.freeze(error);

View file

@ -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(),
});