Implement login
This commit is contained in:
parent
8cd8f52566
commit
f3a0b7bc4f
12 changed files with 145 additions and 41 deletions
|
@ -18,7 +18,7 @@ import React, { Component } from "react"
|
||||||
class Home extends Component {
|
class Home extends Component {
|
||||||
render() {
|
render() {
|
||||||
return <main>
|
return <main>
|
||||||
|
Hello, {localStorage.username}
|
||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { Component } from "react"
|
import React, { Component } from "react"
|
||||||
|
import Spinner from "./Spinner"
|
||||||
|
import api from "./api"
|
||||||
|
|
||||||
class Login extends Component {
|
class Login extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
|
@ -21,24 +23,38 @@ class Login extends Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputChanged = event => this.setState({ [event.target.name]: event.target.value })
|
inputChanged = event => this.setState({ [event.target.name]: event.target.value })
|
||||||
|
|
||||||
login = () => {
|
login = async () => {
|
||||||
|
this.setState({ loading: true })
|
||||||
|
const resp = await api.login(this.state.username, this.state.password)
|
||||||
|
if (resp.token) {
|
||||||
|
await this.props.onLogin(resp.token)
|
||||||
|
} else if (resp.error) {
|
||||||
|
this.setState({ error: resp.error, loading: false })
|
||||||
|
} else {
|
||||||
|
this.setState({ error: "Unknown error", loading: false })
|
||||||
|
console.log("Unknown error:", resp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="login-wrapper">
|
return <div className="login-wrapper">
|
||||||
<div className="login">
|
<div className={`login ${this.state.error && "errored"}`}>
|
||||||
<h1 className="title">Maubot Manager</h1>
|
<h1>Maubot Manager</h1>
|
||||||
<input type="text" placeholder="Username" value={this.state.username}
|
<input type="text" placeholder="Username" value={this.state.username}
|
||||||
name="username" onChange={this.inputChanged}/>
|
name="username" onChange={this.inputChanged}/>
|
||||||
<input type="password" placeholder="Password" value={this.state.password}
|
<input type="password" placeholder="Password" value={this.state.password}
|
||||||
name="password" onChange={this.inputChanged}/>
|
name="password" onChange={this.inputChanged}/>
|
||||||
<button onClick={this.login}>Log in</button>
|
<button onClick={this.login}>
|
||||||
|
{this.state.loading ? <Spinner/> : "Log in"}
|
||||||
|
</button>
|
||||||
|
{this.state.error && <div className="error">{this.state.error}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,21 +18,52 @@ import { BrowserRouter as Router, Route, Redirect } from "react-router-dom"
|
||||||
import PrivateRoute from "./PrivateRoute"
|
import PrivateRoute from "./PrivateRoute"
|
||||||
import Home from "./Home"
|
import Home from "./Home"
|
||||||
import Login from "./Login"
|
import Login from "./Login"
|
||||||
|
import Spinner from "./Spinner"
|
||||||
|
import api from "./api"
|
||||||
|
|
||||||
class MaubotRouter extends Component {
|
class MaubotRouter extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
authed: localStorage.accessToken !== undefined,
|
pinged: false,
|
||||||
|
authed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentWillMount() {
|
||||||
|
if (localStorage.accessToken) {
|
||||||
|
await this.ping()
|
||||||
|
}
|
||||||
|
this.setState({ pinged: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async ping() {
|
||||||
|
try {
|
||||||
|
const username = await api.ping()
|
||||||
|
if (username) {
|
||||||
|
localStorage.username = username
|
||||||
|
this.setState({ authed: true })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login = async (token) => {
|
||||||
|
localStorage.accessToken = token
|
||||||
|
await this.ping()
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (!this.state.pinged) {
|
||||||
|
return <Spinner className="maubot-loading"/>
|
||||||
|
}
|
||||||
return <Router>
|
return <Router>
|
||||||
<div className={`maubot-wrapper ${this.state.authed ? "authenticated" : ""}`}>
|
<div className={`maubot-wrapper ${this.state.authed ? "authenticated" : ""}`}>
|
||||||
<Route path="/" exact render={() => <Redirect to={{ pathname: "/dashboard" }}/>}/>
|
<Route path="/" exact render={() => <Redirect to={{ pathname: "/dashboard" }}/>}/>
|
||||||
<PrivateRoute path="/dashboard" component={Home} authed={this.state.authed}/>
|
<PrivateRoute path="/dashboard" component={Home} authed={this.state.authed}/>
|
||||||
<Route path="/login" component={Login}/>
|
<PrivateRoute path="/login" render={() => <Login onLogin={this.login}/>}
|
||||||
|
authed={!this.state.authed} to="/dashboard"/>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
}
|
}
|
|
@ -1,15 +1,27 @@
|
||||||
import React, { Component } from "react"
|
// maubot - A plugin-based Matrix bot system.
|
||||||
|
// Copyright (C) 2018 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import React from "react"
|
||||||
import { Route, Redirect } from "react-router-dom"
|
import { Route, Redirect } from "react-router-dom"
|
||||||
|
|
||||||
const PrivateRoute = ({ component, authed, ...rest }) => (
|
const PrivateRoute = ({ component, render, authed, to = "/login", ...args }) => (
|
||||||
<Route
|
<Route
|
||||||
{...rest}
|
{...args}
|
||||||
render={(props) => authed === true
|
render={(props) => authed === true
|
||||||
? <Component {...props} />
|
? (component ? React.createElement(component, props) : render())
|
||||||
: <Redirect to={{
|
: <Redirect to={{ pathname: to }}/>}
|
||||||
pathname: "/login",
|
|
||||||
state: { from: props.location },
|
|
||||||
}}/>}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
const Spinner = () => (
|
const Spinner = (props) => (
|
||||||
<div className="loader">
|
<div {...props} className={`spinner ${props["className"] || ""}`}>
|
||||||
<svg viewBox="25 25 50 50">
|
<svg viewBox="25 25 50 50">
|
||||||
<circle cx="50" cy="50" r="20" fill="none" strokeWidth="2" strokeMiterlimit="10"/>
|
<circle cx="50" cy="50" r="20" fill="none" strokeWidth="2" strokeMiterlimit="10"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export default Spinner
|
||||||
|
|
|
@ -16,14 +16,15 @@
|
||||||
|
|
||||||
const BASE_PATH = "/_matrix/maubot/v1"
|
const BASE_PATH = "/_matrix/maubot/v1"
|
||||||
|
|
||||||
export function login(username, password) {
|
export async function login(username, password) {
|
||||||
return fetch(`${BASE_PATH}/auth/login`, {
|
const resp = await fetch(`${BASE_PATH}/auth/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
return await resp.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHeaders(contentType = "application/json") {
|
function getHeaders(contentType = "application/json") {
|
||||||
|
@ -84,3 +85,10 @@ export async function getClient(id) {
|
||||||
const resp = await fetch(`${BASE_PATH}/client/${id}`, { headers: getHeaders() })
|
const resp = await fetch(`${BASE_PATH}/client/${id}`, { headers: getHeaders() })
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
login, ping,
|
||||||
|
getInstances, getInstance,
|
||||||
|
getPlugins, getPlugin, uploadPlugin,
|
||||||
|
getClients, getClient,
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,6 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import ReactDOM from "react-dom"
|
import ReactDOM from "react-dom"
|
||||||
import "./style/index.sass"
|
import "./style/index.sass"
|
||||||
import App from "./Router"
|
import App from "./MaubotRouter"
|
||||||
|
|
||||||
ReactDOM.render(<App/>, document.getElementById("root"))
|
ReactDOM.render(<App/>, document.getElementById("root"))
|
||||||
|
|
|
@ -34,6 +34,10 @@ body
|
||||||
left: 0
|
left: 0
|
||||||
right: 0
|
right: 0
|
||||||
|
|
||||||
|
.maubot-loading
|
||||||
|
margin-top: 10rem
|
||||||
|
width: 10rem
|
||||||
|
|
||||||
//.lindeb
|
//.lindeb
|
||||||
> header
|
> header
|
||||||
position: absolute
|
position: absolute
|
||||||
|
|
|
@ -14,9 +14,11 @@
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
=button()
|
=button($width: null, $height: null, $padding: .375rem 1rem)
|
||||||
font-family: $font-stack
|
font-family: $font-stack
|
||||||
padding: .375rem 1rem
|
padding: $padding
|
||||||
|
width: $width
|
||||||
|
height: $height
|
||||||
background-color: $background-color
|
background-color: $background-color
|
||||||
border: none
|
border: none
|
||||||
border-radius: .25rem
|
border-radius: .25rem
|
||||||
|
@ -38,7 +40,7 @@
|
||||||
&:hover
|
&:hover
|
||||||
background-color: $dark-color
|
background-color: $dark-color
|
||||||
|
|
||||||
button, .button
|
.button
|
||||||
+button
|
+button
|
||||||
|
|
||||||
&.main-color
|
&.main-color
|
||||||
|
@ -76,15 +78,17 @@ button, .button
|
||||||
&:first-of-type:last-of-type
|
&:first-of-type:last-of-type
|
||||||
border-radius: .25rem
|
border-radius: .25rem
|
||||||
|
|
||||||
input, textarea
|
=input($width: null, $height: null, $vertical-padding: .375rem, $horizontal-padding: 1rem, $font-size: 1rem)
|
||||||
font-family: $font-stack
|
font-family: $font-stack
|
||||||
border: 1px solid $border-color
|
border: 1px solid $border-color
|
||||||
background-color: $background-color
|
background-color: $background-color
|
||||||
color: $text-color
|
color: $text-color
|
||||||
|
width: $width
|
||||||
|
height: $height
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
border-radius: .25rem
|
border-radius: .25rem
|
||||||
padding: .375rem 1rem
|
padding: $vertical-padding $horizontal-padding
|
||||||
font-size: 1rem
|
font-size: $font-size
|
||||||
resize: vertical
|
resize: vertical
|
||||||
|
|
||||||
&:hover, &:focus
|
&:hover, &:focus
|
||||||
|
@ -92,4 +96,13 @@ input, textarea
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
border-width: 2px
|
border-width: 2px
|
||||||
padding: calc(.375rem - 1px) 1rem
|
padding: calc(#{$vertical-padding} - 1px) calc(#{$horizontal-padding} - 1px)
|
||||||
|
|
||||||
|
.input, .textarea
|
||||||
|
+input
|
||||||
|
|
||||||
|
=notification($color: $error-color)
|
||||||
|
padding: 1rem
|
||||||
|
border-radius: .25rem
|
||||||
|
border: 2px solid $color
|
||||||
|
background-color: lighten($color, 25%)
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
@import lib/spinner
|
||||||
|
|
||||||
@import base/vars
|
@import base/vars
|
||||||
@import base/body
|
@import base/body
|
||||||
@import base/elements
|
@import base/elements
|
||||||
|
|
||||||
@import lib/spinner
|
|
||||||
|
|
||||||
@import pages/login
|
@import pages/login
|
||||||
|
|
|
@ -2,14 +2,11 @@ $green: #008744
|
||||||
$blue: #0057e7
|
$blue: #0057e7
|
||||||
$red: #d62d20
|
$red: #d62d20
|
||||||
$yellow: #ffa700
|
$yellow: #ffa700
|
||||||
$white: #eee
|
|
||||||
|
|
||||||
$width: 100px
|
.spinner
|
||||||
|
|
||||||
.loader
|
|
||||||
position: relative
|
position: relative
|
||||||
margin: 0 auto
|
margin: 0 auto
|
||||||
width: $width
|
width: 5rem
|
||||||
|
|
||||||
&:before
|
&:before
|
||||||
content: ""
|
content: ""
|
||||||
|
@ -34,6 +31,14 @@ $width: 100px
|
||||||
animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite
|
animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite
|
||||||
stroke-linecap: round
|
stroke-linecap: round
|
||||||
|
|
||||||
|
=white-spinner()
|
||||||
|
circle
|
||||||
|
stroke: white !important
|
||||||
|
|
||||||
|
=thick-spinner($thickness: 5)
|
||||||
|
svg > circle
|
||||||
|
stroke-width: $thickness
|
||||||
|
|
||||||
@keyframes rotate
|
@keyframes rotate
|
||||||
100%
|
100%
|
||||||
transform: rotate(360deg)
|
transform: rotate(360deg)
|
||||||
|
|
|
@ -21,24 +21,37 @@
|
||||||
|
|
||||||
.login
|
.login
|
||||||
width: 25rem
|
width: 25rem
|
||||||
height: 23.5rem
|
height: 23rem
|
||||||
display: inline-block
|
display: inline-block
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
background-color: white
|
background-color: white
|
||||||
border-radius: .25rem
|
border-radius: .25rem
|
||||||
margin-top: 3rem
|
margin-top: 3rem
|
||||||
|
|
||||||
.title
|
h1
|
||||||
color: $main-color
|
color: $main-color
|
||||||
margin: 3rem 0
|
margin: 3rem 0
|
||||||
|
|
||||||
input, button
|
input, button
|
||||||
width: calc(100% - 5rem)
|
|
||||||
margin: .5rem 2.5rem
|
margin: .5rem 2.5rem
|
||||||
padding: 1rem
|
height: 3rem
|
||||||
|
width: 20rem
|
||||||
|
|
||||||
input:focus
|
input
|
||||||
padding: calc(1rem - 1px)
|
+input
|
||||||
|
|
||||||
button
|
button
|
||||||
|
+button($width: 20rem, $height: 3rem, $padding: 0)
|
||||||
+main-color-button
|
+main-color-button
|
||||||
|
|
||||||
|
.spinner
|
||||||
|
+white-spinner
|
||||||
|
+thick-spinner
|
||||||
|
width: 2rem
|
||||||
|
|
||||||
|
&.errored
|
||||||
|
height: 26.5rem
|
||||||
|
|
||||||
|
.error
|
||||||
|
+notification($error-color)
|
||||||
|
margin: .5rem 2.5rem
|
||||||
|
|
Loading…
Reference in a new issue