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()) {
 | 
					    public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
 | 
				
			||||||
      parent::__construct($context, $externalCall, $params);
 | 
					      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 {
 | 
					    public function _execute(): bool {
 | 
				
			||||||
 | 
					      $sql = $this->context->getSQL();
 | 
				
			||||||
      $id = $this->getParam("id");
 | 
					      $id = $this->getParam("id");
 | 
				
			||||||
      if (!$this->apiKeyExists($id)) {
 | 
					      $apiKey = ApiKey::find($sql, $id);
 | 
				
			||||||
        return false;
 | 
					      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");
 | 
					      $this->success = $apiKey->refresh($sql, 30) !== false;
 | 
				
			||||||
      $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->lastError = $sql->getLastError();
 | 
					      $this->lastError = $sql->getLastError();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if ($this->success) {
 | 
					      if ($this->success) {
 | 
				
			||||||
        $this->result["valid_until"] = $validUntil;
 | 
					        $this->result["validUntil"] = $apiKey->getValidUntil()->getTimestamp();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return $this->success;
 | 
					      return $this->success;
 | 
				
			||||||
@ -147,17 +125,16 @@ namespace Core\API\ApiKey {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function _execute(): bool {
 | 
					    public function _execute(): bool {
 | 
				
			||||||
 | 
					      $sql = $this->context->getSQL();
 | 
				
			||||||
      $id = $this->getParam("id");
 | 
					      $id = $this->getParam("id");
 | 
				
			||||||
      if (!$this->apiKeyExists($id)) {
 | 
					      $apiKey = ApiKey::find($sql, $id);
 | 
				
			||||||
        return false;
 | 
					      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 = $apiKey->revoke($sql);
 | 
				
			||||||
      $this->success = $sql->update("ApiKey")
 | 
					 | 
				
			||||||
        ->set("active", false)
 | 
					 | 
				
			||||||
        ->whereEq("id", $id)
 | 
					 | 
				
			||||||
        ->whereEq("user_id", $this->context->getUser()->getId())
 | 
					 | 
				
			||||||
        ->execute();
 | 
					 | 
				
			||||||
      $this->lastError = $sql->getLastError();
 | 
					      $this->lastError = $sql->getLastError();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return $this->success;
 | 
					      return $this->success;
 | 
				
			||||||
 | 
				
			|||||||
@ -128,10 +128,7 @@ namespace Core\API\Groups {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      $sql = $this->context->getSQL();
 | 
					      $sql = $this->context->getSQL();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      $group = new Group();
 | 
					      $group = new Group(null, $name, $color);
 | 
				
			||||||
      $group->name = $name;
 | 
					 | 
				
			||||||
      $group->color = $color;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      $this->success = ($group->save($sql) !== FALSE);
 | 
					      $this->success = ($group->save($sql) !== FALSE);
 | 
				
			||||||
      $this->lastError = $sql->getLastError();
 | 
					      $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) {
 | 
					    public function __construct(Context $context, $externalCall = false) {
 | 
				
			||||||
      parent::__construct($context, $externalCall, array(
 | 
					      parent::__construct($context, $externalCall, array(
 | 
				
			||||||
        'langId' => new Parameter('langId', Parameter::TYPE_INT, true, NULL),
 | 
					        'id' => new Parameter('id', Parameter::TYPE_INT, true, NULL),
 | 
				
			||||||
        'langCode' => new StringType('langCode', 5, true, NULL),
 | 
					        'code' => new StringType('code', 5, true, NULL),
 | 
				
			||||||
      ));
 | 
					      ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function checkLanguage(): bool {
 | 
					    private function checkLanguage(): bool {
 | 
				
			||||||
      $langId = $this->getParam("langId");
 | 
					      $langId = $this->getParam("id");
 | 
				
			||||||
      $langCode = $this->getParam("langCode");
 | 
					      $langCode = $this->getParam("code");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (is_null($langId) && is_null($langCode)) {
 | 
					      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();
 | 
					      $sql = $this->context->getSQL();
 | 
				
			||||||
@ -88,15 +88,10 @@ namespace Core\API\Language {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function updateLanguage(): bool {
 | 
					    private function updateLanguage(): bool {
 | 
				
			||||||
      $languageId = $this->language->getId();
 | 
					 | 
				
			||||||
      $userId = $this->context->getUser()->getId();
 | 
					 | 
				
			||||||
      $sql = $this->context->getSQL();
 | 
					      $sql = $this->context->getSQL();
 | 
				
			||||||
 | 
					      $currentUser = $this->context->getUser();
 | 
				
			||||||
      $this->success = $sql->update("User")
 | 
					      $currentUser->language = $this->language;
 | 
				
			||||||
        ->set("language_id", $languageId)
 | 
					      $this->success = $currentUser->save($sql, ["language_id"]);
 | 
				
			||||||
        ->whereEq("id", $userId)
 | 
					 | 
				
			||||||
        ->execute();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      $this->lastError = $sql->getLastError();
 | 
					      $this->lastError = $sql->getLastError();
 | 
				
			||||||
      return $this->success;
 | 
					      return $this->success;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -27,7 +27,7 @@ namespace Core\API {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      $route->setActive($active);
 | 
					      $route->setActive($active);
 | 
				
			||||||
      $this->success = $route->save($sql);
 | 
					      $this->success = $route->save($sql, ["active"]);
 | 
				
			||||||
      $this->lastError = $sql->getLastError();
 | 
					      $this->lastError = $sql->getLastError();
 | 
				
			||||||
      return $this->success && $this->regenerateCache();
 | 
					      return $this->success && $this->regenerateCache();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -142,13 +142,13 @@ namespace Core\API\TFA {
 | 
				
			|||||||
      if ($twoFactorToken && $twoFactorToken->isConfirmed()) {
 | 
					      if ($twoFactorToken && $twoFactorToken->isConfirmed()) {
 | 
				
			||||||
        return $this->createError("You already added a two factor token");
 | 
					        return $this->createError("You already added a two factor token");
 | 
				
			||||||
      } else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
 | 
					      } else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
 | 
				
			||||||
        $twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32"));
 | 
					 | 
				
			||||||
        $sql = $this->context->getSQL();
 | 
					        $sql = $this->context->getSQL();
 | 
				
			||||||
 | 
					        $twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32"));
 | 
				
			||||||
        $this->success = $twoFactorToken->save($sql) !== false;
 | 
					        $this->success = $twoFactorToken->save($sql) !== false;
 | 
				
			||||||
        $this->lastError = $sql->getLastError();
 | 
					        $this->lastError = $sql->getLastError();
 | 
				
			||||||
        if ($this->success) {
 | 
					        if ($this->success) {
 | 
				
			||||||
          $currentUser->setTwoFactorToken($twoFactorToken);
 | 
					          $currentUser->setTwoFactorToken($twoFactorToken);
 | 
				
			||||||
          $this->success = $currentUser->save($sql);
 | 
					          $this->success = $currentUser->save($sql, ["two_factor_token_id"]);
 | 
				
			||||||
          $this->lastError = $sql->getLastError();
 | 
					          $this->lastError = $sql->getLastError();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -503,7 +503,7 @@ namespace Core\API\User {
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        $user->password = $this->hashPassword($password);
 | 
					        $user->password = $this->hashPassword($password);
 | 
				
			||||||
        $user->confirmed = true;
 | 
					        $user->confirmed = true;
 | 
				
			||||||
        if ($user->save($sql)) {
 | 
					        if ($user->save($sql, ["password", "confirmed"])) {
 | 
				
			||||||
          $userToken->invalidate($sql);
 | 
					          $userToken->invalidate($sql);
 | 
				
			||||||
          return true;
 | 
					          return true;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@ -542,7 +542,7 @@ namespace Core\API\User {
 | 
				
			|||||||
        return $this->createError("Your email address is already confirmed.");
 | 
					        return $this->createError("Your email address is already confirmed.");
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        $user->confirmed = true;
 | 
					        $user->confirmed = true;
 | 
				
			||||||
        if ($user->save($sql)) {
 | 
					        if ($user->save($sql, ["confirmed"])) {
 | 
				
			||||||
          $userToken->invalidate($sql);
 | 
					          $userToken->invalidate($sql);
 | 
				
			||||||
          return true;
 | 
					          return true;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@ -826,20 +826,37 @@ namespace Core\API\User {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($usernameChanged) $user->name = $username;
 | 
					        $columnsToUpdate = [];
 | 
				
			||||||
        if ($fullNameChanged) $user->fullName = $fullName;
 | 
					        if ($usernameChanged) {
 | 
				
			||||||
        if ($emailChanged) $user->email = $email;
 | 
					          $user->name = $username;
 | 
				
			||||||
        if (!is_null($password)) $user->password = $this->hashPassword($password);
 | 
					          $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 (!is_null($confirmed)) {
 | 
				
			||||||
          if ($id === $currentUser->getId() && $confirmed === false) {
 | 
					          if ($id === $currentUser->getId() && $confirmed === false) {
 | 
				
			||||||
            return $this->createError("Cannot make own account unconfirmed.");
 | 
					            return $this->createError("Cannot make own account unconfirmed.");
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            $user->confirmed = $confirmed;
 | 
					            $user->confirmed = $confirmed;
 | 
				
			||||||
 | 
					            $columnsToUpdate[] = "confirmed";
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($user->save($sql)) {
 | 
					        if (empty($columnsToUpdate) || $user->save($sql, $columnsToUpdate)) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          $deleteQuery = $sql->delete("UserGroup")->whereEq("user_id", $id);
 | 
					          $deleteQuery = $sql->delete("UserGroup")->whereEq("user_id", $id);
 | 
				
			||||||
          $insertQuery = $sql->insert("UserGroup", array("user_id", "group_id"));
 | 
					          $insertQuery = $sql->insert("UserGroup", array("user_id", "group_id"));
 | 
				
			||||||
@ -1270,7 +1287,7 @@ namespace Core\API\User {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if ($this->success) {
 | 
					      if ($this->success) {
 | 
				
			||||||
        $currentUser->gpgKey = $gpgKey;
 | 
					        $currentUser->gpgKey = $gpgKey;
 | 
				
			||||||
        if ($currentUser->save($sql)) {
 | 
					        if ($currentUser->save($sql, ["gpg_key_id"])) {
 | 
				
			||||||
          $this->result["gpg"] = $gpgKey->jsonSerialize();
 | 
					          $this->result["gpg"] = $gpgKey->jsonSerialize();
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          return $this->createError("Error updating user details: " . $sql->getLastError());
 | 
					          return $this->createError("Error updating user details: " . $sql->getLastError());
 | 
				
			||||||
@ -1524,7 +1541,7 @@ namespace Core\API\User {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      $sql = $this->context->getSQL();
 | 
					      $sql = $this->context->getSQL();
 | 
				
			||||||
      $currentUser->profilePicture = $fileName;
 | 
					      $currentUser->profilePicture = $fileName;
 | 
				
			||||||
      if ($currentUser->save($sql)) {
 | 
					      if ($currentUser->save($sql, ["profile_picture"])) {
 | 
				
			||||||
        $this->result["profilePicture"] = $fileName;
 | 
					        $this->result["profilePicture"] = $fileName;
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        return $this->createError("Error updating user details: " . $sql->getLastError());
 | 
					        return $this->createError("Error updating user details: " . $sql->getLastError());
 | 
				
			||||||
@ -1551,7 +1568,7 @@ namespace Core\API\User {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      $currentUser->profilePicture = null;
 | 
					      $currentUser->profilePicture = null;
 | 
				
			||||||
      if (!$currentUser->save($sql)) {
 | 
					      if (!$currentUser->save($sql, ["profile_picture"])) {
 | 
				
			||||||
        return $this->createError("Error updating user details: " . $sql->getLastError());
 | 
					        return $this->createError("Error updating user details: " . $sql->getLastError());
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Core\Objects\DatabaseEntity;
 | 
					namespace Core\Objects\DatabaseEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Core\Driver\SQL\SQL;
 | 
				
			||||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
 | 
					use Core\Objects\DatabaseEntity\Attribute\MaxLength;
 | 
				
			||||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
 | 
					use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -25,4 +26,18 @@ class ApiKey extends DatabaseEntity {
 | 
				
			|||||||
      "validUntil" => $this->validUntil->getTimestamp()
 | 
					      "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 {
 | 
					  public function save(SQL $sql, ?array $columns = null, bool $saveNM = false): bool {
 | 
				
			||||||
    $handler = self::getHandler($sql);
 | 
					    $handler = self::getHandler($sql);
 | 
				
			||||||
    $res = $handler->insertOrUpdate($this, $columns, $saveNM);
 | 
					    $res = $handler->insertOrUpdate($this, $columns, $saveNM);
 | 
				
			||||||
 | 
				
			|||||||
@ -136,6 +136,6 @@ class GpgKey extends DatabaseEntity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public function confirm(SQL $sql): bool {
 | 
					  public function confirm(SQL $sql): bool {
 | 
				
			||||||
    $this->confirmed = true;
 | 
					    $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->status = self::STATUS_SUCCESS;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $this->save($context->getSQL());
 | 
					    $this->save($context->getSQL(), ["status", "retry_count", "next_try", "error_message"]);
 | 
				
			||||||
    return $success;
 | 
					    return $success;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -109,7 +109,7 @@ class Session extends DatabaseEntity {
 | 
				
			|||||||
  public function destroy(): bool {
 | 
					  public function destroy(): bool {
 | 
				
			||||||
    session_destroy();
 | 
					    session_destroy();
 | 
				
			||||||
    $this->active = false;
 | 
					    $this->active = false;
 | 
				
			||||||
    return $this->save($this->context->getSQL());
 | 
					    return $this->save($this->context->getSQL(), ["active"]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public function update(): bool {
 | 
					  public function update(): bool {
 | 
				
			||||||
@ -120,7 +120,7 @@ class Session extends DatabaseEntity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    $sql = $this->context->getSQL();
 | 
					    $sql = $this->context->getSQL();
 | 
				
			||||||
    return $this->user->update($sql) &&
 | 
					    return $this->user->update($sql) &&
 | 
				
			||||||
           $this->save($sql);
 | 
					           $this->save($sql, ["expires", "data"]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public function getCsrfToken(): string {
 | 
					  public function getCsrfToken(): string {
 | 
				
			||||||
 | 
				
			|||||||
@ -55,7 +55,7 @@ class UserToken extends DatabaseEntity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public function invalidate(SQL $sql): bool {
 | 
					  public function invalidate(SQL $sql): bool {
 | 
				
			||||||
    $this->used = true;
 | 
					    $this->used = true;
 | 
				
			||||||
    return $this->save($sql);
 | 
					    return $this->save($sql, ["used"]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public function getUser(): User {
 | 
					  public function getUser(): User {
 | 
				
			||||||
@ -64,7 +64,7 @@ class UserToken extends DatabaseEntity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public function updateDurability(SQL $sql, int $validHours): bool {
 | 
					  public function updateDurability(SQL $sql, int $validHours): bool {
 | 
				
			||||||
    $this->validUntil = (new \DateTime())->modify("+$validHours HOURS");
 | 
					    $this->validUntil = (new \DateTime())->modify("+$validHours HOURS");
 | 
				
			||||||
    return $this->save($sql);
 | 
					    return $this->save($sql, ["valid_until"]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public function getToken(): string {
 | 
					  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 firefox version",
 | 
				
			||||||
      "last 1 safari 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,
 | 
					  "private": true,
 | 
				
			||||||
  "targets": {
 | 
					  "targets": {
 | 
				
			||||||
    "admin-panel": {
 | 
					    "admin-panel": {
 | 
				
			||||||
      "source": "./admin-panel/src/index.jsx",
 | 
					      "source": "./admin-panel/src/index.js",
 | 
				
			||||||
      "distDir": "./dist/admin-panel"
 | 
					      "distDir": "./dist/admin-panel"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@ -32,6 +32,11 @@
 | 
				
			|||||||
    "maxParallelRequests": 1
 | 
					    "maxParallelRequests": 1
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "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": "^18.2.0",
 | 
				
			||||||
    "react-dom": "^18.2.0",
 | 
					    "react-dom": "^18.2.0",
 | 
				
			||||||
    "react-router-dom": "^6.4.3"
 | 
					    "react-router-dom": "^6.4.3"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,3 @@
 | 
				
			|||||||
// import 'babel-polyfill';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default class API {
 | 
					export default class API {
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        this.loggedIn = false;
 | 
					        this.loggedIn = false;
 | 
				
			||||||
@ -27,16 +25,31 @@ export default class API {
 | 
				
			|||||||
        return res;
 | 
					        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() {
 | 
					    async fetchUser() {
 | 
				
			||||||
        let response = await fetch("/api/user/info");
 | 
					        let response = await fetch("/api/user/info");
 | 
				
			||||||
        let data = await response.json();
 | 
					        let data = await response.json();
 | 
				
			||||||
 | 
					        if (data) {
 | 
				
			||||||
            this.user = data["user"];
 | 
					            this.user = data["user"];
 | 
				
			||||||
            this.loggedIn = data["loggedIn"];
 | 
					            this.loggedIn = data["loggedIn"];
 | 
				
			||||||
        return data && data.success && data.loggedIn;
 | 
					        }
 | 
				
			||||||
 | 
					        return data;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async editUser(id, username, email, password, groups, confirmed) {
 | 
					    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() {
 | 
					    async logout() {
 | 
				
			||||||
@ -67,10 +80,12 @@ export default class API {
 | 
				
			|||||||
        return this.apiCall("user/create", { username: username, email: email, password: password, confirmPassword: confirmPassword });
 | 
					        return this.apiCall("user/create", { username: username, email: email, password: password, confirmPassword: confirmPassword });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** Stats **/
 | 
				
			||||||
    async getStats() {
 | 
					    async getStats() {
 | 
				
			||||||
        return this.apiCall("stats");
 | 
					        return this.apiCall("stats");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** RoutesAPI **/
 | 
				
			||||||
    async getRoutes() {
 | 
					    async getRoutes() {
 | 
				
			||||||
        return this.apiCall("routes/fetch");
 | 
					        return this.apiCall("routes/fetch");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -79,6 +94,7 @@ export default class API {
 | 
				
			|||||||
        return this.apiCall("routes/save", { routes: routes });
 | 
					        return this.apiCall("routes/save", { routes: routes });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** GroupAPI **/
 | 
				
			||||||
    async createGroup(name, color) {
 | 
					    async createGroup(name, color) {
 | 
				
			||||||
        return this.apiCall("groups/create", { name: name, color: 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 });
 | 
					        return this.apiCall("groups/delete", { id: id });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** SettingsAPI **/
 | 
				
			||||||
    async getSettings(key = "") {
 | 
					    async getSettings(key = "") {
 | 
				
			||||||
        return this.apiCall("settings/get", { key: key });
 | 
					        return this.apiCall("settings/get", { key: key });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -95,10 +112,12 @@ export default class API {
 | 
				
			|||||||
        return this.apiCall("settings/set", { settings: settings });
 | 
					        return this.apiCall("settings/set", { settings: settings });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** MailAPI **/
 | 
				
			||||||
    async sendTestMail(receiver) {
 | 
					    async sendTestMail(receiver) {
 | 
				
			||||||
        return this.apiCall("mail/test", { receiver: receiver });
 | 
					        return this.apiCall("mail/test", { receiver: receiver });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** PermissionAPI **/
 | 
				
			||||||
    async fetchPermissions() {
 | 
					    async fetchPermissions() {
 | 
				
			||||||
        return this.apiCall("permission/fetch");
 | 
					        return this.apiCall("permission/fetch");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -107,7 +126,21 @@ export default class API {
 | 
				
			|||||||
        return this.apiCall("permission/save", { permissions: permissions });
 | 
					        return this.apiCall("permission/save", { permissions: permissions });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** VisitorsAPI **/
 | 
				
			||||||
    async getVisitors(type, date) {
 | 
					    async getVisitors(type, date) {
 | 
				
			||||||
        return this.apiCall("visitors/stats", { type: type, date: 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"
 | 
					    core-js-pure "^3.25.1"
 | 
				
			||||||
    regenerator-runtime "^0.13.10"
 | 
					    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"
 | 
					  version "7.20.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9"
 | 
					  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9"
 | 
				
			||||||
  integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==
 | 
					  integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==
 | 
				
			||||||
@ -1185,6 +1185,11 @@
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36"
 | 
					  resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36"
 | 
				
			||||||
  integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==
 | 
					  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":
 | 
					"@eslint/eslintrc@^1.3.3":
 | 
				
			||||||
  version "1.3.3"
 | 
					  version "1.3.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.5.2.tgz#28f643fbc0bec30b07fbe95b137879b6b4d1c9c5"
 | 
				
			||||||
  integrity sha512-zrBczSbXKxEyK2ijtbRdICDygRqWSRPpZMN5dD1T8VMEW5RIhIbwFWw2phDRXuBQdVDpSjalCIUMWMV2h3JaZA==
 | 
					  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":
 | 
					"@mischnic/json-sourcemap@^0.1.0":
 | 
				
			||||||
  version "0.1.0"
 | 
					  version "0.1.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@mischnic/json-sourcemap/-/json-sourcemap-0.1.0.tgz#38af657be4108140a548638267d02a2ea3336507"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e"
 | 
				
			||||||
  integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==
 | 
					  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":
 | 
					"@types/q@^1.5.1":
 | 
				
			||||||
  version "1.5.5"
 | 
					  version "1.5.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
 | 
				
			||||||
  integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
 | 
					  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":
 | 
					"@types/resolve@1.17.1":
 | 
				
			||||||
  version "1.17.1"
 | 
					  version "1.17.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
 | 
				
			||||||
  integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
 | 
					  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":
 | 
					"@types/semver@^7.3.12":
 | 
				
			||||||
  version "7.3.13"
 | 
					  version "7.3.13"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
 | 
				
			||||||
  integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
 | 
					  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:
 | 
					co@^4.6.0:
 | 
				
			||||||
  version "4.6.0"
 | 
					  version "4.6.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
 | 
					  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"
 | 
					    mdn-data "2.0.14"
 | 
				
			||||||
    source-map "^0.6.1"
 | 
					    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:
 | 
					css-what@^3.2.1:
 | 
				
			||||||
  version "3.4.2"
 | 
					  version "3.4.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
 | 
					  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
 | 
				
			||||||
@ -3987,6 +4113,16 @@ cssstyle@^2.3.0:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    cssom "~0.3.6"
 | 
					    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:
 | 
					customize-cra@^1.0.0:
 | 
				
			||||||
  version "1.0.0"
 | 
					  version "1.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/customize-cra/-/customize-cra-1.0.0.tgz#73286563631aa08127ad4d30a2e3c89cf4e93c8d"
 | 
					  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-mimetype "^2.3.0"
 | 
				
			||||||
    whatwg-url "^8.0.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:
 | 
					debug@2.6.9, debug@^2.6.0, debug@^2.6.9:
 | 
				
			||||||
  version "2.6.9"
 | 
					  version "2.6.9"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
 | 
					  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
 | 
				
			||||||
@ -4181,6 +4322,14 @@ dom-converter@^0.2.0:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    utila "~0.4"
 | 
					    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:
 | 
					dom-serializer@0:
 | 
				
			||||||
  version "0.2.2"
 | 
					  version "0.2.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
 | 
				
			||||||
  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 | 
					  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:
 | 
					hoopy@^0.1.4:
 | 
				
			||||||
  version "0.1.4"
 | 
					  version "0.1.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
 | 
				
			||||||
  integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
 | 
					  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:
 | 
					iconv-lite@0.4.24:
 | 
				
			||||||
  version "0.4.24"
 | 
					  version "0.4.24"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
 | 
					  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:
 | 
					  dependencies:
 | 
				
			||||||
    is-extglob "^2.1.1"
 | 
					    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:
 | 
					is-json@^2.0.1:
 | 
				
			||||||
  version "2.0.1"
 | 
					  version "2.0.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/is-json/-/is-json-2.0.1.tgz#6be166d144828a131d686891b983df62c39491ff"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
 | 
				
			||||||
  integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
 | 
					  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:
 | 
					"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2:
 | 
				
			||||||
  version "3.3.3"
 | 
					  version "3.3.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea"
 | 
					  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:
 | 
					  dependencies:
 | 
				
			||||||
    find-up "^3.0.0"
 | 
					    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:
 | 
					postcss-attribute-case-insensitive@^5.0.2:
 | 
				
			||||||
  version "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"
 | 
					  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"
 | 
					    kleur "^3.0.3"
 | 
				
			||||||
    sisteransi "^1.0.5"
 | 
					    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"
 | 
					  version "15.8.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
 | 
					  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
 | 
				
			||||||
  integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
 | 
				
			||||||
  integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
 | 
					  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"
 | 
					  version "16.13.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
 | 
					  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
 | 
				
			||||||
  integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 | 
					  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"
 | 
					  version "17.0.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
 | 
					  resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
 | 
				
			||||||
  integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
 | 
					  integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
 | 
				
			||||||
@ -8067,6 +8308,16 @@ react-scripts@^5.0.1:
 | 
				
			|||||||
  optionalDependencies:
 | 
					  optionalDependencies:
 | 
				
			||||||
    fsevents "^2.3.2"
 | 
					    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:
 | 
					react@^18.2.0:
 | 
				
			||||||
  version "18.2.0"
 | 
					  version "18.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
 | 
				
			||||||
  integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
 | 
					  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:
 | 
					tmpl@1.0.5:
 | 
				
			||||||
  version "1.0.5"
 | 
					  version "1.0.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
 | 
					  resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user