Cleanup
This commit is contained in:
@@ -22,5 +22,6 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"proxy": "http://localhost"
|
||||
}
|
||||
|
||||
179
react/admin-panel/src/App.jsx
Normal file
179
react/admin-panel/src/App.jsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React from 'react';
|
||||
import './res/adminlte.min.css';
|
||||
import './res/index.css';
|
||||
import API from "shared/api";
|
||||
import Icon from "shared/elements/icon";
|
||||
import {BrowserRouter, Route, Routes} from "react-router-dom";
|
||||
import Dialog from "./elements/dialog";
|
||||
import Footer from "./elements/footer";
|
||||
import Header from "./elements/header";
|
||||
import Sidebar from "./elements/sidebar";
|
||||
import LoginForm from "./views/login";
|
||||
import {Alert} from "@material-ui/lab";
|
||||
import {Button} from "@material-ui/core";
|
||||
|
||||
export default class AdminDashboard extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = new API();
|
||||
this.state = {
|
||||
loaded: false,
|
||||
dialog: { onClose: () => this.hideDialog() },
|
||||
info: { },
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
}
|
||||
|
||||
showDialog(message, title, options=["Close"], onOption = null) {
|
||||
const props = { show: true, message: message, title: title, options: options, onOption: onOption };
|
||||
this.setState({ ...this.state, dialog: { ...this.state.dialog, ...props } });
|
||||
}
|
||||
|
||||
hideDialog() {
|
||||
this.setState({ ...this.state, dialog: { ...this.state.dialog, show: false } });
|
||||
}
|
||||
|
||||
onInit() {
|
||||
this.setState({ ...this.state, loaded: false, error: null });
|
||||
this.api.info().then(data => {
|
||||
if (data.success) {
|
||||
this.setState({...this.state, info: data.info })
|
||||
this.api.fetchUser().then(data => {
|
||||
if (data.success) {
|
||||
setInterval(this.onUpdate.bind(this), 60*1000);
|
||||
this.setState({...this.state, loaded: true});
|
||||
} else {
|
||||
this.setState({ ...this.state, error: data.msg })
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setState({ ...this.state, error: data.msg })
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onInit();
|
||||
}
|
||||
|
||||
onUpdateLocale() {
|
||||
this.setState({ ...this.state, locale: currentLocale })
|
||||
}
|
||||
|
||||
onLogin(username, password, rememberMe, callback) {
|
||||
this.setState({ ...this.state, error: "" });
|
||||
return this.api.login(username, password, rememberMe).then((res) => {
|
||||
if (res.success) {
|
||||
this.api.fetchUser().then(() => {
|
||||
this.setState({ ...this.state, user: res });
|
||||
callback(res);
|
||||
})
|
||||
} else {
|
||||
this.setState({ ...this.state, error: res.msg });
|
||||
callback(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onLogout(callback) {
|
||||
this.api.logout().then(() => {
|
||||
this.api.loggedIn = false;
|
||||
this.setState({ ...this.state, user: { } })
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
|
||||
onTotp2FA(code, callback) {
|
||||
this.setState({ ...this.state, error: "" });
|
||||
return this.api.verifyTotp2FA(code).then((res) => {
|
||||
if (res.success) {
|
||||
this.api.fetchUser().then(() => {
|
||||
this.setState({ ...this.state, user: res });
|
||||
callback(res);
|
||||
})
|
||||
} else {
|
||||
this.setState({ ...this.state, error: res.msg });
|
||||
callback(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onKey2FA(credentialID, clientDataJson, authData, signature, callback) {
|
||||
this.setState({ ...this.state, error: "" });
|
||||
return this.api.verifyKey2FA(credentialID, clientDataJson, authData, signature).then((res) => {
|
||||
if (res.success) {
|
||||
this.api.fetchUser().then(() => {
|
||||
this.setState({ ...this.state, user: res });
|
||||
callback(res);
|
||||
})
|
||||
} else {
|
||||
this.setState({ ...this.state, error: res.msg });
|
||||
callback(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
if (!this.state.loaded) {
|
||||
if (this.state.error) {
|
||||
return <Alert severity={"error"} title={"An error occurred"}>
|
||||
<div>{this.state.error}</div>
|
||||
<Button type={"button"} variant={"outlined"} onClick={() => this.onInit()}>
|
||||
Retry
|
||||
</Button>
|
||||
</Alert>
|
||||
} else {
|
||||
return <b>Loading… <Icon icon={"spinner"}/></b>
|
||||
}
|
||||
}
|
||||
|
||||
this.controlObj = {
|
||||
showDialog: this.showDialog.bind(this),
|
||||
api: this.api,
|
||||
info: this.state.info,
|
||||
onUpdateLocale: this.onUpdateLocale.bind(this),
|
||||
onLogout: this.onLogout.bind(this),
|
||||
onLogin: this.onLogin.bind(this),
|
||||
onTotp2FA: this.onTotp2FA.bind(this),
|
||||
onKey2FA: this.onKey2FA.bind(this),
|
||||
};
|
||||
|
||||
if (!this.api.loggedIn) {
|
||||
return <LoginForm {...this.controlObj}/>
|
||||
}
|
||||
|
||||
return <BrowserRouter>
|
||||
<Header {...this.controlObj} />
|
||||
<Sidebar {...this.controlObj} />
|
||||
<div className={"content-wrapper p-2"}>
|
||||
<section className={"content"}>
|
||||
<Routes>
|
||||
{/*<Route path={"/admin/dashboard"}><Overview {...this.controlObj} notifications={this.state.notifications} /></Route>
|
||||
<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/user/add"}><CreateUser {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/user/edit/:userId"} render={(props) => {
|
||||
let newProps = {...props, ...this.controlObj};
|
||||
return <EditUser {...newProps} />
|
||||
}}/>
|
||||
<Route path={"/admin/user/permissions"}><PermissionSettings {...this.controlObj}/></Route>
|
||||
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
|
||||
<Route exact={true} path={"/admin/contact/"}><ContactRequestOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/visitors"}><Visitors {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
|
||||
<Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>
|
||||
<Route path={"*"}><View404 /></Route>*/}
|
||||
</Routes>
|
||||
<Dialog {...this.state.dialog}/>
|
||||
</section>
|
||||
</div>
|
||||
<Footer />
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
47
react/admin-panel/src/elements/dialog.jsx
Normal file
47
react/admin-panel/src/elements/dialog.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export default function Dialog(props) {
|
||||
|
||||
const show = props.show;
|
||||
const classes = ["modal", "fade"];
|
||||
const style = { paddingRight: "12px", display: (show ? "block" : "none") };
|
||||
const onClose = props.onClose || function() { };
|
||||
const onOption = props.onOption || function() { };
|
||||
const options = props.options || ["Close"];
|
||||
|
||||
let buttons = [];
|
||||
for (let name of options) {
|
||||
let type = "default";
|
||||
if (name === "Yes") type = "warning";
|
||||
else if(name === "No") type = "danger";
|
||||
|
||||
buttons.push(
|
||||
<button type="button" key={"button-" + name} className={"btn btn-" + type}
|
||||
data-dismiss={"modal"} onClick={() => { onClose(); onOption(name); }}>
|
||||
{name}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(classes, show && "show")} style={style} aria-modal={"true"} onClick={() => onClose()}>
|
||||
<div className="modal-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">{props.title}</h4>
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close" onClick={() => onClose()}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>{props.message}</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{ buttons }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
react/admin-panel/src/elements/footer.jsx
Normal file
11
react/admin-panel/src/elements/footer.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Footer() {
|
||||
|
||||
return (
|
||||
<footer className={"main-footer"}>
|
||||
Theme: <strong>Copyright © 2014-2019 <a href={"https://adminlte.io"}>AdminLTE.io</a>. <b>Version</b> 3.0.3</strong>
|
||||
CMS: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> 1.2.6
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
57
react/admin-panel/src/elements/header.jsx
Normal file
57
react/admin-panel/src/elements/header.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import Icon from "shared/elements/icon";
|
||||
|
||||
export default function Header(props) {
|
||||
|
||||
const parent = {
|
||||
api: props.api
|
||||
};
|
||||
|
||||
function onToggleSidebar() {
|
||||
let classes = document.body.classList;
|
||||
if (classes.contains("sidebar-collapse")) {
|
||||
classes.remove("sidebar-collapse");
|
||||
classes.add("sidebar-open");
|
||||
} else {
|
||||
classes.add("sidebar-collapse");
|
||||
classes.remove("sidebar-add");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={"main-header navbar navbar-expand navbar-white navbar-light"}>
|
||||
|
||||
{/*Left navbar links */}
|
||||
<ul className={"navbar-nav"}>
|
||||
<li className={"nav-item"}>
|
||||
<a href={"#"} className={"nav-link"} role={"button"} onClick={onToggleSidebar}>
|
||||
<Icon icon={"bars"}/>
|
||||
</a>
|
||||
</li>
|
||||
<li className={"nav-item d-none d-sm-inline-block"}>
|
||||
<Link to={"/admin/dashboard"} className={"nav-link"}>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{/* SEARCH FORM */}
|
||||
<form className={"form-inline ml-3"}>
|
||||
<div className={"input-group input-group-sm"}>
|
||||
<input className={"form-control form-control-navbar"} type={"search"} placeholder={"Search"} aria-label={"Search"} />
|
||||
<div className={"input-group-append"}>
|
||||
<button className={"btn btn-navbar"} type={"submit"}>
|
||||
<Icon icon={"search"}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Right navbar links */}
|
||||
<ul className={"navbar-nav ml-auto"}>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
47
react/admin-panel/src/elements/language-selection.js
Normal file
47
react/admin-panel/src/elements/language-selection.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, {useState} from 'react';
|
||||
import {initLocale, L} from "shared/locale/locale";
|
||||
import {Box} from "@material-ui/core";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
languageFlag: {
|
||||
margin: theme.spacing(0.2),
|
||||
cursor: "pointer",
|
||||
border: 0,
|
||||
}
|
||||
}));
|
||||
|
||||
export default function LanguageSelection(props) {
|
||||
|
||||
const api = props.api;
|
||||
const classes = useStyles();
|
||||
let [languages, setLanguages] = useState(null);
|
||||
|
||||
const onSetLanguage = (code) => {
|
||||
api.setLanguageByCode(code).then((res) => {
|
||||
if (res.success) {
|
||||
initLocale(code);
|
||||
props.onUpdateLocale();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let flags = [];
|
||||
if (languages === null) {
|
||||
api.getLanguages().then((res) => {
|
||||
setLanguages(res.languages);
|
||||
});
|
||||
} else {
|
||||
for (const language of Object.values(languages)) {
|
||||
let key = `lang-${language.code}`;
|
||||
flags.push(<button type={"button"} title={language.name} onClick={() => onSetLanguage(language.code)}
|
||||
key={key} className={classes.languageFlag} >
|
||||
<img alt={key} src={`/img/icons/lang/${language.code}.gif`} />
|
||||
</button>);
|
||||
}
|
||||
}
|
||||
|
||||
return <Box mt={1}>
|
||||
{L("Language") + ": "} { flags }
|
||||
</Box>
|
||||
}
|
||||
113
react/admin-panel/src/elements/sidebar.js
Normal file
113
react/admin-panel/src/elements/sidebar.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import {Link, NavLink} from "react-router-dom";
|
||||
import Icon from "shared/elements/icon";
|
||||
|
||||
export default function Sidebar(props) {
|
||||
|
||||
let parent = {
|
||||
showDialog: props.showDialog || function() {},
|
||||
api: props.api,
|
||||
};
|
||||
|
||||
function onLogout() {
|
||||
parent.api.logout().then(obj => {
|
||||
if (obj.success) {
|
||||
document.location = "/admin";
|
||||
} else {
|
||||
parent.showDialog("Error logging out: " + obj.msg, "Error logging out");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const menuItems = {
|
||||
"dashboard": {
|
||||
"name": "Dashboard",
|
||||
"icon": "tachometer-alt"
|
||||
},
|
||||
"visitors": {
|
||||
"name": "Visitor Statistics",
|
||||
"icon": "chart-bar",
|
||||
},
|
||||
"users": {
|
||||
"name": "Users & Groups",
|
||||
"icon": "users"
|
||||
},
|
||||
"pages": {
|
||||
"name": "Pages & Routes",
|
||||
"icon": "copy",
|
||||
},
|
||||
"settings": {
|
||||
"name": "Settings",
|
||||
"icon": "tools"
|
||||
},
|
||||
"logs": {
|
||||
"name": "Logs & Notifications",
|
||||
"icon": "file-medical-alt"
|
||||
},
|
||||
"contact": {
|
||||
"name": "Contact Requests",
|
||||
"icon": "comments"
|
||||
},
|
||||
"help": {
|
||||
"name": "Help",
|
||||
"icon": "question-circle"
|
||||
},
|
||||
};
|
||||
|
||||
let li = [];
|
||||
for (let id in menuItems) {
|
||||
let obj = menuItems[id];
|
||||
const badge = (obj.badge ? <span className={"right badge badge-" + obj.badge.type}>{obj.badge.value}</span> : <></>);
|
||||
|
||||
li.push(
|
||||
<li key={id} className={"nav-item"}>
|
||||
<NavLink to={"/admin/" + id} className={"nav-link"} activeClassName={"active"}>
|
||||
<Icon icon={obj.icon} className={"nav-icon"} /><p>{obj.name}{badge}</p>
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
li.push(<li className={"nav-item"} key={"logout"}>
|
||||
<a href={"#"} onClick={() => onLogout()} className={"nav-link"}>
|
||||
<Icon icon={"arrow-left"} className={"nav-icon"} />
|
||||
<p>Logout</p>
|
||||
</a>
|
||||
</li>);
|
||||
|
||||
return (
|
||||
<aside className={"main-sidebar sidebar-dark-primary elevation-4"}>
|
||||
<Link href={"#"} className={"brand-link"} to={"/admin/dashboard"}>
|
||||
<img src={"/img/icons/logo.png"} alt={"Logo"} className={"brand-image img-circle elevation-3"} style={{opacity: ".8"}} />
|
||||
<span className={"brand-text font-weight-light ml-2"}>WebBase</span>
|
||||
</Link>
|
||||
|
||||
<div className={"sidebar os-host os-theme-light os-host-overflow os-host-overflow-y os-host-resize-disabled os-host-scrollbar-horizontal-hidden os-host-transition"}>
|
||||
{/* IDK what this is */}
|
||||
<div className={"os-resize-observer-host"}>
|
||||
<div className={"os-resize-observer observed"} style={{left: "0px", right: "auto"}}/>
|
||||
</div>
|
||||
<div className={"os-size-auto-observer"} style={{height: "calc(100% + 1px)", float: "left"}}>
|
||||
<div className={"os-resize-observer observed"}/>
|
||||
</div>
|
||||
<div className={"os-content-glue"} style={{margin: "0px -8px"}}/>
|
||||
<div className={"os-padding"}>
|
||||
<div className={"os-viewport os-viewport-native-scrollbars-invisible"} style={{right: "0px", bottom: "0px"}}>
|
||||
<div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}>
|
||||
<div className="user-panel mt-3 pb-3 mb-3 d-flex">
|
||||
<div className="info">
|
||||
<a href="#" className="d-block">Logged in as: {parent.api.user.name}</a>
|
||||
</div>
|
||||
</div>
|
||||
<nav className={"mt-2"}>
|
||||
<ul className={"nav nav-pills nav-sidebar flex-column"} data-widget={"treeview"} role={"menu"} data-accordion={"false"}>
|
||||
{li}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
8
react/admin-panel/src/index.js
Normal file
8
react/admin-panel/src/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import AdminDashboard from "./App";
|
||||
|
||||
|
||||
ReactDOM.render(
|
||||
<AdminDashboard />,
|
||||
document.getElementById('admin-panel')
|
||||
);
|
||||
@@ -1,93 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './res/adminlte.min.css';
|
||||
import './res/index.css';
|
||||
import API from "shared/api";
|
||||
import Icon from "shared/elements/icon";
|
||||
|
||||
class AdminDashboard extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = new API();
|
||||
this.state = {
|
||||
loaded: false,
|
||||
dialog: { onClose: () => this.hideDialog() },
|
||||
notifications: [ ],
|
||||
contactRequests: [ ]
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
}
|
||||
|
||||
showDialog(message, title, options=["Close"], onOption = null) {
|
||||
const props = { show: true, message: message, title: title, options: options, onOption: onOption };
|
||||
this.setState({ ...this.state, dialog: { ...this.state.dialog, ...props } });
|
||||
}
|
||||
|
||||
hideDialog() {
|
||||
this.setState({ ...this.state, dialog: { ...this.state.dialog, show: false } });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.api.fetchUser().then(Success => {
|
||||
if (!Success) {
|
||||
document.location = "/admin";
|
||||
} else {
|
||||
this.fetchNotifications();
|
||||
this.fetchContactRequests();
|
||||
setInterval(this.onUpdate.bind(this), 60*1000);
|
||||
this.setState({...this.state, loaded: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
if (!this.state.loaded) {
|
||||
return <b>Loading… <Icon icon={"spinner"}/></b>
|
||||
}
|
||||
|
||||
this.controlObj = {
|
||||
showDialog: this.showDialog.bind(this),
|
||||
api: this.api
|
||||
};
|
||||
|
||||
return <b>test</b>
|
||||
/*return <Router>
|
||||
<Header {...this.controlObj} notifications={this.state.notifications} />
|
||||
<Sidebar {...this.controlObj} notifications={this.state.notifications} contactRequests={this.state.contactRequests}/>
|
||||
<div className={"content-wrapper p-2"}>
|
||||
<section className={"content"}>
|
||||
<Switch>
|
||||
<Route path={"/admin/dashboard"}><Overview {...this.controlObj} notifications={this.state.notifications} /></Route>
|
||||
<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/user/add"}><CreateUser {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/user/edit/:userId"} render={(props) => {
|
||||
let newProps = {...props, ...this.controlObj};
|
||||
return <EditUser {...newProps} />
|
||||
}}/>
|
||||
<Route path={"/admin/user/permissions"}><PermissionSettings {...this.controlObj}/></Route>
|
||||
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
|
||||
<Route exact={true} path={"/admin/contact/"}><ContactRequestOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/visitors"}><Visitors {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
|
||||
<Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>
|
||||
<Route path={"*"}><View404 /></Route>
|
||||
</Switch>
|
||||
<Dialog {...this.state.dialog}/>
|
||||
</section>
|
||||
</div>
|
||||
<Footer />
|
||||
</Router>
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<AdminDashboard />,
|
||||
document.getElementById('admin-panel')
|
||||
);
|
||||
294
react/admin-panel/src/views/login.jsx
Normal file
294
react/admin-panel/src/views/login.jsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox, CircularProgress, Container,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
Link,
|
||||
TextField,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {makeStyles} from '@material-ui/core/styles';
|
||||
import {Alert} from '@material-ui/lab';
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import {Navigate} from "react-router-dom";
|
||||
import {L} from "shared/locale/locale";
|
||||
import ReplayIcon from '@material-ui/icons/Replay';
|
||||
import LanguageSelection from "../elements/language-selection";
|
||||
import {decodeText, encodeText, getParameter, removeParameter} from "shared/util";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatar: {
|
||||
margin: theme.spacing(2),
|
||||
width: "60px",
|
||||
height: "60px"
|
||||
},
|
||||
form: {
|
||||
width: '100%', // Fix IE 11 issue.
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
logo: {
|
||||
marginRight: theme.spacing(3)
|
||||
},
|
||||
headline: {
|
||||
width: "100%",
|
||||
},
|
||||
container: {
|
||||
marginTop: theme.spacing(5),
|
||||
paddingBottom: theme.spacing(1),
|
||||
borderColor: theme.palette.primary.main,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderRadius: 5
|
||||
},
|
||||
buttons2FA: {
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
error2FA: {
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
"& > div": {
|
||||
fontSize: 16
|
||||
},
|
||||
"& > button": {
|
||||
marginTop: theme.spacing(1)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default function LoginForm(props) {
|
||||
|
||||
const api = props.api;
|
||||
const classes = useStyles();
|
||||
let [username, setUsername] = useState("");
|
||||
let [password, setPassword] = useState("");
|
||||
let [rememberMe, setRememberMe] = useState(true);
|
||||
let [isLoggingIn, setLoggingIn] = useState(false);
|
||||
let [emailConfirmed, setEmailConfirmed] = useState(null);
|
||||
let [tfaCode, set2FACode] = useState("");
|
||||
let [tfaState, set2FAState] = useState(0); // 0: not sent, 1: sent, 2: retry
|
||||
let [tfaError, set2FAError] = useState("");
|
||||
|
||||
const getNextUrl = () => {
|
||||
return getParameter("next") || "/admin";
|
||||
}
|
||||
|
||||
const onLogin = useCallback(() => {
|
||||
if (!isLoggingIn) {
|
||||
setLoggingIn(true);
|
||||
removeParameter("success");
|
||||
props.onLogin(username, password, rememberMe, (res) => {
|
||||
set2FAState(0);
|
||||
setLoggingIn(false);
|
||||
setPassword("");
|
||||
if (!res.success) {
|
||||
setEmailConfirmed(res.emailConfirmed);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isLoggingIn, password, props, rememberMe, username]);
|
||||
|
||||
const onSubmit2FA = useCallback(() => {
|
||||
setLoggingIn(true);
|
||||
props.onTotp2FA(tfaCode, (res) => {
|
||||
setLoggingIn(false);
|
||||
});
|
||||
}, [tfaCode, props]);
|
||||
|
||||
const onCancel2FA = useCallback(() => {
|
||||
props.onLogout();
|
||||
}, [props]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api.loggedIn || !api.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
let twoFactor = api.user["2fa"];
|
||||
if (!twoFactor || !twoFactor.confirmed ||
|
||||
twoFactor.authenticated || twoFactor.type !== "fido") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tfaState === 0) {
|
||||
set2FAState(1);
|
||||
set2FAError("");
|
||||
navigator.credentials.get({
|
||||
publicKey: {
|
||||
challenge: encodeText(window.atob(twoFactor.challenge)),
|
||||
allowCredentials: [{
|
||||
id: encodeText(window.atob(twoFactor.credentialID)),
|
||||
type: "public-key",
|
||||
}],
|
||||
userVerification: "discouraged",
|
||||
},
|
||||
}).then((res) => {
|
||||
let credentialID = res.id;
|
||||
let clientDataJson = decodeText(res.response.clientDataJSON);
|
||||
let authData = window.btoa(decodeText(res.response.authenticatorData));
|
||||
let signature = window.btoa(decodeText(res.response.signature));
|
||||
props.onKey2FA(credentialID, clientDataJson, authData, signature, res => {
|
||||
if (!res.success) {
|
||||
set2FAState(2);
|
||||
}
|
||||
});
|
||||
}).catch(e => {
|
||||
set2FAState(2);
|
||||
set2FAError(e.toString());
|
||||
});
|
||||
}
|
||||
}, [api.loggedIn, api.user, tfaState, props])
|
||||
|
||||
if (api.loggedIn) {
|
||||
if (!api.user["2fa"] || !api.user["2fa"].confirmed || api.user["2fa"].authenticated) {
|
||||
// Redirect by default takes only path names
|
||||
return <Navigate to={getNextUrl()}/>
|
||||
}
|
||||
}
|
||||
|
||||
const createForm = () => {
|
||||
|
||||
// 2FA
|
||||
if (api.loggedIn && api.user["2fa"]) {
|
||||
return <>
|
||||
<div>Additional information is required for logging in: {api.user["2fa"].type}</div>
|
||||
{ api.user["2fa"].type === "totp" ?
|
||||
<TextField
|
||||
variant="outlined" margin="normal"
|
||||
id="code" label={L("6-Digit Code")} name="code"
|
||||
autoComplete="code"
|
||||
required fullWidth autoFocus
|
||||
value={tfaCode} onChange={(e) => set2FACode(e.target.value)}
|
||||
/> : <>
|
||||
Plugin your 2FA-Device. Interaction might be required, e.g. typing in a PIN or touching it.
|
||||
<Box mt={2} textAlign={"center"}>
|
||||
{tfaState !== 2
|
||||
? <CircularProgress/>
|
||||
: <div className={classes.error2FA}>
|
||||
<div>{L("Something went wrong:")}<br />{tfaError}</div>
|
||||
<Button onClick={() => set2FAState(0)}
|
||||
variant={"outlined"} color={"secondary"} size={"small"}>
|
||||
<ReplayIcon /> {L("Retry")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
{
|
||||
props.error ? <Alert severity="error">{props.error}</Alert> : <></>
|
||||
}
|
||||
<Grid container spacing={2} className={classes.buttons2FA}>
|
||||
<Grid item xs={6}>
|
||||
<Button
|
||||
fullWidth variant="contained"
|
||||
color="inherit" size={"medium"}
|
||||
disabled={isLoggingIn}
|
||||
onClick={onCancel2FA}>
|
||||
{L("Go back")}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Button
|
||||
type="submit" fullWidth variant="contained"
|
||||
color="primary" size={"medium"}
|
||||
disabled={isLoggingIn || api.user["2fa"].type !== "totp"}
|
||||
onClick={onSubmit2FA}>
|
||||
{isLoggingIn ?
|
||||
<>{L("Submitting…")}… <CircularProgress size={15}/></> :
|
||||
L("Submit")
|
||||
}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
|
||||
return <>
|
||||
<TextField
|
||||
variant="outlined" margin="normal"
|
||||
id="username" label={L("Username")} name="username"
|
||||
autoComplete="username" disabled={isLoggingIn}
|
||||
required fullWidth autoFocus
|
||||
value={username} onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
variant="outlined" margin="normal"
|
||||
name="password" label={L("Password")} type="password" id="password"
|
||||
autoComplete="current-password"
|
||||
required fullWidth disabled={isLoggingIn}
|
||||
value={password} onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox value="remember" color="primary"/>}
|
||||
label={L("Remember me")}
|
||||
checked={rememberMe} onClick={(e) => setRememberMe(!rememberMe)}
|
||||
/>
|
||||
{
|
||||
props.error ?
|
||||
<Alert severity="error">
|
||||
{props.error}
|
||||
{emailConfirmed === false
|
||||
? <> <Link href={"/resendConfirmation"}>Click here</Link> to resend the confirmation email.</>
|
||||
: <></>
|
||||
}
|
||||
</Alert> :
|
||||
successMessage
|
||||
? <Alert severity="success">{successMessage}</Alert>
|
||||
: <></>
|
||||
}
|
||||
<Button
|
||||
type={"submit"} fullWidth variant={"contained"}
|
||||
color={"primary"} className={classes.submit}
|
||||
size={"large"}
|
||||
disabled={isLoggingIn}
|
||||
onClick={onLogin}>
|
||||
{isLoggingIn ?
|
||||
<>{L("Signing in")}… <CircularProgress size={15}/></> :
|
||||
L("Sign In")
|
||||
}
|
||||
</Button>
|
||||
<Grid container>
|
||||
<Grid item xs>
|
||||
<Link href="/resetPassword" variant="body2">
|
||||
{L("Forgot password?")}
|
||||
</Link>
|
||||
</Grid>
|
||||
{ props.info.registrationAllowed ?
|
||||
<Grid item>
|
||||
<Link href="/register" variant="body2">
|
||||
{L("Don't have an account? Sign Up")}
|
||||
</Link>
|
||||
</Grid> : <></>
|
||||
}
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
|
||||
let successMessage = getParameter("success");
|
||||
return <Container maxWidth={"xs"} className={classes.container}>
|
||||
<div className={classes.paper}>
|
||||
<div className={classes.headline}>
|
||||
<Typography component="h1" variant="h4">
|
||||
<img src={"/img/icons/logo.png"} alt={"Logo"} height={48} className={classes.logo}/>
|
||||
{props.info.siteName}
|
||||
</Typography>
|
||||
</div>
|
||||
<form className={classes.form} onSubmit={(e) => e.preventDefault()}>
|
||||
{ createForm() }
|
||||
<LanguageSelection api={api} locale={props.locale} onUpdateLocale={props.onUpdateLocale}/>
|
||||
</form>
|
||||
</div>
|
||||
</Container>
|
||||
}
|
||||
Reference in New Issue
Block a user