Skip to content

Commit 408de54

Browse files
committed
Added authentication
1 parent 5023124 commit 408de54

File tree

15 files changed

+508
-81
lines changed

15 files changed

+508
-81
lines changed

dashboard/assets.go

Lines changed: 54 additions & 54 deletions
Large diffs are not rendered by default.

dashboard/src/App.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
22
import DeploymentOperator from './deployment/DeploymentOperator.js';
33
import NoOperator from './NoOperator.js';
44
import Loading from './util/Loading.js';
5-
import { apiGet } from './api/api.js';
5+
import api from './api/api.js';
66
import { Container, Segment, Message } from 'semantic-ui-react';
77
import './App.css';
88

@@ -40,7 +40,7 @@ class App extends Component {
4040
}
4141

4242
reloadOperators = async() => {
43-
const operators = await apiGet('/api/operators');
43+
const operators = await api.get('/api/operators');
4444
this.setState({operators});
4545
}
4646

dashboard/src/api/api.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,49 @@
1-
// apiGet performs a GET request on the API with given local URL.
2-
// The result is decoded from JSON and returned.
3-
export async function apiGet(localURL) {
4-
const result = await fetch(localURL);
5-
const decoded = await result.json();
6-
return decoded;
1+
class Api {
2+
token = '';
3+
4+
async decodeResults(result) {
5+
const decoded = await result.json();
6+
if (result.status === 401) {
7+
throw Error(decoded.error || "Unauthorized")
8+
}
9+
if (result.status !== 200) {
10+
throw Error(`Unexpected status ${result.status}`);
11+
}
12+
return decoded;
13+
}
14+
15+
// apiGet performs a GET request on the API with given local URL.
16+
// The result is decoded from JSON and returned.
17+
async get(localURL) {
18+
let headers = {
19+
'Accept': 'application/json'
20+
};
21+
if (this.token) {
22+
headers['Authorization'] = `bearer ${this.token}`;
23+
}
24+
const result = await fetch(localURL, {headers});
25+
return this.decodeResults(result);
26+
}
27+
28+
// apiPost performs a POST request on the API with given local URL and given data.
29+
// The result is decoded from JSON and returned.
30+
async post(localURL, body) {
31+
let headers = {
32+
'Accept': 'application/json',
33+
'Content-Type': 'application/json'
34+
};
35+
if (this.token) {
36+
headers['Authorization'] = `bearer ${this.token}`;
37+
}
38+
const result = await fetch(localURL, {
39+
method: 'POST',
40+
headers,
41+
body: JSON.stringify(body)
42+
});
43+
return this.decodeResults(result);
44+
}
745
}
846

47+
var api = new Api();
48+
49+
export default api;

dashboard/src/auth/Auth.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { Component } from 'react';
2+
import api from '../api/api.js';
3+
import Login from './Login.js';
4+
import LogoutContext from './LogoutContext.js';
5+
import { getSessionItem, setSessionItem } from "../util/Storage.js";
6+
7+
const tokenSessionKey = "auth-token";
8+
9+
class Auth extends Component {
10+
state = {
11+
authenticated: false,
12+
token: getSessionItem(tokenSessionKey) || ""
13+
};
14+
15+
async componentDidMount() {
16+
try {
17+
api.token = this.state.token;
18+
await api.get('/api/operators');
19+
this.setState({
20+
authenticated: true,
21+
token: api.token
22+
});
23+
} catch (e) {
24+
this.setState({
25+
authenticated: false,
26+
token: ''
27+
});
28+
}
29+
}
30+
31+
handleLogin = async (username, password) => {
32+
try {
33+
this.setState({error:undefined});
34+
const res = await api.post('/login', { username, password });
35+
api.token = res.token;
36+
setSessionItem(tokenSessionKey, res.token);
37+
this.setState({
38+
authenticated: true,
39+
token: res.token
40+
});
41+
return true;
42+
} catch (e) {
43+
this.setState({
44+
authenticated: false,
45+
token: '',
46+
error: e.message
47+
});
48+
return false;
49+
}
50+
};
51+
52+
handleLogout = () => {
53+
api.token = '';
54+
setSessionItem(tokenSessionKey, '');
55+
this.setState({
56+
authenticated: false,
57+
token: '',
58+
error: undefined
59+
});
60+
};
61+
62+
componentWillUnmount() {
63+
}
64+
65+
render() {
66+
return (
67+
<LogoutContext.Provider value={this.handleLogout}>
68+
{(!this.state.authenticated) ?
69+
<Login doLogin={this.handleLogin} error={this.state.error}/> :
70+
this.props.children
71+
}
72+
</LogoutContext.Provider>
73+
);
74+
}
75+
}
76+
77+
export default Auth;

dashboard/src/auth/Login.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { Component } from 'react';
2+
import { Button, Container, Form, Icon, Message, Modal } from 'semantic-ui-react';
3+
4+
const LoginView = ({username, password, usernameChanged, passwordChanged, doLogin, error}) => (
5+
<Container>
6+
<Form onSubmit={doLogin}>
7+
<Form.Field>
8+
<label>Name</label>
9+
<input focus="true" value={username} onChange={(e) => usernameChanged(e.target.value)}/>
10+
</Form.Field>
11+
<Form.Field>
12+
<label>Password</label>
13+
<input type="password" value={password} onChange={(e) => passwordChanged(e.target.value)}/>
14+
</Form.Field>
15+
</Form>
16+
{(error) ? <Message error content={error}/> : null}
17+
</Container>
18+
);
19+
20+
class Login extends Component {
21+
state = {
22+
username: '',
23+
password: ''
24+
};
25+
26+
handleLogin = () => {
27+
this.props.doLogin(this.state.username, this.state.password);
28+
}
29+
30+
render() {
31+
return (
32+
<Modal open>
33+
<Modal.Header>Login</Modal.Header>
34+
<Modal.Content>
35+
<LoginView
36+
error={this.props.error}
37+
username={this.state.username}
38+
password={this.state.password}
39+
usernameChanged={(v) => this.setState({username:v})}
40+
passwordChanged={(v) => this.setState({password:v})}
41+
doLogin={this.handleLogin}
42+
/>
43+
</Modal.Content>
44+
<Modal.Actions>
45+
<Button color='green' disabled={((!this.state.username) || (!this.state.password))} onClick={this.handleLogin}>
46+
<Icon name='checkmark' /> Login
47+
</Button>
48+
</Modal.Actions>
49+
</Modal>
50+
);
51+
}
52+
}
53+
54+
export default Login;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import React from 'react';
2+
3+
const LogoutContext = React.createContext(undefined);
4+
5+
export default LogoutContext;
6+

dashboard/src/deployment/DeploymentDetails.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React, { Component } from 'react';
2-
import { apiGet } from '../api/api.js';
2+
import api from '../api/api.js';
33
import Loading from '../util/Loading.js';
44
import MemberList from './MemberList.js';
55

66
const MemberGroupsView = ({memberGroups, namespace}) => (
77
<div>
88
{memberGroups.map((item) => <MemberList
9+
key={`server-group-${item.group}`}
910
group={item.group}
1011
members={item.members}
1112
namespace={namespace}
@@ -27,7 +28,7 @@ class DeploymentDetails extends Component {
2728

2829
reloadDeployment = async() => {
2930
// TODO
30-
const result = await apiGet(`/api/deployment/${this.props.name}`);
31+
const result = await api.get(`/api/deployment/${this.props.name}`);
3132
this.setState({deployment:result});
3233
}
3334

dashboard/src/deployment/DeploymentList.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { Component } from 'react';
2-
import { apiGet } from '../api/api.js';
2+
import api from '../api/api.js';
33
import { Icon, Popup, Table } from 'semantic-ui-react';
44
import Loading from '../util/Loading.js';
55
import CommandInstruction from '../util/CommandInstruction.js';
@@ -121,7 +121,7 @@ class DeploymentList extends Component {
121121
}
122122

123123
reloadDeployments = async() => {
124-
const result = await apiGet('/api/deployment');
124+
const result = await api.get('/api/deployment');
125125
this.setState({items:result.deployments});
126126
}
127127

dashboard/src/deployment/DeploymentOperator.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { Component } from 'react';
2+
import LogoutContext from '../auth/LogoutContext.js';
23
import DeploymentDetails from './DeploymentDetails.js';
34
import DeploymentList from './DeploymentList.js';
45
import { Header, Menu, Segment } from 'semantic-ui-react';
@@ -42,11 +43,18 @@ class DeploymentOperator extends Component {
4243
return (
4344
<Router>
4445
<div>
45-
<StyledMenu fixed="left" vertical>
46-
<Menu.Item>
47-
<Link to="/">Deployments</Link>
48-
</Menu.Item>
49-
</StyledMenu>
46+
<LogoutContext.Consumer>
47+
{doLogout =>
48+
<StyledMenu fixed="left" vertical>
49+
<Menu.Item>
50+
<Link to="/">Deployments</Link>
51+
</Menu.Item>
52+
<Menu.Item position="right" onClick={() => doLogout()}>
53+
Logout
54+
</Menu.Item>
55+
</StyledMenu>
56+
}
57+
</LogoutContext.Consumer>
5058
<StyledContentBox>
5159
<Segment basic clearing>
5260
<div>

dashboard/src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import React from 'react';
22
import ReactDOM from 'react-dom';
33
import './index.css';
44
import App from './App';
5+
import Auth from './auth/Auth.js';
56
import registerServiceWorker from './registerServiceWorker';
67

7-
ReactDOM.render(<App />, document.getElementById('root'));
8+
ReactDOM.render(<Auth><App /></Auth>, document.getElementById('root'));
89
registerServiceWorker();

0 commit comments

Comments
 (0)