This commit is contained in:
Roman 2022-11-29 14:17:11 +01:00
parent c9a7da688f
commit 25ef07b0b7
32 changed files with 1275 additions and 507 deletions

@ -10,26 +10,6 @@ namespace Core\API {
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($context, $externalCall, $params);
}
protected function apiKeyExists(int $id): bool {
$sql = $this->context->getSQL();
$res = $sql->select($sql->count())
->from("ApiKey")
->whereEq("id", $id)
->whereEq("user_id", $this->context->getUser()->getId())
->whereGt("valid_until", $sql->currentTimestamp())
->whereEq("active", 1)
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if($this->success && $res[0]["count"] === 0) {
return $this->createError("This API-Key does not exist.");
}
return $this->success;
}
}
}
@ -115,22 +95,20 @@ namespace Core\API\ApiKey {
}
public function _execute(): bool {
$sql = $this->context->getSQL();
$id = $this->getParam("id");
if (!$this->apiKeyExists($id)) {
return false;
$apiKey = ApiKey::find($sql, $id);
if ($apiKey === false) {
return $this->createError("Error fetching API-Key details: " . $sql->getLastError());
} else if ($apiKey === null) {
return $this->createError("API-Key does not exit");
}
$validUntil = (new \DateTime())->modify("+30 DAY");
$sql = $this->context->getSQL();
$this->success = $sql->update("ApiKey")
->set("valid_until", $validUntil)
->whereEq("id", $id)
->whereEq("user_id", $this->context->getUser()->getId())
->execute();
$this->success = $apiKey->refresh($sql, 30) !== false;
$this->lastError = $sql->getLastError();
if ($this->success) {
$this->result["valid_until"] = $validUntil;
$this->result["validUntil"] = $apiKey->getValidUntil()->getTimestamp();
}
return $this->success;
@ -147,17 +125,16 @@ namespace Core\API\ApiKey {
}
public function _execute(): bool {
$sql = $this->context->getSQL();
$id = $this->getParam("id");
if (!$this->apiKeyExists($id)) {
return false;
$apiKey = ApiKey::find($sql, $id);
if ($apiKey === false) {
return $this->createError("Error fetching API-Key details: " . $sql->getLastError());
} else if ($apiKey === null) {
return $this->createError("API-Key does not exit");
}
$sql = $this->context->getSQL();
$this->success = $sql->update("ApiKey")
->set("active", false)
->whereEq("id", $id)
->whereEq("user_id", $this->context->getUser()->getId())
->execute();
$this->success = $apiKey->revoke($sql);
$this->lastError = $sql->getLastError();
return $this->success;

@ -128,10 +128,7 @@ namespace Core\API\Groups {
$sql = $this->context->getSQL();
$group = new Group();
$group->name = $name;
$group->color = $color;
$group = new Group(null, $name, $color);
$this->success = ($group->save($sql) !== FALSE);
$this->lastError = $sql->getLastError();

26
Core/API/Info.class.php Normal file

@ -0,0 +1,26 @@
<?php
namespace Core\API;
use Core\Objects\Context;
class Info extends Request {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, []);
$this->csrfTokenRequired = false;
}
protected function _execute(): bool {
$settings = $this->context->getSettings();
$this->result["info"] = [
"registrationAllowed" => $settings->isRegistrationAllowed(),
"recaptchaEnabled" => $settings->isRecaptchaEnabled(),
"version" => WEBBASE_VERSION,
"siteName" => $settings->getSiteName(),
];
return true;
}
}

@ -54,18 +54,18 @@ namespace Core\API\Language {
public function __construct(Context $context, $externalCall = false) {
parent::__construct($context, $externalCall, array(
'langId' => new Parameter('langId', Parameter::TYPE_INT, true, NULL),
'langCode' => new StringType('langCode', 5, true, NULL),
'id' => new Parameter('id', Parameter::TYPE_INT, true, NULL),
'code' => new StringType('code', 5, true, NULL),
));
}
private function checkLanguage(): bool {
$langId = $this->getParam("langId");
$langCode = $this->getParam("langCode");
$langId = $this->getParam("id");
$langCode = $this->getParam("code");
if (is_null($langId) && is_null($langCode)) {
return $this->createError(L("Either langId or langCode must be given"));
return $this->createError(L("Either 'id' or 'code' must be given"));
}
$sql = $this->context->getSQL();
@ -88,15 +88,10 @@ namespace Core\API\Language {
}
private function updateLanguage(): bool {
$languageId = $this->language->getId();
$userId = $this->context->getUser()->getId();
$sql = $this->context->getSQL();
$this->success = $sql->update("User")
->set("language_id", $languageId)
->whereEq("id", $userId)
->execute();
$currentUser = $this->context->getUser();
$currentUser->language = $this->language;
$this->success = $currentUser->save($sql, ["language_id"]);
$this->lastError = $sql->getLastError();
return $this->success;
}

@ -27,7 +27,7 @@ namespace Core\API {
}
$route->setActive($active);
$this->success = $route->save($sql);
$this->success = $route->save($sql, ["active"]);
$this->lastError = $sql->getLastError();
return $this->success && $this->regenerateCache();
}

@ -142,13 +142,13 @@ namespace Core\API\TFA {
if ($twoFactorToken && $twoFactorToken->isConfirmed()) {
return $this->createError("You already added a two factor token");
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
$twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32"));
$sql = $this->context->getSQL();
$twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32"));
$this->success = $twoFactorToken->save($sql) !== false;
$this->lastError = $sql->getLastError();
if ($this->success) {
$currentUser->setTwoFactorToken($twoFactorToken);
$this->success = $currentUser->save($sql);
$this->success = $currentUser->save($sql, ["two_factor_token_id"]);
$this->lastError = $sql->getLastError();
}

@ -503,7 +503,7 @@ namespace Core\API\User {
} else {
$user->password = $this->hashPassword($password);
$user->confirmed = true;
if ($user->save($sql)) {
if ($user->save($sql, ["password", "confirmed"])) {
$userToken->invalidate($sql);
return true;
} else {
@ -542,7 +542,7 @@ namespace Core\API\User {
return $this->createError("Your email address is already confirmed.");
} else {
$user->confirmed = true;
if ($user->save($sql)) {
if ($user->save($sql, ["confirmed"])) {
$userToken->invalidate($sql);
return true;
} else {
@ -826,20 +826,37 @@ namespace Core\API\User {
}
}
if ($usernameChanged) $user->name = $username;
if ($fullNameChanged) $user->fullName = $fullName;
if ($emailChanged) $user->email = $email;
if (!is_null($password)) $user->password = $this->hashPassword($password);
$columnsToUpdate = [];
if ($usernameChanged) {
$user->name = $username;
$columnsToUpdate[] = "name";
}
if ($fullNameChanged) {
$user->fullName = $fullName;
$columnsToUpdate[] = "full_name";
}
if ($emailChanged) {
$user->email = $email;
$columnsToUpdate[] = "email";
}
if (!is_null($password)) {
$user->password = $this->hashPassword($password);
$columnsToUpdate[] = "password";
}
if (!is_null($confirmed)) {
if ($id === $currentUser->getId() && $confirmed === false) {
return $this->createError("Cannot make own account unconfirmed.");
} else {
$user->confirmed = $confirmed;
$columnsToUpdate[] = "confirmed";
}
}
if ($user->save($sql)) {
if (empty($columnsToUpdate) || $user->save($sql, $columnsToUpdate)) {
$deleteQuery = $sql->delete("UserGroup")->whereEq("user_id", $id);
$insertQuery = $sql->insert("UserGroup", array("user_id", "group_id"));
@ -1270,7 +1287,7 @@ namespace Core\API\User {
if ($this->success) {
$currentUser->gpgKey = $gpgKey;
if ($currentUser->save($sql)) {
if ($currentUser->save($sql, ["gpg_key_id"])) {
$this->result["gpg"] = $gpgKey->jsonSerialize();
} else {
return $this->createError("Error updating user details: " . $sql->getLastError());
@ -1524,7 +1541,7 @@ namespace Core\API\User {
$sql = $this->context->getSQL();
$currentUser->profilePicture = $fileName;
if ($currentUser->save($sql)) {
if ($currentUser->save($sql, ["profile_picture"])) {
$this->result["profilePicture"] = $fileName;
} else {
return $this->createError("Error updating user details: " . $sql->getLastError());
@ -1551,7 +1568,7 @@ namespace Core\API\User {
}
$currentUser->profilePicture = null;
if (!$currentUser->save($sql)) {
if (!$currentUser->save($sql, ["profile_picture"])) {
return $this->createError("Error updating user details: " . $sql->getLastError());
}

@ -2,6 +2,7 @@
namespace Core\Objects\DatabaseEntity;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
@ -25,4 +26,18 @@ class ApiKey extends DatabaseEntity {
"validUntil" => $this->validUntil->getTimestamp()
];
}
public function getValidUntil(): \DateTime {
return $this->validUntil;
}
public function refresh(SQL $sql, int $days): bool {
$this->validUntil = (new \DateTime())->modify("+$days days");
return $this->save($sql, ["valid_until"]);
}
public function revoke(SQL $sql): bool {
$this->active = false;
return $this->save($sql, ["active"]);
}
}

@ -75,6 +75,7 @@ abstract class DatabaseEntity {
}
}
// TODO: rather take property names here instead of $columns? and translate then using DatabaseEntityHandler::columns[$propertyName]
public function save(SQL $sql, ?array $columns = null, bool $saveNM = false): bool {
$handler = self::getHandler($sql);
$res = $handler->insertOrUpdate($this, $columns, $saveNM);

@ -136,6 +136,6 @@ class GpgKey extends DatabaseEntity {
public function confirm(SQL $sql): bool {
$this->confirmed = true;
return $this->save($sql);
return $this->save($sql, ["confirmed"]);
}
}

@ -118,7 +118,7 @@ class MailQueueItem extends DatabaseEntity {
$this->status = self::STATUS_SUCCESS;
}
$this->save($context->getSQL());
$this->save($context->getSQL(), ["status", "retry_count", "next_try", "error_message"]);
return $success;
}
}

@ -109,7 +109,7 @@ class Session extends DatabaseEntity {
public function destroy(): bool {
session_destroy();
$this->active = false;
return $this->save($this->context->getSQL());
return $this->save($this->context->getSQL(), ["active"]);
}
public function update(): bool {
@ -120,7 +120,7 @@ class Session extends DatabaseEntity {
$sql = $this->context->getSQL();
return $this->user->update($sql) &&
$this->save($sql);
$this->save($sql, ["expires", "data"]);
}
public function getCsrfToken(): string {

@ -55,7 +55,7 @@ class UserToken extends DatabaseEntity {
public function invalidate(SQL $sql): bool {
$this->used = true;
return $this->save($sql);
return $this->save($sql, ["used"]);
}
public function getUser(): User {
@ -64,7 +64,7 @@ class UserToken extends DatabaseEntity {
public function updateDurability(SQL $sql, int $validHours): bool {
$this->validUntil = (new \DateTime())->modify("+$validHours HOURS");
return $this->save($sql);
return $this->save($sql, ["valid_until"]);
}
public function getToken(): string {

302
js/files.min.js vendored

File diff suppressed because one or more lines are too long

@ -22,5 +22,6 @@
"last 1 firefox version",
"last 1 safari version"
]
}
},
"proxy": "http://localhost"
}

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

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

@ -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>&nbsp;
CMS: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> 1.2.6
</footer>
)
}

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

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

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

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

@ -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 />&nbsp;{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>
}

@ -5,7 +5,7 @@
"private": true,
"targets": {
"admin-panel": {
"source": "./admin-panel/src/index.jsx",
"source": "./admin-panel/src/index.js",
"distDir": "./dist/admin-panel"
}
},
@ -32,6 +32,11 @@
"maxParallelRequests": 1
},
"dependencies": {
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.61",
"clsx": "^1.2.1",
"date-fns": "^2.29.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.3"

@ -1,5 +1,3 @@
// import 'babel-polyfill';
export default class API {
constructor() {
this.loggedIn = false;
@ -27,16 +25,31 @@ export default class API {
return res;
}
/** Info **/
async info() {
return this.apiCall("info");
}
/** UserAPI **/
async login(username, password, rememberMe=false) {
return this.apiCall("user/login", { username: username, password: password, stayLoggedIn: rememberMe })
}
async fetchUser() {
let response = await fetch("/api/user/info");
let data = await response.json();
this.user = data["user"];
this.loggedIn = data["loggedIn"];
return data && data.success && data.loggedIn;
if (data) {
this.user = data["user"];
this.loggedIn = data["loggedIn"];
}
return data;
}
async editUser(id, username, email, password, groups, confirmed) {
return this.apiCall("user/edit", { id: id, username: username, email: email, password: password, groups: groups, confirmed: confirmed });
return this.apiCall("user/edit", {
id: id, username: username, email: email,
password: password, groups: groups, confirmed: confirmed
});
}
async logout() {
@ -67,10 +80,12 @@ export default class API {
return this.apiCall("user/create", { username: username, email: email, password: password, confirmPassword: confirmPassword });
}
/** Stats **/
async getStats() {
return this.apiCall("stats");
}
/** RoutesAPI **/
async getRoutes() {
return this.apiCall("routes/fetch");
}
@ -79,6 +94,7 @@ export default class API {
return this.apiCall("routes/save", { routes: routes });
}
/** GroupAPI **/
async createGroup(name, color) {
return this.apiCall("groups/create", { name: name, color: color });
}
@ -87,6 +103,7 @@ export default class API {
return this.apiCall("groups/delete", { id: id });
}
/** SettingsAPI **/
async getSettings(key = "") {
return this.apiCall("settings/get", { key: key });
}
@ -95,10 +112,12 @@ export default class API {
return this.apiCall("settings/set", { settings: settings });
}
/** MailAPI **/
async sendTestMail(receiver) {
return this.apiCall("mail/test", { receiver: receiver });
}
/** PermissionAPI **/
async fetchPermissions() {
return this.apiCall("permission/fetch");
}
@ -107,7 +126,21 @@ export default class API {
return this.apiCall("permission/save", { permissions: permissions });
}
/** VisitorsAPI **/
async getVisitors(type, date) {
return this.apiCall("visitors/stats", { type: type, date: date });
}
/** LanguageAPI **/
async getLanguages() {
return this.apiCall("language/get");
}
async setLanguageByCode(code) {
return this.apiCall("language/set", { code: code });
}
async setLanguageByName(name) {
return this.apiCall("language/set", { name: name });
}
};

@ -1,26 +0,0 @@
import moment from 'moment';
function getPeriodString(date) {
return moment(date).fromNow();
}
export default function humanReadableSize(bytes, dp = 1) {
const thresh = 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10**dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
export { getPeriodString };

@ -0,0 +1,14 @@
import React from "react";
import { enUS as dateFnsEN } from "date-fns/locale/index.js";
export default class LocaleEnglish {
constructor() {
this.code = "en_US";
this.name = "American English";
this.entries = {};
}
toDateFns() {
return dateFnsEN;
}
};

@ -0,0 +1,14 @@
import React from "react";
import { de as dateFnsDE } from "date-fns/locale/index.js";
export default class LocaleGerman {
constructor() {
this.code = "de_DE";
this.name = "Deutsch Standard";
this.entries = {};
}
toDateFns() {
return dateFnsDE;
}
}

@ -0,0 +1,35 @@
import LocaleGerman from "./german";
import LocaleEnglish from "./english";
let INSTANCE = new LocaleEnglish();
function initLocale(code) {
if (!INSTANCE || INSTANCE.code !== code) {
const constructors = {
"de_DE": LocaleGerman,
"en_US": LocaleEnglish,
}
if (constructors.hasOwnProperty(code)) {
INSTANCE = new (constructors[code])();
} else {
INSTANCE = { code: code, entries: { } };
}
}
return INSTANCE;
}
function translate(key) {
return (INSTANCE.entries[key] || key);
}
function useLanguageModule(module) {
if (module[INSTANCE.code]) {
for (const [key, value] of Object.entries(module[INSTANCE.code])) {
INSTANCE.entries[key] = value;
}
}
}
export { translate as L, initLocale, useLanguageModule, INSTANCE as currentLocale };

47
react/shared/util.js Normal file

@ -0,0 +1,47 @@
function humanReadableSize(bytes, dp = 1) {
const thresh = 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10**dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
const removeParameter = (key) => {
const url = new URL(window.location);
url.searchParams.delete(key);
window.history.replaceState(null, '', url);
}
const getParameter = (key) => {
const url = new URL(window.location);
if (url.searchParams.has(key)) {
return decodeURIComponent(url.searchParams.get(key));
} else {
return null;
}
}
const encodeText = (str) => {
return Uint8Array.from(str, c => c.charCodeAt(0));
}
const decodeText = (buffer) => {
return String.fromCharCode(...new Uint8Array(buffer));
}
const getBaseUrl = () => {
return window.location.protocol + "//" + window.location.host;
}
export { humanReadableSize, removeParameter, getParameter, encodeText, decodeText, getBaseUrl };

@ -1028,7 +1028,7 @@
core-js-pure "^3.25.1"
regenerator-runtime "^0.13.10"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.8.4":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.20.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9"
integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==
@ -1185,6 +1185,11 @@
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36"
integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==
"@emotion/hash@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
"@eslint/eslintrc@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95"
@ -1540,6 +1545,88 @@
resolved "https://registry.yarnpkg.com/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.5.2.tgz#28f643fbc0bec30b07fbe95b137879b6b4d1c9c5"
integrity sha512-zrBczSbXKxEyK2ijtbRdICDygRqWSRPpZMN5dD1T8VMEW5RIhIbwFWw2phDRXuBQdVDpSjalCIUMWMV2h3JaZA==
"@material-ui/core@^4.12.4":
version "4.12.4"
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.4.tgz#4ac17488e8fcaf55eb6a7f5efb2a131e10138a73"
integrity sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==
dependencies:
"@babel/runtime" "^7.4.4"
"@material-ui/styles" "^4.11.5"
"@material-ui/system" "^4.12.2"
"@material-ui/types" "5.1.0"
"@material-ui/utils" "^4.11.3"
"@types/react-transition-group" "^4.2.0"
clsx "^1.0.4"
hoist-non-react-statics "^3.3.2"
popper.js "1.16.1-lts"
prop-types "^15.7.2"
react-is "^16.8.0 || ^17.0.0"
react-transition-group "^4.4.0"
"@material-ui/icons@^4.11.3":
version "4.11.3"
resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.11.3.tgz#b0693709f9b161ce9ccde276a770d968484ecff1"
integrity sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==
dependencies:
"@babel/runtime" "^7.4.4"
"@material-ui/lab@^4.0.0-alpha.61":
version "4.0.0-alpha.61"
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz#9bf8eb389c0c26c15e40933cc114d4ad85e3d978"
integrity sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==
dependencies:
"@babel/runtime" "^7.4.4"
"@material-ui/utils" "^4.11.3"
clsx "^1.0.4"
prop-types "^15.7.2"
react-is "^16.8.0 || ^17.0.0"
"@material-ui/styles@^4.11.5":
version "4.11.5"
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.5.tgz#19f84457df3aafd956ac863dbe156b1d88e2bbfb"
integrity sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==
dependencies:
"@babel/runtime" "^7.4.4"
"@emotion/hash" "^0.8.0"
"@material-ui/types" "5.1.0"
"@material-ui/utils" "^4.11.3"
clsx "^1.0.4"
csstype "^2.5.2"
hoist-non-react-statics "^3.3.2"
jss "^10.5.1"
jss-plugin-camel-case "^10.5.1"
jss-plugin-default-unit "^10.5.1"
jss-plugin-global "^10.5.1"
jss-plugin-nested "^10.5.1"
jss-plugin-props-sort "^10.5.1"
jss-plugin-rule-value-function "^10.5.1"
jss-plugin-vendor-prefixer "^10.5.1"
prop-types "^15.7.2"
"@material-ui/system@^4.12.2":
version "4.12.2"
resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.2.tgz#f5c389adf3fce4146edd489bf4082d461d86aa8b"
integrity sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==
dependencies:
"@babel/runtime" "^7.4.4"
"@material-ui/utils" "^4.11.3"
csstype "^2.5.2"
prop-types "^15.7.2"
"@material-ui/types@5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2"
integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==
"@material-ui/utils@^4.11.3":
version "4.11.3"
resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.11.3.tgz#232bd86c4ea81dab714f21edad70b7fdf0253942"
integrity sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==
dependencies:
"@babel/runtime" "^7.4.4"
prop-types "^15.7.2"
react-is "^16.8.0 || ^17.0.0"
"@mischnic/json-sourcemap@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@mischnic/json-sourcemap/-/json-sourcemap-0.1.0.tgz#38af657be4108140a548638267d02a2ea3336507"
@ -2559,6 +2646,11 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e"
integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==
"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/q@^1.5.1":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
@ -2574,6 +2666,22 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-transition-group@^4.2.0":
version "4.4.5"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==
dependencies:
"@types/react" "*"
"@types/react@*":
version "18.0.25"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.25.tgz#8b1dcd7e56fe7315535a4af25435e0bb55c8ae44"
integrity sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@ -2586,6 +2694,11 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/semver@^7.3.12":
version "7.3.13"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
@ -3592,6 +3705,11 @@ clone@^2.1.1:
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
clsx@^1.0.4, clsx@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@ -3894,6 +4012,14 @@ css-tree@^1.1.2, css-tree@^1.1.3:
mdn-data "2.0.14"
source-map "^0.6.1"
css-vendor@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d"
integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==
dependencies:
"@babel/runtime" "^7.8.3"
is-in-browser "^1.0.2"
css-what@^3.2.1:
version "3.4.2"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
@ -3987,6 +4113,16 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
csstype@^2.5.2:
version "2.6.21"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
csstype@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
customize-cra@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/customize-cra/-/customize-cra-1.0.0.tgz#73286563631aa08127ad4d30a2e3c89cf4e93c8d"
@ -4008,6 +4144,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^2.29.3:
version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
debug@2.6.9, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -4181,6 +4322,14 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"
dom-helpers@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@ -5211,6 +5360,13 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
hoopy@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d"
@ -5369,6 +5525,11 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
hyphenate-style-name@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -5546,6 +5707,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
is-in-browser@^1.0.2, is-in-browser@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
integrity sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==
is-json@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-json/-/is-json-2.0.1.tgz#6be166d144828a131d686891b983df62c39491ff"
@ -6319,6 +6485,76 @@ jsonpointer@^5.0.0:
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
jss-plugin-camel-case@^10.5.1:
version "10.9.2"
resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.2.tgz#76dddfa32f9e62d17daa4e3504991fd0933b89e1"
integrity sha512-wgBPlL3WS0WDJ1lPJcgjux/SHnDuu7opmgQKSraKs4z8dCCyYMx9IDPFKBXQ8Q5dVYij1FFV0WdxyhuOOAXuTg==
dependencies:
"@babel/runtime" "^7.3.1"
hyphenate-style-name "^1.0.3"
jss "10.9.2"
jss-plugin-default-unit@^10.5.1:
version "10.9.2"
resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.2.tgz#3e7f4a1506b18d8fe231554fd982439feb2a9c53"
integrity sha512-pYg0QX3bBEFtTnmeSI3l7ad1vtHU42YEEpgW7pmIh+9pkWNWb5dwS/4onSfAaI0kq+dOZHzz4dWe+8vWnanoSg==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.2"
jss-plugin-global@^10.5.1:
version "10.9.2"
resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.2.tgz#e7f2ad4a5e8e674fb703b04b57a570b8c3e5c2c2"
integrity sha512-GcX0aE8Ef6AtlasVrafg1DItlL/tWHoC4cGir4r3gegbWwF5ZOBYhx04gurPvWHC8F873aEGqge7C17xpwmp2g==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.2"
jss-plugin-nested@^10.5.1:
version "10.9.2"
resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.2.tgz#3aa2502816089ecf3981e1a07c49b276d67dca63"
integrity sha512-VgiOWIC6bvgDaAL97XCxGD0BxOKM0K0zeB/ECyNaVF6FqvdGB9KBBWRdy2STYAss4VVA7i5TbxFZN+WSX1kfQA==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.2"
tiny-warning "^1.0.2"
jss-plugin-props-sort@^10.5.1:
version "10.9.2"
resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.2.tgz#645f6c8f179309667b3e6212f66b59a32fb3f01f"
integrity sha512-AP1AyUTbi2szylgr+O0OB7gkIxEGzySLITZ2GpsaoX72YMCGI2jYAc+WUhPfvUnZYiauF4zTnN4V4TGuvFjJlw==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.2"
jss-plugin-rule-value-function@^10.5.1:
version "10.9.2"
resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.2.tgz#9afe07596e477123cbf11120776be6a64494541f"
integrity sha512-vf5ms8zvLFMub6swbNxvzsurHfUZ5Shy5aJB2gIpY6WNA3uLinEcxYyraQXItRHi5ivXGqYciFDRM2ZoVoRZ4Q==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.2"
tiny-warning "^1.0.2"
jss-plugin-vendor-prefixer@^10.5.1:
version "10.9.2"
resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.2.tgz#410a0f3b9f8dbbfba58f4d329134df4849aa1237"
integrity sha512-SxcEoH+Rttf9fEv6KkiPzLdXRmI6waOTcMkbbEFgdZLDYNIP9UKNHFy6thhbRKqv0XMQZdrEsbDyV464zE/dUA==
dependencies:
"@babel/runtime" "^7.3.1"
css-vendor "^2.0.8"
jss "10.9.2"
jss@10.9.2, jss@^10.5.1:
version "10.9.2"
resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.2.tgz#9379be1f195ef98011dfd31f9448251bd61b95a9"
integrity sha512-b8G6rWpYLR4teTUbGd4I4EsnWjg7MN0Q5bSsjKhVkJVjhQDy2KzkbD2AW3TuT0RYZVmZZHKIrXDn6kjU14qkUg==
dependencies:
"@babel/runtime" "^7.3.1"
csstype "^3.0.2"
is-in-browser "^1.1.3"
tiny-warning "^1.0.2"
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2:
version "3.3.3"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea"
@ -7179,6 +7415,11 @@ pkg-up@^3.1.0:
dependencies:
find-up "^3.0.0"
popper.js@1.16.1-lts:
version "1.16.1-lts"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05"
integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==
postcss-attribute-case-insensitive@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741"
@ -7822,7 +8063,7 @@ prompts@^2.0.1, prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.8.1:
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -7972,12 +8213,12 @@ react-error-overlay@^6.0.11:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
react-is@^16.13.1:
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^17.0.1:
"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
@ -8067,6 +8308,16 @@ react-scripts@^5.0.1:
optionalDependencies:
fsevents "^2.3.2"
react-transition-group@^4.4.0:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@ -8988,6 +9239,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmpl@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"