Initial QSfera import
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import React, {ReactElement, Suspense, lazy, useState, useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {MuiThemeProvider} from '@material-ui/core/styles';
|
||||
import muiTheme from './theme';
|
||||
|
||||
import Spinner from './components/Spinner';
|
||||
import * as version from './version';
|
||||
import {QsferaContext} from './qsferaContext';
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
|
||||
if (loading) {
|
||||
return <Spinner/>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<QsferaContext.Provider value={{theme, config}}>
|
||||
<div
|
||||
className={`oc-login-bg ${bgImg ? 'oc-login-bg-image' : ''}`}
|
||||
style={{backgroundImage: bgImg ? `url(${bgImg})` : undefined}}
|
||||
>
|
||||
<MuiThemeProvider theme={muiTheme}>
|
||||
<Suspense fallback={<Spinner/>}>
|
||||
<LazyMain/>
|
||||
</Suspense>
|
||||
</MuiThemeProvider>
|
||||
{!bgImg &&
|
||||
<img
|
||||
src={`${process.env.PUBLIC_URL}/static/icon-lilac.svg`}
|
||||
className={'oc-login-bg-icon'}
|
||||
alt=''
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</QsferaContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
bgImg: PropTypes.string
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import Routes from './Routes';
|
||||
|
||||
class Main extends PureComponent {
|
||||
render() {
|
||||
const { hello, pathPrefix } = this.props;
|
||||
|
||||
return (
|
||||
<BrowserRouter basename={pathPrefix}>
|
||||
<Routes hello={hello}/>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
reload(event) {
|
||||
event.preventDefault();
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
Main.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
|
||||
hello: PropTypes.object,
|
||||
updateAvailable: PropTypes.bool.isRequired,
|
||||
pathPrefix: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { hello, updateAvailable, pathPrefix } = state.common;
|
||||
|
||||
return {
|
||||
hello,
|
||||
updateAvailable,
|
||||
pathPrefix
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Main);
|
||||
@@ -0,0 +1,8 @@
|
||||
all: images
|
||||
|
||||
.PHONY: images
|
||||
images:
|
||||
@$(MAKE) -C images
|
||||
|
||||
clean:
|
||||
@$(MAKE) -C images clean
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { ReactElement, lazy } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import PrivateRoute from './components/PrivateRoute';
|
||||
|
||||
const AsyncLogin = lazy(() =>
|
||||
import(/* webpackChunkName: "containers-login" */ './containers/Login'));
|
||||
const AsyncWelcome = lazy(() =>
|
||||
import(/* webpackChunkName: "containers-welcome" */ './containers/Welcome'));
|
||||
const AsyncGoodbye = lazy(() =>
|
||||
import(/* webpackChunkName: "containers-goodbye" */ './containers/Goodbye'));
|
||||
|
||||
const Routes = ({ hello }: {hello: PropTypes.object}): ReactElement => (
|
||||
<Switch>
|
||||
<PrivateRoute
|
||||
path="/welcome"
|
||||
exact
|
||||
component={AsyncWelcome}
|
||||
hello={hello}
|
||||
/>
|
||||
<Route
|
||||
path="/goodbye"
|
||||
exact
|
||||
component={AsyncGoodbye}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
component={AsyncLogin}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
|
||||
Routes.propTypes = {
|
||||
hello: PropTypes.object
|
||||
};
|
||||
|
||||
export default Routes;
|
||||
@@ -0,0 +1,133 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { newHelloRequest } from '../models/hello';
|
||||
import { withClientRequestState } from '../utils';
|
||||
import {
|
||||
ExtendedError,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATE
|
||||
} from '../errors';
|
||||
|
||||
import { handleAxiosError } from './utils';
|
||||
import * as types from './types';
|
||||
|
||||
export function receiveError(error) {
|
||||
return {
|
||||
type: types.RECEIVE_ERROR,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
export function resetHello() {
|
||||
return {
|
||||
type: types.RESET_HELLO
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveHello(hello) {
|
||||
const { success, username, displayName } = hello;
|
||||
|
||||
return {
|
||||
type: types.RECEIVE_HELLO,
|
||||
state: success === true,
|
||||
username,
|
||||
displayName,
|
||||
hello
|
||||
};
|
||||
}
|
||||
|
||||
export function executeHello() {
|
||||
return function(dispatch, getState) {
|
||||
dispatch(resetHello());
|
||||
|
||||
const { flow, query } = getState().common;
|
||||
|
||||
const r = withClientRequestState(newHelloRequest(flow, query));
|
||||
return axios.post('./identifier/_/hello', r, {
|
||||
headers: {
|
||||
'Kopano-Konnect-XSRF': '1'
|
||||
}
|
||||
}).then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
// success.
|
||||
return response.data;
|
||||
case 204:
|
||||
// not signed-in.
|
||||
return {
|
||||
success: false,
|
||||
state: response.headers['kopano-konnect-state']
|
||||
};
|
||||
default:
|
||||
// error.
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response);
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.state !== r.state) {
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response);
|
||||
}
|
||||
|
||||
dispatch(receiveHello(response));
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
error = handleAxiosError(error);
|
||||
|
||||
dispatch(receiveError(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function retryHello() {
|
||||
return function(dispatch) {
|
||||
dispatch(receiveError(null));
|
||||
|
||||
return dispatch(executeHello());
|
||||
};
|
||||
}
|
||||
|
||||
export function requestLogoff() {
|
||||
return {
|
||||
type: types.REQUEST_LOGOFF
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveLogoff(state) {
|
||||
return {
|
||||
type: types.RECEIVE_LOGOFF,
|
||||
state
|
||||
};
|
||||
}
|
||||
|
||||
export function executeLogoff() {
|
||||
return function(dispatch) {
|
||||
dispatch(resetHello());
|
||||
dispatch(requestLogoff());
|
||||
|
||||
const r = withClientRequestState({});
|
||||
return axios.post('./identifier/_/logoff', r, {
|
||||
headers: {
|
||||
'Kopano-Konnect-XSRF': '1'
|
||||
}
|
||||
}).then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
// success.
|
||||
return response.data;
|
||||
default:
|
||||
// error.
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response);
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.state !== r.state) {
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response);
|
||||
}
|
||||
|
||||
dispatch(receiveLogoff(response.success === true));
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
error = handleAxiosError(error);
|
||||
|
||||
dispatch(receiveError(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import axios from 'axios';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { newHelloRequest } from '../models/hello';
|
||||
import { withClientRequestState } from '../utils';
|
||||
import {
|
||||
ExtendedError,
|
||||
ERROR_LOGIN_VALIDATE_MISSINGUSERNAME,
|
||||
ERROR_LOGIN_VALIDATE_MISSINGPASSWORD,
|
||||
ERROR_LOGIN_FAILED,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATE
|
||||
} from '../errors';
|
||||
|
||||
import * as types from './types';
|
||||
import { receiveHello } from './common';
|
||||
import { handleAxiosError } from './utils';
|
||||
|
||||
// Modes for logon.
|
||||
export const ModeLogonUsernameEmptyPasswordCookie = '0';
|
||||
export const ModeLogonUsernamePassword = '1';
|
||||
|
||||
export function updateInput(name, value) {
|
||||
return {
|
||||
type: types.UPDATE_INPUT,
|
||||
name,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveValidateLogon(errors) {
|
||||
return {
|
||||
type: types.RECEIVE_VALIDATE_LOGON,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
export function requestLogon(username, password) {
|
||||
return {
|
||||
type: types.REQUEST_LOGON,
|
||||
username,
|
||||
password
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveLogon(logon) {
|
||||
const { success, errors } = logon;
|
||||
|
||||
return {
|
||||
type: types.RECEIVE_LOGON,
|
||||
success,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
export function requestConsent(allow=false) {
|
||||
return {
|
||||
type: allow ? types.REQUEST_CONSENT_ALLOW : types.REQUEST_CONSENT_CANCEL
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveConsent(logon) {
|
||||
const { success, errors } = logon;
|
||||
|
||||
return {
|
||||
type: types.RECEIVE_CONSENT,
|
||||
success,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
export function executeLogon(username, password, mode=ModeLogonUsernamePassword) {
|
||||
return function(dispatch, getState) {
|
||||
dispatch(requestLogon(username, password));
|
||||
dispatch(receiveHello({
|
||||
username
|
||||
})); // Reset any hello state on logon.
|
||||
|
||||
const { flow, query } = getState().common;
|
||||
|
||||
// Prepare params based on mode.
|
||||
const params = [];
|
||||
switch (mode) {
|
||||
case ModeLogonUsernamePassword:
|
||||
// Username with password.
|
||||
params.push(username, password, mode);
|
||||
break;
|
||||
|
||||
case ModeLogonUsernameEmptyPasswordCookie:
|
||||
// Username with empty password - this only works when the user is already signed in.
|
||||
params.push(username, '', mode);
|
||||
break;
|
||||
|
||||
// no default
|
||||
}
|
||||
|
||||
const r = withClientRequestState({
|
||||
params: params,
|
||||
hello: newHelloRequest(flow, query)
|
||||
});
|
||||
return axios.post('./identifier/_/logon', r, {
|
||||
headers: {
|
||||
'Kopano-Konnect-XSRF': '1'
|
||||
}
|
||||
}).then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
// success.
|
||||
return response.data;
|
||||
case 204:
|
||||
// login failed.
|
||||
return {
|
||||
success: false,
|
||||
state: response.headers['kopano-konnect-state'],
|
||||
errors: {
|
||||
http: new Error(ERROR_LOGIN_FAILED)
|
||||
}
|
||||
};
|
||||
default:
|
||||
// error.
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response);
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.state !== r.state) {
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response);
|
||||
}
|
||||
|
||||
let { hello } = response;
|
||||
if (!hello) {
|
||||
hello = {
|
||||
success: response.success,
|
||||
username
|
||||
};
|
||||
}
|
||||
dispatch(receiveHello(hello));
|
||||
dispatch(receiveLogon(response));
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
error = handleAxiosError(error);
|
||||
const errors = {
|
||||
http: error
|
||||
};
|
||||
|
||||
dispatch(receiveValidateLogon(errors));
|
||||
return {
|
||||
success: false,
|
||||
errors: errors
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function executeConsent(allow=false, scope='') {
|
||||
return function(dispatch, getState) {
|
||||
dispatch(requestConsent(allow));
|
||||
|
||||
const { query } = getState().common;
|
||||
|
||||
const r = withClientRequestState({
|
||||
allow,
|
||||
scope,
|
||||
client_id: query.client_id || '', // eslint-disable-line camelcase
|
||||
redirect_uri: query.redirect_uri || '', // eslint-disable-line camelcase
|
||||
ref: query.state || '',
|
||||
flow_nonce: query.nonce || '' // eslint-disable-line camelcase
|
||||
});
|
||||
return axios.post('./identifier/_/consent', r, {
|
||||
headers: {
|
||||
'Kopano-Konnect-XSRF': '1'
|
||||
}
|
||||
}).then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
// success.
|
||||
return response.data;
|
||||
case 204:
|
||||
// cancel reply.
|
||||
return {
|
||||
success: true,
|
||||
state: response.headers['kopano-konnect-state']
|
||||
};
|
||||
default:
|
||||
// error.
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response);
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.state !== r.state) {
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response);
|
||||
}
|
||||
|
||||
dispatch(receiveConsent(response));
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
error = handleAxiosError(error);
|
||||
const errors = {
|
||||
http: error
|
||||
};
|
||||
|
||||
dispatch(receiveValidateLogon(errors));
|
||||
return {
|
||||
success: false,
|
||||
errors: errors
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function validateUsernamePassword(username, password, isSignedIn) {
|
||||
return function(dispatch) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const errors = {};
|
||||
|
||||
if (!username) {
|
||||
errors.username = new Error(ERROR_LOGIN_VALIDATE_MISSINGUSERNAME);
|
||||
}
|
||||
if (!password && !isSignedIn) {
|
||||
errors.password = new Error(ERROR_LOGIN_VALIDATE_MISSINGPASSWORD);
|
||||
}
|
||||
|
||||
dispatch(receiveValidateLogon(errors));
|
||||
if (Object.keys(errors).length === 0) {
|
||||
resolve(errors);
|
||||
} else {
|
||||
reject(errors);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function executeLogonIfFormValid(username, password, isSignedIn) {
|
||||
return (dispatch) => {
|
||||
return dispatch(
|
||||
validateUsernamePassword(username, password, isSignedIn)
|
||||
).then(() => {
|
||||
const mode = isSignedIn ? ModeLogonUsernameEmptyPasswordCookie : ModeLogonUsernamePassword;
|
||||
return dispatch(executeLogon(username, password, mode));
|
||||
}).catch((errors) => {
|
||||
return {
|
||||
success: false,
|
||||
errors: errors
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function advanceLogonFlow(success, history, done=false, extraQuery={}) {
|
||||
return (dispatch, getState) => {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { flow, query, hello } = getState().common;
|
||||
const q = Object.assign({}, query, extraQuery);
|
||||
|
||||
switch (flow) {
|
||||
case 'oauth':
|
||||
case 'consent':
|
||||
case 'oidc':
|
||||
if (hello.details.flow !== flow) {
|
||||
// Ignore requested flow if hello flow does not match.
|
||||
break;
|
||||
}
|
||||
|
||||
if (!done && hello.details.next === 'consent') {
|
||||
history.replace(`/consent${history.location.search}${history.location.hash}`);
|
||||
return;
|
||||
}
|
||||
if (hello.details.continue_uri) {
|
||||
q.prompt = 'none';
|
||||
window.location.replace(hello.details.continue_uri + '?' + queryString.stringify(q));
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Legacy stupid modes.
|
||||
if (q.continue && q.continue.indexOf(document.location.origin) === 0) {
|
||||
window.location.replace(q.continue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Default action.
|
||||
let target = '/welcome';
|
||||
if (history.action === 'REPLACE') {
|
||||
target = target + history.location.search + history.location.hash;
|
||||
}
|
||||
|
||||
dispatch(receiveValidateLogon({})); // XXX(longsleep): hack to reset loading and errors.
|
||||
history.push(target);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export const RECEIVE_ERROR = 'RECEIVE_ERROR';
|
||||
|
||||
export const RESET_HELLO = 'RESET_HELLO';
|
||||
export const EXECUTE_HELLO = 'EXECUTE_HELLO';
|
||||
export const RECEIVE_HELLO = 'RECEIVE_HELLO';
|
||||
|
||||
export const RECEIVE_VALIDATE_LOGON = 'RECEIVE_VALIDATE_LOGON';
|
||||
export const REQUEST_LOGON = 'REQUEST_LOGON';
|
||||
export const EXECUTE_LOGON = 'EXECUTE_LOGON';
|
||||
export const RECEIVE_LOGON = 'RECEIVE_LOGON';
|
||||
export const UPDATE_INPUT = 'UPDATE_INPUT';
|
||||
|
||||
export const REQUEST_CONSENT_ALLOW = 'REQUEST_CONSENT_ALLOW';
|
||||
export const REQUEST_CONSENT_CANCEL = 'REQUEST_CONSENT_CANCEL';
|
||||
export const EXECUTE_CONSENT = 'EXECUTE_CONSENT';
|
||||
export const RECEIVE_CONSENT = 'RECEIVE_CONSENT';
|
||||
|
||||
export const REQUEST_LOGOFF = 'REQUEST_LOGOFF';
|
||||
export const EXECUTE_LOGOFF = 'EXECUTE_LOGOFF';
|
||||
export const RECEIVE_LOGOFF = 'RECEIVE_LOGOFF';
|
||||
|
||||
export const SERVICE_WORKER_NEW_CONTENT = 'SERVICE_WORKER_NEW_CONTENT';
|
||||
export const SERVICE_WORKER_READY = 'SERVICE_WORKER_READY';
|
||||
export const SERVICE_WORKER_ERROR = 'SERVICE_WORKER_ERROR';
|
||||
export const SERVICE_WORKER_OFFLINE = 'SERVICE_WORKER_OFFLINE';
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
ExtendedError,
|
||||
ERROR_HTTP_NETWORK_ERROR,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS
|
||||
} from '../errors';
|
||||
|
||||
export function handleAxiosError(error) {
|
||||
if (error.request) {
|
||||
// Axios errors.
|
||||
if (error.response) {
|
||||
error = new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, error.response);
|
||||
} else {
|
||||
error = new ExtendedError(ERROR_HTTP_NETWORK_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
@font-face {
|
||||
font-family: QSfera;
|
||||
src: url('./fonts/QSfera500-Regular.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: QSfera;
|
||||
src: url('./fonts/QSfera750-Bold.woff2') format('woff2');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
font-feature-settings: "cv11";
|
||||
color: #20434f !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: QSfera, sans-serif;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.oc-font-weight-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.oc-login-bg {
|
||||
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;
|
||||
}
|
||||
|
||||
.oc-logo {
|
||||
height: 80px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.oc-logo-container {
|
||||
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;
|
||||
}
|
||||
|
||||
.oc-progress > div {
|
||||
/* Needs to be important to overwrite material-ui */
|
||||
background-color: #4a76ac !important;
|
||||
}
|
||||
|
||||
.oc-input {
|
||||
border: 1px solid #20434f;
|
||||
border-radius: 4px;
|
||||
height: 45px;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.oc-label {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.oc-input.error {
|
||||
outline: none;
|
||||
border: 1px solid #fe4600;
|
||||
}
|
||||
|
||||
.MuiTypography-colorError {
|
||||
color: #fe4600 !important;
|
||||
}
|
||||
|
||||
.oc-input:focus {
|
||||
outline: 2px solid #e2baff;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.oc-input + .oc-input {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.MuiTouchRipple-root {
|
||||
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;
|
||||
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-radius: 100px !important;
|
||||
color: #20434f !important;
|
||||
box-shadow: none !important;
|
||||
font-weight: 700 !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;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.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;
|
||||
}
|
||||
|
||||
.oc-mb-m {
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.oc-login-form div:not(:last-of-type) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Special SR classes
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ClientDisplayName = ({ client, ...rest }) => (
|
||||
<span {...rest}>{client.display_name ? client.display_name : client.id}</span>
|
||||
);
|
||||
|
||||
ClientDisplayName.propTypes = {
|
||||
client: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default ClientDisplayName;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { retryHello } from '../actions/common';
|
||||
import { ErrorMessage } from '../errors';
|
||||
|
||||
class Loading extends React.PureComponent {
|
||||
render() {
|
||||
const { error, t } = this.props;
|
||||
|
||||
return (
|
||||
<Grid item align="center">
|
||||
{error === null && (
|
||||
<LinearProgress className="oc-progress" />
|
||||
)}
|
||||
{error !== null && (
|
||||
<div>
|
||||
<Typography variant="h5" gutterBottom align="center">
|
||||
{t("konnect.loading.error.headline", "Failed to connect to server")}
|
||||
</Typography>
|
||||
<Typography align="center" color="error">
|
||||
<ErrorMessage error={error}></ErrorMessage>
|
||||
</Typography>
|
||||
<Button
|
||||
autoFocus
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
className="oc-button-primary oc-mt-l"
|
||||
onClick={(event) => this.retry(event)}
|
||||
>
|
||||
{t("konnect.login.retryButton.label", "Retry")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
retry(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch(retryHello());
|
||||
}
|
||||
}
|
||||
|
||||
Loading.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
|
||||
error: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { error } = state.common;
|
||||
|
||||
return {
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withTranslation()(Loading));
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
|
||||
import allLocales from '../locales';
|
||||
|
||||
function LocaleSelect({ locales: localesProp, ...other } = {}) {
|
||||
const { i18n, ready } = useTranslation();
|
||||
|
||||
const handleChange = useCallback((event) => {
|
||||
i18n.changeLanguage(event.target.value);
|
||||
}, [ i18n ])
|
||||
|
||||
const locales = useMemo(() => {
|
||||
if (!localesProp) {
|
||||
return allLocales;
|
||||
}
|
||||
const supported = allLocales.filter(locale => {
|
||||
return localesProp.includes(locale.locale);
|
||||
});
|
||||
return supported;
|
||||
}, [localesProp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (locales) {
|
||||
const found = locales.find((locale) => {
|
||||
return locale.locale === i18n.language;
|
||||
});
|
||||
if (found) {
|
||||
// Have language -> is supported all good.
|
||||
return;
|
||||
}
|
||||
const wanted = i18n.modules.languageDetector.detectors.navigator.lookup();
|
||||
i18n.modules.languageDetector.services.languageUtils.options.supportedLngs = locales.map(locale => locale.locale);
|
||||
i18n.modules.languageDetector.services.languageUtils.options.fallbackLng = null;
|
||||
|
||||
let best = i18n.modules.languageDetector.services.languageUtils.getBestMatchFromCodes(wanted);
|
||||
if (!best) {
|
||||
best = locales[0].locale;
|
||||
}
|
||||
|
||||
// Auto change language to best one found if the current selected one is not enabled.
|
||||
if (i18n.language !== best) {
|
||||
i18n.changeLanguage(best);
|
||||
}
|
||||
}
|
||||
}, [i18n, locales]);
|
||||
|
||||
if (!ready || !locales || locales.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Select
|
||||
value={i18n.language}
|
||||
onChange={handleChange}
|
||||
{...other}
|
||||
>
|
||||
{locales.map(language => {
|
||||
return <MenuItem
|
||||
key={language.locale}
|
||||
value={language.locale}>
|
||||
{language.nativeName}
|
||||
</MenuItem>;
|
||||
})}
|
||||
</Select>;
|
||||
}
|
||||
|
||||
LocaleSelect.propTypes = {
|
||||
locales: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
export default LocaleSelect;
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
import RedirectWithQuery from './RedirectWithQuery';
|
||||
|
||||
const PrivateRoute = ({ component: Target, hello, ...rest }) => (
|
||||
<Route {...rest} render={props => (
|
||||
hello ? (
|
||||
<Target {...props}/>
|
||||
) : (
|
||||
<RedirectWithQuery target='/identifier' />
|
||||
)
|
||||
)}/>
|
||||
);
|
||||
|
||||
PrivateRoute.propTypes = {
|
||||
component: PropTypes.elementType.isRequired,
|
||||
hello: PropTypes.object
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
const RedirectWithQuery = ({target, location, ...rest}) => {
|
||||
const to = {
|
||||
pathname: target,
|
||||
search: location.search,
|
||||
hash: location.hash
|
||||
};
|
||||
|
||||
return (
|
||||
<Redirect to={to} {...rest}></Redirect>
|
||||
);
|
||||
};
|
||||
|
||||
RedirectWithQuery.propTypes = {
|
||||
target: PropTypes.string.isRequired,
|
||||
location: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withRouter(RedirectWithQuery);
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import withMobileDialog from '@material-ui/core/withMobileDialog';
|
||||
|
||||
const ResponsiveDialog = (props) => {
|
||||
return <Dialog {...props}/>;
|
||||
};
|
||||
|
||||
ResponsiveDialog.propTypes = {
|
||||
fullScreen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default withMobileDialog()(ResponsiveDialog);
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
|
||||
import Loading from './Loading';
|
||||
import { QsferaContext } from "../qsferaContext";
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
zIndex: 999
|
||||
},
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
actions: {
|
||||
marginTop: -40,
|
||||
justifyContent: 'flex-start',
|
||||
paddingLeft: theme.spacing(3),
|
||||
paddingRight: theme.spacing(3)
|
||||
},
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
alignItems: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
const ResponsiveScreen = (props) => {
|
||||
const {
|
||||
classes,
|
||||
withoutLogo,
|
||||
withoutPadding,
|
||||
loading,
|
||||
children,
|
||||
className,
|
||||
branding,
|
||||
...other
|
||||
} = props;
|
||||
const { theme } = useContext(QsferaContext);
|
||||
|
||||
const logo = (theme && !withoutLogo) ? (
|
||||
<img src={'/' + theme.common?.logo} className="oc-logo" alt="КуСфера logo"/>
|
||||
) : null;
|
||||
|
||||
const content = loading ? <Loading/> : (withoutPadding ? children : <DialogContent>{children}</DialogContent>);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
spacing={0}
|
||||
className={[classes.root, className].filter(Boolean).join(' ')}
|
||||
{...other}
|
||||
>
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.content}>
|
||||
{branding?.signinPageLogoURI ? (
|
||||
<a
|
||||
href={branding.signinPageLogoURI}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className='oc-logo-container'
|
||||
>
|
||||
{logo}
|
||||
</a>
|
||||
) : (
|
||||
<div className='oc-logo-container'>
|
||||
{logo}
|
||||
</div>
|
||||
)}
|
||||
<div className={"oc-card"}>
|
||||
<div className={"oc-card-body"}>{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="oc-footer-message">
|
||||
<Trans i18nKey="konnect.footer.slogan">
|
||||
<strong>КуСфера</strong>
|
||||
</Trans>
|
||||
</footer>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
ResponsiveScreen.defaultProps = {
|
||||
withoutLogo: false,
|
||||
withoutPadding: false,
|
||||
loading: false
|
||||
};
|
||||
|
||||
ResponsiveScreen.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
withoutLogo: PropTypes.bool,
|
||||
withoutPadding: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
branding: PropTypes.object,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
PaperProps: PropTypes.object,
|
||||
DialogProps: PropTypes.object
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ResponsiveScreen);
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import PropTypes from 'prop-types';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const styles = () => ({
|
||||
row: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
}
|
||||
});
|
||||
|
||||
const ScopesList = ({scopes, meta, classes, ...rest}) => {
|
||||
const { mapping, definitions } = meta;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const rows = [];
|
||||
const known = {};
|
||||
|
||||
// TODO(longsleep): Sort scopes according to priority.
|
||||
for (let scope in scopes) {
|
||||
if (!scopes[scope]) {
|
||||
continue;
|
||||
}
|
||||
let id = mapping[scope];
|
||||
if (id) {
|
||||
if (known[id]) {
|
||||
continue;
|
||||
}
|
||||
known[id] = true;
|
||||
} else {
|
||||
id = scope;
|
||||
}
|
||||
let definition = definitions[id];
|
||||
let label;
|
||||
if (definition) {
|
||||
switch (definition.id) {
|
||||
case 'scope_alias_basic':
|
||||
label = t("konnect.scopeDescription.aliasBasic", "Access your basic account information");
|
||||
break;
|
||||
case 'scope_offline_access':
|
||||
label = t("konnect.scopeDescription.offlineAccess", "Keep the allowed access persistently and forever");
|
||||
break;
|
||||
default:
|
||||
}
|
||||
if (!label) {
|
||||
label = definition.description;
|
||||
}
|
||||
}
|
||||
if (!label) {
|
||||
label = t("konnect.scopeDescription.scope", "Scope: {{scope}}", { scope });
|
||||
}
|
||||
|
||||
rows.push(
|
||||
<ListItem
|
||||
disableGutters
|
||||
dense
|
||||
key={id}
|
||||
className={classes.row}
|
||||
><Checkbox
|
||||
checked
|
||||
disableRipple
|
||||
disabled
|
||||
/>
|
||||
<ListItemText primary={label} />
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<List {...rest}>
|
||||
{rows}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
ScopesList.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
|
||||
scopes: PropTypes.object.isRequired,
|
||||
meta: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ScopesList);
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Fade,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
},
|
||||
}));
|
||||
|
||||
const Spinner = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return <div className={classes.root}>
|
||||
<Fade
|
||||
in
|
||||
style={{
|
||||
transitionDelay: '800ms',
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<CircularProgress size={70} thickness={1}/>
|
||||
</Fade>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default Spinner;
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
const TextInput = (props) => {
|
||||
const label = props.label;
|
||||
const extraClassName = props.extraClassName;
|
||||
|
||||
delete props.label;
|
||||
delete props.extraClassName;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="oc-label"
|
||||
htmlFor={props.id}>{label}</label>
|
||||
<input className={`oc-input ${extraClassName ? extraClassName : ''}`} {...props}
|
||||
placeholder={props.placeholder ? props.placeholder : null}/>
|
||||
</div>);
|
||||
};
|
||||
|
||||
TextInput.propTypes = {
|
||||
placeholder: PropTypes.object,
|
||||
label: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
extraClassName: PropTypes.string,
|
||||
}
|
||||
|
||||
export default TextInput;
|
||||
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
|
||||
import ResponsiveScreen from '../../components/ResponsiveScreen';
|
||||
import { executeHello, executeLogoff } from '../../actions/common';
|
||||
|
||||
const styles = theme => ({
|
||||
subHeader: {
|
||||
marginBottom: theme.spacing(5)
|
||||
},
|
||||
wrapper: {
|
||||
marginTop: theme.spacing(5),
|
||||
position: 'relative',
|
||||
display: 'inline-block'
|
||||
}
|
||||
});
|
||||
|
||||
class Goodbyescreen extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(executeHello());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, branding, hello, t } = this.props;
|
||||
|
||||
const loading = hello === null;
|
||||
return (
|
||||
<ResponsiveScreen loading={loading} branding={branding}>
|
||||
{hello !== null && !hello.state && (
|
||||
<div>
|
||||
<Typography variant="h5" component="h3">
|
||||
{t("konnect.goodbye.headline", "Goodbye")}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" className={classes.subHeader}>
|
||||
{t("konnect.goodbye.subHeader", "you have been signed out from your account")}
|
||||
</Typography>
|
||||
<Typography gutterBottom>
|
||||
{t("konnect.goodbye.message.close", "You can close this window now.")}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{hello !== null && hello.state === true && (
|
||||
<div>
|
||||
<Typography variant="h5" component="h3">
|
||||
{t("konnect.goodbye.confirm.headline", "Hello {{displayName}}", { displayName: hello.displayName })}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" className={classes.subHeader}>
|
||||
{t("konnect.goodbye.confirm.subHeader", "please confirm sign out")}
|
||||
</Typography>
|
||||
|
||||
<Typography gutterBottom>
|
||||
{t("konnect.goodbye.message.confirm", "Press the button below, to sign out from your account now.")}
|
||||
</Typography>
|
||||
|
||||
<DialogActions>
|
||||
<div className={classes.wrapper}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
className={classes.button}
|
||||
onClick={(event) => this.logoff(event)}
|
||||
>
|
||||
{t("konnect.goodbye.signoutButton.label", "Sign out")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
</div>
|
||||
)}
|
||||
</ResponsiveScreen>
|
||||
);
|
||||
}
|
||||
|
||||
logoff(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch(executeLogoff()).then((response) => {
|
||||
const { history } = this.props;
|
||||
|
||||
if (response.success) {
|
||||
this.props.dispatch(executeHello());
|
||||
history.push('/goodbye');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Goodbyescreen.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
|
||||
branding: PropTypes.object,
|
||||
hello: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { branding, hello } = state.common;
|
||||
|
||||
return {
|
||||
branding,
|
||||
hello
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Goodbyescreen)));
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Goodbyescreen';
|
||||
@@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
|
||||
import { executeLogonIfFormValid, advanceLogonFlow } from '../../actions/login';
|
||||
import { ErrorMessage } from '../../errors';
|
||||
|
||||
const styles = theme => ({
|
||||
content: {
|
||||
overflowY: 'visible',
|
||||
},
|
||||
subHeader: {
|
||||
marginBottom: theme.spacing(2)
|
||||
},
|
||||
message: {
|
||||
marginTop: theme.spacing(2)
|
||||
},
|
||||
accountList: {
|
||||
marginLeft: theme.spacing(-5),
|
||||
marginRight: theme.spacing(-5)
|
||||
},
|
||||
accountListItem: {
|
||||
paddingLeft: theme.spacing(5),
|
||||
paddingRight: theme.spacing(5)
|
||||
}
|
||||
});
|
||||
|
||||
class Chooseaccount extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { hello, history } = this.props;
|
||||
if ((!hello || !hello.state) && history.action !== 'PUSH') {
|
||||
history.replace(`/identifier${history.location.search}${history.location.hash}`);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errors, classes, hello, t } = this.props;
|
||||
|
||||
let errorMessage = null;
|
||||
if (errors.http) {
|
||||
errorMessage = <Typography color="error" className={classes.message}>
|
||||
<ErrorMessage error={errors.http}></ErrorMessage>
|
||||
</Typography>;
|
||||
}
|
||||
|
||||
let username = '';
|
||||
if (hello && hello.state) {
|
||||
username = hello.username;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogContent className={classes.content}>
|
||||
<Typography variant="h5" component="h3">
|
||||
{t("konnect.chooseaccount.headline", "Choose an account")}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" className={classes.subHeader}>
|
||||
{t("konnect.chooseaccount.subHeader", "to sign in")}
|
||||
</Typography>
|
||||
|
||||
<form action="" onSubmit={(event) => this.logon(event)}>
|
||||
<List disablePadding className={classes.accountList}>
|
||||
<ListItem
|
||||
button
|
||||
disableGutters
|
||||
className={classes.accountListItem}
|
||||
disabled={!!loading}
|
||||
onClick={(event) => this.logon(event)}
|
||||
><ListItemAvatar><Avatar>{username.substr(0, 1)}</Avatar></ListItemAvatar>
|
||||
<ListItemText primary={username} />
|
||||
</ListItem>
|
||||
<ListItem
|
||||
button
|
||||
disableGutters
|
||||
className={classes.accountListItem}
|
||||
disabled={!!loading}
|
||||
onClick={(event) => this.logoff(event)}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
{t("konnect.chooseaccount.useOther.persona.label", "?")}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
t("konnect.chooseaccount.useOther.label", "Use another account")
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
{errorMessage}
|
||||
</form>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
logon(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const { hello, dispatch, history } = this.props;
|
||||
dispatch(executeLogonIfFormValid(hello.username, '', true)).then((response) => {
|
||||
if (response.success) {
|
||||
dispatch(advanceLogonFlow(response.success, history));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logoff(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const { history} = this.props;
|
||||
history.push(`/identifier${history.location.search}${history.location.hash}`);
|
||||
}
|
||||
}
|
||||
|
||||
Chooseaccount.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
|
||||
loading: PropTypes.string.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
hello: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { loading, errors } = state.login;
|
||||
const { hello } = state.common;
|
||||
|
||||
return {
|
||||
loading,
|
||||
errors,
|
||||
hello
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Chooseaccount)));
|
||||
@@ -0,0 +1,202 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {withTranslation, Trans} from 'react-i18next';
|
||||
|
||||
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';
|
||||
import green from '@material-ui/core/colors/green';
|
||||
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 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
|
||||
},
|
||||
header: {
|
||||
margin: 0,
|
||||
},
|
||||
subHeader: {
|
||||
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>;
|
||||
}
|
||||
|
||||
Tooltip.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
dispatch(receiveValidateLogon({})); // XXX(longsleep): hack to reset loading and errors.
|
||||
}
|
||||
|
||||
action = (allow = false, scopes = {}) => (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
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}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes, loading, hello, errors, client, t} = this.props;
|
||||
|
||||
const scopes = hello.details.scopes || {};
|
||||
const meta = hello.details.meta || {};
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{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,
|
||||
|
||||
loading: PropTypes.string.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
hello: PropTypes.object,
|
||||
client: PropTypes.object.isRequired,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const {hello} = state.common;
|
||||
const {loading, errors} = state.login;
|
||||
|
||||
return {
|
||||
loading: loading,
|
||||
errors,
|
||||
hello,
|
||||
client: hello.details.client || {}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Consent)));
|
||||
@@ -0,0 +1,238 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
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 Typography from "@material-ui/core/Typography";
|
||||
import Link from "@material-ui/core/Link";
|
||||
|
||||
import TextInput from "../../components/TextInput";
|
||||
|
||||
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,
|
||||
},
|
||||
main: {
|
||||
width: "100%",
|
||||
},
|
||||
header: {
|
||||
textAlign: "center",
|
||||
marginTop: 0,
|
||||
marginBottom: theme.spacing(6),
|
||||
},
|
||||
wrapper: {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
},
|
||||
message: {
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
},
|
||||
signinPageText: {
|
||||
marginBottom: 12
|
||||
}
|
||||
});
|
||||
|
||||
function Login(props) {
|
||||
const {
|
||||
hello,
|
||||
query,
|
||||
dispatch,
|
||||
history,
|
||||
loading,
|
||||
errors,
|
||||
classes,
|
||||
username,
|
||||
password,
|
||||
passwordResetLink,
|
||||
branding,
|
||||
} = 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",
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
const handleChange = (name) => (event) => {
|
||||
dispatch(updateInput(name, event.target.value));
|
||||
};
|
||||
|
||||
const handleNextClick = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
dispatch(executeLogonIfFormValid(username, password, false)).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
dispatch(advanceLogonFlow(response.success, history));
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.main}>
|
||||
<h1 className={classes.header}>
|
||||
{" "}
|
||||
{t("konnect.login.headline", "Sign in")}
|
||||
</h1>
|
||||
{branding?.signinPageText && (
|
||||
<Typography
|
||||
className={classes.signinPageText}
|
||||
variant="body2"
|
||||
dangerouslySetInnerHTML={{ __html: branding.signinPageText }}
|
||||
/>
|
||||
)}
|
||||
<form
|
||||
action=""
|
||||
className="oc-login-form"
|
||||
onSubmit={(event) => handleNextClick(event)}
|
||||
>
|
||||
<TextInput
|
||||
autoFocus
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={username}
|
||||
onChange={handleChange("username")}
|
||||
autoComplete="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="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,
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { loading, username, password, errors } = state.login;
|
||||
const { branding, hello, query, passwordResetLink } = state.common;
|
||||
|
||||
return {
|
||||
loading,
|
||||
username,
|
||||
password,
|
||||
errors,
|
||||
branding,
|
||||
hello,
|
||||
query,
|
||||
passwordResetLink,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(Login));
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
import ResponsiveScreen from '../../components/ResponsiveScreen';
|
||||
import RedirectWithQuery from '../../components/RedirectWithQuery';
|
||||
import { executeHello } from '../../actions/common';
|
||||
|
||||
import Login from './Login';
|
||||
import Chooseaccount from './Chooseaccount';
|
||||
import Consent from './Consent';
|
||||
|
||||
const styles = () => ({
|
||||
});
|
||||
|
||||
class Loginscreen extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(executeHello());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { branding, hello } = this.props;
|
||||
|
||||
const loading = hello === null;
|
||||
return (
|
||||
<ResponsiveScreen loading={loading} branding={branding} withoutPadding>
|
||||
<Switch>
|
||||
<Route path="/identifier" exact component={Login}></Route>
|
||||
<Route path="/chooseaccount" exact component={Chooseaccount}></Route>
|
||||
<Route path="/consent" exact component={Consent}></Route>
|
||||
<RedirectWithQuery target="/identifier"/>
|
||||
</Switch>
|
||||
</ResponsiveScreen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loginscreen.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
|
||||
branding: PropTypes.object,
|
||||
hello: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { branding, hello } = state.common;
|
||||
|
||||
return {
|
||||
branding,
|
||||
hello
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(Loginscreen));
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Loginscreen';
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
|
||||
import ResponsiveScreen from '../../components/ResponsiveScreen';
|
||||
import { executeLogoff } from '../../actions/common';
|
||||
|
||||
const styles = theme => ({
|
||||
button: {
|
||||
margin: theme.spacing(1),
|
||||
minWidth: 100
|
||||
},
|
||||
subHeader: {
|
||||
marginBottom: theme.spacing(5)
|
||||
}
|
||||
});
|
||||
|
||||
class Welcomescreen extends React.PureComponent {
|
||||
render() {
|
||||
const { classes, branding, hello, t } = this.props;
|
||||
|
||||
const loading = hello === null;
|
||||
return (
|
||||
<ResponsiveScreen loading={loading} branding={branding}>
|
||||
<Typography variant="h5" component="h3">
|
||||
{t("konnect.welcome.headline", "Welcome {{displayName}}", {displayName: hello.displayName})}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" className={classes.subHeader}>
|
||||
{hello.username}
|
||||
</Typography>
|
||||
|
||||
<Typography gutterBottom>
|
||||
{t("konnect.welcome.message", "You are signed in - awesome!")}
|
||||
</Typography>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
variant="contained"
|
||||
onClick={(event) => this.logoff(event)}
|
||||
>
|
||||
{t("konnect.welcome.signoutButton.label", "Sign out")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveScreen>
|
||||
);
|
||||
}
|
||||
|
||||
logoff(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch(executeLogoff()).then((response) => {
|
||||
const { history } = this.props;
|
||||
|
||||
if (response.success) {
|
||||
history.push('/identifier');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Welcomescreen.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
|
||||
branding: PropTypes.object,
|
||||
hello: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { branding, hello } = state.common;
|
||||
|
||||
return {
|
||||
branding,
|
||||
hello
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Welcomescreen)));
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Welcomescreen';
|
||||
@@ -0,0 +1,67 @@
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ERROR_LOGIN_VALIDATE_MISSINGUSERNAME = 'konnect.error.login.validate.missingUsername';
|
||||
export const ERROR_LOGIN_VALIDATE_MISSINGPASSWORD = 'konnect.error.login.validate.missingPassword';
|
||||
export const ERROR_LOGIN_FAILED = 'konnect.error.login.failed';
|
||||
export const ERROR_HTTP_NETWORK_ERROR = 'konnect.error.http.networkError';
|
||||
export const ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS = 'konnect.error.http.unexpectedResponseStatus';
|
||||
export const ERROR_HTTP_UNEXPECTED_RESPONSE_STATE = 'konnect.error.http.unexpectedResponseState';
|
||||
|
||||
// Error with values.
|
||||
export class ExtendedError extends Error {
|
||||
values = undefined;
|
||||
|
||||
constructor(message, values) {
|
||||
super(message);
|
||||
if (Error.captureStackTrace !== undefined) {
|
||||
Error.captureStackTrace(this, ExtendedError);
|
||||
}
|
||||
this.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
// Component to translate error text with values.
|
||||
function ErrorMessageComponent(props) {
|
||||
const { error, t, values } = props;
|
||||
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = error.id ? error.id : error.message;
|
||||
const messageDescriptor = Object.assign({}, {
|
||||
id,
|
||||
defaultMessage: error.id ? error.message : undefined,
|
||||
values: {
|
||||
...error.values,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
|
||||
switch (messageDescriptor.id) {
|
||||
case ERROR_LOGIN_VALIDATE_MISSINGUSERNAME:
|
||||
return t("konnect.error.login.validate.missingUsername", "Enter a valid value.", messageDescriptor.values);
|
||||
case ERROR_LOGIN_VALIDATE_MISSINGPASSWORD:
|
||||
return t("konnect.error.login.validate.missingPassword", "Enter your password.");
|
||||
case ERROR_LOGIN_FAILED:
|
||||
return t("konnect.error.login.failed", "Logon failed. Please verify your credentials and try again.");
|
||||
case ERROR_HTTP_NETWORK_ERROR:
|
||||
return t("konnect.error.http.networkError", "Network error. Please check your connection and try again.");
|
||||
case ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS:
|
||||
return t("konnect.error.http.unexpectedResponseStatus", "Unexpected HTTP response: {{status}}. Please check your connection and try again.", messageDescriptor.values);
|
||||
case ERROR_HTTP_UNEXPECTED_RESPONSE_STATE:
|
||||
return t("konnect.error.http.unexpectedResponseState", "Unexpected response state: {{state}}", messageDescriptor.values);
|
||||
default:
|
||||
}
|
||||
|
||||
const f = t;
|
||||
return f(messageDescriptor.defaultMessage, messageDescriptor.values);
|
||||
}
|
||||
|
||||
ErrorMessageComponent.propTypes = {
|
||||
error: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
values: PropTypes.any,
|
||||
};
|
||||
export const ErrorMessage = withTranslation()(ErrorMessageComponent);
|
||||
@@ -0,0 +1,17 @@
|
||||
/* not so fancy background */
|
||||
|
||||
#bg {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#bg > div {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-image: url(./images/background.png);
|
||||
z-index: 0;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import resourcesToBackend from 'i18next-resources-to-backend';
|
||||
import LanguageDetector, { CustomDetector } from 'i18next-browser-languagedetector';
|
||||
|
||||
import queryString from 'query-string';
|
||||
|
||||
import locales from './locales';
|
||||
|
||||
const config = {
|
||||
uiLocalesQueryName: 'ui_locales', // Same as OIDC uses.
|
||||
uiLocaleCookieName: 'ui_locale', // For domain wide syncing, not set here.
|
||||
uiLocaleLocalStorageName: 'lico.identifier_ui_locale', // Sufficiently unique, set here.
|
||||
}
|
||||
|
||||
const supportedLanguages = ['en-GB', 'en', ...locales.map((locale) => {
|
||||
return locale.locale;
|
||||
})];
|
||||
|
||||
const queryUiLocalesDetector: CustomDetector = {
|
||||
name: 'queryUiLocales',
|
||||
lookup: (options): string | string[] | undefined => {
|
||||
const query = queryString.parse(document.location.search);
|
||||
const ui_locales = query[config.uiLocalesQueryName];
|
||||
if (!ui_locales) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(ui_locales)) {
|
||||
return ui_locales as string[];
|
||||
} else {
|
||||
return ui_locales.split(' ');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.addDetector(queryUiLocalesDetector);
|
||||
|
||||
i18n
|
||||
.use(resourcesToBackend((language, namespace, callback) => {
|
||||
import(
|
||||
/* webpackMode: "lazy-once" */
|
||||
/* webpackInclude: /\.json$/ */
|
||||
/* webpackChunkName: "all-i18n-data" */
|
||||
/* webpackPrefetch: true */
|
||||
`./locales/${language}/${namespace}.json`
|
||||
)
|
||||
.then((resources) => {
|
||||
callback(null, resources)
|
||||
})
|
||||
.catch((error) => {
|
||||
callback(error, null)
|
||||
})
|
||||
}))
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: 'en-GB',
|
||||
supportedLngs: [...supportedLanguages],
|
||||
cleanCode: true,
|
||||
returnEmptyString: false,
|
||||
debug: false,
|
||||
|
||||
detection: {
|
||||
/* https://github.com/i18next/i18next-browser-languageDetector */
|
||||
order: [queryUiLocalesDetector.name, 'cookie', 'localStorage', 'navigator'],
|
||||
lookupCookie: config.uiLocaleCookieName,
|
||||
lookupLocalStorage: config.uiLocaleLocalStorageName,
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,2 @@
|
||||
app-icon.svg
|
||||
app-icon-*.png
|
||||
@@ -0,0 +1,42 @@
|
||||
# Tools
|
||||
|
||||
CONVERT ?= convert
|
||||
IDENTIFY ?= identify
|
||||
BASE64 ?= base64
|
||||
ENVSUBST ?= envsubst
|
||||
SCOUR ?= scour
|
||||
INKSCAPE ?= inkscape
|
||||
|
||||
# Variables
|
||||
|
||||
STATIC ?= ../../public/static
|
||||
ICON ?= lico-icon.svg
|
||||
|
||||
# Build
|
||||
|
||||
all: app-icon.svg
|
||||
|
||||
.PHONY: icons
|
||||
icons: $(STATIC)/favicon.ico
|
||||
|
||||
.PHONY: $(STATIC)/favicon.ico
|
||||
$(STATIC)/favicon.ico: app-icon-rounded-256x256.png
|
||||
$(CONVERT) -background transparent $< -define icon:auto-resize=16,32,48,64,128,256 $@
|
||||
|
||||
app-icon.svg: $(ICON)
|
||||
cp -vaf $< $@
|
||||
|
||||
app-icon-whitebox-256x256.png: app-icon.svg
|
||||
$(INKSCAPE) -z -e $@.tmp -w 204.8 -h 204.8 -b white -y 1.0 $<
|
||||
$(CONVERT) $@.tmp -background white -gravity center -extent 256x256 $@
|
||||
@$(RM) $@.tmp
|
||||
|
||||
app-icon-rounded-256x256.png: app-icon-whitebox-256x256.png
|
||||
$(CONVERT) -size 256x256 xc:none -draw "roundrectangle 2,2,252,252,126,126" $@.tmp.png
|
||||
$(CONVERT) $< -matte $@.tmp.png -compose DstIn -composite $@
|
||||
@$(RM) $@.tmp.png
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(RM) app-icon-*.png || true
|
||||
$(RM) app-icon.svg || true
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 180 180" role="img" aria-label="КуСфера">
|
||||
<path fill="#061D2B" d="M34,12h112C158,12 168,22 168,34v112C168,158 158,168 146,168H34C22,168 12,158 12,146V34C12,22 22,12 34,12z"/>
|
||||
<path fill="#123E55" fill-opacity="0.74" d="M12,45C48,19 87,20 121,48C145,68 158,68 168,60V34C168,22 158,12 146,12H34C22,12 12,22 12,34z"/>
|
||||
<path fill="#19B8C9" fill-opacity="0.18" d="M12,126C46,105 82,104 114,124C139,139 156,139 168,128V146C168,158 158,168 146,168H34C22,168 12,158 12,146z"/>
|
||||
<path fill="none" stroke="#20D6E6" stroke-linecap="round" stroke-width="10" d="M75,47C100,29 138,39 153,70C167,100 151,135 121,147"/>
|
||||
<path fill="none" stroke="#9AF8FF" stroke-linecap="round" stroke-width="4" d="M81,45C101,35 130,39 146,62"/>
|
||||
<path fill="#21D1D6" d="M48,52h25v50h-25z"/>
|
||||
<path fill="#F8FCFF" d="M48,102h25v42h-25z"/>
|
||||
<path fill="#18BFD0" d="M77,94L111,55H143L99,103z"/>
|
||||
<path fill="#F8FCFF" d="M77,104L100,82L148,144H116z"/>
|
||||
<path fill="#9DF8FF" fill-opacity="0.45" d="M48,52h25v8h-25z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="КуСфера">
|
||||
<path fill="#061D2B" d="M34,12h112C158,12 168,22 168,34v112C168,158 158,168 146,168H34C22,168 12,158 12,146V34C12,22 22,12 34,12z"/>
|
||||
<path fill="#123E55" fill-opacity="0.74" d="M12,45C48,19 87,20 121,48C145,68 158,68 168,60V34C168,22 158,12 146,12H34C22,12 12,22 12,34z"/>
|
||||
<path fill="#19B8C9" fill-opacity="0.18" d="M12,126C46,105 82,104 114,124C139,139 156,139 168,128V146C168,158 158,168 146,168H34C22,168 12,158 12,146z"/>
|
||||
<path fill="none" stroke="#20D6E6" stroke-linecap="round" stroke-width="10" d="M75,47C100,29 138,39 153,70C167,100 151,135 121,147"/>
|
||||
<path fill="none" stroke="#9AF8FF" stroke-linecap="round" stroke-width="4" d="M81,45C101,35 130,39 146,62"/>
|
||||
<path fill="#21D1D6" d="M48,52h25v50h-25z"/>
|
||||
<path fill="#F8FCFF" d="M48,102h25v42h-25z"/>
|
||||
<path fill="#18BFD0" d="M77,94L111,55H143L99,103z"/>
|
||||
<path fill="#F8FCFF" d="M77,104L100,82L148,144H116z"/>
|
||||
<path fill="#9DF8FF" fill-opacity="0.45" d="M48,52h25v8h-25z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="КуСфера">
|
||||
<path fill="#061D2B" d="M34,12h112C158,12 168,22 168,34v112C168,158 158,168 146,168H34C22,168 12,158 12,146V34C12,22 22,12 34,12z"/>
|
||||
<path fill="#123E55" fill-opacity="0.74" d="M12,45C48,19 87,20 121,48C145,68 158,68 168,60V34C168,22 158,12 146,12H34C22,12 12,22 12,34z"/>
|
||||
<path fill="#19B8C9" fill-opacity="0.18" d="M12,126C46,105 82,104 114,124C139,139 156,139 168,128V146C168,158 158,168 146,168H34C22,168 12,158 12,146z"/>
|
||||
<path fill="none" stroke="#20D6E6" stroke-linecap="round" stroke-width="10" d="M75,47C100,29 138,39 153,70C167,100 151,135 121,147"/>
|
||||
<path fill="none" stroke="#9AF8FF" stroke-linecap="round" stroke-width="4" d="M81,45C101,35 130,39 146,62"/>
|
||||
<path fill="#21D1D6" d="M48,52h25v50h-25z"/>
|
||||
<path fill="#F8FCFF" d="M48,102h25v42h-25z"/>
|
||||
<path fill="#18BFD0" d="M77,94L111,55H143L99,103z"/>
|
||||
<path fill="#F8FCFF" d="M77,104L100,82L148,144H116z"/>
|
||||
<path fill="#9DF8FF" fill-opacity="0.45" d="M48,52h25v8h-25z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import './i18n';
|
||||
|
||||
import App from './App';
|
||||
import store from './store';
|
||||
|
||||
import './app.css';
|
||||
|
||||
const root = document.getElementById('root')
|
||||
|
||||
// if a custom background image has been configured, make use of it
|
||||
const bgImg = root.getAttribute('data-bg-img')
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store as any}>
|
||||
<App bgImg={bgImg}/>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
root
|
||||
);
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"message": "<0><0><0></0></0></0> möchte",
|
||||
"allowButton": {
|
||||
"label": "Einverstanden"
|
||||
},
|
||||
"question": "<1><0></0></1> den Zugriff gestatten?",
|
||||
"consequence": "Wenn Sie Einverstanden klicken, erhält die App Zugriff auf Ihre Informationen.",
|
||||
"cancelButton": {
|
||||
"label": "Abbrechen"
|
||||
},
|
||||
"tooltip": {
|
||||
"client": "Wenn Sie \"Einverstanden\" klicken werden Sie zu {{redirectURI}} weitergeleitet"
|
||||
},
|
||||
"headline": "Hallo {{displayName}}"
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {
|
||||
"label": "?"
|
||||
},
|
||||
"label": "Anderes Konto"
|
||||
},
|
||||
"headline": "Konto auswählen",
|
||||
"subHeader": "das angemeldet werden soll"
|
||||
},
|
||||
"scopeDescription": {
|
||||
"aliasBasic": "Zugriff auf Ihre grundlegenden Benutzerinformationen",
|
||||
"offlineAccess": "Dauerhaften Zugriff (läuft nicht ab)",
|
||||
"scope": "Geltungsbereich: {{scope}}"
|
||||
},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"label": "Benutzername",
|
||||
"placeholder": {
|
||||
"email": "E-Mail",
|
||||
"identity": "Identität",
|
||||
"username": "Benutzername"
|
||||
}
|
||||
},
|
||||
"nextButton": {
|
||||
"label": "Weiter"
|
||||
},
|
||||
"passwordField": {
|
||||
"label": "Passwort"
|
||||
},
|
||||
"retryButton": {
|
||||
"label": "Wiederholen"
|
||||
},
|
||||
"headline": "Anmelden"
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {
|
||||
"missingUsername": "Geben Sie einen gültigen Wert ein.",
|
||||
"missingPassword": "Geben Sie ein Passwort ein."
|
||||
},
|
||||
"failed": "Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Eingabe und versuchen Sie es noch einmal."
|
||||
},
|
||||
"http": {
|
||||
"networkError": "Netzwerkfehler. Bitte prüfen Sie Ihre Verbindung und versuchen Sie es noch einmal.",
|
||||
"unexpectedResponseStatus": "Unerwartete HTTP-Antwort: {{status}}. Bitte prüfen Sie Ihre Verbindung und versuchen Sie es noch einmal.",
|
||||
"unexpectedResponseState": "Unerwarteter Antwort-Status: {{state}}"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"error": {
|
||||
"headline": "Verbindung zum Server fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"goodbye": {
|
||||
"headline": "Bis bald",
|
||||
"confirm": {
|
||||
"headline": "Hallo {{displayName}}",
|
||||
"subHeader": "Bitte bestätigen Sie, dass Sie sich abmelden möchten"
|
||||
},
|
||||
"message": {
|
||||
"confirm": "Klicken Sie auf die Schaltfläche unten um sich aus Ihrem Konto abzumelden.",
|
||||
"close": "Sie können dieses Fenster jetzt schließen."
|
||||
},
|
||||
"signoutButton": {
|
||||
"label": "Abmelden"
|
||||
},
|
||||
"subHeader": "sie sind jetzt abgemeldet"
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {
|
||||
"label": "Abmelden"
|
||||
},
|
||||
"headline": "Willkommen {{displayName}}",
|
||||
"message": "Sie sind angemeldet - super!"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"allowButton": {},
|
||||
"cancelButton": {},
|
||||
"tooltip": {}
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {}
|
||||
}
|
||||
},
|
||||
"scopeDescription": {},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {}
|
||||
},
|
||||
"nextButton": {},
|
||||
"passwordField": {},
|
||||
"retryButton": {}
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {}
|
||||
},
|
||||
"http": {}
|
||||
},
|
||||
"loading": {
|
||||
"error": {}
|
||||
},
|
||||
"goodbye": {
|
||||
"confirm": {},
|
||||
"message": {},
|
||||
"signoutButton": {}
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"message": "<0><0><0></0></0></0> souhaite",
|
||||
"allowButton": {
|
||||
"label": "Autoriser"
|
||||
},
|
||||
"question": "Autoriser <1><0></0></1> à faire cela?",
|
||||
"consequence": "En cliquant, vous autoriser l'app à accéder à vos informations.",
|
||||
"cancelButton": {
|
||||
"label": "Annuler"
|
||||
},
|
||||
"tooltip": {
|
||||
"client": "En cliquant \"Autoriser\" vous serez redirigé vers: {{redirectURI}}"
|
||||
},
|
||||
"headline": "Bonjour {{displayName}}"
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {
|
||||
"label": "?"
|
||||
},
|
||||
"label": "Utiliser un autre compte"
|
||||
},
|
||||
"headline": "Choisir un compte",
|
||||
"subHeader": "identification"
|
||||
},
|
||||
"scopeDescription": {
|
||||
"aliasBasic": "Consulter les informations de base de votre compte",
|
||||
"offlineAccess": "Conserver les autorisations d'accès à l'avenir",
|
||||
"scope": "Portée : {{scope}}"
|
||||
},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {
|
||||
"username": "Utilisateur"
|
||||
}
|
||||
},
|
||||
"nextButton": {
|
||||
"label": "Suivant"
|
||||
},
|
||||
"passwordField": {
|
||||
"label": "Mot de passe"
|
||||
},
|
||||
"retryButton": {
|
||||
"label": "Réessayer"
|
||||
},
|
||||
"headline": "Identification"
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {
|
||||
"missingPassword": "Saisir un mot de passe."
|
||||
},
|
||||
"failed": "Echec de connexion. Vérifier vos identifiants et essayer à nouveau."
|
||||
},
|
||||
"http": {
|
||||
"networkError": "Erreur réseau. Vérifier votre connexion, et réessayer.",
|
||||
"unexpectedResponseStatus": "Erreur HTTP inattendue : {{status}}. Vérifier votre connexion et réessayer.",
|
||||
"unexpectedResponseState": "Erreur d'état inattendue : {{state}}"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"error": {
|
||||
"headline": "La connexion au serveur a échoué"
|
||||
}
|
||||
},
|
||||
"goodbye": {
|
||||
"headline": "Au revoir",
|
||||
"confirm": {
|
||||
"headline": "Bonjour {{displayName}}",
|
||||
"subHeader": "confirmer votre déconnexion"
|
||||
},
|
||||
"message": {
|
||||
"confirm": "Cliquer le bouton ci-dessous, pour quitter.",
|
||||
"close": "Vous pouvez fermer cette fenêtre à présent."
|
||||
},
|
||||
"signoutButton": {
|
||||
"label": "Quitter"
|
||||
},
|
||||
"subHeader": "vous avez été déconnecté"
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {
|
||||
"label": "Quitter"
|
||||
},
|
||||
"headline": "Bienvenue {{displayName}}",
|
||||
"message": "Magnifique - Vous êtes connecté!"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import allLocales from './locales.json';
|
||||
|
||||
interface Locale {
|
||||
locale: string,
|
||||
name: string,
|
||||
nativeName: string,
|
||||
}
|
||||
|
||||
function enableLocales(locales: Locale[]): Locale[] {
|
||||
return locales;
|
||||
}
|
||||
|
||||
export const locales = enableLocales(allLocales);
|
||||
|
||||
export default locales;
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"allowButton": {},
|
||||
"cancelButton": {},
|
||||
"tooltip": {}
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {}
|
||||
}
|
||||
},
|
||||
"scopeDescription": {},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {}
|
||||
},
|
||||
"nextButton": {},
|
||||
"passwordField": {},
|
||||
"retryButton": {}
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {}
|
||||
},
|
||||
"http": {}
|
||||
},
|
||||
"loading": {
|
||||
"error": {}
|
||||
},
|
||||
"goodbye": {
|
||||
"confirm": {},
|
||||
"message": {},
|
||||
"signoutButton": {}
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
[
|
||||
{
|
||||
"locale": "de",
|
||||
"name": "German",
|
||||
"nativeName": "Deutsch",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "en-GB",
|
||||
"name": "English",
|
||||
"nativeName": "English",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "es",
|
||||
"name": "Spanish",
|
||||
"nativeName": "Español",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "fr",
|
||||
"name": "French",
|
||||
"nativeName": "Français",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "it",
|
||||
"name": "Italian",
|
||||
"nativeName": "Italiano",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "nl",
|
||||
"name": "Dutch",
|
||||
"nativeName": "Nederlands",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "zh-CN",
|
||||
"name": "Simplified Chinese",
|
||||
"nativeName": "简体中文",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
},
|
||||
"ietf": "zh_hans"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"message": "<0><0><0></0></0></0> wil",
|
||||
"allowButton": {
|
||||
"label": "Toestaan"
|
||||
},
|
||||
"question": "<1><0></0></1> toestaan dit te doen?",
|
||||
"consequence": "Door op Toestaan te klikken, krijgt deze app toestemming je informatie te gebruiken.",
|
||||
"cancelButton": {
|
||||
"label": "Annuleren"
|
||||
},
|
||||
"tooltip": {
|
||||
"client": "Door op \"Toestaan\" te klikken word je doorverwezen naar: {{redirectURI}}"
|
||||
},
|
||||
"headline": "Hoi {{displayName}}"
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {
|
||||
"label": "?"
|
||||
},
|
||||
"label": "Gebruik een ander account"
|
||||
},
|
||||
"headline": "Account kiezen",
|
||||
"subHeader": "aanmelden"
|
||||
},
|
||||
"scopeDescription": {
|
||||
"aliasBasic": "Basis accountgegevens weergeven",
|
||||
"offlineAccess": "De toestemming voor altijd onthouden",
|
||||
"scope": "Scope: {{scope}}"
|
||||
},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {
|
||||
"username": "Gebruikersnaam"
|
||||
}
|
||||
},
|
||||
"nextButton": {
|
||||
"label": "Volgende"
|
||||
},
|
||||
"passwordField": {
|
||||
"label": "Wachtwoord"
|
||||
},
|
||||
"retryButton": {
|
||||
"label": "Opnieuw"
|
||||
},
|
||||
"headline": "Aanmelden"
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {
|
||||
"missingPassword": "Voer een wachtwoord in."
|
||||
},
|
||||
"failed": "Inloggen mislukt. Controleer logingegevens en probeer opnieuw."
|
||||
},
|
||||
"http": {
|
||||
"networkError": "Netwerk probleem. Controleer je verbinding en probeer opnieuw.",
|
||||
"unexpectedResponseStatus": "Onverwachte HTTP respons: {{status}}. Controleer je verbinding en probeer opnieuw.",
|
||||
"unexpectedResponseState": "Onverwachte respons status: {{state}}"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"error": {
|
||||
"headline": "Kon niet met server verbinden"
|
||||
}
|
||||
},
|
||||
"goodbye": {
|
||||
"headline": "Tot ziens",
|
||||
"confirm": {
|
||||
"headline": "Hallo {{displayName}}",
|
||||
"subHeader": "bevestig afmelden"
|
||||
},
|
||||
"message": {
|
||||
"confirm": "Klik op onderstaande knop om af te melden van je account.",
|
||||
"close": "Dit venster kan nu worden gesloten."
|
||||
},
|
||||
"signoutButton": {
|
||||
"label": "Afmelden"
|
||||
},
|
||||
"subHeader": "je bent afgemeld van je account"
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {
|
||||
"label": "Afmelden"
|
||||
},
|
||||
"headline": "Welkom {{displayName}}",
|
||||
"message": "Je bent aangemeld - fantastisch!"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"allowButton": {},
|
||||
"cancelButton": {},
|
||||
"tooltip": {}
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {}
|
||||
}
|
||||
},
|
||||
"scopeDescription": {},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {}
|
||||
},
|
||||
"nextButton": {},
|
||||
"passwordField": {},
|
||||
"retryButton": {}
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {}
|
||||
},
|
||||
"http": {}
|
||||
},
|
||||
"loading": {
|
||||
"error": {}
|
||||
},
|
||||
"goodbye": {
|
||||
"confirm": {},
|
||||
"message": {},
|
||||
"signoutButton": {}
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export function newHelloRequest(flow, query) {
|
||||
const r = {};
|
||||
|
||||
if (query.prompt) {
|
||||
// TODO(longsleep): Validate prompt values?
|
||||
r.prompt = query.prompt;
|
||||
}
|
||||
|
||||
let selectedFlow = flow;
|
||||
switch (flow) {
|
||||
case 'oauth':
|
||||
case 'consent':
|
||||
case 'oidc':
|
||||
r.scope = query.scope || '';
|
||||
r.client_id = query.client_id || ''; // eslint-disable-line camelcase
|
||||
r.redirect_uri = query.redirect_uri || ''; // eslint-disable-line camelcase
|
||||
if (query.id_token_hint) {
|
||||
r.id_token_hint = query.id_token_hint; // eslint-disable-line camelcase
|
||||
}
|
||||
if (query.max_age) {
|
||||
r.max_age = query.max_age; // eslint-disable-line camelcase
|
||||
}
|
||||
if (query.claims_scope) {
|
||||
// Add additional scopes from claims request if given.
|
||||
r.scope += ' ' + query.claims_scope;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
selectedFlow = null;
|
||||
}
|
||||
|
||||
if (selectedFlow) {
|
||||
r.flow = selectedFlow;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const QsferaContext = createContext({
|
||||
theme: null,
|
||||
config: null,
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
RECEIVE_ERROR,
|
||||
RESET_HELLO,
|
||||
RECEIVE_HELLO,
|
||||
SERVICE_WORKER_NEW_CONTENT
|
||||
} from '../actions/types';
|
||||
import queryString from 'query-string';
|
||||
|
||||
const query = queryString.parse(document.location.search);
|
||||
const flow = query.flow || '';
|
||||
delete query.flow;
|
||||
|
||||
const defaultPathPrefix = (() => {
|
||||
const root = document.getElementById('root');
|
||||
let pathPrefix = root ? root.getAttribute('data-path-prefix') : null;
|
||||
if (!pathPrefix || pathPrefix === '__PATH_PREFIX__') {
|
||||
// Not replaced, probably we are running in debug mode or whatever. Use sane default.
|
||||
pathPrefix = '/signin/v1';
|
||||
}
|
||||
return pathPrefix;
|
||||
})();
|
||||
|
||||
const defaultPasswordResetLink = (() => {
|
||||
const root = document.getElementById('root');
|
||||
let link = root ? root.getAttribute('passwort-reset-link') : null;
|
||||
if (!link || link === '__PASSWORD_RESET_LINK__') {
|
||||
// Not replaced, probably we are running in debug mode or whatever. Use sane default.
|
||||
link = '';
|
||||
}
|
||||
return link;
|
||||
})();
|
||||
|
||||
const defaultState = {
|
||||
hello: null,
|
||||
branding: null,
|
||||
error: null,
|
||||
flow: flow,
|
||||
query: query,
|
||||
updateAvailable: false,
|
||||
pathPrefix: defaultPathPrefix,
|
||||
passwordResetLink: defaultPasswordResetLink
|
||||
};
|
||||
|
||||
function commonReducer(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
case RECEIVE_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
error: action.error
|
||||
});
|
||||
|
||||
case RESET_HELLO:
|
||||
return Object.assign({}, state, {
|
||||
hello: null,
|
||||
branding: null
|
||||
});
|
||||
|
||||
case RECEIVE_HELLO:
|
||||
return Object.assign({}, state, {
|
||||
hello: {
|
||||
state: action.state,
|
||||
username: action.username,
|
||||
displayName: action.displayName,
|
||||
details: action.hello
|
||||
},
|
||||
branding: action.hello.branding ? action.hello.branding : state.branding
|
||||
});
|
||||
|
||||
case SERVICE_WORKER_NEW_CONTENT:
|
||||
return Object.assign({}, state, {
|
||||
updateAvailable: true
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default commonReducer;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import commonReducer from './common';
|
||||
import loginReducer from './login';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
common: commonReducer,
|
||||
login: loginReducer
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
RECEIVE_VALIDATE_LOGON,
|
||||
REQUEST_LOGON,
|
||||
RECEIVE_LOGON,
|
||||
RECEIVE_LOGOFF,
|
||||
REQUEST_CONSENT_ALLOW,
|
||||
REQUEST_CONSENT_CANCEL,
|
||||
RECEIVE_CONSENT,
|
||||
UPDATE_INPUT
|
||||
} from '../actions/types';
|
||||
|
||||
function loginReducer(state = {
|
||||
loading: '',
|
||||
username: '',
|
||||
password: '',
|
||||
errors: {}
|
||||
}, action) {
|
||||
switch (action.type) {
|
||||
case RECEIVE_VALIDATE_LOGON:
|
||||
return Object.assign({}, state, {
|
||||
errors: action.errors,
|
||||
loading: ''
|
||||
});
|
||||
|
||||
case REQUEST_CONSENT_ALLOW:
|
||||
case REQUEST_CONSENT_CANCEL:
|
||||
case REQUEST_LOGON:
|
||||
return Object.assign({}, state, {
|
||||
loading: action.type,
|
||||
errors: {}
|
||||
});
|
||||
|
||||
case RECEIVE_CONSENT:
|
||||
case RECEIVE_LOGON:
|
||||
return Object.assign({}, state, {
|
||||
errors: !action.success && action.errors ? action.errors : {},
|
||||
loading: ''
|
||||
});
|
||||
|
||||
case RECEIVE_LOGOFF:
|
||||
return Object.assign({}, state, {
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
case UPDATE_INPUT:
|
||||
delete state.errors[action.name];
|
||||
return Object.assign({}, state, {
|
||||
[action.name]: action.value
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default loginReducer;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import rootReducer from './reducers';
|
||||
|
||||
const middlewares = [
|
||||
thunkMiddleware
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') { // eslint-disable-line no-undef
|
||||
middlewares.push(createLogger()); // must be last middleware in the chain.
|
||||
}
|
||||
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
composeEnhancers(applyMiddleware(
|
||||
...middlewares,
|
||||
))
|
||||
);
|
||||
|
||||
export default store;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { unstable_createMuiStrictModeTheme as createMuiTheme } from '@material-ui/core';
|
||||
|
||||
import blueGrey from '@material-ui/core/colors/blueGrey';
|
||||
import blue from '@material-ui/core/colors/blue';
|
||||
import red from '@material-ui/core/colors/red';
|
||||
|
||||
const primaryColor = blue;
|
||||
const secondaryColor = blueGrey;
|
||||
const errorColor = red;
|
||||
|
||||
// All the following keys are optional.
|
||||
// We try our best to provide a great default value.
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
primary: primaryColor,
|
||||
secondary: secondaryColor,
|
||||
error: errorColor,
|
||||
// Used by `getContrastText()` to maximize the contrast between the background and
|
||||
// the text.
|
||||
// NOTE(longsleep): KopanoBlue is too light and thus needs 2.4 contrastThreshold
|
||||
// to make sure the default 500 color is still using white text. It will
|
||||
// show warnings in development mode that the contrast is too low as W3C
|
||||
// recommends the threshold to be 3 or more. This cannot be helped.
|
||||
contrastThreshold: 2.4,
|
||||
// Used to shift a color's luminance by approximately
|
||||
// two indexes within its tonal palette.
|
||||
// E.g., shift from Red 500 to Red 300 or Red 700.
|
||||
tonalOffset: 0.2,
|
||||
},
|
||||
typography: {
|
||||
fontSize: 16,
|
||||
useNextVariants: true,
|
||||
button: {
|
||||
textTransform: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
@@ -0,0 +1,16 @@
|
||||
export function withClientRequestState(obj) {
|
||||
obj.state = generateState(16);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function dec2hex (dec) {
|
||||
return dec.toString(16).padStart(2, "0")
|
||||
}
|
||||
|
||||
// generateState :: Integer -> String
|
||||
function generateState (len) {
|
||||
var arr = new Uint8Array((len || 16) / 2)
|
||||
window.crypto.getRandomValues(arr)
|
||||
return Array.from(arr, dec2hex).join('')
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const build = process.env.REACT_APP_KOPANO_BUILD || '0.0.0-no-proper-build';
|
||||
|
||||
export {
|
||||
build
|
||||
};
|
||||
Reference in New Issue
Block a user