Redesign idp (#174)

This commit is contained in:
Alex
2025-02-10 15:54:30 +01:00
committed by GitHub
parent bde3a64a85
commit c5c81546e8
13 changed files with 538 additions and 456 deletions

3
.gitignore vendored
View File

@@ -39,7 +39,7 @@ vendor-php
.php-cs-fixer.cache
suite-logs
# QA activity reports
# QA activity reports
tests/qa-activity-report/reports/
# drone CI is in .drone.star, do not let someone accidentally commit a local .drone.yml
@@ -60,3 +60,4 @@ go.work
go.work.sum
.env
.envrc
.DS_Store

View File

@@ -32,7 +32,9 @@ ci-node-generate: assets
.PHONY: assets
assets: pnpm-build \
assets/identifier/static \
assets/identifier/static/favicon.ico
assets/identifier/static/favicon.ico \
assets/identifier/static/icon-lilac.svg
assets/identifier/static:
mkdir -p assets/identifier/static
@@ -41,6 +43,10 @@ assets/identifier/static:
assets/identifier/static/favicon.ico:
cp src/images/favicon.ico assets/identifier/static/favicon.ico
.PHONY: assets/identifier/static/icon-lilac.svg
assets/identifier/static/icon-lilac.svg:
cp src/images/icon-lilac.svg assets/identifier/static/icon-lilac.svg
.PHONY: pnpm-build
pnpm-build: node_modules
#pnpm lint #TODO: activate

View File

@@ -1,69 +1,75 @@
import React, { ReactElement, Suspense, lazy, useState, useEffect } from 'react';
import React, {ReactElement, Suspense, lazy, useState, useEffect} from 'react';
import PropTypes from 'prop-types';
import { MuiThemeProvider } from '@material-ui/core/styles';
import { defaultTheme } from 'kpop/es/theme';
import {MuiThemeProvider} from '@material-ui/core/styles';
import {defaultTheme} from 'kpop/es/theme';
import 'kpop/static/css/base.css';
import 'kpop/static/css/scrollbar.css';
import Spinner from './components/Spinner';
import * as version from './version';
import { OpenCloudContext } from './openCloudContext';
import {OpenCloudContext} from './openCloudContext';
const LazyMain = lazy(() => import(/* webpackChunkName: "identifier-main" */ './Main'));
console.info(`Kopano Identifier build version: ${version.build}`); // eslint-disable-line no-console
const App = ({ bgImg }): ReactElement => {
const [theme, setTheme] = useState(null);
const [config, setConfig] = useState(null);
const [loading, setLoading] = useState(true);
const [theme, setTheme] = useState(null);
const [config, setConfig] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const configResponse = await fetch('/config.json');
const configData = await configResponse.json();
setConfig(configData);
useEffect(() => {
const fetchData = async () => {
try {
const configResponse = await fetch('/config.json');
const configData = await configResponse.json();
setConfig(configData);
const themeResponse = await fetch(configData.theme);
const themeData = await themeResponse.json();
setTheme(themeData);
} catch (error) {
console.error('Error fetching config/theme data:', error);
} finally {
setLoading(false);
}
};
const themeResponse = await fetch(configData.theme);
const themeData = await themeResponse.json();
setTheme(themeData);
} catch (error) {
console.error('Error fetching config/theme data:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
fetchData();
}, []);
if (loading) {
return <Spinner />;
}
if (loading) {
return <Spinner/>;
}
return (
<OpenCloudContext.Provider value={{ theme, config }}>
<div
className='oc-login-bg'
style={{ backgroundImage: bgImg ? `url(${bgImg})` : undefined }}
>
<MuiThemeProvider theme={defaultTheme}>
<Suspense fallback={<Spinner />}>
<LazyMain />
</Suspense>
</MuiThemeProvider>
</div>
</OpenCloudContext.Provider>
);
return (
<OpenCloudContext.Provider value={{theme, config}}>
<div
className={`oc-login-bg ${bgImg ? 'oc-login-bg-image' : ''}`}
style={{backgroundImage: bgImg ? `url(${bgImg})` : undefined}}
>
<MuiThemeProvider theme={defaultTheme}>
<Suspense fallback={<Spinner/>}>
<LazyMain/>
</Suspense>
</MuiThemeProvider>
{!bgImg &&
<img
src={`${process.env.PUBLIC_URL}/static/icon-lilac.svg`}
className={'oc-login-bg-icon'}
/>
}
</div>
</OpenCloudContext.Provider>
);
}
App.propTypes = {
bgImg: PropTypes.string
bgImg: PropTypes.string
};
export default App;

View File

@@ -1,151 +1,194 @@
/* additional css on top of kpop */
@font-face {
font-family: OpenCloud;
src: url('./fonts/OpenCloud500-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-family: OpenCloud;
src: url('./fonts/OpenCloud500-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: OpenCloud;
src: url('./fonts/OpenCloud750-Bold.woff2') format('woff2');
font-weight: bold;
font-style: normal;
font-family: OpenCloud;
src: url('./fonts/OpenCloud750-Bold.woff2') format('woff2');
font-weight: bold;
font-style: normal;
}
html {
font-feature-settings: "cv11";
font-feature-settings: "cv11";
color: #20434f !important;
}
body {
font-family: OpenCloud, sans-serif;
font-family: OpenCloud, sans-serif;
}
strong {
font-weight: 600;
font-weight: 600;
}
.oc-font-weight-light {
font-weight: 300;
}
.oc-login-bg {
background-image: url(./images/background.png);
background-size: cover;
background-repeat: no-repeat;
background-position: center;
z-index: 0;
display: flex;
width: 100%;
background: #20434F;
display: flex;
width: 100%;
}
.oc-login-bg-icon {
position: fixed;
right: -3vw;
bottom: -3vh;
height: 50vh;
z-index: 0;
}
.oc-login-bg-image {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
#loader {
/* NOTE(longsleep): White here needed because of the background image */
color: white;
text-shadow: #000 0 0 1px;
/* NOTE(longsleep): White here needed because of the background image */
color: white;
text-shadow: #000 0 0 1px;
}
.oc-logo {
position: absolute;
top: -130px;
left: 50%;
height: 80px;
transform: translateX(-50%);
height: 80px;
z-index: 0;
margin-bottom: 40px;
}
.oc-progress {
/* Needs to be important to overwrite material-ui */
background-color: rgba(78, 133, 200, 0.8) !important;
height: 4px;
width: 100px;
/* Needs to be important to overwrite material-ui */
background-color: rgba(78, 133, 200, 0.8) !important;
height: 4px;
width: 100px;
}
.oc-progress > div {
/* Needs to be important to overwrite material-ui */
background-color: #4a76ac !important;
/* Needs to be important to overwrite material-ui */
background-color: #4a76ac !important;
}
.oc-input {
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
height: 40px;
width: 300px;
padding: 16px;
box-sizing: border-box;
font-size: 1rem;
border: 1px solid #20434f;
border-radius: 4px;
height: 45px;
width: 100%;
padding: 16px;
box-sizing: border-box;
font-size: 1rem;
}
.oc-label {
color: #fff;
display: inline-block;
margin-bottom: 5px;
display: block;
margin-bottom: 12px;
font-weight: 700;
}
.oc-input.error {
outline: none;
border: 1px solid #fe4600;
outline: none;
border: 1px solid #fe4600;
}
.MuiTypography-colorError {
color: #fe4600 !important;
color: #fe4600 !important;
}
.oc-input:focus {
outline: none;
border: 1px solid #fff;
outline: 2px solid #e2baff;
border: 1px solid #fff;
}
.oc-input + .oc-input {
margin-top: 15px;
margin-top: 15px;
}
.MuiTouchRipple-root {
display: none !important;
display: none !important;
}
.oc-card {
background: white;
display: inline-flex;
width: 500px;
}
.oc-card-body {
padding: 40px;
width: 100%;
}
.oc-button {
/* Needs to be important to overwrite material-ui */
font-size: 1.0625rem !important;
/* Needs to be important to overwrite material-ui */
font-size: 1.0625rem !important;
border-radius: 100px !important;
box-shadow: none !important;
font-weight: 700 !important;
}
.oc-button-primary {
/* Needs to be important to overwrite material-ui */
width: 100%;
background: #e2baff !important;
border: 1px solid transparent !important;
color: #19353f !important;
/* Needs to be important to overwrite material-ui */
width: 100%;
background: #e2baff !important;
border-radius: 100px !important;
color: #20434f !important;
box-shadow: none !important;
font-weight: 700 !important;
}
.oc-checkbox-dark svg {
/* Needs to be important to overwrite material-ui */
fill: white !important;
.oc-button-secondary {
/* Needs to be important to overwrite material-ui */
width: 100%;
border-radius: 100px !important;
background: #F1F3F4 !important;
color: #20434f !important;
box-shadow: none !important;
}
.oc-footer-message {
color: white;
padding: 10px;
font-size: 0.8rem;
color: white;
padding: 10px;
font-size: 0.8rem;
}
@media only screen and (max-width: 768px) {
.oc-logo {
height: 60px;
top: -90px;
}
.oc-logo {
height: 60px;
top: -90px;
}
.oc-login-bg-icon {
display: none;
}
.oc-card {
min-width: 50vw !important;
}
}
@media only screen and (max-width: 600px) {
.oc-card {
width: 100vw !important;
}
}
/* Helpers */
.oc-mt-l {
margin-top: 24px !important;
margin-top: 24px !important;
}
.oc-mb-m {
margin-bottom: 20px !important;
}
.oc-light {
color: #fff !important;
margin-bottom: 20px !important;
}
.oc-login-form div:not(:last-of-type) {
margin-bottom: 15px;
margin-bottom: 20px;
}
@@ -154,14 +197,14 @@ strong {
* Used to hide an element visually, but keeping it accessible for accessibility tools.
*/
.oc-invisible-sr {
border: 0 !important;
clip: rect(1px, 1px, 1px, 1px) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
/* Need to make sure we override any existing styles. */
position: absolute !important;
top: 0;
white-space: nowrap;
width: 1px !important;
border: 0 !important;
clip: rect(1px, 1px, 1px, 1px) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
/* Need to make sure we override any existing styles. */
position: absolute !important;
top: 0;
white-space: nowrap;
width: 1px !important;
}

View File

@@ -24,7 +24,7 @@ class Loading extends React.PureComponent {
))}
{renderIf(error !== null)(() => (
<div>
<Typography className="oc-light" variant="h5" gutterBottom align="center">
<Typography variant="h5" gutterBottom align="center">
{t("konnect.loading.error.headline", "Failed to connect to server")}
</Typography>
<Typography align="center" color="error">

View File

@@ -15,10 +15,13 @@ const styles = theme => ({
root: {
display: 'flex',
flex: 1,
zIndex: 999
},
content: {
position: 'relative',
width: '100%'
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
},
actions: {
marginTop: -40,
@@ -27,8 +30,6 @@ const styles = theme => ({
paddingRight: theme.spacing(3)
},
wrapper: {
width: '100%',
maxWidth: 300,
display: 'flex',
flex: 1,
alignItems: 'center'
@@ -59,7 +60,11 @@ const ResponsiveScreen = (props) => {
<div className={classes.wrapper}>
<div className={classes.content}>
{logo}
{content}
<div className={'oc-card'}>
<div className={'oc-card-body'}>
{content}
</div>
</div>
</div>
</div>
<footer className="oc-footer-message">

View File

@@ -67,9 +67,8 @@ const ScopesList = ({scopes, meta, classes, ...rest}) => {
checked
disableRipple
disabled
className="oc-checkbox-dark"
/>
<ListItemText primary={label} className="oc-light" />
<ListItemText primary={label} />
</ListItem>
);
}

View File

@@ -61,10 +61,10 @@ class Chooseaccount extends React.PureComponent {
return (
<DialogContent className={classes.content}>
<Typography variant="h5" component="h3" className="oc-light">
<Typography variant="h5" component="h3">
{t("konnect.chooseaccount.headline", "Choose an account")}
</Typography>
<Typography variant="subtitle1" className={classes.subHeader + " oc-light"}>
<Typography variant="subtitle1" className={classes.subHeader}>
{t("konnect.chooseaccount.subHeader", "to sign in")}
</Typography>
@@ -77,7 +77,7 @@ class Chooseaccount extends React.PureComponent {
disabled={!!loading}
onClick={(event) => this.logon(event)}
><ListItemAvatar><Avatar>{username.substr(0, 1)}</Avatar></ListItemAvatar>
<ListItemText className="oc-light" primary={username} />
<ListItemText primary={username} />
</ListItem>
<ListItem
button
@@ -92,7 +92,6 @@ class Chooseaccount extends React.PureComponent {
</Avatar>
</ListItemAvatar>
<ListItemText
className="oc-light"
primary={
t("konnect.chooseaccount.useOther.label", "Use another account")
}

View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {connect} from 'react-redux';
import { withTranslation, Trans } from 'react-i18next';
import {withTranslation, Trans} from 'react-i18next';
import renderIf from 'render-if';
import { withStyles } from '@material-ui/core/styles';
import {withStyles} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import BaseTooltip from '@material-ui/core/Tooltip';
import CircularProgress from '@material-ui/core/CircularProgress';
@@ -15,46 +15,54 @@ import Typography from '@material-ui/core/Typography';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import { executeConsent, advanceLogonFlow, receiveValidateLogon } from '../../actions/login';
import { ErrorMessage } from '../../errors';
import { REQUEST_CONSENT_ALLOW } from '../../actions/types';
import {executeConsent, advanceLogonFlow, receiveValidateLogon} from '../../actions/login';
import {ErrorMessage} from '../../errors';
import {REQUEST_CONSENT_ALLOW} from '../../actions/types';
import ClientDisplayName from '../../components/ClientDisplayName';
import ScopesList from '../../components/ScopesList';
const styles = theme => ({
button: {
margin: theme.spacing(1),
minWidth: 100
},
buttonProgress: {
color: green[500],
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginLeft: -12
},
subHeader: {
marginBottom: theme.spacing(2)
},
scopesList: {
marginBottom: theme.spacing(2)
},
wrapper: {
marginTop: theme.spacing(2),
position: 'relative',
display: 'inline-block'
},
message: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2)
}
button: {
margin: theme.spacing(1),
minWidth: 100
},
buttonProgress: {
color: green[500],
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginLeft: -12
},
header: {
textAlign: 'center',
margin: 0,
},
subHeader: {
textAlign: 'center',
marginBottom: theme.spacing(2)
},
scopesList: {
marginBottom: theme.spacing(2)
},
wrapper: {
marginTop: theme.spacing(2),
position: 'relative',
display: 'inline-block'
},
dialogActions: {
gap: theme.spacing(1)
},
message: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2)
}
});
const Tooltip = ({children, ...other } = {}) => {
// Ensures that there is only a single child for the tooltip element to
// make it compatible with the Trans component.
return <BaseTooltip {...other}><span>{children}</span></BaseTooltip>;
const Tooltip = ({children, ...other} = {}) => {
// Ensures that there is only a single child for the tooltip element to
// make it compatible with the Trans component.
return <BaseTooltip {...other}><span>{children}</span></BaseTooltip>;
}
Tooltip.propTypes = {
@@ -62,134 +70,137 @@ Tooltip.propTypes = {
};
class Consent extends React.PureComponent {
componentDidMount() {
const { dispatch, hello, history, client } = this.props;
if ((!hello || !hello.state || !client) && history.action !== 'PUSH') {
history.replace(`/identifier${history.location.search}${history.location.hash}`);
componentDidMount() {
const {dispatch, hello, history, client} = this.props;
if ((!hello || !hello.state || !client) && history.action !== 'PUSH') {
history.replace(`/identifier${history.location.search}${history.location.hash}`);
}
dispatch(receiveValidateLogon({})); // XXX(longsleep): hack to reset loading and errors.
}
dispatch(receiveValidateLogon({})); // XXX(longsleep): hack to reset loading and errors.
}
action = (allow = false, scopes = {}) => (event) => {
event.preventDefault();
action = (allow=false, scopes={}) => (event) => {
event.preventDefault();
if (allow === undefined) {
return;
}
if (allow === undefined) {
return;
// Convert all scopes which are true to a scope value.
const scope = Object.keys(scopes).filter(scope => {
return !!scopes[scope];
}).join(' ');
const {dispatch, history} = this.props;
dispatch(executeConsent(allow, scope)).then((response) => {
if (response.success) {
dispatch(advanceLogonFlow(response.success, history, true, {konnect: response.state}));
}
});
}
// Convert all scopes which are true to a scope value.
const scope = Object.keys(scopes).filter(scope => {
return !!scopes[scope];
}).join(' ');
render() {
const {classes, loading, hello, errors, client, t} = this.props;
const { dispatch, history } = this.props;
dispatch(executeConsent(allow, scope)).then((response) => {
if (response.success) {
dispatch(advanceLogonFlow(response.success, history, true, {konnect: response.state}));
}
});
}
const scopes = hello.details.scopes || {};
const meta = hello.details.meta || {};
render() {
const { classes, loading, hello, errors, client, t } = this.props;
return (
<DialogContent>
<h1 className={classes.header}>
{t("konnect.consent.headline", "Hi {{displayName}}", {displayName: hello.displayName})}
</h1>
<Typography variant="subtitle1" className={classes.subHeader + " oc-mb-m"}>
{hello.username}
</Typography>
const scopes = hello.details.scopes || {};
const meta = hello.details.meta || {};
<Typography variant="subtitle1" gutterBottom>
<Trans t={t} i18nKey="konnect.consent.message">
<Tooltip
placement="bottom"
title={t("konnect.consent.tooltip.client", 'Clicking "Allow" will redirect you to: {{redirectURI}}', {redirectURI: client.redirect_uri})}
>
<span className={'oc-font-weight-light'}><ClientDisplayName client={client}/></span>
</Tooltip> wants to
</Trans>
</Typography>
<ScopesList dense disablePadding className={classes.scopesList} scopes={scopes}
meta={meta.scopes}></ScopesList>
return (
<DialogContent>
<Typography variant="h5" component="h3" className="oc-light">
{t("konnect.consent.headline", "Hi {{displayName}}", { displayName: hello.displayName })}
</Typography>
<Typography variant="subtitle1" className={classes.subHeader + " oc-light oc-mb-m"}>
{hello.username}
</Typography>
<Typography variant="subtitle1" gutterBottom>
<Trans t={t} i18nKey="konnect.consent.question">
Allow <span className={'oc-font-weight-light'}><ClientDisplayName client={client}/></span> to do
this?
</Trans>
</Typography>
<Typography>
{t("konnect.consent.consequence", "By clicking Allow, you allow this app to use your information.")}
</Typography>
<Typography variant="subtitle1" gutterBottom className="oc-light">
<Trans t={t} i18nKey="konnect.consent.message">
<Tooltip
placement="bottom"
title={t("konnect.consent.tooltip.client", 'Clicking "Allow" will redirect you to: {{redirectURI}}', { redirectURI: client.redirect_uri })}
>
<em><ClientDisplayName client={client}/></em>
</Tooltip> wants to
</Trans>
</Typography>
<ScopesList dense disablePadding className={classes.scopesList} scopes={scopes} meta={meta.scopes}></ScopesList>
<form action="" onSubmit={this.action(undefined, scopes)}>
<DialogActions className={classes.dialogActions}>
<div className={classes.wrapper}>
<Button
color="secondary"
className={classes.button + ' oc-button-secondary'}
disabled={!!loading}
onClick={this.action(false, scopes)}
>
{t("konnect.consent.cancelButton.label", "Cancel")}
</Button>
{(loading && loading !== REQUEST_CONSENT_ALLOW) &&
<CircularProgress size={24} className={classes.buttonProgress}/>}
</div>
<div className={classes.wrapper}>
<Button
type="submit"
color="primary"
variant="contained"
className="oc-button-primary"
disabled={!!loading}
onClick={this.action(true, scopes)}
>
{t("konnect.consent.allowButton.label", "Allow")}
</Button>
{loading === REQUEST_CONSENT_ALLOW &&
<CircularProgress size={24} className={classes.buttonProgress}/>}
</div>
</DialogActions>
<Typography variant="subtitle1" gutterBottom className="oc-light">
<Trans t={t} i18nKey="konnect.consent.question">
Allow <em><ClientDisplayName client={client}/></em> to do this?
</Trans>
</Typography>
<Typography className="oc-light">
{t("konnect.consent.consequence", "By clicking Allow, you allow this app to use your information.")}
</Typography>
<form action="" onSubmit={this.action(undefined, scopes)}>
<DialogActions>
<div className={classes.wrapper}>
<Button
color="secondary"
className={classes.button}
disabled={!!loading}
onClick={this.action(false, scopes)}
>
{t("konnect.consent.cancelButton.label", "Cancel")}
</Button>
{(loading && loading !== REQUEST_CONSENT_ALLOW) &&
<CircularProgress size={24} className={classes.buttonProgress} />}
</div>
<div className={classes.wrapper}>
<Button
type="submit"
color="primary"
variant="contained"
className="oc-button-primary"
disabled={!!loading}
onClick={this.action(true, scopes)}
>
{t("konnect.consent.allowButton.label", "Allow")}
</Button>
{loading === REQUEST_CONSENT_ALLOW && <CircularProgress size={24} className={classes.buttonProgress} />}
</div>
</DialogActions>
{renderIf(errors.http)(() => (
<Typography variant="subtitle2" color="error" className={classes.message}>
<ErrorMessage error={errors.http}></ErrorMessage>
</Typography>
))}
</form>
</DialogContent>
);
}
{renderIf(errors.http)(() => (
<Typography variant="subtitle2" color="error" className={classes.message}>
<ErrorMessage error={errors.http}></ErrorMessage>
</Typography>
))}
</form>
</DialogContent>
);
}
}
Consent.propTypes = {
classes: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
classes: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
loading: PropTypes.string.isRequired,
errors: PropTypes.object.isRequired,
hello: PropTypes.object,
client: PropTypes.object.isRequired,
loading: PropTypes.string.isRequired,
errors: PropTypes.object.isRequired,
hello: PropTypes.object,
client: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
dispatch: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
};
const mapStateToProps = (state) => {
const { hello } = state.common;
const { loading, errors } = state.login;
const {hello} = state.common;
const {loading, errors} = state.login;
return {
loading: loading,
errors,
hello,
client: hello.details.client || {}
};
return {
loading: loading,
errors,
hello,
client: hello.details.client || {}
};
};
export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Consent)));

View File

@@ -1,207 +1,209 @@
import React, { useEffect, useMemo } from 'react';
import React, {useEffect, useMemo} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {connect} from 'react-redux';
import { useTranslation } from 'react-i18next';
import {useTranslation} from 'react-i18next';
import renderIf from 'render-if';
import { withStyles } from '@material-ui/core/styles';
import {withStyles} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import CircularProgress from '@material-ui/core/CircularProgress';
import green from '@material-ui/core/colors/green';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import Link from '@material-ui/core/Link';
import TextInput from '../../components/TextInput'
import { updateInput, executeLogonIfFormValid, advanceLogonFlow } from '../../actions/login';
import { ErrorMessage } from '../../errors';
import {updateInput, executeLogonIfFormValid, advanceLogonFlow} from '../../actions/login';
import {ErrorMessage} from '../../errors';
const styles = theme => ({
buttonProgress: {
color: green[500],
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginLeft: -12
},
subHeader: {
marginBottom: theme.spacing(3)
},
wrapper: {
position: 'relative',
width: '100%',
textAlign: 'center'
},
message: {
marginTop: 5,
marginBottom: 5
}
buttonProgress: {
color: green[500],
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginLeft: -12
},
main: {
width: '100%'
},
header: {
textAlign: 'center',
marginTop: 0,
marginBottom: theme.spacing(6)
},
wrapper: {
position: 'relative',
width: '100%',
textAlign: 'center'
},
message: {
marginTop: 5,
marginBottom: 5
}
});
function Login(props) {
const {
hello,
query,
dispatch,
history,
loading,
errors,
classes,
username,
password,
passwordResetLink,
} = props;
const {
hello,
query,
dispatch,
history,
loading,
errors,
classes,
username,
password,
passwordResetLink,
} = props;
const { t } = useTranslation();
const loginFailed = errors.http;
const hasError = errors.http || errors.username || errors.password;
const errorMessage = errors.http
? <ErrorMessage error={errors.http}></ErrorMessage>
: (errors.username
? <ErrorMessage error={errors.username}></ErrorMessage>
: <ErrorMessage error={errors.password}></ErrorMessage>);
const extraPropsUsername = {
"aria-invalid" : (errors.username || errors.http) ? 'true' : 'false'
};
const extraPropsPassword = {
"aria-invalid" : (errors.password || errors.http) ? 'true' : 'false',
};
const {t} = useTranslation();
const loginFailed = errors.http;
const hasError = errors.http || errors.username || errors.password;
const errorMessage = errors.http
? <ErrorMessage error={errors.http}></ErrorMessage>
: (errors.username
? <ErrorMessage error={errors.username}></ErrorMessage>
: <ErrorMessage error={errors.password}></ErrorMessage>);
const extraPropsUsername = {
"aria-invalid": (errors.username || errors.http) ? 'true' : 'false'
};
const extraPropsPassword = {
"aria-invalid": (errors.password || errors.http) ? 'true' : 'false',
};
if(errors.username || errors.http){
extraPropsUsername['extraClassName'] = 'error';
extraPropsUsername['aria-describedby'] = 'oc-login-error-message';
}
if(errors.password || errors.http){
extraPropsPassword['extraClassName'] = 'error';
extraPropsPassword['aria-describedby'] = 'oc-login-error-message';
}
useEffect(() => {
if (hello && hello.state && history.action !== 'PUSH') {
if (!query.prompt || query.prompt.indexOf('select_account') === -1) {
dispatch(advanceLogonFlow(true, history));
return;
}
history.replace(`/chooseaccount${history.location.search}${history.location.hash}`);
return;
if (errors.username || errors.http) {
extraPropsUsername['extraClassName'] = 'error';
extraPropsUsername['aria-describedby'] = 'oc-login-error-message';
}
});
const handleChange = (name) => (event) => {
dispatch(updateInput(name, event.target.value));
};
if (errors.password || errors.http) {
extraPropsPassword['extraClassName'] = 'error';
extraPropsPassword['aria-describedby'] = 'oc-login-error-message';
}
const handleNextClick = (event) => {
event.preventDefault();
useEffect(() => {
if (hello && hello.state && history.action !== 'PUSH') {
if (!query.prompt || query.prompt.indexOf('select_account') === -1) {
dispatch(advanceLogonFlow(true, history));
return;
}
dispatch(executeLogonIfFormValid(username, password, false)).then((response) => {
if (response.success) {
dispatch(advanceLogonFlow(response.success, history));
}
history.replace(`/chooseaccount${history.location.search}${history.location.hash}`);
return;
}
});
};
const usernamePlaceHolder = useMemo(() => {
if (hello?.details?.branding?.usernameHintText ) {
switch (hello.details.branding.usernameHintText) {
case "Username":
break;
case "Email":
return t("konnect.login.usernameField.placeholder.email", "Email");
case "Identity":
return t("konnect.login.usernameField.placeholder.identity", "Identity");
default:
return hello.details.branding.usernameHintText;
}
}
const handleChange = (name) => (event) => {
dispatch(updateInput(name, event.target.value));
};
return t("konnect.login.usernameField.placeholder.username", "Username");
}, [hello, t]);
const handleNextClick = (event) => {
event.preventDefault();
return (
<div>
<h1 className="oc-invisible-sr"> Login </h1>
<form action="" className="oc-login-form" onSubmit={(event) => handleNextClick(event)}>
<TextInput
autoFocus
autoCapitalize="off"
spellCheck="false"
value={username}
onChange={handleChange('username')}
autoComplete="kopano-account username"
placeholder={t("konnect.login.usernameField.label", "Username")}
label={t("konnect.login.usernameField.label", "Username")}
id="oc-login-username"
{...extraPropsUsername}
/>
<TextInput
type="password"
margin="normal"
onChange={handleChange('password')}
autoComplete="kopano-account current-password"
placeholder={t("konnect.login.passwordField.label", "Password")}
label={t("konnect.login.passwordField.label", "Password")}
id="oc-login-password"
{...extraPropsPassword}
/>
{hasError && <Typography id="oc-login-error-message" variant="subtitle2" component="span" color="error" className={classes.message}>{errorMessage}</Typography>}
<div className={classes.wrapper}>
{loginFailed && passwordResetLink && <Link id="oc-login-password-reset" href={passwordResetLink} variant="subtitle2">{"Reset password?"}</Link>}
<Button
type="submit"
color="primary"
variant="contained"
className="oc-button-primary oc-mt-l"
disabled={!!loading}
onClick={handleNextClick}
>
{t("konnect.login.nextButton.label", "Log in")}
</Button>
{loading && <CircularProgress size={24} className={classes.buttonProgress} />}
</div>
</form>
</div>
);
dispatch(executeLogonIfFormValid(username, password, false)).then((response) => {
if (response.success) {
dispatch(advanceLogonFlow(response.success, history));
}
});
};
const usernamePlaceHolder = useMemo(() => {
if (hello?.details?.branding?.usernameHintText) {
switch (hello.details.branding.usernameHintText) {
case "Username":
break;
case "Email":
return t("konnect.login.usernameField.placeholder.email", "Email");
case "Identity":
return t("konnect.login.usernameField.placeholder.identity", "Identity");
default:
return hello.details.branding.usernameHintText;
}
}
return t("konnect.login.usernameField.placeholder.username", "Username");
}, [hello, t]);
return (
<div className={classes.main}>
<h1 className={classes.header}> {t("konnect.login.headline", "Sign in")}</h1>
<form action="" className="oc-login-form" onSubmit={(event) => handleNextClick(event)}>
<TextInput
autoFocus
autoCapitalize="off"
spellCheck="false"
value={username}
onChange={handleChange('username')}
autoComplete="kopano-account username"
placeholder={t("konnect.login.usernameField.label", "Username")}
label={t("konnect.login.usernameField.label", "Username")}
id="oc-login-username"
{...extraPropsUsername}
/>
<TextInput
type="password"
margin="normal"
onChange={handleChange('password')}
autoComplete="kopano-account current-password"
placeholder={t("konnect.login.passwordField.label", "Password")}
label={t("konnect.login.passwordField.label", "Password")}
id="oc-login-password"
{...extraPropsPassword}
/>
{hasError && <Typography id="oc-login-error-message" variant="subtitle2" component="span" color="error"
className={classes.message}>{errorMessage}</Typography>}
<div className={classes.wrapper}>
{loginFailed && passwordResetLink && <Link id="oc-login-password-reset" href={passwordResetLink}
variant="subtitle2">{"Reset password?"}</Link>}
<Button
type="submit"
color="primary"
variant="contained"
className="oc-button-primary oc-mt-l"
disabled={!!loading}
onClick={handleNextClick}
>
{t("konnect.login.nextButton.label", "Log in")}
</Button>
{loading && <CircularProgress size={24} className={classes.buttonProgress}/>}
</div>
</form>
</div>
);
}
Login.propTypes = {
classes: PropTypes.object.isRequired,
classes: PropTypes.object.isRequired,
loading: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
password: PropTypes.string.isRequired,
passwordResetLink: PropTypes.string.isRequired,
errors: PropTypes.object.isRequired,
branding: PropTypes.object,
hello: PropTypes.object,
query: PropTypes.object.isRequired,
loading: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
password: PropTypes.string.isRequired,
passwordResetLink: PropTypes.string.isRequired,
errors: PropTypes.object.isRequired,
branding: PropTypes.object,
hello: PropTypes.object,
query: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
dispatch: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
};
const mapStateToProps = (state) => {
const { loading, username, password, errors} = state.login;
const { branding, hello, query, passwordResetLink } = state.common;
const {loading, username, password, errors} = state.login;
const {branding, hello, query, passwordResetLink} = state.common;
return {
loading,
username,
password,
errors,
branding,
hello,
query,
passwordResetLink
};
return {
loading,
username,
password,
errors,
branding,
hello,
query,
passwordResetLink
};
};
export default connect(mapStateToProps)(withStyles(styles)(Login));

View File

@@ -29,14 +29,14 @@ class Welcomescreen extends React.PureComponent {
const loading = hello === null;
return (
<ResponsiveScreen loading={loading} branding={branding}>
<Typography variant="h5" component="h3" className="oc-light" >
<Typography variant="h5" component="h3">
{t("konnect.welcome.headline", "Welcome {{displayName}}", {displayName: hello.displayName})}
</Typography>
<Typography variant="subtitle1" className={classes.subHeader + " oc-light"}>
<Typography variant="subtitle1" className={classes.subHeader}>
{hello.username}
</Typography>
<Typography gutterBottom className="oc-light">
<Typography gutterBottom>
{t("konnect.welcome.message", "You are signed in - awesome!")}
</Typography>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="2.83 2.84 21.45 27.82">
<polygon
points="13.55 23.12 14.72 22.45 14.72 17.57 18.92 15.14 18.92 13.8 17.75 13.12 13.53 15.56 9.36 13.16 8.19 13.83 8.19 15.18 12.39 17.6 12.39 22.45 13.55 23.12"
fill="#e2baff"/>
<polygon points="24.28 9.02 13.56 2.84 13.56 2.84 13.56 2.84 2.83 9.02 2.83 11.72 13.56 5.53 24.28 11.72 24.28 9.02"
fill="#e2baff"/>
<polygon
points="24.28 21.78 13.56 27.97 2.83 21.78 2.83 24.48 13.56 30.66 13.56 30.66 13.56 30.66 24.28 24.48 24.28 21.78"
fill="#e2baff"/>
</svg>

After

Width:  |  Height:  |  Size: 613 B