Merge pull request #10681 from strapi/migration/login-view-ce

DS migration : Login view (CE & EE)
This commit is contained in:
ELABBASSI Hicham 2021-08-12 15:06:55 +02:00 committed by GitHub
commit 56135d69a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 446 additions and 462 deletions

View File

@ -11,7 +11,7 @@ import { basename, createHook } from './core/utils';
import configureStore from './core/store/configureStore';
import { Plugin } from './core/apis';
import App from './pages/App';
import AuthLogo from './assets/images/logo_strapi_auth.png';
import AuthLogo from './assets/images/logo_strapi_auth_v4.png';
import MenuLogo from './assets/images/strapi-img.png';
import Providers from './components/Providers';
import Theme from './components/Theme';

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import get from 'lodash/get';
import { useHistory } from 'react-router-dom';
import {
MainNav,
NavBrand,
@ -12,6 +13,7 @@ import {
NavUser,
NavCondense,
Divider,
Button,
} from '@strapi/parts';
import ContentIcon from '@strapi/icons/ContentIcon';
import { auth, usePersistentState } from '@strapi/helper-plugin';
@ -19,6 +21,7 @@ import useConfigurations from '../../hooks/useConfigurations';
const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
const { menuLogo } = useConfigurations();
const { push } = useHistory();
const [condensed, setCondensed] = usePersistentState('navbar-condensed', false);
const userInfo = auth.getUserInfo();
@ -63,6 +66,16 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
<FormattedMessage {...link.intlLabel} />
</NavLink>
))}
{/* This is temporary */}
<Button
type="button"
onClick={() => {
auth.clearAppStorage();
push('/auth/login');
}}
>
Logout
</Button>
</NavSection>
) : null}
</NavSections>

View File

@ -4,42 +4,22 @@
*
*/
import React, { useState } from 'react';
import React from 'react';
import { useIntl } from 'react-intl';
import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import { SimpleMenu, MenuItem } from '@strapi/parts';
import useLocalesProvider from '../LocalesProvider/useLocalesProvider';
import Wrapper from './Wrapper';
const LocaleToggle = () => {
const { changeLocale, localeNames } = useLocalesProvider();
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);
const { locale } = useIntl();
return (
<Wrapper>
<ButtonDropdown isOpen={isOpen} toggle={toggle}>
<DropdownToggle className="localeDropdownContent">
<span>{localeNames[locale]}</span>
</DropdownToggle>
<DropdownMenu className="localeDropdownMenu">
{Object.keys(localeNames).map(lang => {
return (
<DropdownItem
key={lang}
onClick={() => changeLocale(lang)}
className={`localeToggleItem ${locale === lang ? 'localeToggleItemActive' : ''}`}
>
{localeNames[lang]}
</DropdownItem>
);
})}
</DropdownMenu>
</ButtonDropdown>
</Wrapper>
<SimpleMenu label={localeNames[locale]}>
{Object.keys(localeNames).map(lang => (
<MenuItem onClick={() => changeLocale(lang)} key={lang}>{localeNames[lang]}</MenuItem>
))}
</SimpleMenu>
);
};
export default LocaleToggle;
export default LocaleToggle;

View File

@ -1,217 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import LanguageProvider from '../../LanguageProvider';
import en from '../../../translations/en.json';
import LocaleToggle from '../index';
const messages = { en };
const localeNames = { en: 'English' };
describe('<LocaleToggle />', () => {
it('should not crash', () => {
const App = (
<LanguageProvider messages={messages} localeNames={localeNames}>
<LocaleToggle />
</LanguageProvider>
);
const { container } = render(App);
expect(container.firstChild).toMatchInlineSnapshot(`
.c0 {
-webkit-font-smoothing: antialiased;
}
.c0 > div {
height: 6rem;
line-height: 5.8rem;
z-index: 999;
}
.c0 > div > button {
width: 100%;
padding: 0 30px;
background: transparent;
border: none;
border-radius: 0;
color: #333740;
font-weight: 500;
text-align: right;
cursor: pointer;
-webkit-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.c0 > div > button:hover,
.c0 > div > button:focus,
.c0 > div > button:active {
color: #333740;
background-color: #fafafb !important;
}
.c0 > div > button > i,
.c0 > div > button > svg {
margin-left: 10px;
-webkit-transition: -webkit-transform 0.3s ease-out;
-webkit-transition: transform 0.3s ease-out;
transition: transform 0.3s ease-out;
}
.c0 > div > button > i[alt='true'],
.c0 > div > button > svg[alt='true'] {
-webkit-transform: rotateX(180deg);
-ms-transform: rotateX(180deg);
transform: rotateX(180deg);
}
.c0 .localeDropdownContent {
-webkit-font-smoothing: antialiased;
}
.c0 .localeDropdownContent span {
color: #333740;
font-size: 13px;
font-family: Lato;
font-weight: 500;
-webkit-letter-spacing: 0.5;
-moz-letter-spacing: 0.5;
-ms-letter-spacing: 0.5;
letter-spacing: 0.5;
vertical-align: baseline;
}
.c0 .localeDropdownMenu {
min-width: 90px !important;
max-height: 162px !important;
overflow: auto !important;
margin: 0 !important;
padding: 0;
line-height: 1.8rem;
border: none !important;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
box-shadow: 0 1px 4px 0px rgba(40,42,49,0.05);
}
.c0 .localeDropdownMenu:before {
content: '';
position: absolute;
top: -3px;
left: -1px;
width: calc(100% + 1px);
height: 3px;
box-shadow: 0 1px 2px 0 rgba(40,42,49,0.16);
}
.c0 .localeDropdownMenu > button {
height: 40px;
padding: 0px 15px;
line-height: 40px;
color: #f75b1d;
font-size: 13px;
font-weight: 500;
-webkit-letter-spacing: 0.5;
-moz-letter-spacing: 0.5;
-ms-letter-spacing: 0.5;
letter-spacing: 0.5;
}
.c0 .localeDropdownMenu > button:hover,
.c0 .localeDropdownMenu > button:focus,
.c0 .localeDropdownMenu > button:active {
background-color: #fafafb !important;
border-radius: 0px;
cursor: pointer;
}
.c0 .localeDropdownMenu > button:first-child {
line-height: 50px;
margin-bottom: 4px;
}
.c0 .localeDropdownMenu > button:first-child:hover,
.c0 .localeDropdownMenu > button:first-child:active {
color: #333740;
}
.c0 .localeDropdownMenu > button:not(:first-child) {
height: 36px;
line-height: 36px;
}
.c0 .localeDropdownMenu > button:not(:first-child) > i,
.c0 .localeDropdownMenu > button:not(:first-child) > svg {
margin-left: 10px;
}
.c0 .localeDropdownMenuNotLogged {
background: transparent !important;
box-shadow: none !important;
border: 1px solid #e3e9f3 !important;
border-top: 0px !important;
}
.c0 .localeDropdownMenuNotLogged button {
padding-left: 17px;
}
.c0 .localeDropdownMenuNotLogged button:hover {
background-color: #f7f8f8 !important;
}
.c0 .localeDropdownMenuNotLogged:before {
box-shadow: none !important;
}
.c0 .localeToggleItem img {
max-height: 13.37px;
margin-left: 9px;
}
.c0 .localeToggleItem:active {
color: black;
}
.c0 .localeToggleItem:hover {
background-color: #fafafb !important;
}
.c0 .localeToggleItemActive {
color: #333740 !important;
}
<div
class="c0"
>
<div
class="btn-group"
>
<button
aria-expanded="false"
aria-haspopup="true"
class="localeDropdownContent btn btn-secondary"
type="button"
>
<span>
English
</span>
</button>
<div
aria-hidden="true"
class="localeDropdownMenu dropdown-menu"
role="menu"
tabindex="-1"
>
<button
class="localeToggleItem localeToggleItemActive dropdown-item"
role="menuitem"
tabindex="0"
type="button"
>
English
</button>
</div>
</div>
</div>
`);
});
});

View File

@ -0,0 +1,43 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { Box, Row } from '@strapi/parts';
import LocaleToggle from '../components/LocaleToggle';
const Wrapper = styled(Box)`
margin: 0 auto;
width: 552px;
`;
export const Column = styled(Row)`
flex-direction: column;
`;
const UnauthenticatedLayout = ({ children }) => {
return (
<div>
<Row as="header" justifyContent="flex-end">
<Box paddingTop={6} paddingRight={8}>
<LocaleToggle isLogged />
</Box>
</Row>
<Box paddingTop="11" paddingBottom="11">
<Wrapper
shadow="tableShadow"
hasRadius
padding="10"
background="neutral0"
justifyContent="center"
>
{children}
</Wrapper>
</Box>
</div>
);
};
UnauthenticatedLayout.propTypes = {
children: PropTypes.node.isRequired,
};
export default UnauthenticatedLayout;

View File

@ -13,6 +13,8 @@ import {
useNotification,
TrackingContext,
} from '@strapi/helper-plugin';
import { SkipToContent } from '@strapi/parts';
import { useIntl } from 'react-intl';
import PrivateRoute from '../../components/PrivateRoute';
import { createRoute, makeUniqueRoutes } from '../../utils';
import AuthPage from '../AuthPage';
@ -26,6 +28,7 @@ const AuthenticatedApp = lazy(() =>
function App() {
const toggleNotification = useNotification();
const { formatMessage } = useIntl();
const [{ isLoading, hasAdmin, uuid }, setState] = useState({ isLoading: true, hasAdmin: false });
const authRoutes = useMemo(() => {
@ -105,6 +108,7 @@ function App() {
return (
<Suspense fallback={<LoadingIndicatorPage />}>
<SkipToContent>{formatMessage({ id: 'skipToContent' })}</SkipToContent>
<TrackingContext.Provider value={uuid}>
<Switch>
{authRoutes}

View File

@ -1,84 +1,139 @@
import React from 'react';
import { Checkbox } from '@buffetjs/core';
import { useIntl } from 'react-intl';
import { get } from 'lodash';
import React, { useState } from 'react';
import { Show, Hide } from '@strapi/icons';
import {
Box,
Stack,
H1,
Text,
Subtitle,
Button,
Checkbox,
TextInput,
Main,
FieldAction,
} from '@strapi/parts';
import PropTypes from 'prop-types';
import { BaselineAlignment } from '@strapi/helper-plugin';
import styled from 'styled-components';
import { useIntl } from 'react-intl';
import { Formik } from 'formik';
import Button from '../../../../components/FullWidthButton';
import AuthLink from '../AuthLink';
import Box from '../Box';
import Input from '../Input';
import { Column } from '../../../../layouts/UnauthenticatedLayout';
import Form from './Form';
import Logo from '../Logo';
import Section from '../Section';
const Login = ({ children, formErrors, modifiedData, onChange, onSubmit, requestError }) => {
const AuthButton = styled(Button)`
display: inline-block;
width: 100%;
`;
const FieldActionWrapper = styled(FieldAction)`
svg {
height: 16px;
width: 16px;
path {
fill: ${({ theme }) => theme.colors.neutral600};
}
}
`;
const Login = ({ onSubmit, schema }) => {
const [passwordShown, setPasswordShown] = useState(false);
const { formatMessage } = useIntl();
return (
<>
<Section textAlign="center">
<Logo />
</Section>
<Section withBackground>
<BaselineAlignment top size="25px">
<Box errorMessage={get(requestError, 'errorMessage', null)}>
<form onSubmit={onSubmit}>
<Input
autoFocus
error={formErrors.email}
label="Auth.form.email.label"
<Main labelledBy="welcome">
<Formik
enableReinitialize
initialValues={{
email: '',
password: '',
}}
onSubmit={onSubmit}
validationSchema={schema}
validateOnChange={false}
>
{({ values, errors, handleChange }) => (
<Form noValidate>
<Column>
<Logo />
<Box paddingTop="6" paddingBottom="1">
<H1 id="welcome">{formatMessage({ id: 'Auth.form.welcome.title' })}</H1>
</Box>
<Box paddingBottom="7">
<Subtitle textColor="neutral600">
{formatMessage({ id: 'Auth.form.welcome.subtitle' })}
</Subtitle>
</Box>
{errors.errorMessage && (
<Text id="global-form-error" role="alert" tabIndex={-1} textColor="danger600">
{errors.errorMessage}
</Text>
)}
</Column>
<Stack size={6}>
<TextInput
error={errors.email ? formatMessage({ id: errors.email }) : ''}
value={values.email}
onChange={handleChange}
label={formatMessage({ id: 'Auth.form.email.label' })}
placeholder={formatMessage({ id: 'Auth.form.email.placeholder' })}
name="email"
onChange={onChange}
placeholder="Auth.form.email.placeholder"
type="email"
validations={{ required: true }}
value={modifiedData.email}
required
/>
<Input
error={formErrors.password}
label="Auth.form.password.label"
<TextInput
error={errors.password ? formatMessage({ id: errors.password }) : ''}
onChange={handleChange}
value={values.password}
label={formatMessage({ id: 'Auth.form.password.label' })}
name="password"
onChange={onChange}
type="password"
validations={{ required: true }}
value={modifiedData.password}
type={passwordShown ? 'text' : 'password'}
endAction={
// eslint-disable-next-line react/jsx-wrap-multilines
<FieldActionWrapper
onClick={e => {
e.stopPropagation();
setPasswordShown(prev => !prev);
}}
label={formatMessage({
id: passwordShown
? 'Auth.form.password.show-password'
: 'Auth.form.password.hide-password',
})}
>
{passwordShown ? <Show /> : <Hide />}
</FieldActionWrapper>
}
required
/>
<Checkbox
type="checkbox"
message={formatMessage({ id: 'Auth.form.rememberMe.label' })}
onValueChange={checked => {
handleChange({ target: { value: checked, name: 'rememberMe' } });
}}
value={values.rememberMe}
name="rememberMe"
onChange={onChange}
value={modifiedData.rememberMe}
/>
<BaselineAlignment top size="27px">
<Button type="submit" color="primary" textTransform="uppercase">
{formatMessage({ id: 'Auth.form.button.login' })}
</Button>
</BaselineAlignment>
</form>
{children}
</Box>
</BaselineAlignment>
</Section>
<AuthLink label="Auth.link.forgot-password" to="/auth/forgot-password" />
</>
>
{formatMessage({ id: 'Auth.form.rememberMe.label' })}
</Checkbox>
<AuthButton type="submit">
{formatMessage({ id: 'Auth.form.button.login' })}
</AuthButton>
</Stack>
</Form>
)}
</Formik>
</Main>
);
};
Login.defaultProps = {
children: null,
onSubmit: e => e.preventDefault(),
requestError: null,
onSubmit: () => {},
};
Login.propTypes = {
children: PropTypes.node,
formErrors: PropTypes.object.isRequired,
modifiedData: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func,
requestError: PropTypes.object,
schema: PropTypes.shape({
type: PropTypes.string.isRequired,
}).isRequired,
};
export default Login;

View File

@ -0,0 +1,43 @@
import React, { useEffect } from 'react';
import { Form, useFormikContext, getIn } from 'formik';
const FormWithFocus = props => {
const { isSubmitting, isValidating, errors, touched } = useFormikContext();
useEffect(() => {
if (isSubmitting && !isValidating) {
const errorNames = Object.keys(touched).reduce((prev, key) => {
if (getIn(errors, key)) {
prev.push(key);
}
return prev;
}, []);
if (errorNames.length) {
let errorEl;
errorNames.forEach(errorKey => {
const selector = `[name="${errorKey}"]`;
if (!errorEl) {
errorEl = document.querySelector(selector);
}
});
errorEl.focus();
}
}
if (!isSubmitting && !isValidating && Object.keys(errors).length) {
const el = document.getElementById('global-form-error');
if (el) {
el.focus();
}
}
}, [errors, isSubmitting, isValidating, touched]);
return <Form {...props} noValidate />;
};
export default FormWithFocus;

View File

@ -1,10 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import BaseLogin from './BaseLogin';
import UnauthenticatedLayout from '../../../../layouts/UnauthenticatedLayout';
const Login = loginProps => {
return <BaseLogin {...loginProps} />;
return (
<UnauthenticatedLayout>
<BaseLogin {...loginProps} />
</UnauthenticatedLayout>
);
};
Login.defaultProps = {

View File

@ -1,7 +0,0 @@
import styled from 'styled-components';
const Img = styled.img`
height: 40px;
`;
export default Img;

View File

@ -1,11 +1,15 @@
import React from 'react';
import styled from 'styled-components';
import { useConfigurations } from '../../../../hooks';
import Img from './Img';
const Img = styled.img`
height: 72px;
`;
const Logo = () => {
const { authLogo } = useConfigurations();
return <Img src={authLogo} alt="strapi" />;
return <Img src={authLogo} aria-hidden alt="" />;
};
export default Logo;

View File

@ -1,16 +1,11 @@
import React, { useEffect, useReducer } from 'react';
import axios from 'axios';
import { camelCase, get, omit, upperFirst } from 'lodash';
import { camelCase, get, omit } from 'lodash';
import { Redirect, useRouteMatch, useHistory } from 'react-router-dom';
import { BaselineAlignment, auth, useNotification, useQuery } from '@strapi/helper-plugin';
import { Padded } from '@buffetjs/core';
import { auth, useNotification, useQuery } from '@strapi/helper-plugin';
import PropTypes from 'prop-types';
import forms from 'ee_else_ce/pages/AuthPage/utils/forms';
import useLocalesProvider from '../../components/LocalesProvider/useLocalesProvider';
import NavTopRightWrapper from '../../components/NavTopRightWrapper';
import PageTitle from '../../components/PageTitle';
import LocaleToggle from '../../components/LocaleToggle';
import checkFormValidity from '../../utils/checkFormValidity';
import formatAPIErrors from '../../utils/formatAPIErrors';
import init from './init';
import { initialState, reducer } from './reducer';
@ -95,40 +90,25 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
});
};
const handleSubmit = async e => {
e.preventDefault();
const handleSubmit = async (e, { setSubmitting, setErrors }) => {
setSubmitting(true);
const body = omit(e, fieldsToOmit);
const requestURL = `/admin/${endPoint}`;
dispatch({
type: 'SET_ERRORS',
errors: {},
});
if (authType === 'login') {
await loginRequest(body, requestURL, { setSubmitting, setErrors });
}
const errors = await checkFormValidity(modifiedData, schema);
if (authType === 'register' || authType === 'register-admin') {
await registerRequest(body, requestURL);
}
dispatch({
type: 'SET_ERRORS',
errors: errors || {},
});
if (authType === 'forgot-password') {
await forgotPasswordRequest(body, requestURL);
}
if (!errors) {
const body = omit(modifiedData, fieldsToOmit);
const requestURL = `/admin/${endPoint}`;
if (authType === 'login') {
await loginRequest(body, requestURL);
}
if (authType === 'register' || authType === 'register-admin') {
await registerRequest(body, requestURL);
}
if (authType === 'forgot-password') {
await forgotPasswordRequest(body, requestURL);
}
if (authType === 'reset-password') {
await resetPasswordRequest(body, requestURL);
}
if (authType === 'reset-password') {
await resetPasswordRequest(body, requestURL);
}
};
@ -152,7 +132,7 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
}
};
const loginRequest = async (body, requestURL) => {
const loginRequest = async (body, requestURL, { setSubmitting, setErrors }) => {
try {
const {
data: {
@ -175,8 +155,8 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
push('/');
} catch (err) {
if (err.response) {
setSubmitting(false);
const errorMessage = get(err, ['response', 'data', 'message'], 'Something went wrong');
const errorStatus = get(err, ['response', 'data', 'statusCode'], 400);
if (camelCase(errorMessage).toLowerCase() === 'usernotactive') {
push('/auth/oops');
@ -188,11 +168,7 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
return;
}
dispatch({
type: 'SET_REQUEST_ERROR',
errorMessage,
errorStatus,
});
setErrors({ errorMessage });
}
}
};
@ -288,24 +264,17 @@ const AuthPage = ({ hasAdmin, setHasAdmin }) => {
}
return (
<Padded bottom size="md">
<PageTitle title={upperFirst(authType)} />
<NavTopRightWrapper>
<LocaleToggle isLogged className="localeDropdownMenuNotLogged" />
</NavTopRightWrapper>
<BaselineAlignment top size="78px">
<Component
{...rest}
fieldsToDisable={fieldsToDisable}
formErrors={formErrors}
inputsPrefix={inputsPrefix}
modifiedData={modifiedData}
onChange={handleChange}
onSubmit={handleSubmit}
requestError={requestError}
/>
</BaselineAlignment>
</Padded>
<Component
{...rest}
fieldsToDisable={fieldsToDisable}
formErrors={formErrors}
inputsPrefix={inputsPrefix}
modifiedData={modifiedData}
onChange={handleChange}
onSubmit={handleSubmit}
requestError={requestError}
schema={schema}
/>
);
};

View File

@ -3,7 +3,7 @@
"Auth.components.Oops.text": "Your account has been suspended",
"Auth.form.button.forgot-password": "Send Email",
"Auth.form.button.go-home": "GO BACK HOME",
"Auth.form.button.login": "Log in",
"Auth.form.button.login": "Login",
"Auth.form.button.login.providers.error": "We cannot connect you through the selected provider.",
"Auth.form.button.login.providers.see-more": "See more",
"Auth.form.button.login.strapi": "LOG IN VIA STRAPI",
@ -34,14 +34,19 @@
"Auth.form.lastname.label": "Last name",
"Auth.form.lastname.placeholder": "Doe",
"Auth.form.password.label": "Password",
"Auth.form.password.show-password": "Show password",
"Auth.form.password.hide-password": "Hide password",
"Auth.form.register.news.label": "Keep me updated about the new features and upcoming improvements (by doing this you accept the {terms} and the {policy}).",
"Auth.form.rememberMe.label": "Remember me",
"Auth.form.username.label": "Username",
"Auth.form.username.placeholder": "Kai Doe",
"Auth.form.welcome.subtitle": "Log in to your Strapi account",
"Auth.form.welcome.title": "Welcome back!",
"Auth.link.forgot-password": "Forgot your password?",
"Auth.link.ready": "Ready to sign in?",
"Auth.link.signin": "Sign in",
"Auth.link.signin.account": "Already have an account?",
"Auth.login.sso.divider": "Or login with",
"Auth.privacy-policy-agreement.policy": "privacy policy",
"Auth.privacy-policy-agreement.terms": "terms",
"Content Manager": "Content Manager",
@ -316,7 +321,7 @@
"components.Input.error.contentTypeName.taken": "This name already exists",
"components.Input.error.custom-error": "{errorMessage} ",
"components.Input.error.password.noMatch": "Passwords do not match",
"components.Input.error.validation.email": "This is not an email",
"components.Input.error.validation.email": "This is an invalid email",
"components.Input.error.validation.json": "This doesn't match the JSON format",
"components.Input.error.validation.max": "The value is too high.",
"components.Input.error.validation.maxLength": "The value is too long.",

View File

@ -1,73 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTheme } from 'styled-components';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Stack, Row, Divider, TableLabel, Box } from '@strapi/parts';
import { useIntl } from 'react-intl';
import { Flex, Padded, Separator } from '@buffetjs/core';
import { LoadingIndicator, Tooltip } from '@buffetjs/styles';
import { Dots } from '@buffetjs/icons';
import { BaselineAlignment } from '@strapi/helper-plugin';
import BaseLogin from '../../../../../../admin/src/pages/AuthPage/components/Login/BaseLogin';
import ProviderButton from '../../../../components/ProviderButton';
import {
ProviderButtonWrapper,
ProviderLink,
} from '../../../../components/ProviderButton/ProviderButtonStyles';
import { useAuthProviders } from '../../../../hooks';
import UnauthenticatedLayout from '../../../../../../admin/src/layouts/UnauthenticatedLayout';
import SSOProviders from '../Providers/SSOProviders';
const DividerFull = styled(Divider)`
flex: 1;
`;
const Login = loginProps => {
const ssoEnabled = strapi.features.isEnabled(strapi.features.SSO);
const theme = useTheme();
const { isLoading, data: providers } = useAuthProviders({ ssoEnabled });
const { formatMessage } = useIntl();
if (!ssoEnabled || (!isLoading && providers.length === 0)) {
return <BaseLogin {...loginProps} />;
return (
<UnauthenticatedLayout>
<BaseLogin {...loginProps} />
</UnauthenticatedLayout>
);
}
return (
<BaseLogin {...loginProps}>
<Padded top size="md">
<BaselineAlignment top size="6px" />
<Separator
label={formatMessage({
id: 'or',
defaultMessage: 'OR',
})}
/>
<Padded bottom size="md" />
{isLoading ? (
<LoadingIndicator />
) : (
<Flex justifyContent="center">
{providers.slice(0, 2).map((provider, index) => (
<Padded key={provider.uid} left={index !== 0} right size="xs">
<ProviderButton provider={provider} />
</Padded>
))}
{providers.length > 2 && (
<Padded left size="xs">
<ProviderLink as={Link} to="/auth/providers">
<ProviderButtonWrapper
justifyContent="center"
alignItems="center"
data-for="see-more-tooltip"
data-tip={formatMessage({
id: 'Auth.form.button.login.providers.see-more',
defaultMessage: 'See more',
})}
>
<Dots width="18" height="8" fill={theme.main.colors.black} />
</ProviderButtonWrapper>
</ProviderLink>
<Tooltip id="see-more-tooltip" />
</Padded>
)}
</Flex>
)}
</Padded>
</BaseLogin>
<UnauthenticatedLayout>
<BaseLogin {...loginProps} />
<Box paddingTop={7}>
<Stack size={7}>
<Row>
<DividerFull />
<Box paddingLeft={3} paddingRight={3}>
<TableLabel textColor="neutral600">
{formatMessage({ id: 'Auth.login.sso.divider' })}
</TableLabel>
</Box>
<DividerFull />
</Row>
<SSOProviders providers={providers} displayAllProviders={false} />
</Stack>
</Box>
</UnauthenticatedLayout>
);
};

View File

@ -0,0 +1,113 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, Row, Grid, GridItem, Tooltip } from '@strapi/parts';
import styled from 'styled-components';
import { useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
const SSOButton = styled.a`
width: ${136 / 16}rem;
display: flex;
justify-content: center;
align-items: center;
height: ${48 / 16}rem;
border: 1px solid ${({ theme }) => theme.colors.neutral150};
border-radius: ${({ theme }) => theme.borderRadius};
text-decoration: inherit;
&:link {
text-decoration: none;
}
color: ${({ theme }) => theme.colors.neutral600};
`;
const SSOProvidersWrapper = styled(Row)`
& a:not(:first-child):not(:last-child) {
margin: 0 ${({ theme }) => theme.spaces[2]};
}
& a:first-child {
margin-right: ${({ theme }) => theme.spaces[2]};
}
& a:last-child {
margin-left: ${({ theme }) => theme.spaces[2]};
}
`;
const SSOProviderButton = ({ provider }) => {
return (
<Tooltip label={provider.displayName}>
<SSOButton href={`${strapi.backendURL}/admin/connect/${provider.uid}`}>
{provider.icon ? (
<img src={provider.icon} aria-hidden alt="" height="32px" />
) : (
<Text>{provider.displayName}</Text>
)}
</SSOButton>
</Tooltip>
);
};
SSOProviderButton.propTypes = {
provider: PropTypes.shape({
icon: PropTypes.string,
displayName: PropTypes.string.isRequired,
uid: PropTypes.string.isRequired,
}).isRequired,
};
const SSOProviders = ({ providers, displayAllProviders }) => {
const { formatMessage } = useIntl();
if (displayAllProviders) {
return (
<Grid gap={4}>
{providers.map(provider => (
<GridItem key={provider.uid} col={4}>
<SSOProviderButton provider={provider} />
</GridItem>
))}
</Grid>
);
}
if (providers.length > 2 && !displayAllProviders) {
return (
<Grid gap={4}>
{providers.slice(0, 2).map(provider => (
<GridItem key={provider.uid} col={4}>
<SSOProviderButton provider={provider} />
</GridItem>
))}
<GridItem col={4}>
<Tooltip
label={formatMessage({
id: 'Auth.form.button.login.providers.see-more',
})}
>
<SSOButton as={Link} to="/auth/providers">
<span aroa-hidden></span>
</SSOButton>
</Tooltip>
</GridItem>
</Grid>
);
}
return (
<SSOProvidersWrapper justifyContent="center">
{providers.map(provider => (
<SSOProviderButton key={provider.uid} provider={provider} />
))}
</SSOProvidersWrapper>
);
};
SSOProviders.defaultProps = {
displayAllProviders: true,
};
SSOProviders.propTypes = {
providers: PropTypes.arrayOf(PropTypes.object).isRequired,
displayAllProviders: PropTypes.bool,
};
export default SSOProviders;

View File

@ -1,16 +1,16 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- The first thing in any HTML file should be the charset -->
<meta charset="utf-8">
<!-- Make the page mobile compatible -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<title>Strapi Admin</title>
</head>
<body>
<!-- The app hooks into this div -->
<div id="app"></div>
<!-- A lot of magic happens in this file. HtmlWebpackPlugin automatically includes all assets (e.g. bundle.js, main.css) with the correct HTML tags, which is why they are missing in this HTML file. Don't add any assets here! (Check out webpackconfig.js if you want to know more) -->
</body>
<head>
<!-- The first thing in any HTML file should be the charset -->
<meta charset="utf-8" />
<!-- Make the page mobile compatible -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="mobile-web-app-capable" content="yes" />
<title>Strapi Admin</title>
</head>
<body>
<!-- The app hooks into this div -->
<div style="min-height:100%;background-color:#f6f6f9;" id="app"></div>
<!-- A lot of magic happens in this file. HtmlWebpackPlugin automatically includes all assets (e.g. bundle.js, main.css) with the correct HTML tags, which is why they are missing in this HTML file. Don't add any assets here! (Check out webpackconfig.js if you want to know more) -->
</body>
</html>

View File

@ -41,8 +41,8 @@
"@strapi/babel-plugin-switch-ee-ce": "1.0.0",
"@strapi/helper-plugin": "3.6.6",
"@strapi/utils": "3.6.6",
"@strapi/icons": "0.0.1-alpha.5",
"@strapi/parts": "0.0.1-alpha.5",
"@strapi/icons": "0.0.1-alpha.6",
"@strapi/parts": "0.0.1-alpha.6",
"axios": "^0.21.1",
"babel-loader": "8.2.2",
"babel-plugin-styled-components": "1.12.0",

View File

@ -109,7 +109,7 @@
"components.Input.error.attribute.taken": "This field name already exists",
"components.Input.error.contentTypeName.taken": "This name already exists",
"components.Input.error.custom-error": "{errorMessage} ",
"components.Input.error.validation.email": "This is not an email",
"components.Input.error.validation.email": "This is an invalid email",
"components.Input.error.validation.json": "This doesn't match the JSON format",
"components.Input.error.validation.max": "The value is too high.",
"components.Input.error.validation.maxLength": "The value is too long.",

View File

@ -109,7 +109,7 @@
"components.Input.error.attribute.taken": "This field name already exists",
"components.Input.error.contentTypeName.taken": "This name already exists",
"components.Input.error.custom-error": "{errorMessage} ",
"components.Input.error.validation.email": "This is not an email",
"components.Input.error.validation.email": "This is an invalid email",
"components.Input.error.validation.json": "This doesn't match the JSON format",
"components.Input.error.validation.max": "The value is too high.",
"components.Input.error.validation.maxLength": "The value is too long.",

View File

@ -3490,15 +3490,15 @@
tslib "^2.0.0"
upath "2.0.1"
"@strapi/icons@0.0.1-alpha.5":
version "0.0.1-alpha.5"
resolved "https://registry.yarnpkg.com/@strapi/icons/-/icons-0.0.1-alpha.5.tgz#04f2d62a516e8da6b14f0b1bacc040b10c5f44db"
integrity sha512-PCTQXIkBxfV/qWQZPrbABEdkLSYO7w4+RWVHA7eNlSkcKPP/b5Q/ZEW+56GZ/o9DLexivsGdZaC/aVUzK7G7IA==
"@strapi/icons@0.0.1-alpha.6":
version "0.0.1-alpha.6"
resolved "https://registry.yarnpkg.com/@strapi/icons/-/icons-0.0.1-alpha.6.tgz#d714c4f0f44d5a53b813989f0c890af62278ce1b"
integrity sha512-QB5ghVyTh+vWlFAbDMmZGS//0+mZLWnB0ejxZfHjvT/d3ByVDycxsugLpk+jwALmD7pPdcqK66+3vR9o97Pl1g==
"@strapi/parts@0.0.1-alpha.5":
version "0.0.1-alpha.5"
resolved "https://registry.yarnpkg.com/@strapi/parts/-/parts-0.0.1-alpha.5.tgz#6abddbcf4ee58da506065aa49f736b0b51e9cbfa"
integrity sha512-Vb55oaD0G9N9OzsVM4SB3WG30w4Mo3oDITq9lCvwQT9uR0xKuN+GwYK1XdlBTItLcjnJOpRJ3vSoI7sgI1L8rQ==
"@strapi/parts@0.0.1-alpha.6":
version "0.0.1-alpha.6"
resolved "https://registry.yarnpkg.com/@strapi/parts/-/parts-0.0.1-alpha.6.tgz#7259e26edb7b4195352713a73f23d54dfefc79c8"
integrity sha512-4RhcguoPf41tJ6TbrHrUffOMEmWIJGgT9dxgqy5aFLZFHXnO1x1463Sw5hVSqdT+D05SiJLcBCO3zY+NhpEsDQ==
dependencies:
compute-scroll-into-view "^1.0.17"
prop-types "^15.7.2"