Add login flow

Present user with a login form to input token when connection fails or
the token is not present in local storage.  Replaces the settings page.
This commit is contained in:
Hornwitser committed 2020-10-29 19:11:10 +01:00
1 parent 019ffc25f0
commit 0c0626564a
9 files changed
+128 -96

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ module.exports = {
},
},
{
"files": ["packages/master/web/**/*.jsx"],
"files": ["packages/web_ui/src/**/*.jsx", "plugins/*/web/**/*.jsx"],
"env": {
"browser": true,
},
+3 -8
View File
@@ -8,6 +8,7 @@ import libConfig from "@clusterio/lib/config";
import App from "./components/App";
import { Control, ControlConnector } from "./util/websocket";
async function loadScript(url) {
let script = document.createElement("script");
script.src = url;
@@ -61,17 +62,11 @@ async function load() {
let wsUrl = new URL(window.webRoot, document.location);
wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
let controlConnector = new ControlConnector(wsUrl, 10, localStorage.getItem("master_token"));
let controlConnector = new ControlConnector(wsUrl, 10);
// XXX come up with a better way of sharing this
window.control = new Control(controlConnector, []);
try {
await controlConnector.connect();
} catch (err) {
// XXX: What now?
console.log(err);
}
ReactDOM.render(<App />, document.getElementById("root"));
ReactDOM.render(<App control={control}/>, document.getElementById("root"));
}
load().catch(console.log);
+78 -6
View File
@@ -1,13 +1,85 @@
import React from "react";
import events from "events";
import React, { useEffect, useState } from "react";
import { BrowserRouter } from "react-router-dom";
import basename from "../basename";
import Layout from "../layout";
import SiteLayout from "../layout";
import ControlContext from "./ControlContext";
import LoginForm from "./LoginForm";
import { Card, Spin } from "antd";
export default function App(props) {
let [connected, setConnected] = useState(false);
let [token, setToken] = useState(localStorage.getItem("master_token") || null);
let connector = props.control.connector;
function clearToken() {
setToken(null);
localStorage.removeItem("master_token");
}
useEffect(() => {
console.log("Effect called");
function onConnect() {
localStorage.setItem("master_token", token);
setConnected(true);
}
function onClose() {
setConnected(false);
}
function onError(err) {
console.error("Unexpected error in connector", err);
clearToken();
setConnected(false);
}
connector.on("connect", onConnect);
connector.on("close", onClose);
connector.on("error", onError);
if (token && !connected) {
connector.token = token;
connector.connect();
}
return () => {
connector.off("error", onError);
connector.off("close", onClose);
connector.off("connect", onConnect);
};
});
let page;
if (connected) {
page = <SiteLayout/>;
} else if (token) {
page = <div className="login-container">
<Card>
<h1>Connecting</h1>
<Spin size="large"/>
</Card>
</div>;
} else {
page = <div className="login-container">
<Card>
<h1>Clusterio</h1>
<LoginForm setToken={setToken} />
</Card>
</div>;
}
export default function App() {
return (
<BrowserRouter basename={basename}>
<Layout />
</BrowserRouter>
<ControlContext.Provider value={props.control}>
<BrowserRouter basename={basename}>
{page}
</BrowserRouter>
</ControlContext.Provider>
);
}
@@ -0,0 +1,3 @@
import React from "react";
export default React.createContext();
@@ -0,0 +1,27 @@
import React from "react";
import { Form, Input, Button } from "antd";
import LockOutlined from "@ant-design/icons/LockOutlined";
export default function LoginForm(props) {
return (
<Form
name="login"
onFinish={(values) => { props.setToken(values.token); }}
>
<Form.Item
name="token"
rules={[
{ required: true, message: "Please input authentication token" },
]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder="Authentication Token"
/>
</Form.Item>
<Button type="primary" htmlType="submit">Log in</Button>
</Form>
);
}
+6
View File
@@ -23,3 +23,9 @@ code {
cursor: pointer;
}
.login-container {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
-6
View File
@@ -12,7 +12,6 @@ import { PermissionsTable } from "../pages/permissions/permissionsTable";
import { InstanceView } from "../pages/instances/instance";
import { UserView } from "../pages/users/user";
import { RoleView } from "../pages/roles/role";
import { Settings } from "../pages/settings/settings";
const { Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;
@@ -118,10 +117,6 @@ class SiteLayout extends Component {
<span>Permissions</span>
<Link to="/permissions" />
</Menu.Item>
<Menu.Item key="settings">
<span>Settings</span>
<Link to="/settings" />
</Menu.Item>
</Menu>
</Sider>
) : (
@@ -149,7 +144,6 @@ class SiteLayout extends Component {
<Route exact path="/roles/:id/view" component={RoleView} />
<Route exact path="/permissions" component={PermissionsTable} />
<Route exact path="/permissions/:id/view" component={PermissionsTable} />
<Route exact path="/settings" component={Settings} />
</Content>
<Footer style={{
// background: "#F4F4F4",
@@ -1,70 +0,0 @@
import React, { Component } from "react";
import { Card, Button, Input, Form } from "antd";
const layout = {
labelCol: {
span: 4,
},
wrapperCol: {
span: 16,
},
};
const tailLayout = {
wrapperCol: {
offset: 4,
span: 16,
},
};
export class Settings extends Component {
constructor(props) {
super(props);
this.state = {
role: {},
};
}
navigate(url) {
this.props.history.push(url);
}
render() {
console.log(this.state);
return <Card>
<h2>Web interface settings</h2>
<Form
{...layout}
name="basic"
initialValues={{
master_token: localStorage.getItem("master_token"),
}}
onFinish={values => {
console.log("Saving settings",values);
for (let key of Object.keys(values)){
localStorage.setItem(key, values[key].replace("https://", "wss://").replace("http://", "ws://"));
}
// Force refresh to apply changes
document.location = document.location;
}}
>
<Form.Item
label="User auth token"
name="master_token"
rules={[
{
required: true,
message: "Please input your user auth token from config-control.json",
},
]}
>
<Input />
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Save
</Button>
</Form.Item>
</Form>
</Card>;
}
}
+10 -5
View File
@@ -1,6 +1,7 @@
const libLink = require("@clusterio/lib/link");
const libPlugin = require("@clusterio/lib/plugin");
const libErrors = require("@clusterio/lib/errors");
const version = require("../../package.json").version;
/**
* Format a parsed Factorio output message
@@ -36,17 +37,21 @@ function formatOutput(output) {
* @private
*/
export class ControlConnector extends libLink.WebSocketClientConnector {
constructor(url, reconnectDelay, token) {
constructor(url, reconnectDelay) {
super(url, reconnectDelay);
this._token = token;
this.token = null;
}
register() {
if (!this.token) {
throw new Error("Token not set");
}
console.log("SOCKET | registering control");
this.sendHandshake("register_control", {
token: this._token,
agent: "web",
version: "2.0.0-alpha",
token: this.token,
agent: "Clusterio Web UI",
version,
});
}
}