Implement login

This commit is contained in:
Tulir Asokan 2018-11-06 23:27:17 +02:00
parent 8cd8f52566
commit f3a0b7bc4f
12 changed files with 145 additions and 41 deletions

View File

@ -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>
} }
} }

View File

@ -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>
} }

View File

@ -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>
} }

View File

@ -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 },
}}/>}
/> />
) )

View File

@ -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

View File

@ -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,
}

View File

@ -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"))

View File

@ -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

View File

@ -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%)

View File

@ -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

View File

@ -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)

View File

@ -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