mirror of
https://github.com/strapi/strapi.git
synced 2025-11-01 02:16:03 +00:00
Merge pull request #10681 from strapi/migration/login-view-ce
DS migration : Login view (CE & EE)
This commit is contained in:
commit
56135d69a6
@ -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 |
@ -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>
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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 = {
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Img = styled.img`
|
||||
height: 40px;
|
||||
`;
|
||||
|
||||
export default Img;
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user