Cleanup
This commit is contained in:
parent
c9a7da688f
commit
25ef07b0b7
@ -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
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
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"
|
||||
}
|
||||
|
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>
|
||||
}
|
@ -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 };
|
14
react/shared/locale/english.js
Normal file
14
react/shared/locale/english.js
Normal file
@ -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;
|
||||
}
|
||||
};
|
14
react/shared/locale/german.js
Normal file
14
react/shared/locale/german.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
35
react/shared/locale/locale.js
Normal file
35
react/shared/locale/locale.js
Normal file
@ -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
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 };
|
264
react/yarn.lock
264
react/yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user