From 2d4424c28ac35763ef44c42ae2f01664d42b268c Mon Sep 17 00:00:00 2001 From: Herbert Eiselt Date: Tue, 12 Mar 2019 18:00:21 +0100 Subject: Security provider for UX-Client-Login Use ODL provided oauth2/token for UX clients Change-Id: I9f9ae931fc5e74dc13076bd23551d163c0685606 Issue-ID: SDNC-648 Signed-off-by: Herbert Eiselt --- .../odlux/framework/src/actions/authentication.ts | 3 +- sdnr/wt/odlux/framework/src/app.tsx | 15 +- .../framework/src/components/navigationMenu.tsx | 12 +- .../wt/odlux/framework/src/components/titleBar.tsx | 6 +- .../src/handlers/authenticationHandler.ts | 10 +- sdnr/wt/odlux/framework/src/index.html | 2 +- .../odlux/framework/src/middleware/navigation.ts | 15 +- .../odlux/framework/src/models/authentication.ts | 59 ++-- .../src/services/authenticationService.ts | 36 ++- .../framework/src/services/notificationService.ts | 6 +- .../wt/odlux/framework/src/services/restService.ts | 33 +- .../odlux/framework/src/utilities/elasticSearch.ts | 17 +- sdnr/wt/odlux/framework/src/views/login.tsx | 336 ++++++++++++--------- 13 files changed, 327 insertions(+), 223 deletions(-) (limited to 'sdnr/wt/odlux/framework/src') 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(, 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 & Connect) => { + const { user } = state.framework.authenticationState return ( -
+ {user && user.isValid && <> +
{ /* https://fiffty.github.io/react-treeview-mui/ */} - { process.env.NODE_ENV === "development" ? } /> : null } + { process.env.NODE_ENV === "development" ? } /> : null } { state.framework.applicationRegistraion && Object.keys(state.framework.applicationRegistraion).map(key => { @@ -51,8 +53,10 @@ export const NavigationMenu = withStyles(styles)(connect()(({ classes, state }: }) || null } - { process.env.NODE_ENV === "development" ? } /> : null } - + { process.env.NODE_ENV === "development" ? } /> : null } + + || null + } ) })); 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 = (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 @@ 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 { - 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 { + const result = await requestRest(`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(scope: string | string[], callback: SubscriptionCallback): Promise { 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 => { 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(path: string = '', init: RequestInit = {}, authenticate: boolean = false): Promise { +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(path: string = '', init: RequestInit = {}, authenticate: boolean = true): Promise { const isAbsUrl = absUrlPattern.test(path); const uri = isAbsUrl ? path : (baseUri) + ('/' + path).replace(/\/{2,}/i, '/'); init.headers = { @@ -11,13 +22,23 @@ export async function requestRest(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 = { page: number, rowCount: number, rows: TData[] }; export function createSearchDataHandler(uri: string, additionalParameters?: {}): DataCallback<(TResult & { _id: string })>; -export function createSearchDataHandler(uri: string, additionalParameters: {} | null | undefined, mapResult: (res: HitEntry, index: number, arr: HitEntry[]) => (TData & { _id: string }), mapRequest?: (name?: string | null) => string): DataCallback<(TData & { _id: string })> +export function createSearchDataHandler(uri: string, additionalParameters: {} | null | undefined, mapResult: (res: HitEntry, index: number, arr: HitEntry[]) => (TData & { _id: string }), mapRequest?: (name?: string | null) => string): DataCallback<(TData & { _id: string })> export function createSearchDataHandler(uri: string, additionalParameters?: {} | null | undefined, mapResult?: (res: HitEntry, index: number, arr: HitEntry[]) => (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(uri: string, additionalP : null; const filterKeys = filter && Object.keys(filter) || []; - + const query = { ...filterKeys.length > 0 ? { query: { @@ -35,7 +37,7 @@ export function createSearchDataHandler(uri: string, additionalP ...orderBy && order ? { "sort": [{ [mapRequest ? mapRequest(orderBy) : orderBy]: order }] } : {}, ...additionalParameters ? additionalParameters : {} }; - const result = await fetch(url, { + const result = await requestRest>(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(uri: string, additionalP body: JSON.stringify(query), // body data type must match "Content-Type" header }); - if (result.ok) { - const queryResult: Result = 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 & Connect ; - -interface ILoginState { - busy: boolean; - email: string; - password: string; -} - - -// todo: ggf. redirect to einbauen -class LoginComponent extends React.Component { - - constructor(props: LoginProps) { - super(props); - - this.state = { - busy: false, - email: '', - password: '' - }; - } - - render(): JSX.Element { - const { classes } = this.props; - return ( - - -
- - - - - Sign in -
- - Email Address - { this.setState({ email: event.target.value }) } }/> - - - Password - { this.setState({ password: event.target.value }) } } - /> - - } - label="Remember me" - /> - - -
-
-
- ); - } - - private onSignIn = async (event: React.MouseEvent) => { - 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 & Connect ; + +interface ILoginState { + busy: boolean; + email: string; + password: string; + scope: string; + message: string; +} + + +// todo: ggf. redirect to einbauen +class LoginComponent extends React.Component { + + constructor(props: LoginProps) { + super(props); + + this.state = { + busy: false, + email: '', + password: '', + scope: 'sdn', + message: '' + }; + } + + render(): JSX.Element { + const { classes } = this.props; + return ( + + +
+ + + + + Sign in +
+ + Email Address + { this.setState({ email: event.target.value }) } }/> + + + Password + { this.setState({ password: event.target.value }) } } + /> + + + Scope + { this.setState({ scope: event.target.value }) }} + /> + + } + label="Remember me" + /> + + +
+
+ { this.setState({message: ''})}} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {"Error"} + + + { this.state.message } + + + + + + +
+ ); + } + + private onSignIn = async (event: React.MouseEvent) => { + 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 -- cgit 1.2.3-korg