UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

269 lines (267 loc) 9.71 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import Dialog from '@mui/material/Dialog'; import DialogTitle from '@mui/material/DialogTitle'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import DialogContent from '@mui/material/DialogContent'; import TextField from '@mui/material/TextField'; import DialogActions from '@mui/material/DialogActions'; import Button from '@mui/material/Button'; import React, { useEffect, useRef, useState } from 'react'; import { makeStyles } from 'tss-react/mui'; import { useDispatch } from 'react-redux'; import { login, loginComplete, logout } from '../../state/actions/auth'; import loginGraphicUrl from '../../assets/authenticate.svg'; import { isBlank } from '../../utils/string'; import Typography from '@mui/material/Typography'; import OpenInNewRounded from '@mui/icons-material/OpenInNewRounded'; import LogInForm from '../LoginForm'; import { me } from '../../services/users'; import ApiResponseErrorState from '../ApiResponseErrorState'; import ErrorState from '../ErrorState/ErrorState'; import { useSelection } from '../../hooks/useSelection'; const translations = defineMessages({ sessionExpired: { id: 'authMonitor.sessionExpiredMessage', defaultMessage: 'Your session has expired. Please log back in.' }, incorrectPasswordMessage: { id: 'authMonitor.incorrectPasswordMessage', defaultMessage: 'Incorrect password. Please try again.' }, postSSOLoginMismatch: { id: 'authMonitor.postSSOLoginMismatchMessage', defaultMessage: "Looks like you've logged in with a user different from the owner of this session. For security reasons, your screen will now be refreshed." } }); const useStyles = makeStyles()((theme) => ({ actions: { placeContent: 'center space-between' }, dialog: { width: 400 }, graphic: { width: 150 }, title: { textAlign: 'center' }, ssoAction: { textAlign: 'center', display: 'flex', flexDirection: 'column', marginTop: theme.spacing(1) } })); export function AuthMonitor() { var _a; const dispatch = useDispatch(); const { formatMessage } = useIntl(); const { username, authenticationType } = (_a = useSelection((state) => state.user)) !== null && _a !== void 0 ? _a : { username: '', authenticationType: 'db' }; const { authoringBase, logoutUrl } = useSelection((state) => state.env); const { active } = useSelection((state) => state.auth); const isSSO = authenticationType === 'saml'; const firstRender = useRef(true); useEffect(() => { // On regular login dialog, the username is locked to the user whose session expired; on the // SSO form however, users can enter any username/password. So this check ensures that if a // different user logs in after timeout, he won't be working on top of the previous user's work/session. if (firstRender.current) { firstRender.current = false; } else if (active) { // Move this call to the next tick to avoid it getting dispatched // before the epics set the new JWT header. setTimeout(() => { me().subscribe((user) => { if (user.username !== username) { alert(formatMessage(translations.postSSOLoginMismatch)); window.location.reload(); } }); }, 0); } }, [active, dispatch, formatMessage, username]); return React.createElement( Dialog, { open: !active, id: 'authMonitorDialog', 'aria-labelledby': 'craftercmsReLoginDialog' }, React.createElement(AuthMonitorBody, { isSSO: isSSO, username: username, logoutUrl: logoutUrl, authoringUrl: authoringBase, dispatch: dispatch, formatMessage: formatMessage }) ); } function AuthMonitorBody(props) { const { authoringUrl, username, isSSO, dispatch, formatMessage } = props; const { classes } = useStyles(); const { error, isFetching } = useSelection((state) => state.auth); const [password, setPassword] = useState(''); const [ssoButtonClicked, setSSOButtonClicked] = useState(false); const styles = isFetching ? { visibility: 'hidden' } : {}; const onSubmit = (e) => { e.preventDefault(); e.stopPropagation(); if (isSSO) { dispatch(loginComplete()); setSSOButtonClicked(false); } else { !isBlank(password) && dispatch(login({ username, password })); } }; const onClose = () => dispatch(logout()); return React.createElement( React.Fragment, null, React.createElement( DialogTitle, { id: 'craftercmsReLoginDialog', className: classes.title, style: styles }, React.createElement(FormattedMessage, { id: 'authMonitor.dialogTitleText', defaultMessage: 'Session Expired' }) ), React.createElement( DialogContent, { className: classes.dialog }, React.createElement( React.Fragment, null, error ? React.createElement(ApiResponseErrorState, { error: error, classes: { image: classes.graphic } }) : React.createElement(ErrorState, { imageUrl: loginGraphicUrl, classes: { image: classes.graphic }, message: formatMessage(translations.sessionExpired) }), isSSO ? React.createElement(SSOForm, { classes: classes, authoringUrl: authoringUrl, username: username, onSubmit: onSubmit, ssoButtonClicked: ssoButtonClicked, onSetSSOButtonClicked: setSSOButtonClicked }) : React.createElement(LogInForm, { username: username, isFetching: isFetching, onSubmit: onSubmit, password: password, onSetPassword: setPassword }) ) ), React.createElement( DialogActions, { className: classes.actions, style: styles }, isSSO && React.createElement( Button, { fullWidth: true, type: 'button', color: 'primary', onClick: onSubmit, disabled: isFetching, variant: 'contained' }, React.createElement(FormattedMessage, { id: 'authMonitor.validateSessionButtonLabel', defaultMessage: 'Resume' }) ), React.createElement( Button, { fullWidth: true, type: 'button', color: 'primary', onClick: onClose, disabled: isFetching, variant: 'text' }, React.createElement(FormattedMessage, { id: 'authMonitor.logOutButtonLabel', defaultMessage: 'Log Out' }) ) ) ); } function SSOForm(props) { const { username, onSubmit, authoringUrl, ssoButtonClicked, onSetSSOButtonClicked, classes } = props; const onOpenLogin = () => { window.open(`${authoringUrl}/login/resume`, '_blank', 'toolbar=0,location=0,menubar=0,dependent=true'); onSetSSOButtonClicked(true); }; return React.createElement( 'form', { onSubmit: onSubmit }, React.createElement(TextField, { fullWidth: true, disabled: true, type: 'email', value: username, className: classes === null || classes === void 0 ? void 0 : classes.input, label: React.createElement(FormattedMessage, { id: 'authMonitor.usernameTextFieldLabel', defaultMessage: 'Username' }) }), React.createElement( 'section', { className: classes === null || classes === void 0 ? void 0 : classes.ssoAction }, React.createElement( Button, { type: 'button', color: 'primary', variant: ssoButtonClicked ? 'outlined' : 'contained', onClick: onOpenLogin, endIcon: React.createElement(OpenInNewRounded, null) }, React.createElement(FormattedMessage, { id: 'authMonitor.openSSOLoginButtonLabel', defaultMessage: 'Open Login Form' }) ), React.createElement( Typography, { variant: 'caption' }, React.createElement(FormattedMessage, { id: 'authMonitor.ssoOpenPopupMessage', defaultMessage: 'Make sure pop ups are not blocked. Once you log in, come back to ' + 'this window and click on `Resume` button below.' }) ) ) ); } export default AuthMonitor;