Composer update, Groups frontend, API improvements
This commit is contained in:
parent
0125c83bea
commit
1e33c3b8d4
@ -4,6 +4,8 @@ namespace Core\API {
|
|||||||
|
|
||||||
use Core\Driver\SQL\Expression\Count;
|
use Core\Driver\SQL\Expression\Count;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
|
use Core\Objects\DatabaseEntity\Group;
|
||||||
|
use Core\Objects\DatabaseEntity\User;
|
||||||
|
|
||||||
abstract class GroupsAPI extends Request {
|
abstract class GroupsAPI extends Request {
|
||||||
|
|
||||||
@ -22,6 +24,30 @@ namespace Core\API {
|
|||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
return $this->success && $res[0]["count"] > 0;
|
return $this->success && $res[0]["count"] > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getGroup(int $groupId): Group|false {
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
$group = Group::find($sql, $groupId);
|
||||||
|
if ($group === false) {
|
||||||
|
return $this->createError("Error fetching group: " . $sql->getLastError());
|
||||||
|
} else if ($group === null) {
|
||||||
|
return $this->createError("This group does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $group;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getUser(int $userId): User|false {
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
$user = User::find($sql, $userId, true);
|
||||||
|
if ($user === false) {
|
||||||
|
return $this->createError("Error fetching user: " . $sql->getLastError());
|
||||||
|
} else if ($user === null) {
|
||||||
|
return $this->createError("This user does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,12 +59,14 @@ namespace Core\API\Groups {
|
|||||||
use Core\API\Traits\Pagination;
|
use Core\API\Traits\Pagination;
|
||||||
use Core\Driver\SQL\Column\Column;
|
use Core\Driver\SQL\Column\Column;
|
||||||
use Core\Driver\SQL\Condition\Compare;
|
use Core\Driver\SQL\Condition\Compare;
|
||||||
|
use Core\Driver\SQL\Condition\CondAnd;
|
||||||
use Core\Driver\SQL\Expression\Alias;
|
use Core\Driver\SQL\Expression\Alias;
|
||||||
use Core\Driver\SQL\Expression\Count;
|
use Core\Driver\SQL\Expression\Count;
|
||||||
use Core\Driver\SQL\Join\InnerJoin;
|
use Core\Driver\SQL\Join\InnerJoin;
|
||||||
use Core\Driver\SQL\Query\Insert;
|
use Core\Driver\SQL\Query\Insert;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
use Core\Objects\DatabaseEntity\Group;
|
use Core\Objects\DatabaseEntity\Group;
|
||||||
|
use Core\Objects\DatabaseEntity\Route;
|
||||||
use Core\Objects\DatabaseEntity\User;
|
use Core\Objects\DatabaseEntity\User;
|
||||||
|
|
||||||
class Fetch extends GroupsAPI {
|
class Fetch extends GroupsAPI {
|
||||||
@ -49,7 +77,7 @@ namespace Core\API\Groups {
|
|||||||
|
|
||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall,
|
parent::__construct($context, $externalCall,
|
||||||
self::getPaginationParameters(['id', 'name', 'member_count'])
|
self::getPaginationParameters(['id', 'name', 'memberCount'])
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->groupCount = 0;
|
$this->groupCount = 0;
|
||||||
@ -97,14 +125,9 @@ namespace Core\API\Groups {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
$groupId = $this->getParam("id");
|
$groupId = $this->getParam("id");
|
||||||
$group = Group::find($sql, $groupId);
|
$group = $this->getGroup($groupId);
|
||||||
if ($group === false) {
|
if ($group) {
|
||||||
return $this->createError("Error fetching group: " . $sql->getLastError());
|
|
||||||
} else if ($group === null) {
|
|
||||||
return $this->createError("Group not found");
|
|
||||||
} else {
|
|
||||||
$this->result["group"] = $group->jsonSerialize();
|
$this->result["group"] = $group->jsonSerialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,10 +180,10 @@ namespace Core\API\Groups {
|
|||||||
|
|
||||||
class Create extends GroupsAPI {
|
class Create extends GroupsAPI {
|
||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, [
|
||||||
'name' => new StringType('name', 32),
|
'name' => new StringType('name', 32),
|
||||||
'color' => new StringType('color', 10),
|
'color' => new StringType('color', 10),
|
||||||
));
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -199,6 +222,55 @@ namespace Core\API\Groups {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Update extends GroupsAPI {
|
||||||
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
|
parent::__construct($context, $externalCall, [
|
||||||
|
"id" => new Parameter("id", Parameter::TYPE_INT),
|
||||||
|
'name' => new StringType('name', 32),
|
||||||
|
'color' => new StringType('color', 10),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function _execute(): bool {
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
$groupId = $this->getParam("id");
|
||||||
|
$name = $this->getParam("name");
|
||||||
|
if (preg_match("/^[a-zA-Z][a-zA-Z0-9_-]*$/", $name) !== 1) {
|
||||||
|
return $this->createError("Invalid name");
|
||||||
|
}
|
||||||
|
|
||||||
|
$color = $this->getParam("color");
|
||||||
|
if (preg_match("/^#[a-fA-F0-9]{3,6}$/", $color) !== 1) {
|
||||||
|
return $this->createError("Invalid color");
|
||||||
|
}
|
||||||
|
|
||||||
|
$group = $this->getGroup($groupId);
|
||||||
|
if ($group === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$otherGroup = Group::findBy(Group::createBuilder($sql, true)
|
||||||
|
->whereNeq("id", $groupId)
|
||||||
|
->whereEq("name", $name)
|
||||||
|
->first());
|
||||||
|
|
||||||
|
if ($otherGroup) {
|
||||||
|
return $this->createError("This name is already in use");
|
||||||
|
}
|
||||||
|
|
||||||
|
$group->name = $name;
|
||||||
|
$group->color = $color;
|
||||||
|
$this->success = ($group->save($sql) !== FALSE);
|
||||||
|
$this->lastError = $sql->getLastError();
|
||||||
|
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
|
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to update existing groups", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Delete extends GroupsAPI {
|
class Delete extends GroupsAPI {
|
||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, array(
|
||||||
@ -213,16 +285,13 @@ namespace Core\API\Groups {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$group = Group::find($sql, $id);
|
$group = $this->getGroup($id);
|
||||||
if ($group === false) {
|
if ($group) {
|
||||||
return $this->createError("Error fetching group: " . $sql->getLastError());
|
|
||||||
} else if ($group === null) {
|
|
||||||
return $this->createError("This group does not exist.");
|
|
||||||
} else {
|
|
||||||
$this->success = ($group->delete($sql) !== FALSE);
|
$this->success = ($group->delete($sql) !== FALSE);
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
return $this->success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getDefaultACL(Insert $insert): void {
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
@ -233,32 +302,31 @@ namespace Core\API\Groups {
|
|||||||
class AddMember extends GroupsAPI {
|
class AddMember extends GroupsAPI {
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, [
|
parent::__construct($context, $externalCall, [
|
||||||
new Parameter("id", Parameter::TYPE_INT),
|
"id" => new Parameter("id", Parameter::TYPE_INT),
|
||||||
new Parameter("userId", Parameter::TYPE_INT)
|
"userId" => new Parameter("userId", Parameter::TYPE_INT)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
$groupId = $this->getParam("id");
|
$groupId = $this->getParam("id");
|
||||||
$userId = $this->getParam("userId");
|
$group = $this->getGroup($groupId);
|
||||||
$group = Group::find($sql, $groupId);
|
|
||||||
if ($group === false) {
|
if ($group === false) {
|
||||||
return $this->createError("Error fetching group: " . $sql->getLastError());
|
return false;
|
||||||
} else if ($group === null) {
|
|
||||||
return $this->createError("This group does not exist.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = User::find($sql, $userId, true);
|
$userId = $this->getParam("userId");
|
||||||
|
$currentUser = $this->context->getUser();
|
||||||
|
$user = $this->getUser($userId);
|
||||||
if ($user === false) {
|
if ($user === false) {
|
||||||
return $this->createError("Error fetching user: " . $sql->getLastError());
|
return false;
|
||||||
} else if ($user === null) {
|
|
||||||
return $this->createError("This user does not exist.");
|
|
||||||
} else if (isset($user->getGroups()[$groupId])) {
|
} else if (isset($user->getGroups()[$groupId])) {
|
||||||
return $this->createError("This user is already member of this group.");
|
return $this->createError("This user is already member of this group.");
|
||||||
|
} else if ($groupId === Group::ADMIN && !$currentUser->hasGroup(Group::ADMIN)) {
|
||||||
|
return $this->createError("You cannot add the administrator group to other users.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$user->groups[$groupId] = $group;
|
$user->groups[$groupId] = $group;
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
$this->success = $user->save($sql, ["groups"], true);
|
$this->success = $user->save($sql, ["groups"], true);
|
||||||
if (!$this->success) {
|
if (!$this->success) {
|
||||||
return $this->createError("Error saving user: " . $sql->getLastError());
|
return $this->createError("Error saving user: " . $sql->getLastError());
|
||||||
@ -275,32 +343,31 @@ namespace Core\API\Groups {
|
|||||||
class RemoveMember extends GroupsAPI {
|
class RemoveMember extends GroupsAPI {
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, [
|
parent::__construct($context, $externalCall, [
|
||||||
new Parameter("id", Parameter::TYPE_INT),
|
"id" => new Parameter("id", Parameter::TYPE_INT),
|
||||||
new Parameter("userId", Parameter::TYPE_INT)
|
"userId" => new Parameter("userId", Parameter::TYPE_INT)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
$groupId = $this->getParam("id");
|
$groupId = $this->getParam("id");
|
||||||
$userId = $this->getParam("userId");
|
$group = $this->getGroup($groupId);
|
||||||
$group = Group::find($sql, $groupId);
|
|
||||||
if ($group === false) {
|
if ($group === false) {
|
||||||
return $this->createError("Error fetching group: " . $sql->getLastError());
|
return false;
|
||||||
} else if ($group === null) {
|
|
||||||
return $this->createError("This group does not exist.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = User::find($sql, $userId, true);
|
$userId = $this->getParam("userId");
|
||||||
|
$currentUser = $this->context->getUser();
|
||||||
|
$user = $this->getUser($userId);
|
||||||
if ($user === false) {
|
if ($user === false) {
|
||||||
return $this->createError("Error fetching user: " . $sql->getLastError());
|
return false;
|
||||||
} else if ($user === null) {
|
|
||||||
return $this->createError("This user does not exist.");
|
|
||||||
} else if (!isset($user->getGroups()[$groupId])) {
|
} else if (!isset($user->getGroups()[$groupId])) {
|
||||||
return $this->createError("This user is not member of this group.");
|
return $this->createError("This user is not member of this group.");
|
||||||
|
} else if ($userId === $currentUser->getId() && $groupId === Group::ADMIN) {
|
||||||
|
return $this->createError("Cannot remove Administrator group from own user.");
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($user->groups[$groupId]);
|
unset($user->groups[$groupId]);
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
$this->success = $user->save($sql, ["groups"], true);
|
$this->success = $user->save($sql, ["groups"], true);
|
||||||
if (!$this->success) {
|
if (!$this->success) {
|
||||||
return $this->createError("Error saving user: " . $sql->getLastError());
|
return $this->createError("Error saving user: " . $sql->getLastError());
|
||||||
|
@ -88,13 +88,12 @@ namespace Core\API\Language {
|
|||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateLanguage(): bool {
|
private function updateLanguage(): void {
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$currentUser = $this->context->getUser();
|
$currentUser = $this->context->getUser();
|
||||||
$currentUser->language = $this->language;
|
$currentUser->language = $this->language;
|
||||||
$this->success = $currentUser->save($sql, ["language"]);
|
$this->success = $currentUser->save($sql, ["language"]);
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
return $this->success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -141,12 +140,12 @@ namespace Core\API\Language {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$moduleFound = false;
|
$moduleFound = false;
|
||||||
foreach (["Site", "Core"] as $baseDir) {
|
foreach (["Core", "Site"] as $baseDir) {
|
||||||
$filePath = realpath(implode("/", [$baseDir, "Localization", $code, "$module.php"]));
|
$filePath = realpath(implode("/", [WEBROOT, $baseDir, "Localization", $code, "$module.php"]));
|
||||||
if ($filePath && is_file($filePath)) {
|
if ($filePath && is_file($filePath)) {
|
||||||
$moduleFound = true;
|
$moduleFound = true;
|
||||||
$moduleEntries = @include_once $filePath;
|
$moduleEntries = @include_once $filePath;
|
||||||
$entries[$module] = $moduleEntries;
|
$entries[$module] = array_merge($entries[$module] ?? [], $moduleEntries);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,8 +106,11 @@ class Swagger extends Request {
|
|||||||
"post" => [
|
"post" => [
|
||||||
"produces" => ["application/json"],
|
"produces" => ["application/json"],
|
||||||
"responses" => [
|
"responses" => [
|
||||||
"200" => ["description" => ""],
|
"200" => ["description" => "OK!"],
|
||||||
|
"400" => ["description" => "Parameter validation failed"],
|
||||||
"401" => ["description" => "Login or 2FA Authorization is required"],
|
"401" => ["description" => "Login or 2FA Authorization is required"],
|
||||||
|
"403" => ["description" => "CSRF-Token validation failed or insufficient permissions"],
|
||||||
|
"503" => ["description" => "Function is disabled"],
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
@ -136,6 +136,7 @@ namespace Core\API\User {
|
|||||||
use Core\API\UserAPI;
|
use Core\API\UserAPI;
|
||||||
use Core\API\VerifyCaptcha;
|
use Core\API\VerifyCaptcha;
|
||||||
use Core\Driver\SQL\Condition\CondBool;
|
use Core\Driver\SQL\Condition\CondBool;
|
||||||
|
use Core\Driver\SQL\Condition\CondLike;
|
||||||
use Core\Driver\SQL\Condition\CondOr;
|
use Core\Driver\SQL\Condition\CondOr;
|
||||||
use Core\Driver\SQL\Expression\Alias;
|
use Core\Driver\SQL\Expression\Alias;
|
||||||
use Core\Driver\SQL\Query\Insert;
|
use Core\Driver\SQL\Query\Insert;
|
||||||
@ -320,6 +321,39 @@ namespace Core\API\User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Search extends UserAPI {
|
||||||
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
|
parent::__construct($context, $externalCall, [
|
||||||
|
"query" => new StringType("query", 64)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _execute(): bool {
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
$query = $this->getParam("query");
|
||||||
|
|
||||||
|
$users = User::findBy(User::createBuilder($sql, false)
|
||||||
|
->where(new CondOr(
|
||||||
|
new CondLike(new Column("name"), "%$query%"),
|
||||||
|
new CondLike(new Column("full_name"), "%$query%"),
|
||||||
|
new CondLike(new Column("email"), "%$query%"),
|
||||||
|
))
|
||||||
|
->whereTrue("active")
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($users === false) {
|
||||||
|
return $this->createError($sql->getLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->result["users"] = $users;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
|
$insert->addRow(self::getEndpoint(), "Allows users to search other users", [Group::ADMIN, Group::SUPPORT], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Info extends UserAPI {
|
class Info extends UserAPI {
|
||||||
|
|
||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
|
@ -12,5 +12,6 @@ class Admin extends TemplateDocument {
|
|||||||
$this->searchable = false;
|
$this->searchable = false;
|
||||||
$this->enableCSP();
|
$this->enableCSP();
|
||||||
$this->addCSPWhitelist("/react/dist/admin-panel/");
|
$this->addCSPWhitelist("/react/dist/admin-panel/");
|
||||||
|
$this->languageModules[] = "admin";
|
||||||
}
|
}
|
||||||
}
|
}
|
12
Core/External/composer.json
vendored
12
Core/External/composer.json
vendored
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"require": {
|
"require": {
|
||||||
"php-mqtt/client": "^1.1",
|
"php-mqtt/client": "^2.0",
|
||||||
"twig/twig": "^3.0",
|
"twig/twig": "^3.8",
|
||||||
"chillerlan/php-qrcode": "^4.3",
|
"chillerlan/php-qrcode": "^5.0",
|
||||||
"christian-riesen/base32": "^1.6",
|
"christian-riesen/base32": "^1.6",
|
||||||
"spomky-labs/cbor-php": "2.1.0",
|
"spomky-labs/cbor-php": "^3.0",
|
||||||
"web-auth/cose-lib": "3.3.12",
|
"web-auth/cose-lib": "^4.0",
|
||||||
"html2text/html2text": "^4.3"
|
"html2text/html2text": "^4.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^9.5"
|
"phpunit/phpunit": "^9.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
691
Core/External/composer.lock
generated
vendored
691
Core/External/composer.lock
generated
vendored
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
{% extends "base.twig" %}
|
{% extends "base.twig" %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>{{ site.name }} - {{ L("admin.admin") }}</title>
|
<title>{{ site.name }} - {{ L("admin.title") }}</title>
|
||||||
<link rel="stylesheet" href="/css/fontawesome.min.css" nonce="{{ site.csp.nonce }}">
|
<link rel="stylesheet" href="/css/fontawesome.min.css" nonce="{{ site.csp.nonce }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -397,7 +397,7 @@ zend.exception_string_param_max_len = 0
|
|||||||
; threat in any way, but it makes it possible to determine whether you use PHP
|
; threat in any way, but it makes it possible to determine whether you use PHP
|
||||||
; on your server or not.
|
; on your server or not.
|
||||||
; https://php.net/expose-php
|
; https://php.net/expose-php
|
||||||
expose_php = On
|
expose_php = Off
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;
|
||||||
; Resource Limits ;
|
; Resource Limits ;
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import {useCallback, useContext, useEffect, useState} from "react";
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {Link, useNavigate, useParams} from "react-router-dom";
|
import {Link, useNavigate, useParams} from "react-router-dom";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
import {CircularProgress} from "@material-ui/core";
|
import {Button, CircularProgress} from "@material-ui/core";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ColorPicker from "material-ui-color-picker";
|
import ColorPicker from "material-ui-color-picker";
|
||||||
|
import {ControlsColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import usePagination from "shared/hooks/pagination";
|
||||||
|
import {Delete, KeyboardArrowLeft, Save} from "@material-ui/icons";
|
||||||
|
import Dialog from "shared/elements/dialog";
|
||||||
|
import {Box, FormControl, FormGroup, FormLabel, styled, TextField} from "@mui/material";
|
||||||
|
|
||||||
const defaultGroupData = {
|
const defaultGroupData = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -11,6 +17,12 @@ const defaultGroupData = {
|
|||||||
members: []
|
members: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ButtonBar = styled(Box)((props) => ({
|
||||||
|
"& > button": {
|
||||||
|
marginRight: props.theme.spacing(1)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
export default function EditGroupView(props) {
|
export default function EditGroupView(props) {
|
||||||
|
|
||||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||||
@ -18,14 +30,30 @@ export default function EditGroupView(props) {
|
|||||||
const { groupId } = useParams();
|
const { groupId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isNewGroup = groupId === "new";
|
const isNewGroup = groupId === "new";
|
||||||
|
const pagination = usePagination();
|
||||||
|
const api = props.api;
|
||||||
|
|
||||||
|
// data
|
||||||
const [fetchGroup, setFetchGroup] = useState(!isNewGroup);
|
const [fetchGroup, setFetchGroup] = useState(!isNewGroup);
|
||||||
const [group, setGroup] = useState(isNewGroup ? defaultGroupData : null);
|
const [group, setGroup] = useState(isNewGroup ? defaultGroupData : null);
|
||||||
|
const [members, setMembers] = useState([]);
|
||||||
|
|
||||||
|
// ui
|
||||||
|
const [dialogData, setDialogData] = useState({open: false});
|
||||||
|
const [isSaving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
props.showDialog(data.msg, "Error fetching localization");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [currentLocale]);
|
||||||
|
|
||||||
const onFetchGroup = useCallback((force = false) => {
|
const onFetchGroup = useCallback((force = false) => {
|
||||||
if (force || fetchGroup) {
|
if (force || fetchGroup) {
|
||||||
setFetchGroup(false);
|
setFetchGroup(false);
|
||||||
props.api.getGroup(groupId).then(res => {
|
api.getGroup(groupId).then(res => {
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
props.showDialog(res.msg, "Error fetching group");
|
props.showDialog(res.msg, "Error fetching group");
|
||||||
navigate("/admin/groups");
|
navigate("/admin/groups");
|
||||||
@ -34,7 +62,51 @@ export default function EditGroupView(props) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, [api, fetchGroup]);
|
||||||
|
|
||||||
|
const onFetchMembers = useCallback(async (page, count, orderBy, sortOrder) => {
|
||||||
|
api.fetchGroupMembers(groupId, page, count, orderBy, sortOrder).then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
setMembers(res.users);
|
||||||
|
pagination.update(res.pagination);
|
||||||
|
} else {
|
||||||
|
props.showDialog(res.msg, "Error fetching group members");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [groupId, api, pagination]);
|
||||||
|
|
||||||
|
const onRemoveMember = useCallback(userId => {
|
||||||
|
api.removeGroupMember(groupId, userId).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
let newMembers = members.filter(u => u.id !== userId);
|
||||||
|
setMembers(newMembers);
|
||||||
|
} else {
|
||||||
|
props.showDialog(data.msg, "Error removing group member");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [api, groupId, members]);
|
||||||
|
|
||||||
|
const onSave = useCallback(() => {
|
||||||
|
setSaving(true);
|
||||||
|
if (isNewGroup) {
|
||||||
|
api.createGroup(group.name, group.color).then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
props.showDialog(data.msg, "Error creating group");
|
||||||
|
setSaving(false);
|
||||||
|
} else {
|
||||||
|
navigate(`/admin/groups/${data.id}`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
api.updateGroup(groupId, group.name, group.color).then(data => {
|
||||||
|
setSaving(false);
|
||||||
|
if (!data.success) {
|
||||||
|
props.showDialog(data.msg, "Error updating group");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [api, groupId, isNewGroup, group]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onFetchGroup();
|
onFetchGroup();
|
||||||
@ -65,19 +137,24 @@ export default function EditGroupView(props) {
|
|||||||
</div>
|
</div>
|
||||||
<div className={"content"}>
|
<div className={"content"}>
|
||||||
<div className={"row"}>
|
<div className={"row"}>
|
||||||
<div className={"col-6 pl-5 pr-5"}>
|
<div className={"col-4 pl-5 pr-5"}>
|
||||||
<form role={"form"} onSubmit={(e) => this.submitForm(e)}>
|
<FormGroup className={"my-2"}>
|
||||||
<div className={"form-group"}>
|
<FormLabel htmlFor={"name"}>
|
||||||
<label htmlFor={"name"}>{L("account.group_name")}</label>
|
{L("account.group_name")}
|
||||||
<input type={"text"} className={"form-control"} placeholder={"Name"}
|
</FormLabel>
|
||||||
name={"name"} id={"name"} maxLength={32} value={group.name}/>
|
<FormControl>
|
||||||
</div>
|
<TextField maxLength={32} value={group.name}
|
||||||
|
size={"small"} name={"name"}
|
||||||
|
placeholder={L("account.name")}
|
||||||
|
onChange={e => setGroup({...group, name: e.target.value})}/>
|
||||||
|
</FormControl>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<div className={"form-group"}>
|
<FormGroup className={"my-2"}>
|
||||||
<label htmlFor={"color"}>
|
<FormLabel htmlFor={"color"}>
|
||||||
{L("account.color")}
|
{L("account.color")}
|
||||||
</label>
|
</FormLabel>
|
||||||
<div>
|
<FormControl>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
value={group.color}
|
value={group.color}
|
||||||
size={"small"}
|
size={"small"}
|
||||||
@ -86,22 +163,71 @@ export default function EditGroupView(props) {
|
|||||||
floatingLabelText={group.color}
|
floatingLabelText={group.color}
|
||||||
onChange={color => setGroup({...group, color: color})}
|
onChange={color => setGroup({...group, color: color})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormControl>
|
||||||
</div>
|
</FormGroup>
|
||||||
|
|
||||||
<Link to={"/admin/groups"} className={"btn btn-info mt-2 mr-2"}>
|
<ButtonBar mt={2}>
|
||||||
{L("general.go_back")}
|
<Button startIcon={<KeyboardArrowLeft />}
|
||||||
</Link>
|
variant={"outlined"}
|
||||||
<button type={"submit"} className={"btn btn-primary mt-2"}>
|
onClick={() => navigate("/admin/groups")}>
|
||||||
{L("general.submit")}
|
{L("general.cancel")}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
<Button startIcon={<Save />} color={"primary"}
|
||||||
|
variant={"outlined"} disabled={isSaving}
|
||||||
|
onClick={onSave}>
|
||||||
|
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||||
|
</Button>
|
||||||
|
</ButtonBar>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isNewGroup && api.hasPermission("groups/getMembers") ?
|
||||||
|
<div className={"m-3"}>
|
||||||
<div className={"col-6"}>
|
<div className={"col-6"}>
|
||||||
<h3>{L("account.members")}</h3>
|
<DataTable
|
||||||
|
data={members}
|
||||||
|
pagination={pagination}
|
||||||
|
defaultSortOrder={"asc"}
|
||||||
|
defaultSortColumn={0}
|
||||||
|
className={"table table-striped"}
|
||||||
|
fetchData={onFetchMembers}
|
||||||
|
placeholder={L("No members in this group")}
|
||||||
|
title={L("account.members")}
|
||||||
|
columns={[
|
||||||
|
new NumericColumn(L("general.id"), "id"),
|
||||||
|
new StringColumn(L("account.name"), "name"),
|
||||||
|
new StringColumn(L("account.full_name"), "fullName"),
|
||||||
|
new ControlsColumn(L("general.controls"), [
|
||||||
|
{
|
||||||
|
label: L("general.edit"),
|
||||||
|
element: EditIcon,
|
||||||
|
onClick: (entry) => navigate(`/admin/user/${entry.id}`)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: L("general.remove"),
|
||||||
|
element: Delete,
|
||||||
|
disabled: !api.hasPermission("groups/removeMember"),
|
||||||
|
onClick: (entry) => setDialogData({
|
||||||
|
open: true,
|
||||||
|
title: L("Remove member"),
|
||||||
|
message: sprintf(L("Do you really want to remove user '%s' from this group?"), entry.fullName || entry.name),
|
||||||
|
onOption: (option) => option === 0 && onRemoveMember(entry.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<Dialog show={dialogData.open}
|
||||||
|
onClose={() => setDialogData({open: false})}
|
||||||
|
title={dialogData.title}
|
||||||
|
message={dialogData.message}
|
||||||
|
onOption={dialogData.onOption}
|
||||||
|
inputs={dialogData.inputs}
|
||||||
|
options={[L("general.ok"), L("general.cancel")]} />
|
||||||
</>
|
</>
|
||||||
|
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import {Link, useNavigate} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import {useCallback, useContext, useEffect, useState} from "react";
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
import {DataColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
import {ControlsColumn, DataColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||||
import {Button, IconButton} from "@material-ui/core";
|
import {Button, IconButton} from "@material-ui/core";
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
@ -28,7 +28,6 @@ export default function GroupListView(props) {
|
|||||||
}, [currentLocale]);
|
}, [currentLocale]);
|
||||||
|
|
||||||
const onFetchGroups = useCallback(async (page, count, orderBy, sortOrder) => {
|
const onFetchGroups = useCallback(async (page, count, orderBy, sortOrder) => {
|
||||||
|
|
||||||
api.fetchGroups(page, count, orderBy, sortOrder).then((res) => {
|
api.fetchGroups(page, count, orderBy, sortOrder).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setGroups(res.groups);
|
setGroups(res.groups);
|
||||||
@ -40,21 +39,13 @@ export default function GroupListView(props) {
|
|||||||
});
|
});
|
||||||
}, [api, pagination]);
|
}, [api, pagination]);
|
||||||
|
|
||||||
const actionColumn = (() => {
|
|
||||||
let column = new DataColumn(L("general.actions"), null, false);
|
|
||||||
column.renderData = (L, entry) => <>
|
|
||||||
<IconButton size={"small"} title={L("general.edit")} onClick={() => navigate("/admin/group/" + entry.id)}>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
return column;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const columnDefinitions = [
|
const columnDefinitions = [
|
||||||
new NumericColumn(L("general.id"), "id"),
|
new NumericColumn(L("general.id"), "id"),
|
||||||
new StringColumn(L("account.name"), "name"),
|
new StringColumn(L("account.name"), "name"),
|
||||||
new NumericColumn(L("account.member_count"), "memberCount"),
|
new NumericColumn(L("account.member_count"), "memberCount", { align: "center" }),
|
||||||
actionColumn,
|
new ControlsColumn(L("general.controls"), [
|
||||||
|
{ label: L("general.edit"), element: EditIcon, onClick: (entry) => navigate(`/admin/group/${entry.id}`) }
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
@ -153,14 +153,6 @@ export default class API {
|
|||||||
return this.apiCall("user/fetch", { page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder });
|
return this.apiCall("user/fetch", { page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder });
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchGroups(pageNum = 1, count = 20, orderBy = 'id', sortOrder = 'asc') {
|
|
||||||
return this.apiCall("groups/fetch", { page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder });
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGroup(id) {
|
|
||||||
return this.apiCall("groups/get", { id: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
async inviteUser(username, email) {
|
async inviteUser(username, email) {
|
||||||
return this.apiCall("user/invite", { username: username, email: email });
|
return this.apiCall("user/invite", { username: username, email: email });
|
||||||
}
|
}
|
||||||
@ -207,6 +199,39 @@ export default class API {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Groups API **/
|
||||||
|
async fetchGroups(pageNum = 1, count = 20, orderBy = 'id', sortOrder = 'asc') {
|
||||||
|
return this.apiCall("groups/fetch", { page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchGroupMembers(groupId, pageNum = 1, count = 20, orderBy = 'id', sortOrder = 'asc') {
|
||||||
|
return this.apiCall("groups/getMembers", { id: groupId, page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder });
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeGroupMember (groupId, userId) {
|
||||||
|
return this.apiCall("groups/removeMember", { id: groupId, userId: userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async addGroupMember (groupId, userId) {
|
||||||
|
return this.apiCall("groups/addMember", { id: groupId, userId: userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroup(id) {
|
||||||
|
return this.apiCall("groups/get", { id: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createGroup(name, color) {
|
||||||
|
return this.apiCall("groups/create", { name: name, color: color });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateGroup(id, name, color) {
|
||||||
|
return this.apiCall("groups/update", { id: id, name: name, color: color });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGroup(id) {
|
||||||
|
return this.apiCall("groups/delete", { id: id });
|
||||||
|
}
|
||||||
|
|
||||||
/** Stats **/
|
/** Stats **/
|
||||||
async getStats() {
|
async getStats() {
|
||||||
return this.apiCall("stats");
|
return this.apiCall("stats");
|
||||||
@ -249,15 +274,6 @@ export default class API {
|
|||||||
return this.apiCall("routes/update", { id, pattern, type, target, extra, exact, active });
|
return this.apiCall("routes/update", { id, pattern, type, target, extra, exact, active });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GroupAPI **/
|
|
||||||
async createGroup(name, color) {
|
|
||||||
return this.apiCall("groups/create", { name: name, color: color });
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteGroup(id) {
|
|
||||||
return this.apiCall("groups/delete", { id: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** SettingsAPI **/
|
/** SettingsAPI **/
|
||||||
async getSettings(key = "") {
|
async getSettings(key = "") {
|
||||||
return this.apiCall("settings/get", { key: key });
|
return this.apiCall("settings/get", { key: key });
|
||||||
|
Loading…
Reference in New Issue
Block a user