Add new user EE for SSO

Signed-off-by: HichamELBSI <elabbassih@gmail.com>
This commit is contained in:
HichamELBSI 2021-02-11 15:49:12 +01:00
parent 189740c92c
commit 3d2538d72e
19 changed files with 297 additions and 107 deletions

View File

@ -0,0 +1,47 @@
// This component is a work in progress
// It's made to be used when the users API is ready
import React from 'react';
import { Flex, Text } from '@buffetjs/core';
import { Duplicate } from '@buffetjs/icons';
import PropTypes from 'prop-types';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import IconWrapper from './IconWrapper';
import Envelope from './Envelope';
import Wrapper from './Wrapper';
const LinkNotification = ({ link, description }) => {
const handleCopy = () => {
strapi.notification.toggle({ type: 'info', message: { id: 'notification.link-copied' } });
};
return (
<Wrapper>
<IconWrapper>
<Envelope />
</IconWrapper>
<Flex flexDirection="column" justifyContent="center">
<Text fontWeight="semiBold" color="black" fontSize="md" lineHeight="18px">
{link}
<CopyToClipboard onCopy={handleCopy} text={link}>
<Duplicate fill="#8B91A0" className="icon-duplicate" />
</CopyToClipboard>
</Text>
<Text fontWeight="regular" color="grey" fontSize="sm" lineHeight="18px">
{description}
</Text>
</Flex>
</Wrapper>
);
};
LinkNotification.defaultProps = {
link: '',
description: '',
};
LinkNotification.propTypes = {
link: PropTypes.string,
description: PropTypes.string,
};
export default LinkNotification;

View File

@ -0,0 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Padded } from '@buffetjs/core';
import { Label, Description as BaseDescription } from '@buffetjs/styles';
import { Col } from 'reactstrap';
import styled from 'styled-components';
import SelectRoles from '../SelectRoles';
const Description = styled(BaseDescription)`
font-size: ${({ theme }) => theme.main.sizes.fonts.md};
line-height: normal;
`;
const RolesSelectComponent = ({ isDisabled, value, error, onChange }) => {
const { formatMessage } = useIntl();
return (
<Col xs="6">
<Padded bottom size="xs">
<Label style={{ display: 'block' }} htmlFor="roles">
{formatMessage({ id: 'app.components.Users.ModalCreateBody.block-title.roles' })}
</Label>
</Padded>
<SelectRoles
isDisabled={isDisabled}
name="roles"
onChange={onChange}
value={value}
error={error}
/>
<Description>
{formatMessage({
id: 'app.components.Users.ModalCreateBody.block-title.roles.description',
})}
</Description>
</Col>
);
};
RolesSelectComponent.defaultProps = {
value: null,
error: null,
};
RolesSelectComponent.propTypes = {
isDisabled: PropTypes.bool.isRequired,
value: PropTypes.array,
error: PropTypes.string,
onChange: PropTypes.func.isRequired,
};
export default RolesSelectComponent;

View File

@ -0,0 +1,74 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ModalSection } from 'strapi-helper-plugin';
import { Padded } from '@buffetjs/core';
import { Row } from 'reactstrap';
import { useIntl } from 'react-intl';
import loginSettingsForm from 'ee_else_ce/components/Users/ModalCreateBody/utils/loginSettingsForm';
import Input from '../../SizedInput';
import Wrapper from '../ModalCreateBody/Wrapper';
const LoginModalSection = ({ isDisabled, modifiedData, onChange, formErrors }) => {
const { formatMessage } = useIntl();
return (
<ModalSection>
<Wrapper>
<Padded top size="smd">
<Row>
{Object.keys(loginSettingsForm).map(inputName => {
if (loginSettingsForm[inputName].Component) {
const { Component } = loginSettingsForm[inputName];
return (
<Component
key={inputName}
value={modifiedData[inputName]}
onChange={onChange}
error={formErrors[inputName]}
isDisabled={isDisabled}
/>
);
}
return (
<Input
{...loginSettingsForm[inputName]}
key={inputName}
description={formatMessage({ id: loginSettingsForm[inputName].description })}
type={loginSettingsForm[inputName].type}
disabled={isDisabled}
name={inputName}
onChange={onChange}
value={modifiedData.useSSORegistration}
error={formErrors.useSSORegistration}
/>
);
})}
</Row>
</Padded>
</Wrapper>
</ModalSection>
);
};
LoginModalSection.defaultProps = {
isDisabled: false,
formErrors: {},
};
LoginModalSection.propTypes = {
isDisabled: PropTypes.bool,
modifiedData: PropTypes.shape({
roles: PropTypes.array,
useSSORegistration: PropTypes.bool,
}).isRequired,
formErrors: PropTypes.shape({
roles: PropTypes.array,
useSSORegistration: PropTypes.bool,
}),
onChange: PropTypes.func.isRequired,
};
export default LoginModalSection;

View File

@ -1,50 +1,22 @@
// This component is a work in progress
// It's made to be used when the users API is ready
import React from 'react';
import { Flex, Text } from '@buffetjs/core';
import { Duplicate } from '@buffetjs/icons';
import { useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import LinkNotification from '../LinkNotification';
import basename from '../../../utils/basename';
import IconWrapper from './IconWrapper';
import Envelope from './Envelope';
import Wrapper from './Wrapper';
const MagicLink = ({ registrationToken }) => {
const { formatMessage } = useIntl();
const handleCopy = () => {
strapi.notification.toggle({ type: 'info', message: { id: 'notification.link-copied' } });
};
const MagicLink = ({ registrationToken, description }) => {
const link = `${window.location.origin}${basename}auth/register?registrationToken=${registrationToken}`;
return (
<Wrapper>
<IconWrapper>
<Envelope />
</IconWrapper>
<Flex flexDirection="column" justifyContent="center">
<Text fontWeight="semiBold" color="black" fontSize="md" lineHeight="18px">
{link}
<CopyToClipboard onCopy={handleCopy} text={link}>
<Duplicate fill="#8B91A0" className="icon-duplicate" />
</CopyToClipboard>
</Text>
<Text fontWeight="regular" color="grey" fontSize="sm" lineHeight="18px">
{formatMessage({ id: 'app.components.Users.MagicLink.connect' })}
</Text>
</Flex>
</Wrapper>
);
return <LinkNotification link={link} description={description} />;
};
MagicLink.defaultProps = {
registrationToken: '',
description: '',
};
MagicLink.propTypes = {
registrationToken: PropTypes.string,
description: PropTypes.string,
};
export default MagicLink;

View File

@ -1,19 +1,20 @@
import React, { forwardRef, useReducer, useImperativeHandle, useRef } from 'react';
import PropTypes from 'prop-types';
import { BaselineAlignment, ModalSection, request } from 'strapi-helper-plugin';
import { FormattedMessage } from 'react-intl';
import { useIntl } from 'react-intl';
import { get } from 'lodash';
import { Padded, Text } from '@buffetjs/core';
import { Col, Row } from 'reactstrap';
import { Row } from 'reactstrap';
import MagicLink from 'ee_else_ce/components/Users/MagicLink';
import checkFormValidity from '../../../utils/checkFormValidity';
import SelectRoles from '../SelectRoles';
import form from './utils/form';
import schema from './utils/schema';
import { initialState, reducer } from './reducer';
import init from './init';
import Input from '../../SizedInput';
import Wrapper from './Wrapper';
import MagicLink from '../MagicLink';
import LoginModalSection from '../LoginModalSection';
// This component accepts a ref so we can have access to the submit handler.
const ModalCreateBody = forwardRef(
@ -21,6 +22,7 @@ const ModalCreateBody = forwardRef(
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
const { formErrors, modifiedData } = reducerState;
const buttonSubmitRef = useRef(null);
const { formatMessage } = useIntl();
useImperativeHandle(ref, () => ({
submit: () => {
@ -86,15 +88,13 @@ const ModalCreateBody = forwardRef(
<ModalSection>
<Padded top size="18px">
<Text fontSize="xs" color="grey" fontWeight="bold" textTransform="uppercase">
<FormattedMessage id="app.components.Users.ModalCreateBody.block-title.details">
{txt => txt}
</FormattedMessage>
{formatMessage({ id: 'app.components.Users.ModalCreateBody.block-title.details' })}
</Text>
</Padded>
</ModalSection>
<ModalSection>
<Wrapper>
<Padded top size="20px">
<Padded top size="smd">
<Row>
{Object.keys(form).map((inputName, i) => (
<Input
@ -115,29 +115,16 @@ const ModalCreateBody = forwardRef(
<ModalSection>
<Padded top size="3px">
<Text fontSize="xs" color="grey" fontWeight="bold" textTransform="uppercase">
<FormattedMessage id="app.components.Users.ModalCreateBody.block-title.roles">
{txt => txt}
</FormattedMessage>
{formatMessage({ id: 'app.components.Users.ModalCreateBody.block-title.login' })}
</Text>
</Padded>
</ModalSection>
<ModalSection>
<Wrapper>
<Padded top size="12px">
<Row>
<Col xs="6">
<SelectRoles
isDisabled={isDisabled}
name="roles"
onChange={handleChange}
value={modifiedData.roles}
error={formErrors.roles}
/>
</Col>
</Row>
</Padded>
</Wrapper>
</ModalSection>
<LoginModalSection
modifiedData={modifiedData}
onChange={handleChange}
formErrors={formErrors}
isDisabled={isDisabled}
/>
<button type="submit" style={{ display: 'none' }} ref={buttonSubmitRef}>
hidden button to use the native form event
</button>

View File

@ -1,18 +1,14 @@
/* eslint-disable consistent-return */
import produce from 'immer';
import { set } from 'lodash';
import formDataModel from 'ee_else_ce/components/Users/ModalCreateBody/utils/formDataModel';
const initialState = {
formErrors: {},
modifiedData: {
firstname: '',
lastname: '',
email: '',
roles: [],
},
modifiedData: formDataModel,
};
const reducer = (state, action) =>
const reducer = (state = initialState, action) =>
produce(state, draftState => {
switch (action.type) {
case 'ON_CHANGE': {

View File

@ -0,0 +1,8 @@
const formDataModel = {
firstname: '',
lastname: '',
email: '',
roles: [],
};
export default formDataModel;

View File

@ -0,0 +1,10 @@
import RolesSelectComponent from '../../LoginModalSection/RolesSelectComponent';
const loginSettingsForm = {
roles: {
label: 'Settings.permissions.users.form.firstname',
Component: RolesSelectComponent,
},
};
export default loginSettingsForm;

View File

@ -19,6 +19,7 @@ const SelectRoles = ({ error, isDisabled, name, onChange, value }) => {
return (
<>
<Select
name={name}
components={{
ClearIndicator,
DropdownIndicator,

View File

@ -32,15 +32,14 @@ const ModalForm = ({ isOpen, onClosed, onToggle }) => {
};
const handleClosed = () => {
setStep('create');
// Fetch data only if the user has submitted a new entry
// We can use the registrationToken to know this
if (registrationToken) {
if (registrationToken || currentStep === 'magic-link') {
onClosed();
}
// Reset the state so we know that the user has created a new entry when there is a registrationToken
setStep('create');
setShowBody(false);
setRegistrationToken(null);
};

View File

@ -100,6 +100,8 @@
"Settings.permissions.users.form.email": "Email",
"Settings.permissions.users.form.firstname": "First name",
"Settings.permissions.users.form.lastname": "Last name",
"Settings.permissions.users.form.sso": "Connect with SSO",
"Settings.permissions.users.form.sso.description": "When enabled (ON), users can login via SSO",
"Settings.permissions.users.listview.header.description.plural": "{number} users found",
"Settings.permissions.users.listview.header.description.singular": "{number} user found",
"Settings.permissions.users.listview.header.title": "Users",
@ -240,8 +242,11 @@
"app.components.UpgradePlanModal.text-power": "Unlock the full power",
"app.components.UpgradePlanModal.text-strapi": "of Strapi by upgrading your plan to the",
"app.components.Users.MagicLink.connect": "Send this link to the user for them to connect.",
"app.components.Users.MagicLink.connect.sso": "Send this link to the user, the first login can be mage via a SSO provider",
"app.components.Users.ModalCreateBody.block-title.details": "Details",
"app.components.Users.ModalCreateBody.block-title.roles": "User's roles",
"app.components.Users.ModalCreateBody.block-title.roles.description": "Your user can have one or several roles",
"app.components.Users.ModalCreateBody.block-title.login": "Login settings",
"app.components.Users.SortPicker.button-label": "Sort by",
"app.components.Users.SortPicker.sortby.email_asc": "Email (A to Z)",
"app.components.Users.SortPicker.sortby.email_desc": "Email (Z to A)",
@ -313,7 +318,7 @@
"components.Input.error.validation.unique": "This value is already used.",
"components.InputSelect.option.placeholder": "Choose here",
"components.ListRow.empty": "There is no data to be shown.",
"components.NotAllowedInput.text":"No permissions to see this field",
"components.NotAllowedInput.text": "No permissions to see this field",
"components.OverlayBlocker.description": "You're using a feature that needs the server to restart. Please wait until the server is up.",
"components.OverlayBlocker.description.serverError": "The server should have restarted, please check your logs in the terminal.",
"components.OverlayBlocker.title": "Waiting for restart...",

View File

@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import LinkNotification from '../../../../../admin/src/components/Users/LinkNotification';
import basename from '../../../../../admin/src/utils/basename';
const MagicLink = ({ registrationToken }) => {
const { formatMessage } = useIntl();
if (registrationToken) {
return (
<LinkNotification
link={`${window.location.origin}${basename}auth/register?registrationToken=${registrationToken}`}
description={formatMessage({ id: 'app.components.Users.MagicLink.connect' })}
/>
);
}
return (
<LinkNotification
link={`${window.location.origin}${basename}`}
description={formatMessage({ id: 'app.components.Users.MagicLink.connect.sso' })}
/>
);
};
MagicLink.defaultProps = {
registrationToken: '',
};
MagicLink.propTypes = {
registrationToken: PropTypes.string,
};
export default MagicLink;

View File

@ -0,0 +1,14 @@
import baseModel from '../../../../../../admin/src/components/Users/ModalCreateBody/utils/formDataModel';
const ssoInputsModel = ENABLED_EE_FEATURES.includes('sso')
? {
useSSORegistration: true,
}
: {};
const formDataModel = {
...baseModel,
...ssoInputsModel,
};
export default formDataModel;

View File

@ -0,0 +1,21 @@
import baseForm from '../../../../../../admin/src/components/Users/ModalCreateBody/utils/loginSettingsForm';
const ssoInputs = ENABLED_EE_FEATURES.includes('sso')
? {
useSSORegistration: {
label: 'Settings.permissions.users.form.sso',
type: 'bool',
validations: {
required: true,
},
description: 'Settings.permissions.users.form.sso.description',
},
}
: {};
const form = {
...baseForm,
...ssoInputs,
};
export default form;

View File

@ -15,7 +15,7 @@ module.exports = () => {
const options = {
backend: 'http://localhost:1337',
publicPath: '/admin/',
features: process.env.ENABLED_EE_FEATURES || [],
features: process.env.ENABLED_EE_FEATURES || ['sso'],
};
const args = {

View File

@ -4727,11 +4727,6 @@ base64-js@^1.0.2, base64-js@^1.3.0:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
base64url@3.x.x:
version "3.0.1"
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
base@^0.11.1:
version "0.11.2"
resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@ -13928,11 +13923,6 @@ oauth-sign@^0.9.0, oauth-sign@~0.9.0:
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
oauth@0.9.x:
version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -14556,13 +14546,6 @@ pascalcase@^0.1.1:
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
passport-google-oauth2@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/passport-google-oauth2/-/passport-google-oauth2-0.2.0.tgz#fc9ea59e7091f02e24fd16d6be9257ea982ebbc3"
integrity sha512-62EdPtbfVdc55nIXi0p1WOa/fFMM8v/M8uQGnbcXA4OexZWCnfsEi3wo2buag+Is5oqpuHzOtI64JpHk0Xi5RQ==
dependencies:
passport-oauth2 "^1.1.2"
passport-local@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee"
@ -14570,17 +14553,6 @@ passport-local@1.0.0:
dependencies:
passport-strategy "1.x.x"
passport-oauth2@^1.1.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108"
integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==
dependencies:
base64url "3.x.x"
oauth "0.9.x"
passport-strategy "1.x.x"
uid2 "0.0.x"
utils-merge "1.x.x"
passport-strategy@1.x.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
@ -19561,11 +19533,6 @@ uid-number@0.0.6:
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
uid2@0.0.x:
version "0.0.3"
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=
umask@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
@ -19904,7 +19871,7 @@ utila@^0.4.0, utila@~0.4:
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
utils-merge@1.0.1, utils-merge@1.x.x:
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=