diff options
author | Dan Timoney <dtimoney@att.com> | 2019-03-25 14:27:10 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2019-03-25 14:27:10 +0000 |
commit | 1f68c64e8bcf88ee485c69d36c67b0ad6590a293 (patch) | |
tree | 34ab01010c3a8b3bb4086d4b683a6fd8040f2992 /sdnr/wt/odlux/framework/src | |
parent | 528f6391de8495f3346b5e22ede404c00b9955b7 (diff) | |
parent | 2d4424c28ac35763ef44c42ae2f01664d42b268c (diff) |
Merge "Security provider for UX-Client-Login"
Diffstat (limited to 'sdnr/wt/odlux/framework/src')
13 files changed, 327 insertions, 223 deletions
diff --git a/sdnr/wt/odlux/framework/src/actions/authentication.ts b/sdnr/wt/odlux/framework/src/actions/authentication.ts index 8cbc22271..0cca179db 100644 --- a/sdnr/wt/odlux/framework/src/actions/authentication.ts +++ b/sdnr/wt/odlux/framework/src/actions/authentication.ts @@ -1,8 +1,9 @@ import { Action } from '../flux/action'; +import { AuthToken } from '../models/authentication'; export class UpdateAuthentication extends Action { - constructor(public bearerToken: string | null) { + constructor (public bearerToken: AuthToken | null) { super(); } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/app.tsx b/sdnr/wt/odlux/framework/src/app.tsx index 1879c7bc6..6a56056d3 100644 --- a/sdnr/wt/odlux/framework/src/app.tsx +++ b/sdnr/wt/odlux/framework/src/app.tsx @@ -1,12 +1,12 @@ /******************************************************************************
* Copyright 2018 highstreet technologies GmbH
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -27,9 +27,12 @@ import { applicationStoreCreator } from './store/applicationStore'; import { ApplicationStoreProvider } from './flux/connect';
import { startHistoryListener } from './middleware/navigation';
+import { startRestService } from './services/restService';
+
import theme from './design/default';
import '!style-loader!css-loader!./app.css';
+import { ReplaceAction } from './actions/navigationActions';
declare module '@material-ui/core/styles/createMuiTheme' {
@@ -54,7 +57,7 @@ export const runApplication = () => { const applicationStore = applicationStoreCreator();
window.onerror = function (msg: string, url: string, line: number, col: number, error: Error) {
- // Note that col & error are new to the HTML 5 spec and may not be
+ // Note that col & error are new to the HTML 5 spec and may not be
// supported in every browser. It worked for me in Chrome.
var extra = !col ? '' : '\ncolumn: ' + col;
extra += !error ? '' : '\nerror: ' + error;
@@ -63,11 +66,12 @@ export const runApplication = () => { applicationStore.dispatch(new AddErrorInfoAction({ error, message: msg, url, line, col, info: { extra } }));
var suppressErrorAlert = true;
- // If you return true, then error alerts (like in older versions of
+ // If you return true, then error alerts (like in older versions of
// Internet Explorer) will be suppressed.
return suppressErrorAlert;
};
+ startRestService(applicationStore);
startHistoryListener(applicationStore);
const App = (): JSX.Element => (
@@ -79,4 +83,5 @@ export const runApplication = () => { );
ReactDOM.render(<App />, document.getElementById('app'));
+
};
diff --git a/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx index f6df244a0..3a7725b1b 100644 --- a/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx +++ b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx @@ -25,6 +25,7 @@ const styles = (theme: Theme) => createStyles({ });
export const NavigationMenu = withStyles(styles)(connect()(({ classes, state }: WithStyles<typeof styles> & Connect) => {
+ const { user } = state.framework.authenticationState
return (
<Drawer
variant="permanent"
@@ -32,10 +33,11 @@ export const NavigationMenu = withStyles(styles)(connect()(({ classes, state }: paper: classes.drawerPaper,
}}
>
- <div className={classes.toolbar} />
+ {user && user.isValid && <>
+ <div className={classes.toolbar} />
{ /* https://fiffty.github.io/react-treeview-mui/ */}
<List component="nav">
- { process.env.NODE_ENV === "development" ? <ListItemLink exact to="/" primary="Home" icon={<FontAwesomeIcon icon={faHome} />} /> : null }
+ { process.env.NODE_ENV === "development" ? <ListItemLink exact to="/" primary="Home" icon={<FontAwesomeIcon icon={faHome} />} /> : null }
<Divider />
{
state.framework.applicationRegistraion && Object.keys(state.framework.applicationRegistraion).map(key => {
@@ -51,8 +53,10 @@ export const NavigationMenu = withStyles(styles)(connect()(({ classes, state }: }) || null
}
<Divider />
- { process.env.NODE_ENV === "development" ? <ListItemLink to="/about" primary="About" icon={<FontAwesomeIcon icon={faAddressBook} />} /> : null }
- </List>
+ { process.env.NODE_ENV === "development" ? <ListItemLink to="/about" primary="About" icon={<FontAwesomeIcon icon={faAddressBook} />} /> : null }
+ </List>
+ </> || null
+ }
</Drawer>)
}));
diff --git a/sdnr/wt/odlux/framework/src/components/titleBar.tsx b/sdnr/wt/odlux/framework/src/components/titleBar.tsx index ed6eb2ccc..439e9bc12 100644 --- a/sdnr/wt/odlux/framework/src/components/titleBar.tsx +++ b/sdnr/wt/odlux/framework/src/components/titleBar.tsx @@ -15,6 +15,7 @@ import Menu from '@material-ui/core/Menu'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { UpdateAuthentication } from '../actions/authentication';
+import { ReplaceAction } from '../actions/navigationActions';
import connect, { Connect, IDispatcher } from '../flux/connect';
import Logo from './logo';
@@ -37,7 +38,10 @@ const styles = (theme: Theme) => createStyles({ const mapDispatch = (dispatcher: IDispatcher) => {
return {
- logout: () => { dispatcher.dispatch(new UpdateAuthentication(null)); }
+ logout: () => {
+ dispatcher.dispatch(new UpdateAuthentication(null));
+ dispatcher.dispatch(new ReplaceAction("/login"));
+ }
}
};
diff --git a/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts b/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts index e0ae1aa8d..2abe82142 100644 --- a/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts +++ b/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts @@ -10,19 +10,19 @@ export interface IAuthenticationState { const initialToken = localStorage.getItem("userToken"); const authenticationStateInit: IAuthenticationState = { - user: initialToken && new User(initialToken) || undefined + user: initialToken && User.fromString(initialToken) || undefined }; export const authenticationStateHandler: IActionHandler<IAuthenticationState> = (state = authenticationStateInit, action) => { if (action instanceof UpdateAuthentication) { - - if (action.bearerToken) { - localStorage.setItem("userToken", action.bearerToken); + + const user = action.bearerToken && new User(action.bearerToken) || undefined; + if (user) { + localStorage.setItem("userToken", user.toString()); } else { localStorage.removeItem("userToken"); } - const user = action.bearerToken && new User(action.bearerToken) || undefined; state = { ...state, user diff --git a/sdnr/wt/odlux/framework/src/index.html b/sdnr/wt/odlux/framework/src/index.html index 3c59b1580..36d937775 100644 --- a/sdnr/wt/odlux/framework/src/index.html +++ b/sdnr/wt/odlux/framework/src/index.html @@ -16,7 +16,7 @@ <script>
// run the application
require(["run"], function (run) {
- run.runApplication();
+ run.runApplication();
});
</script>
</body>
diff --git a/sdnr/wt/odlux/framework/src/middleware/navigation.ts b/sdnr/wt/odlux/framework/src/middleware/navigation.ts index 758b51845..8db01b18c 100644 --- a/sdnr/wt/odlux/framework/src/middleware/navigation.ts +++ b/sdnr/wt/odlux/framework/src/middleware/navigation.ts @@ -6,14 +6,14 @@ import { Dispatch } from '../flux/store'; import { LocationChanged, NavigateToApplication } from "../actions/navigationActions"; import { PushAction, ReplaceAction, GoAction, GoBackAction, GoForwardeAction } from '../actions/navigationActions'; -import applicationManager from "../services/applicationManager"; +import { applicationManager } from "../services/applicationManager"; const routerMiddlewareCreator = (history: History) => () => (next: Dispatch): Dispatch => (action) => { - + if (action instanceof NavigateToApplication) { const application = applicationManager.applications && applicationManager.applications[action.applicationName]; if (application) { - const href = `/${ application.path || application.name }${ action.href ? '/' + action.href : '' }`.replace(/\/{2,}/i, '/'); + const href = `/${application.path || application.name}${action.href ? '/' + action.href : ''}`.replace(/\/{2,}/i, '/'); if (action.replace) { history.replace(href, action.state); } else { @@ -30,6 +30,13 @@ const routerMiddlewareCreator = (history: History) => () => (next: Dispatch): Di history.goBack(); } else if (action instanceof GoForwardeAction) { history.goForward(); + } else if (action instanceof LocationChanged) { + // ensure user is logged in and token is valid + if (!action.pathname.startsWith("/login") && applicationStore && (!applicationStore.state.framework.authenticationState.user || !applicationStore.state.framework.authenticationState.user.isValid)) { + history.replace(`/login?returnTo=${action.pathname}`); + } else { + return next(action); + } } else { return next(action); } @@ -44,8 +51,10 @@ function startListener(history: History, store: ApplicationStore) { } const history = createHashHistory(); +let applicationStore: ApplicationStore | null = null; export function startHistoryListener(store: ApplicationStore) { + applicationStore = store; startListener(history, store); } diff --git a/sdnr/wt/odlux/framework/src/models/authentication.ts b/sdnr/wt/odlux/framework/src/models/authentication.ts index 44b5ae436..6c463ad05 100644 --- a/sdnr/wt/odlux/framework/src/models/authentication.ts +++ b/sdnr/wt/odlux/framework/src/models/authentication.ts @@ -1,50 +1,41 @@ -import * as JWT from 'jsonwebtoken'; - -export interface IUserInfo { - iss: string, - iat: number, - exp: number, - aud: string, - sub: string, - firstName: string, - lastName: string, - email: string, - role: string[] + +export type AuthToken = { + username: string; + access_token: string; + token_type: string; + expires: number; } export class User { - public _userInfo: IUserInfo | null; - - constructor(private _bearerToken: string) { - //const pem = require('raw-loader!../assets/publicKey.pem'); - const pem = "kFfAgpf806IKa4z88EEk6Lim7NMGicrw99OmIB38myM9CS44nEmMNJxnFu3ImViS248wSwkuZ3HvrhsPrA1ZFRNb1a6CEtGN4DaPJbfuo35qMp50tIEpy8nsSFpayOBE"; - - try { - const dec = (JWT.verify(_bearerToken, pem)) as IUserInfo; - this._userInfo = dec; - } catch (ex) { - this._userInfo = null; - } + constructor (private _bearerToken: AuthToken) { + } public get user(): string | null { - return this._userInfo && this._userInfo.email; + return this._bearerToken && this._bearerToken.username; }; - public get roles(): string[] | null { - return this._userInfo && this._userInfo.role; - } public get token(): string | null { - return this._userInfo && this._bearerToken; + return this._bearerToken && this._bearerToken.access_token; } - public isInRole(role: string | string[]): boolean { - return false; + public get tokenType(): string | null { + return this._bearerToken && this._bearerToken.token_type; } -} + public get isValid(): boolean { + return (this._bearerToken && (new Date().valueOf()) < this._bearerToken.expires) || false; + } -// key:kFfAgpf806IKa4z88EEk6Lim7NMGicrw99OmIB38myM9CS44nEmMNJxnFu3ImViS248wSwkuZ3HvrhsPrA1ZFRNb1a6CEtGN4DaPJbfuo35qMp50tIEpy8nsSFpayOBE -// token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPRExVWCIsImlhdCI6MTUzODQ2NDMyMCwiZXhwIjoxNTcwMDAwMzIwLCJhdWQiOiJsb2NhbGhvc3QiLCJzdWIiOiJsb2NhbGhvc3QiLCJmaXJzdE5hbWUiOiJNYXgiLCJsYXN0TmFtZSI6Ik11c3Rlcm1hbm4iLCJlbWFpbCI6Im1heEBvZGx1eC5jb20iLCJyb2xlIjpbInVzZXIiLCJhZG1pbiJdfQ.9e5hDi2uxmIXNwHkJoScBZsHBk0jQ8CcZ7YIcZhDtuI
\ No newline at end of file + public toString() { + return JSON.stringify(this._bearerToken); + } + + public static fromString(data: string) { + return new User(JSON.parse(data)); + } + + +} diff --git a/sdnr/wt/odlux/framework/src/services/authenticationService.ts b/sdnr/wt/odlux/framework/src/services/authenticationService.ts index 5e6fc81a8..42ce061f1 100644 --- a/sdnr/wt/odlux/framework/src/services/authenticationService.ts +++ b/sdnr/wt/odlux/framework/src/services/authenticationService.ts @@ -1,14 +1,34 @@ -function timeout(ms:number) { - return new Promise(resolve => setTimeout(resolve, ms)); +import { requestRest, formEncode } from "./restService"; +import { AuthToken } from "../models/authentication"; + +type AuthTokenResponse = { + access_token: string; + token_type: string; + expires_in: number; } + class AuthenticationService { - public async authenticateUser(email: string, password: string) : Promise<string | null> { - await timeout(650); - if (email === "max@odlux.com" && password === "geheim") { - return "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPRExVWCIsImlhdCI6MTUzODQ2NDMyMCwiZXhwIjoxNTcwMDAwMzIwLCJhdWQiOiJsb2NhbGhvc3QiLCJzdWIiOiJsb2NhbGhvc3QiLCJmaXJzdE5hbWUiOiJNYXgiLCJsYXN0TmFtZSI6Ik11c3Rlcm1hbm4iLCJlbWFpbCI6Im1heEBvZGx1eC5jb20iLCJyb2xlIjpbInVzZXIiLCJhZG1pbiJdfQ.9e5hDi2uxmIXNwHkJoScBZsHBk0jQ8CcZ7YIcZhDtuI" - } - return null; + public async authenticateUser(email: string, password: string, scope: string): Promise<AuthToken | null> { + const result = await requestRest<string>(`oauth2/token`, { + method: "POST", + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: formEncode({ + grant_type: "password", + username: email, + password: password, + scope: scope + }) + }, false); + const resultObj: AuthTokenResponse| null = result && JSON.parse(result); + return resultObj && { + username: email, + access_token: resultObj.access_token, + token_type: resultObj.token_type, + expires: (new Date().valueOf()) + (resultObj.expires_in * 1000) + } || null; } } diff --git a/sdnr/wt/odlux/framework/src/services/notificationService.ts b/sdnr/wt/odlux/framework/src/services/notificationService.ts index 242a6c03b..76132f843 100644 --- a/sdnr/wt/odlux/framework/src/services/notificationService.ts +++ b/sdnr/wt/odlux/framework/src/services/notificationService.ts @@ -1,6 +1,6 @@ import * as X2JS from 'x2js';
-const socketUrl = [ location.protocol === 'https:' ? 'wss://' : 'ws://', 'admin', ':', 'admin', '@', location.hostname, ':',location.port,'/websocket'].join('');
+const socketUrl = [location.protocol === 'https:' ? 'wss://' : 'ws://', 'admin', ':', 'admin', '@', location.hostname, ':', location.port, '/websocket'].join('');
const subscriptions: { [scope: string]: SubscriptionCallback[] } = { };
export interface IFormatedMessage {
@@ -29,7 +29,7 @@ function formatData(event: MessageEvent) : IFormatedMessage | undefined { export function subscribe<TMessage extends IFormatedMessage = IFormatedMessage>(scope: string | string[], callback: SubscriptionCallback<TMessage>): Promise<boolean> {
return socketReady.then((notificationSocket) => {
const scopes = scope instanceof Array ? scope : [scope];
-
+
// send all new scopes to subscribe
const newScopesToSubscribe: string[] = scopes.reduce((acc: string[], cur: string) => {
const currentCallbacks = subscriptions[cur];
@@ -102,7 +102,7 @@ const connect = (): Promise<WebSocket> => { const callbacks = subscriptions[formated.notifType];
if (callbacks) {
callbacks.forEach(cb => {
- // ensure all callbacks will be called
+ // ensure all callbacks will be called
try {
return cb(formated);
} catch (reason) {
diff --git a/sdnr/wt/odlux/framework/src/services/restService.ts b/sdnr/wt/odlux/framework/src/services/restService.ts index 83c005c13..cfa8ec159 100644 --- a/sdnr/wt/odlux/framework/src/services/restService.ts +++ b/sdnr/wt/odlux/framework/src/services/restService.ts @@ -1,8 +1,19 @@ +import { ApplicationStore } from "../store/applicationStore"; +import { ReplaceAction } from "../actions/navigationActions"; const baseUri = `${ window.location.origin }`; -const absUrlPattern = /^https?:\/\//; +const absUrlPattern = /^https?:\/\//; +let applicationStore: ApplicationStore | null = null; -export async function requestRest<TData>(path: string = '', init: RequestInit = {}, authenticate: boolean = false): Promise<TData|false|null> { +export const startRestService = (store: ApplicationStore) => { + applicationStore = store; +}; + +export const formEncode = (params: { [key: string]: string | number }) => Object.keys(params).map((key) => { + return encodeURIComponent(key) + '=' + encodeURIComponent(params[key].toString()); +}).join('&'); + +export async function requestRest<TData>(path: string = '', init: RequestInit = {}, authenticate: boolean = true): Promise<TData|false|null> { const isAbsUrl = absUrlPattern.test(path); const uri = isAbsUrl ? path : (baseUri) + ('/' + path).replace(/\/{2,}/i, '/'); init.headers = { @@ -11,13 +22,23 @@ export async function requestRest<TData>(path: string = '', init: RequestInit = 'Accept': 'application/json', ...init.headers }; - if (!isAbsUrl && authenticate) { - init.headers = { + if (!isAbsUrl && authenticate && applicationStore) { + const { state: { framework: { authenticationState: { user } } } } = applicationStore; + // do not request if the user is not valid + if (!user || !user.isValid) { + return null; + } + (init.headers = { ...init.headers, - 'Authorization': 'Basic YWRtaW46S3A4Yko0U1hzek0wV1hsaGFrM2VIbGNzZTJnQXc4NHZhb0dHbUp2VXkyVQ==' - }; + 'Authorization': `${user.tokenType} ${user.token}` + //'Authorization': 'Basic YWRtaW46YWRtaW4=' + }); } const result = await fetch(uri, init); + if (result.status === 401 || result.status === 403) { + applicationStore && applicationStore.dispatch(new ReplaceAction(`/login?returnTo=${applicationStore.state.framework.navigationState.pathname}`)); + return null; + } const contentType = result.headers.get("Content-Type") || result.headers.get("content-type"); const isJson = contentType && contentType.toLowerCase().startsWith("application/json"); try { diff --git a/sdnr/wt/odlux/framework/src/utilities/elasticSearch.ts b/sdnr/wt/odlux/framework/src/utilities/elasticSearch.ts index aeab8a0d6..690324e1c 100644 --- a/sdnr/wt/odlux/framework/src/utilities/elasticSearch.ts +++ b/sdnr/wt/odlux/framework/src/utilities/elasticSearch.ts @@ -2,12 +2,14 @@ import { DataCallback } from '../components/material-table';
import { Result, HitEntry } from '../models';
+import { requestRest } from '../services/restService';
+
type propType = string | number | null | undefined | (string | number)[];
type dataType = { [prop: string]: propType };
type resultType<TData = dataType> = { page: number, rowCount: number, rows: TData[] };
export function createSearchDataHandler<TResult extends {} = dataType>(uri: string, additionalParameters?: {}): DataCallback<(TResult & { _id: string })>;
-export function createSearchDataHandler<TResult extends {} = dataType, TData = dataType>(uri: string, additionalParameters: {} | null | undefined, mapResult: (res: HitEntry<TResult>, index: number, arr: HitEntry<TResult>[]) => (TData & { _id: string }), mapRequest?: (name?: string | null) => string): DataCallback<(TData & { _id: string })>
+export function createSearchDataHandler<TResult extends {} = dataType, TData = dataType>(uri: string, additionalParameters: {} | null | undefined, mapResult: (res: HitEntry<TResult>, index: number, arr: HitEntry<TResult>[]) => (TData & { _id: string }), mapRequest?: (name?: string | null) => string): DataCallback<(TData & { _id: string })>
export function createSearchDataHandler<TResult, TData>(uri: string, additionalParameters?: {} | null | undefined, mapResult?: (res: HitEntry<TResult>, index: number, arr: HitEntry<TResult>[]) => (TData & { _id: string }), mapRequest?: (name?: string | null) => string): DataCallback<(TData & { _id: string })> {
const url = `${ window.location.origin }/database/${uri}/_search`;
const fetchData: DataCallback<(TData & { _id: string }) > = async (page, rowsPerPage, orderBy, order, filter) => {
@@ -16,7 +18,7 @@ export function createSearchDataHandler<TResult, TData>(uri: string, additionalP : null;
const filterKeys = filter && Object.keys(filter) || [];
-
+
const query = {
...filterKeys.length > 0 ? {
query: {
@@ -35,7 +37,7 @@ export function createSearchDataHandler<TResult, TData>(uri: string, additionalP ...orderBy && order ? { "sort": [{ [mapRequest ? mapRequest(orderBy) : orderBy]: order }] } : {},
...additionalParameters ? additionalParameters : {}
};
- const result = await fetch(url, {
+ const result = await requestRest<Result<TResult & { _id: string }>>(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "no-cors", // no-cors, cors, *same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
@@ -46,18 +48,17 @@ export function createSearchDataHandler<TResult, TData>(uri: string, additionalP body: JSON.stringify(query), // body data type must match "Content-Type" header
});
- if (result.ok) {
- const queryResult: Result<TResult & { _id: string }> = await result.json();
+ if (result) {
let rows: (TData & { _id: string })[] = [];
- if (queryResult && queryResult.hits && queryResult.hits.hits) {
- rows = queryResult.hits.hits.map( mapResult ? mapResult : h => (
+ if (result && result.hits && result.hits.hits) {
+ rows = result.hits.hits.map( mapResult ? mapResult : h => (
{ ...(h._source as any as TData), _id: h._id }
)) || []
}
const data = {
- page: Math.min(page || 0, queryResult.hits.total || 0 / (rowsPerPage || 1)), rowCount: queryResult.hits.total, rows: rows
+ page: Math.min(page || 0, result.hits.total || 0 / (rowsPerPage || 1)), rowCount: result.hits.total, rows: rows
};
return data;
}
diff --git a/sdnr/wt/odlux/framework/src/views/login.tsx b/sdnr/wt/odlux/framework/src/views/login.tsx index 6fa24e4ab..513e24712 100644 --- a/sdnr/wt/odlux/framework/src/views/login.tsx +++ b/sdnr/wt/odlux/framework/src/views/login.tsx @@ -1,145 +1,193 @@ -import * as React from 'react';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
-
-import Avatar from '@material-ui/core/Avatar';
-import Button from '@material-ui/core/Button';
-import CssBaseline from '@material-ui/core/CssBaseline';
-import FormControl from '@material-ui/core/FormControl';
-import FormControlLabel from '@material-ui/core/FormControlLabel';
-import Checkbox from '@material-ui/core/Checkbox';
-import Input from '@material-ui/core/Input';
-import InputLabel from '@material-ui/core/InputLabel';
-import LockIcon from '@material-ui/icons/LockOutlined';
-import Paper from '@material-ui/core/Paper';
-import Typography from '@material-ui/core/Typography';
-import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
-
-import connect, { Connect } from '../flux/connect';
-import authenticationService from '../services/authenticationService';
-
-import { UpdateAuthentication } from '../actions/authentication';
-
-const styles = (theme: Theme) => createStyles({
- layout: {
- width: 'auto',
- display: 'block', // Fix IE11 issue.
- marginLeft: theme.spacing.unit * 3,
- marginRight: theme.spacing.unit * 3,
- [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: {
- width: 400,
- marginLeft: 'auto',
- marginRight: 'auto',
- },
- },
- paper: {
- marginTop: theme.spacing.unit * 8,
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- padding: `${ theme.spacing.unit * 2 }px ${ theme.spacing.unit * 3 }px ${ theme.spacing.unit * 3 }px`,
- },
- avatar: {
- margin: theme.spacing.unit,
- backgroundColor: theme.palette.secondary.main,
- },
- form: {
- width: '100%', // Fix IE11 issue.
- marginTop: theme.spacing.unit,
- },
- submit: {
- marginTop: theme.spacing.unit * 3,
- },
-});
-
-type LoginProps = RouteComponentProps<{}> & WithStyles<typeof styles> & Connect ;
-
-interface ILoginState {
- busy: boolean;
- email: string;
- password: string;
-}
-
-
-// todo: ggf. redirect to einbauen
-class LoginComponent extends React.Component<LoginProps, ILoginState> {
-
- constructor(props: LoginProps) {
- super(props);
-
- this.state = {
- busy: false,
- email: '',
- password: ''
- };
- }
-
- render(): JSX.Element {
- const { classes } = this.props;
- return (
- <React.Fragment>
- <CssBaseline />
- <main className={ classes.layout }>
- <Paper className={ classes.paper }>
- <Avatar className={ classes.avatar }>
- <LockIcon />
- </Avatar>
- <Typography variant="caption">Sign in</Typography>
- <form className={ classes.form }>
- <FormControl margin="normal" required fullWidth>
- <InputLabel htmlFor="email">Email Address</InputLabel>
- <Input id="email" name="email" autoComplete="email" autoFocus
- disabled={ this.state.busy }
- value = {this.state.email }
- onChange={ event => { this.setState({ email: event.target.value }) } }/>
- </FormControl>
- <FormControl margin="normal" required fullWidth>
- <InputLabel htmlFor="password">Password</InputLabel>
- <Input
- name="password"
- type="password"
- id="password"
- autoComplete="current-password"
- disabled={ this.state.busy }
- value={ this.state.password }
- onChange={ event => { this.setState({ password: event.target.value }) } }
- />
- </FormControl>
- <FormControlLabel
- control={ <Checkbox value="remember" color="primary" /> }
- label="Remember me"
- />
- <Button
- type="submit"
- fullWidth
- variant="raised"
- color="primary"
- disabled = { this.state.busy }
- className={ classes.submit }
- onClick = { this.onSignIn }
- >
- Sign in
- </Button>
- </form>
- </Paper>
- </main>
- </React.Fragment>
- );
- }
-
- private onSignIn = async (event: React.MouseEvent<HTMLButtonElement>) => {
- event.preventDefault();
-
- this.setState({ busy: true });
- const token = await authenticationService.authenticateUser(this.state.email, this.state.password);
- this.props.dispatch(new UpdateAuthentication(token));
- this.setState({ busy: false });
-
- if (token) {
- this.props.history.replace("/");
- }
-
- }
-}
-
-export const Login = withStyles(styles)(withRouter(connect()(LoginComponent)));
+import * as React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import Avatar from '@material-ui/core/Avatar'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import FormControl from '@material-ui/core/FormControl'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; +import LockIcon from '@material-ui/icons/LockOutlined'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import connect, { Connect } from '../flux/connect'; +import authenticationService from '../services/authenticationService'; + +import { UpdateAuthentication } from '../actions/authentication'; + +const styles = (theme: Theme) => createStyles({ + layout: { + width: 'auto', + display: 'block', // Fix IE11 issue. + marginLeft: theme.spacing.unit * 3, + marginRight: theme.spacing.unit * 3, + [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: { + width: 400, + marginLeft: 'auto', + marginRight: 'auto', + }, + }, + paper: { + marginTop: theme.spacing.unit * 8, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: `${ theme.spacing.unit * 2 }px ${ theme.spacing.unit * 3 }px ${ theme.spacing.unit * 3 }px`, + }, + avatar: { + margin: theme.spacing.unit, + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: '100%', // Fix IE11 issue. + marginTop: theme.spacing.unit, + }, + submit: { + marginTop: theme.spacing.unit * 3, + }, +}); + +type LoginProps = RouteComponentProps<{}> & WithStyles<typeof styles> & Connect ; + +interface ILoginState { + busy: boolean; + email: string; + password: string; + scope: string; + message: string; +} + + +// todo: ggf. redirect to einbauen +class LoginComponent extends React.Component<LoginProps, ILoginState> { + + constructor(props: LoginProps) { + super(props); + + this.state = { + busy: false, + email: '', + password: '', + scope: 'sdn', + message: '' + }; + } + + render(): JSX.Element { + const { classes } = this.props; + return ( + <React.Fragment> + <CssBaseline /> + <main className={ classes.layout }> + <Paper className={ classes.paper }> + <Avatar className={ classes.avatar }> + <LockIcon /> + </Avatar> + <Typography variant="caption">Sign in</Typography> + <form className={ classes.form }> + <FormControl margin="normal" required fullWidth> + <InputLabel htmlFor="email">Email Address</InputLabel> + <Input id="email" name="email" autoComplete="email" autoFocus + disabled={ this.state.busy } + value = {this.state.email } + onChange={ event => { this.setState({ email: event.target.value }) } }/> + </FormControl> + <FormControl margin="normal" required fullWidth> + <InputLabel htmlFor="password">Password</InputLabel> + <Input + name="password" + type="password" + id="password" + autoComplete="current-password" + disabled={ this.state.busy } + value={ this.state.password } + onChange={ event => { this.setState({ password: event.target.value }) } } + /> + </FormControl> + <FormControl margin="normal" required fullWidth> + <InputLabel htmlFor="password">Scope</InputLabel> + <Input + name="scope" + type="scope" + id="scope" + disabled={this.state.busy} + value={this.state.scope} + onChange={event => { this.setState({ scope: event.target.value }) }} + /> + </FormControl> + <FormControlLabel + control={ <Checkbox value="remember" color="primary" /> } + label="Remember me" + /> + <Button + type="submit" + fullWidth + variant="raised" + color="primary" + disabled = { this.state.busy } + className={ classes.submit } + onClick = { this.onSignIn } + > + Sign in + </Button> + </form> + </Paper> + </main> + <Dialog + open={!!this.state.message} + onClose={() => { this.setState({message: ''})}} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + <DialogTitle id="alert-dialog-title">{"Error"}</DialogTitle> + <DialogContent> + <DialogContentText id="alert-dialog-description"> + { this.state.message } + </DialogContentText> + </DialogContent> + <DialogActions> + <Button onClick={() => { this.setState({ message: '' }) }} color="secondary" autoFocus> + OK + </Button> + </DialogActions> + </Dialog> + </React.Fragment> + ); + } + + private onSignIn = async (event: React.MouseEvent<HTMLButtonElement>) => { + event.preventDefault(); + + this.setState({ busy: true }); + const token = await authenticationService.authenticateUser(this.state.email, this.state.password, this.state.scope); + this.props.dispatch(new UpdateAuthentication(token)); + this.setState({ busy: false }); + + if (token) { + const query = + this.props.state.framework.navigationState.search && + this.props.state.framework.navigationState.search.replace(/^\?/, "") + .split('&').map(e => e.split("=")); + const returnTo = query && query.find(e => e[0] === "returnTo"); + this.props.history.replace(returnTo && returnTo[1] || "/"); + } + else { + this.setState({ + message: "Could not log in. Please check your credentials or ask your administrator for assistence.", + password: "" + }) + } + } +} + +export const Login = withStyles(styles)(withRouter(connect()(LoginComponent))); export default Login;
\ No newline at end of file |