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 {
render() {
return <main>
Hello, {localStorage.username}
</main>
}
}

View File

@ -14,6 +14,8 @@
// 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, { Component } from "react"
import Spinner from "./Spinner"
import api from "./api"
class Login extends Component {
constructor(props, context) {
@ -21,24 +23,38 @@ class Login extends Component {
this.state = {
username: "",
password: "",
loading: false,
error: "",
}
}
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() {
return <div className="login-wrapper">
<div className="login">
<h1 className="title">Maubot Manager</h1>
<div className={`login ${this.state.error && "errored"}`}>
<h1>Maubot Manager</h1>
<input type="text" placeholder="Username" value={this.state.username}
name="username" onChange={this.inputChanged}/>
<input type="password" placeholder="Password" value={this.state.password}
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>
}

View File

@ -18,21 +18,52 @@ import { BrowserRouter as Router, Route, Redirect } from "react-router-dom"
import PrivateRoute from "./PrivateRoute"
import Home from "./Home"
import Login from "./Login"
import Spinner from "./Spinner"
import api from "./api"
class MaubotRouter extends Component {
constructor(props) {
super(props)
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() {
if (!this.state.pinged) {
return <Spinner className="maubot-loading"/>
}
return <Router>
<div className={`maubot-wrapper ${this.state.authed ? "authenticated" : ""}`}>
<Route path="/" exact render={() => <Redirect to={{ pathname: "/dashboard" }}/>}/>
<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>
</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"
const PrivateRoute = ({ component, authed, ...rest }) => (
const PrivateRoute = ({ component, render, authed, to = "/login", ...args }) => (
<Route
{...rest}
{...args}
render={(props) => authed === true
? <Component {...props} />
: <Redirect to={{
pathname: "/login",
state: { from: props.location },
}}/>}
? (component ? React.createElement(component, props) : render())
: <Redirect to={{ pathname: to }}/>}
/>
)

View File

@ -1,9 +1,11 @@
import React from "react"
const Spinner = () => (
<div className="loader">
const Spinner = (props) => (
<div {...props} className={`spinner ${props["className"] || ""}`}>
<svg viewBox="25 25 50 50">
<circle cx="50" cy="50" r="20" fill="none" strokeWidth="2" strokeMiterlimit="10"/>
</svg>
</div>
)
export default Spinner

View File

@ -16,14 +16,15 @@
const BASE_PATH = "/_matrix/maubot/v1"
export function login(username, password) {
return fetch(`${BASE_PATH}/auth/login`, {
export async function login(username, password) {
const resp = await fetch(`${BASE_PATH}/auth/login`, {
method: "POST",
body: JSON.stringify({
username,
password,
}),
})
return await resp.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() })
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 ReactDOM from "react-dom"
import "./style/index.sass"
import App from "./Router"
import App from "./MaubotRouter"
ReactDOM.render(<App/>, document.getElementById("root"))

View File

@ -34,6 +34,10 @@ body
left: 0
right: 0
.maubot-loading
margin-top: 10rem
width: 10rem
//.lindeb
> header
position: absolute

View File

@ -14,9 +14,11 @@
// You should have received a copy of the GNU General Public License
// 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
padding: .375rem 1rem
padding: $padding
width: $width
height: $height
background-color: $background-color
border: none
border-radius: .25rem
@ -38,7 +40,7 @@
&:hover
background-color: $dark-color
button, .button
.button
+button
&.main-color
@ -76,15 +78,17 @@ button, .button
&:first-of-type:last-of-type
border-radius: .25rem
input, textarea
=input($width: null, $height: null, $vertical-padding: .375rem, $horizontal-padding: 1rem, $font-size: 1rem)
font-family: $font-stack
border: 1px solid $border-color
background-color: $background-color
color: $text-color
width: $width
height: $height
box-sizing: border-box
border-radius: .25rem
padding: .375rem 1rem
font-size: 1rem
padding: $vertical-padding $horizontal-padding
font-size: $font-size
resize: vertical
&:hover, &:focus
@ -92,4 +96,13 @@ input, textarea
&:focus
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
// along with this program. If not, see <https://www.gnu.org/licenses/>.
@import lib/spinner
@import base/vars
@import base/body
@import base/elements
@import lib/spinner
@import pages/login

View File

@ -2,14 +2,11 @@ $green: #008744
$blue: #0057e7
$red: #d62d20
$yellow: #ffa700
$white: #eee
$width: 100px
.loader
.spinner
position: relative
margin: 0 auto
width: $width
width: 5rem
&:before
content: ""
@ -34,6 +31,14 @@ $width: 100px
animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite
stroke-linecap: round
=white-spinner()
circle
stroke: white !important
=thick-spinner($thickness: 5)
svg > circle
stroke-width: $thickness
@keyframes rotate
100%
transform: rotate(360deg)

View File

@ -21,24 +21,37 @@
.login
width: 25rem
height: 23.5rem
height: 23rem
display: inline-block
box-sizing: border-box
background-color: white
border-radius: .25rem
margin-top: 3rem
.title
h1
color: $main-color
margin: 3rem 0
input, button
width: calc(100% - 5rem)
margin: .5rem 2.5rem
padding: 1rem
height: 3rem
width: 20rem
input:focus
padding: calc(1rem - 1px)
input
+input
button
+button($width: 20rem, $height: 3rem, $padding: 0)
+main-color-button
.spinner
+white-spinner
+thick-spinner
width: 2rem
&.errored
height: 26.5rem
.error
+notification($error-color)
margin: .5rem 2.5rem