@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
269 lines (267 loc) • 9.71 kB
JavaScript
/*
* 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;