diff --git a/Core/API/Parameter/ArrayType.class.php b/Core/API/Parameter/ArrayType.class.php
index d55b154..3eeee0b 100644
--- a/Core/API/Parameter/ArrayType.class.php
+++ b/Core/API/Parameter/ArrayType.class.php
@@ -16,14 +16,16 @@ class ArrayType extends Parameter {
* @param bool $optional true if the parameter is optional
* @param array|null $defaultValue the default value to use, if the parameter is not given
*/
- public function __construct(string $name, int $elementType = Parameter::TYPE_MIXED, bool $canBeOne = false, bool $optional = FALSE, ?array $defaultValue = NULL) {
+ public function __construct(string $name, int $elementType = Parameter::TYPE_MIXED, bool $canBeOne = false,
+ bool $optional = FALSE, ?array $defaultValue = NULL, ?array $choices = NULL) {
$this->elementType = $elementType;
$this->elementParameter = new Parameter('', $elementType);
$this->canBeOne = $canBeOne;
- parent::__construct($name, Parameter::TYPE_ARRAY, $optional, $defaultValue);
+ parent::__construct($name, Parameter::TYPE_ARRAY, $optional, $defaultValue, $choices);
}
public function parseParam($value): bool {
+
if (!is_array($value)) {
if (!$this->canBeOne) {
return false;
@@ -42,6 +44,14 @@ class ArrayType extends Parameter {
}
}
+ if (!is_null($this->choices)) {
+ foreach ($value as $element) {
+ if (!in_array($element, $this->choices)) {
+ return false;
+ }
+ }
+ }
+
$this->value = $value;
return true;
}
diff --git a/Core/API/UserAPI.class.php b/Core/API/UserAPI.class.php
index 5a26c4f..fc1a42f 100644
--- a/Core/API/UserAPI.class.php
+++ b/Core/API/UserAPI.class.php
@@ -2,8 +2,11 @@
namespace Core\API {
+ use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Condition\Compare;
+ use Core\Driver\SQL\Condition\CondIn;
use Core\Objects\Context;
+ use Core\Objects\DatabaseEntity\Group;
use Core\Objects\DatabaseEntity\Language;
use Core\Objects\DatabaseEntity\User;
use Core\Objects\DatabaseEntity\UserToken;
@@ -75,6 +78,26 @@ namespace Core\API {
$this->checkPasswordRequirements($password, $confirmPassword);
}
+ protected function checkGroups(array &$groups): bool {
+ $sql = $this->context->getSQL();
+ $currentUser = $this->context->getUser();
+ $requestedGroups = array_unique($this->getParam("groups"));
+ if (!empty($requestedGroups)) {
+ $availableGroups = Group::findAll($sql, new CondIn(new Column("id"), $requestedGroups));
+ foreach ($requestedGroups as $groupId) {
+ if (!isset($availableGroups[$groupId])) {
+ return $this->createError("Group with id=$groupId does not exist.");
+ } else if ($this->isExternalCall() && $groupId === Group::ADMIN && !$currentUser->hasGroup(Group::ADMIN)) {
+ return $this->createError("You cannot create users with administrator groups.");
+ } else {
+ $groups[] = $groupId;
+ }
+ }
+ }
+
+ return true;
+ }
+
protected function insertUser(string $username, ?string $email, string $password, bool $confirmed, string $fullName = "", array $groups = []): bool|User {
$sql = $this->context->getSQL();
@@ -153,6 +176,7 @@ namespace Core\API\User {
public function __construct(Context $context, $externalCall = false) {
parent::__construct($context, $externalCall, array(
'username' => new StringType('username', 32),
+ 'fullName' => new StringType('fullName', 64, true, ""),
'email' => new Parameter('email', Parameter::TYPE_EMAIL, true, NULL),
'password' => new StringType('password'),
'confirmPassword' => new StringType('confirmPassword'),
@@ -165,6 +189,7 @@ namespace Core\API\User {
public function _execute(): bool {
$username = $this->getParam('username');
+ $fullName = $this->getParam('fullName');
$email = $this->getParam('email');
$password = $this->getParam('password');
$confirmPassword = $this->getParam('confirmPassword');
@@ -172,31 +197,18 @@ namespace Core\API\User {
return false;
}
+ $groups = [];
+ if (!$this->checkGroups($groups)) {
+ return false;
+ }
+
if (!$this->checkUserExists($username, $email)) {
return false;
}
- $groups = [];
- $sql = $this->context->getSQL();
- $currentUser = $this->context->getUser();
-
- $requestedGroups = array_unique($this->getParam("groups"));
- if (!empty($requestedGroups)) {
- $availableGroups = Group::findAll($sql, new CondIn(new Column("id"), $requestedGroups));
- foreach ($requestedGroups as $groupId) {
- if (!isset($availableGroups[$groupId])) {
- return $this->createError("Group with id=$groupId does not exist.");
- } else if ($this->isExternalCall() && $groupId === Group::ADMIN && !$currentUser->hasGroup(Group::ADMIN)) {
- return $this->createError("You cannot create users with administrator groups.");
- } else {
- $groups[] = $groupId;
- }
- }
- }
-
// prevent duplicate keys
$email = (!is_null($email) && empty($email)) ? null : $email;
- $user = $this->insertUser($username, $email, $password, true, "", $groups);
+ $user = $this->insertUser($username, $email, $password, true, $fullName, $groups);
if ($user !== false) {
$this->user = $user;
$this->result["userId"] = $user->getId();
@@ -432,7 +444,9 @@ namespace Core\API\User {
public function __construct(Context $context, $externalCall = false) {
parent::__construct($context, $externalCall, array(
'username' => new StringType('username', 32),
+ 'fullName' => new StringType('fullName', 64, true, ""),
'email' => new StringType('email', 64),
+ 'groups' => new ArrayType("groups", Parameter::TYPE_INT, true, true, [])
));
$this->loginRequired = true;
@@ -440,24 +454,38 @@ namespace Core\API\User {
public function _execute(): bool {
+ $sql = $this->context->getSQL();
+ $settings = $this->context->getSettings();
+ $currentUser = $this->context->getUser();
+ if (!$settings->isMailEnabled()) {
+ return $this->createError("An invitation cannot be sent because mailing is not enabled.");
+ }
+
$username = $this->getParam('username');
+ $fullName = $this->getParam('fullName');
$email = $this->getParam('email');
+ $groups = [];
+
+ if (!$this->checkGroups($groups)) {
+ return false;
+ }
+
if (!$this->checkUserExists($username, $email)) {
return false;
}
// Create user
- $user = $this->insertUser($username, $email, "", false);
+ $user = $this->insertUser($username, $email, "", false, $fullName, $groups);
if ($user === false) {
return false;
}
+ $this->result["userId"] = $user->getId();
$this->logger->info("A new user with username='$username' and email='$email' was invited by " . $this->logUserId());
// Create Token
$token = generateRandomString(36);
$validDays = 7;
- $sql = $this->context->getSQL();
$userToken = new UserToken($user, $token, UserToken::TYPE_INVITE, $validDays * 24);
if ($userToken->save($sql)) {
diff --git a/react/admin-panel/src/elements/sidebar.js b/react/admin-panel/src/elements/sidebar.js
index b6a6a0c..bb32543 100644
--- a/react/admin-panel/src/elements/sidebar.js
+++ b/react/admin-panel/src/elements/sidebar.js
@@ -10,7 +10,6 @@ import {
import { Dropdown } from '@mui/base/Dropdown';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
-import ProfilePicture from "shared/elements/profile-picture";
import {Dns, Groups, People, QueryStats, Security, Settings, Route, ArrowBack, Translate} from "@mui/icons-material";
import useCurrentPath from "shared/hooks/current-path";
import ProfileLink from "./profile-link";
diff --git a/react/admin-panel/src/views/profile/edit-picture.js b/react/admin-panel/src/views/profile/edit-picture.js
index b15e28c..c3541af 100644
--- a/react/admin-panel/src/views/profile/edit-picture.js
+++ b/react/admin-panel/src/views/profile/edit-picture.js
@@ -107,7 +107,7 @@ export default function EditProfilePicture(props) {
if (croppedSize < 150) {
setImage({ loading: false, file: null, data: null });
- showDialog(L("account.profile_picture_invalid_dimensions"), L("general.error"));
+ showDialog(L("account.profile_picture_invalid_dimensions"), L("general.error_occurred"));
} else {
setImage({ loading: false, file: file, data: imageData });
}
diff --git a/react/admin-panel/src/views/user/user-edit.js b/react/admin-panel/src/views/user/user-edit.js
index 0ab6b86..aa0ba31 100644
--- a/react/admin-panel/src/views/user/user-edit.js
+++ b/react/admin-panel/src/views/user/user-edit.js
@@ -7,17 +7,28 @@ import {
CircularProgress,
FormControl,
FormControlLabel,
- FormLabel,
- TextField
+ FormLabel, Grid,
+ TextField,
+ FormGroup as MuiFormGroup
} from "@mui/material";
import {LocaleContext} from "shared/locale";
import * as React from "react";
import ViewContent from "../../elements/view-content";
import FormGroup from "../../elements/form-group";
import ButtonBar from "../../elements/button-bar";
-import {RestartAlt, Save} from "@mui/icons-material";
-import {parseBool} from "shared/util";
-import SpacedFormGroup from "../../elements/form-group";
+import {RestartAlt, Save, Send} from "@mui/icons-material";
+import PasswordStrength from "shared/elements/password-strength";
+
+const initialUser = {
+ name: "",
+ fullName: "",
+ email: "",
+ password: "",
+ passwordConfirm: "",
+ groups: [],
+ confirmed: false,
+ active: true,
+};
export default function UserEditView(props) {
@@ -30,19 +41,12 @@ export default function UserEditView(props) {
const isNewUser = userId === "new";
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
const [fetchUser, setFetchUser] = useState(!isNewUser);
- const [user, setUser] = useState(isNewUser ? {
- name: "",
- fullName: "",
- email: "",
- password: "",
- groups: [],
- confirmed: false,
- active: true,
- } : null);
+ const [user, setUser] = useState(isNewUser ? initialUser : null);
// ui
const [hasChanged, setChanged] = useState(isNewUser);
const [isSaving, setSaving] = useState(false);
+ const [sendInvite, setSetInvite] = useState(isNewUser);
useEffect(() => {
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
@@ -52,14 +56,6 @@ export default function UserEditView(props) {
});
}, [currentLocale]);
- const onReset = useCallback(() => {
-
- }, []);
-
- const onSaveUser = useCallback(() => {
-
- }, []);
-
const onFetchUser = useCallback((force = false) => {
if (!isNewUser && (force || fetchUser)) {
setFetchUser(false);
@@ -76,9 +72,60 @@ export default function UserEditView(props) {
}
}, [api, showDialog, fetchUser, isNewUser, userId, user]);
- const onChangeValue = useCallback((name, value) => {
+ const onReset = useCallback(() => {
+ if (isNewUser) {
+ setUser({...initialUser});
+ } else {
+ onFetchUser(true);
+ }
+ }, [isNewUser, onFetchUser]);
- }, []);
+ const onSaveUser = useCallback(() => {
+ if (!isSaving) {
+ setSaving(true);
+ if (isNewUser) {
+ if (sendInvite) {
+ api.inviteUser(user.name, user.fullName, user.email).then(res => {
+ setSaving(false);
+ if (res.success) {
+ setChanged(false);
+ navigate("/admin/user/" + res.userId);
+ } else {
+ showDialog(res.msg, L("account.invite_user_error"));
+ }
+ });
+ } else {
+ api.createUser(user.name, user.fullName, user.email, user.password, user.passwordConfirm).then(res => {
+ setSaving(false);
+ if (res.success) {
+ setChanged(false);
+ navigate("/admin/user/" + res.userId);
+ } else {
+ showDialog(res.msg, L("account.create_user_error"));
+ }
+ });
+ }
+ } else {
+ api.editUser(
+ userId, user.name, user.email, user.password,
+ user.groups, user.confirmed, user.active
+ ).then(res => {
+ setSaving(false);
+ if (res.success) {
+ setChanged(false);
+ } else {
+ showDialog(res.msg, L("account.save_user_error"));
+ }
+ });
+ }
+ }
+
+ }, [isSaving, sendInvite, isNewUser, userId, showDialog]);
+
+ const onChangeValue = useCallback((name, value) => {
+ setUser({...user, [name]: value});
+ setChanged(true);
+ }, [user]);
useEffect(() => {
if (!isNewUser) {
@@ -95,7 +142,8 @@ export default function UserEditView(props) {
User,
{isNewUser ? "New" : "Edit"}
]}>
-
+
+
{L("account.name")}
@@ -124,12 +172,22 @@ export default function UserEditView(props) {
{ !isNewUser ?
<>
+ {L("account.password")}
+
+ setUser({...user, password: e.target.value})} />
+
+
+
onChangeValue("active", v)} />}
label={L("account.active")} />
-
+
> : <>
-
-
+
+ setSetInvite(v)} />}
+ label={L("account.send_invite")} />
+
+ {!sendInvite && <>
+
+ {L("account.password")}
+
+ setUser({...user, password: e.target.value})} />
+
+
+
+ {L("account.password_confirm")}
+
+ setUser({...user, passwordConfirm: e.target.value})} />
+
+
+
+
+
+ >
+ }
>
}
-
+
+
: }
+ startIcon={isSaving ?
+ :
+ (sendInvite ? : )}
variant={"outlined"} title={L(hasChanged ? "general.unsaved_changes" : "general.save")}>
- {isSaving ? L("general.saving") + "…" : (L("general.save") + (hasChanged ? " *" : ""))}
+ {isSaving ?
+ L(sendInvite ? "general.sending" : "general.saving") + "…" :
+ (L(sendInvite ? "general.send" : "general.save") + (hasChanged ? " *" : ""))}