Added login page

This commit is contained in:
kunkliricsi 2020-10-23 15:05:20 +02:00
parent 472f43a950
commit 0f0e5d9d1c
17 changed files with 469 additions and 19 deletions

View File

@ -26,7 +26,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="ClientApp\src\common\ErrorDispatcher.ts" />
<None Remove="ClientApp\src\common\ServiceBase.ts" />
<None Remove="ClientApp\src\components\auth\Auth.tsx" /> <None Remove="ClientApp\src\components\auth\Auth.tsx" />
<None Remove="ClientApp\src\components\auth\AuthService.ts" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,31 @@
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
var React = require("react");
var ReactDOM = require("react-dom");
var react_redux_1 = require("react-redux");
var react_router_dom_1 = require("react-router-dom");
var App_1 = require("./App");
it('renders without crashing', function () {
var storeFake = function (state) { return ({
default: function () { },
subscribe: function () { },
dispatch: function () { },
getState: function () { return (__assign({}, state)); }
}); };
var store = storeFake({});
ReactDOM.render(React.createElement(react_redux_1.Provider, { store: store },
React.createElement(react_router_dom_1.MemoryRouter, null,
React.createElement(App_1.default, null))), document.createElement('div'));
});
//# sourceMappingURL=App.test.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"App.test.js","sourceRoot":"","sources":["App.test.tsx"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,6BAA+B;AAC/B,oCAAsC;AACtC,2CAAuC;AACvC,qDAAgD;AAChD,6BAAwB;AAExB,EAAE,CAAC,0BAA0B,EAAE;IAC3B,IAAM,SAAS,GAAG,UAAC,KAAU,IAAK,OAAA,CAAC;QAC/B,OAAO,EAAE,cAAO,CAAC;QACjB,SAAS,EAAE,cAAO,CAAC;QACnB,QAAQ,EAAE,cAAO,CAAC;QAClB,QAAQ,EAAE,cAAM,OAAA,cAAM,KAAK,EAAG,EAAd,CAAc;KACjC,CAAC,EALgC,CAKhC,CAAC;IACH,IAAM,KAAK,GAAG,SAAS,CAAC,EAAE,CAAQ,CAAC;IAEnC,QAAQ,CAAC,MAAM,CACX,oBAAC,sBAAQ,IAAC,KAAK,EAAE,KAAK;QAClB,oBAAC,+BAAY;YACT,oBAAC,aAAG,OAAE,CACK,CACR,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC"}

View File

@ -1,16 +1,93 @@
import * as React from 'react'; import { Box, Container } from '@material-ui/core';
import { Route } from 'react-router'; import AppBar from '@material-ui/core/AppBar';
import Layout from './components/Layout'; import blue from '@material-ui/core/colors/blue';
import { createMuiTheme } from '@material-ui/core/styles';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import { ThemeProvider } from '@material-ui/styles';
import React, { useState } from 'react';
import { BrowserRouter, NavLink, Redirect, Route, Switch } from 'react-router-dom';
import Auth from './components/auth/Auth';
import AuthService from './components/auth/AuthService';
import Home from './components/Home'; import Home from './components/Home';
import Counter from './components/Counter'; import './custom.css';
import FetchData from './components/FetchData';
import './custom.css'
export default () => ( const theme = createMuiTheme({
<Layout> palette: {
<Route exact path='/' component={Home} /> primary: {
<Route path='/counter' component={Counter} /> main: blue[800]
<Route path='/fetch-data/:startDateIndex?' component={FetchData} /> },
</Layout> },
});
function App() {
const [authenticated, setAuthenticated] = useState(AuthService.isAuthenticated());
const onAuthenticated = () => {
setAuthenticated(AuthService.isAuthenticated());
};
const LoginComponent = () => {
return (
<Auth onAuthenticated={onAuthenticated} />
); );
}
return (
<ThemeProvider theme={theme}>
<BrowserRouter>
<Switch>
<PublicRoute path="/login" component={LoginComponent} />
<PrivateRoute path="/" exact authenticated={authenticated} component={Home} />
</Switch>
</BrowserRouter>
</ThemeProvider>
);
}
export default App;
const PublicRoute = ({ component: Component, ...rest }: { [x: string]: any, component: any}) => {
return (
<Route {...rest} render={matchProps => (
<NoLayout component={Component} {...matchProps} />
)} />
);
}
const PrivateRoute = ({ component: Component, authenticated: Authenticated, ...rest }: { [x: string]: any, component: any, authenticated: any }) => {
return (
<Route {...rest} render={matchProps => (
Authenticated
? <DefaultLayout component={Component} {...matchProps} />
: <Redirect to='/login' />
)} />
);
};
const NoLayout = ({ component: Component, ...rest }: { [x: string]: any, component: any }) => {
return (
<Component {...rest} />
);
};
const DefaultLayout = ({ component: Component, ...rest }: { [x: string]: any, component: any }) => {
return (
<React.Fragment>
<AppBar position="static">
<Toolbar>
<Typography component={'span'}>
<Container className="nav-menu">
<NavLink exact to="/" className="nav-menu-item" activeStyle={{ color: 'white' }}>Dashboard</NavLink>
<NavLink exact to="/login" className="nav-menu-item" activeStyle={{ color: 'white' }}>Login</NavLink>
</Container>
</Typography>
</Toolbar>
</AppBar>
<Box style={{ margin: '32px' }}>
<Component {...rest} />
</Box>
</React.Fragment>
);
};

View File

@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ErrorDispatcher = {
errorHandlers: [],
registerErrorHandler: function (errorHandlerFn) {
this.errorHandlers.push(errorHandlerFn);
},
raiseError: function (errorMessage) {
for (var i = 0; i < this.errorHandlers.length; i++)
this.errorHandlers[i](errorMessage);
}
};
exports.default = ErrorDispatcher;
//# sourceMappingURL=ErrorDispatcher.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"ErrorDispatcher.js","sourceRoot":"","sources":["ErrorDispatcher.ts"],"names":[],"mappings":";;AAAA,IAAM,eAAe,GAAG;IACtB,aAAa,EAAE,EAAE;IAEjB,oBAAoB,YAAC,cAAc;QACjC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC1C,CAAC;IAED,UAAU,YAAC,YAAY;QACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE;YAChD,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;IACxC,CAAC;CACF,CAAC;AAEF,kBAAe,eAAe,CAAC"}

View File

@ -0,0 +1,14 @@
const ErrorDispatcher = {
errorHandlers: [],
registerErrorHandler(errorHandlerFn) {
this.errorHandlers.push(errorHandlerFn);
},
raiseError(errorMessage) {
for (let i = 0; i < this.errorHandlers.length; i++)
this.errorHandlers[i](errorMessage);
}
};
export default ErrorDispatcher;

View File

@ -0,0 +1,50 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ErrorDispatcher_1 = require("./ErrorDispatcher");
function get(url) {
var options = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': sessionStorage.getItem('user')
}
};
return makeRequest(url, options);
}
function post(url, request) {
var options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': sessionStorage.getItem('user')
},
body: "",
};
if (request)
options.body = JSON.stringify(request);
return makeRequest(url, options);
}
function makeRequest(url, options) {
return fetch(url, options)
.then(ensureResponseSuccess)
.catch(errorHandler);
}
function ensureResponseSuccess(response) {
if (!response.ok)
return response.json()
.then(function (data) { return errorHandler(data); });
return response.text()
.then(function (text) { return text.length ? JSON.parse(text) : {}; });
}
function errorHandler(response) {
console.log(response);
if (response && response.Error)
ErrorDispatcher_1.default.raiseError(response.Error);
return Promise.reject();
}
exports.default = {
get: get,
post: post,
makeRequest: makeRequest
};
//# sourceMappingURL=ServiceBase.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"ServiceBase.js","sourceRoot":"","sources":["ServiceBase.ts"],"names":[],"mappings":";;AAAA,qDAAgD;AAEhD,SAAS,GAAG,CAAC,GAAG;IACZ,IAAI,OAAO,GAAG;QACV,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACL,cAAc,EAAE,kBAAkB;YAClC,eAAe,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC;SAClD;KACJ,CAAC;IAEF,OAAO,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,IAAI,CAAC,GAAG,EAAE,OAAO;IACtB,IAAI,OAAO,GAAG;QACV,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACL,cAAc,EAAE,kBAAkB;YAClC,eAAe,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC;SAClD;QACD,IAAI,EAAE,EAAE;KACX,CAAC;IAEF,IAAI,OAAO;QACP,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAE3C,OAAO,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,WAAW,CAAC,GAAG,EAAE,OAAO;IAC7B,OAAO,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC;SACrB,IAAI,CAAC,qBAAqB,CAAC;SAC3B,KAAK,CAAC,YAAY,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,qBAAqB,CAAC,QAAQ;IACnC,IAAI,CAAC,QAAQ,CAAC,EAAE;QACZ,OAAO,QAAQ,CAAC,IAAI,EAAE;aACjB,IAAI,CAAC,UAAA,IAAI,IAAI,OAAA,YAAY,CAAC,IAAI,CAAC,EAAlB,CAAkB,CAAC,CAAC;IAE1C,OAAO,QAAQ,CAAC,IAAI,EAAE;SACjB,IAAI,CAAC,UAAA,IAAI,IAAI,OAAA,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,EAAnC,CAAmC,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,YAAY,CAAC,QAAQ;IAC1B,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAEtB,IAAI,QAAQ,IAAI,QAAQ,CAAC,KAAK;QAC1B,yBAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE/C,OAAO,OAAO,CAAC,MAAM,EAAE,CAAC;AAC5B,CAAC;AAED,kBAAe;IACX,GAAG,KAAA;IACH,IAAI,MAAA;IACJ,WAAW,aAAA;CACd,CAAC"}

View File

@ -0,0 +1,59 @@
import ErrorDispatcher from './ErrorDispatcher';
function get(url) {
let options = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': sessionStorage.getItem('user')
}
};
return makeRequest(url, options);
}
function post(url, request) {
let options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': sessionStorage.getItem('user')
},
body: "",
};
if (request)
options.body = JSON.stringify(request);
return makeRequest(url, options);
}
function makeRequest(url, options) {
return fetch(url, options)
.then(ensureResponseSuccess)
.catch(errorHandler);
}
function ensureResponseSuccess(response) {
if (!response.ok)
return response.json()
.then(data => errorHandler(data));
return response.text()
.then(text => text.length ? JSON.parse(text) : {});
}
function errorHandler(response) {
console.log(response);
if (response && response.Error)
ErrorDispatcher.raiseError(response.Error);
return Promise.reject();
}
export default {
get,
post,
makeRequest
};

View File

@ -1 +1,119 @@
 import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Box, Grid, TextField, Button, Typography, Paper } from '@material-ui/core';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
import AuthService from './AuthService';
export default function MenuItem(props: any) {
const history = useHistory();
const classes = useStyles();
const [username, setUsername] = useState(null);
const [password, setPassword] = useState(null);
const [showError, setShowError] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const onUsernameChanged = (event: any) => {
setUsername(event.target.value);
setShowError(false);
setErrorMessage('');
};
const onPasswordChanged = (event: any) => {
setPassword(event.target.value);
setShowError(false);
setErrorMessage('');
};
const onPasswordKeyPress = (event: any) => {
if (event.key === 'Enter') {
onLoginClicked();
}
};
const onLoginClicked = () => {
if (!username) {
setShowError(true);
setErrorMessage('Username required');
return;
}
if (!password) {
setShowError(true);
setErrorMessage('Password required');
return;
}
AuthService.login(username, password).then(() => {
props.onAuthenticated();
history.push('/');
}).catch(() => {
setShowError(true);
setErrorMessage('Invalid credentials');
});
};
const renderErrorLabel = () => {
return showError
? <Typography>{errorMessage}</Typography>
: <React.Fragment />;
};
return (
<Box className={classes.root}>
<Paper className={classes.paper}>
<Grid container className={classes.container} spacing={2}>
<Grid item>
<Typography component="h1" variant="h5">
Sign in
</Typography>
</Grid>
<Grid item xs={12} >
<TextField label="Username" type="text" onChange={onUsernameChanged} />
</Grid>
<Grid item xs={12} >
<TextField label="Password" type="password" onChange={onPasswordChanged} onKeyPress={onPasswordKeyPress} />
</Grid>
<Grid item xs={12} className={classes.error}>
{renderErrorLabel()}
</Grid>
<Grid item xs={12} className={classes.button}>
<Button className={classes.button} variant="contained" color="primary" onClick={onLoginClicked}>Login</Button>
</Grid>
</Grid>
</Paper>
</Box>
);
};
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: 400,
minHeight: 600,
},
container: {
padding: 40,
flexDirection: "column",
justifyContent: "space-around",
alignItems: "center",
},
paper: {
},
button: {
width: '100%',
},
error: {
color: "red",
}
}),
);

View File

@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ServiceBase_1 = require("../../common/ServiceBase");
var login_url = '/auth/authenticate';
exports.default = {
isAuthenticated: function () {
return sessionStorage.getItem('user') !== null;
},
login: function (username, password) {
var body = {
username: username,
password: password
};
var options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
};
return ServiceBase_1.default.makeRequest(login_url, options)
.then(function (response) {
sessionStorage.setItem('user', response.token_type + " " + response.access_token);
return Promise.resolve();
});
}
};
//# sourceMappingURL=AuthService.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"AuthService.js","sourceRoot":"","sources":["AuthService.ts"],"names":[],"mappings":";;AAAA,wDAAmD;AAEnD,IAAM,SAAS,GAAG,oBAAoB,CAAC;AAEvC,kBAAe;IACX,eAAe;QACX,OAAO,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;IACnD,CAAC;IAED,KAAK,YAAC,QAAQ,EAAE,QAAQ;QACpB,IAAI,IAAI,GAAG;YACP,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,QAAQ;SACrB,CAAC;QACF,IAAI,OAAO,GAAG;YACV,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACL,cAAc,EAAE,kBAAkB;aACrC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC7B,CAAC;QAEF,OAAO,qBAAW,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC;aAC7C,IAAI,CAAC,UAAA,QAAQ;YACV,cAAc,CAAC,OAAO,CAAC,MAAM,EAAK,QAAQ,CAAC,UAAU,SAAI,QAAQ,CAAC,YAAc,CAAC,CAAC;YAClF,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC7B,CAAC,CAAC,CAAC;IACX,CAAC;CACJ,CAAA"}

View File

@ -0,0 +1,29 @@
import ServiceBase from '../../common/ServiceBase';
const login_url = '/auth/authenticate';
export default {
isAuthenticated() {
return sessionStorage.getItem('user') !== null;
},
login(username, password) {
let body = {
username: username,
password: password
};
let options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
};
return ServiceBase.makeRequest(login_url, options)
.then(response => {
sessionStorage.setItem('user', `${response.token_type} ${response.access_token}`);
return Promise.resolve();
});
}
}

View File

@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
require("bootstrap/dist/css/bootstrap.css");
var React = require("react");
var ReactDOM = require("react-dom");
var react_redux_1 = require("react-redux");
var connected_react_router_1 = require("connected-react-router");
var history_1 = require("history");
var configureStore_1 = require("./store/configureStore");
var App_1 = require("./App");
var registerServiceWorker_1 = require("./registerServiceWorker");
// Create browser history to use in the Redux store
var baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
var history = history_1.createBrowserHistory({ basename: baseUrl });
// Get the application-wide store instance, prepopulating with state from the server where available.
var store = configureStore_1.default(history);
ReactDOM.render(React.createElement(react_redux_1.Provider, { store: store },
React.createElement(connected_react_router_1.ConnectedRouter, { history: history },
React.createElement(App_1.default, null))), document.getElementById('root'));
registerServiceWorker_1.default();
//# sourceMappingURL=index.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.tsx"],"names":[],"mappings":";;AAAA,4CAA0C;AAE1C,6BAA+B;AAC/B,oCAAsC;AACtC,2CAAuC;AACvC,iEAAyD;AACzD,mCAA+C;AAC/C,yDAAoD;AACpD,6BAAwB;AACxB,iEAA4D;AAE5D,mDAAmD;AACnD,IAAM,OAAO,GAAG,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAW,CAAC;AACxF,IAAM,OAAO,GAAG,8BAAoB,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;AAE5D,qGAAqG;AACrG,IAAM,KAAK,GAAG,wBAAc,CAAC,OAAO,CAAC,CAAC;AAEtC,QAAQ,CAAC,MAAM,CACX,oBAAC,sBAAQ,IAAC,KAAK,EAAE,KAAK;IAClB,oBAAC,wCAAe,IAAC,OAAO,EAAE,OAAO;QAC7B,oBAAC,aAAG,OAAG,CACO,CACX,EACX,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;AAErC,+BAAqB,EAAE,CAAC"}

View File

@ -33,7 +33,7 @@ namespace Birdmap.Controllers
public async Task<IActionResult> AuthenticateAsync([FromBody] AuthenticateRequest model) public async Task<IActionResult> AuthenticateAsync([FromBody] AuthenticateRequest model)
{ {
var user = await _service.AuthenticateUserAsync(model.Username, model.Password); var user = await _service.AuthenticateUserAsync(model.Username, model.Password);
var expires = DateTime.UtcNow.AddHours(2); var expiresInSeconds = TimeSpan.FromHours(2).TotalSeconds;
var tokenHandler = new JwtSecurityTokenHandler(); var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["BasicAuth:Secret"]); var key = Encoding.ASCII.GetBytes(_configuration["BasicAuth:Secret"]);
var tokenDescriptor = new SecurityTokenDescriptor var tokenDescriptor = new SecurityTokenDescriptor
@ -42,7 +42,7 @@ namespace Birdmap.Controllers
{ {
new Claim(ClaimTypes.Name, user.Name) new Claim(ClaimTypes.Name, user.Name)
}), }),
Expires = expires, Expires = DateTime.UtcNow.AddHours(expiresInSeconds),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
}; };
var token = tokenHandler.CreateToken(tokenDescriptor); var token = tokenHandler.CreateToken(tokenDescriptor);
@ -51,9 +51,10 @@ namespace Birdmap.Controllers
return Ok( return Ok(
new new
{ {
Name = user.Name, user_name = user.Name,
Token = tokenString, access_token = tokenString,
Expires = expires, token_type = "Bearer",
expires_in = expiresInSeconds,
}); });
} }
} }