diff --git a/Core/API/GroupsAPI.class.php b/Core/API/GroupsAPI.class.php
index 3f8c7f4..9f4b7a0 100644
--- a/Core/API/GroupsAPI.class.php
+++ b/Core/API/GroupsAPI.class.php
@@ -54,11 +54,16 @@ namespace Core\API {
namespace Core\API\Groups {
use Core\API\GroupsAPI;
+ use Core\API\Parameter\ArrayType;
use Core\API\Parameter\Parameter;
use Core\API\Parameter\RegexType;
+ use Core\API\Parameter\StringType;
use Core\API\Traits\Pagination;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Condition\Compare;
+ use Core\Driver\SQL\Condition\CondIn;
+ use Core\Driver\SQL\Condition\CondLike;
+ use Core\Driver\SQL\Condition\CondNot;
use Core\Driver\SQL\Expression\Alias;
use Core\Driver\SQL\Expression\Count;
use Core\Driver\SQL\Join\InnerJoin;
@@ -118,6 +123,48 @@ namespace Core\API\Groups {
}
}
+ class Search extends GroupsAPI {
+ public function __construct(Context $context, bool $externalCall = false) {
+ parent::__construct($context, $externalCall, [
+ "query" => new StringType("query", -1, true, NULL),
+ "exclude" => new ArrayType("exclude", Parameter::TYPE_INT, true, true, [])
+ ]);
+ }
+
+ protected function _execute(): bool {
+ $sql = $this->context->getSQL();
+ $query = $this->getParam("query");
+ $exclude = array_unique($this->getParam("exclude"));
+
+ $groupsQuery = Group::createBuilder($sql, false)
+ ->limit(5);
+
+ if (!empty($query)) {
+ $groupsQuery->where(new CondLike(new Column("name"), "%$query%"));
+ }
+
+ if (!empty($exclude)) {
+ $groupsQuery->where(new CondNot(new CondIn(new Column("id"), $exclude)));
+ }
+
+ $groups = Group::findBy($groupsQuery);
+ if ($groups === false) {
+ return $this->createError($sql->getLastError());
+ }
+
+ $this->result["groups"] = $groups;
+ return true;
+ }
+
+ public static function getDescription(): string {
+ return "Returns a list of groups matching the search criteria";
+ }
+
+ public static function getDefaultPermittedGroups(): array {
+ return [Group::ADMIN, Group::SUPPORT];
+ }
+ }
+
class Get extends GroupsAPI {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
diff --git a/Core/API/UserAPI.class.php b/Core/API/UserAPI.class.php
index fc1a42f..94253b8 100644
--- a/Core/API/UserAPI.class.php
+++ b/Core/API/UserAPI.class.php
@@ -885,6 +885,7 @@ namespace Core\API\User {
return $this->createError("User not found");
}
+ $columnsToUpdate = [];
$username = $this->getParam("username");
$fullName = $this->getParam("fullName");
$email = $this->getParam("email");
@@ -892,10 +893,8 @@ namespace Core\API\User {
$groups = $this->getParam("groups");
$confirmed = $this->getParam("confirmed");
$active = $this->getParam("active");
-
$email = (!is_null($email) && empty($email)) ? null : $email;
- $groupIds = array();
if (!is_null($groups)) {
$groupIds = array_unique($groups);
if ($id === $currentUser->getId() && !in_array(Group::ADMIN, $groupIds)) {
@@ -910,6 +909,9 @@ namespace Core\API\User {
return $this->createError("Group with id=$groupId does not exist.");
}
}
+
+ $user->groups = $groupIds;
+ $columnsToUpdate[] = "groups";
}
// Check for duplicate username, email
@@ -922,7 +924,6 @@ namespace Core\API\User {
}
}
- $columnsToUpdate = [];
if ($usernameChanged) {
$user->name = $username;
$columnsToUpdate[] = "name";
@@ -961,18 +962,11 @@ namespace Core\API\User {
}
}
- if (empty($columnsToUpdate) || $user->save($sql, $columnsToUpdate)) {
-
- $deleteQuery = $sql->delete("UserGroup")->whereEq("user_id", $id);
- $insertQuery = $sql->insert("UserGroup", array("user_id", "group_id"));
-
- foreach ($groupIds as $groupId) {
- $insertQuery->addRow($id, $groupId);
- }
-
- $this->success = ($deleteQuery->execute() !== FALSE) && (empty($groupIds) || $insertQuery->execute() !== FALSE);
+ if (!empty($columnsToUpdate)) {
+ $this->success = $user->save($sql, $columnsToUpdate, in_array("groups", $columnsToUpdate)) !== FALSE;
$this->lastError = $sql->getLastError();
}
+
} else {
return $this->createError("Error fetching user details: " . $sql->getLastError());
}
@@ -1402,7 +1396,7 @@ namespace Core\API\User {
}
$oldPfp = $currentUser->getProfilePicture();
- if ($oldPfp) {
+ if ($oldPfp && preg_match("/[a-fA-F0-9-]+\.(jpg|jpeg|png|gif)/", $oldPfp)) {
$path = "$uploadDir/$oldPfp";
if (is_file($path)) {
@unlink($path);
@@ -1446,9 +1440,11 @@ namespace Core\API\User {
return $this->createError("Error updating user details: " . $sql->getLastError());
}
- $path = WEBROOT . "/img/uploads/user/$userId/$pfp";
- if (is_file($path)) {
- @unlink($path);
+ if (preg_match("/[a-fA-F0-9-]+\.(jpg|jpeg|png|gif)/", $pfp)) {
+ $path = WEBROOT . "/img/uploads/user/$userId/$pfp";
+ if (is_file($path)) {
+ @unlink($path);
+ }
}
return $this->success;
diff --git a/Core/Localization/de_DE/account.php b/Core/Localization/de_DE/account.php
index 7e6cb5d..5e7174d 100644
--- a/Core/Localization/de_DE/account.php
+++ b/Core/Localization/de_DE/account.php
@@ -57,6 +57,15 @@ return [
"no_members" => "Keine Mitglieder in dieser Gruppe",
"user_list_placeholder" => "Keine Benutzer zum Anzeigen",
+ # user edit page
+ "edit_user" => "Benutzer bearbeiten",
+ "new_user" => "Neuer Benutzer",
+ "send_invite" => "Einladung versenden",
+ "get_user_error" => "Fehler beim Holen des Benutzers",
+ "invite_user_error" => "Fehler beim Versenden der Einladung",
+ "create_user_error" => "Fehler beim Erstellen des Benutzers",
+ "save_user_error" => "Fehler beim Speichern des Benutzers",
+
# profile picture
"remove_picture" => "Profilbild entfernen",
"remove_picture_text" => "Möchten Sie wirklich Ihr aktuelles Profilbild entfernen?",
@@ -74,6 +83,7 @@ return [
"update_group_error" => "Error beim Aktualisieren der Gruppe",
"delete_group_error" => "Error beim Löschen der Gruppe",
"search_users_error" => "Fehler beim Suchen des Benutzers",
+ "search_groups_error" => "Fehler beim Suchen der Gruppen",
"delete_group_title" => "Gruppe löschen",
"delete_group_text" => "Möchten Sie diese Gruppe wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
"remove_group_member_title" => "Mitglied entfernen",
diff --git a/Core/Localization/en_US/account.php b/Core/Localization/en_US/account.php
index 31a8b8b..989668a 100644
--- a/Core/Localization/en_US/account.php
+++ b/Core/Localization/en_US/account.php
@@ -59,6 +59,15 @@ return [
"edit_profile" => "Edit Profile",
"user_list_placeholder" => "No users to display",
+ # user edit page
+ "edit_user" => "Edit User",
+ "new_user" => "New User",
+ "send_invite" => "Send Invitation",
+ "get_user_error" => "Error fetching user",
+ "invite_user_error" => "Error sending invitation",
+ "create_user_error" => "Error creating user",
+ "save_user_error" => "Error saving user",
+
# profile picture
"remove_picture" => "Remove profile picture",
"remove_picture_text" => "Do you really want to remove your current profile picture?",
@@ -76,6 +85,7 @@ return [
"update_group_error" => "Error updating group",
"delete_group_error" => "Error deleting group",
"search_users_error" => "Error searching users",
+ "search_groups_error" => "Error searching groups",
"delete_group_title" => "Delete Group",
"delete_group_text" => "Do you really want to delete this group? This action cannot be undone.",
"remove_group_member_title" => "Remove member",
diff --git a/react/admin-panel/src/elements/sidebar.js b/react/admin-panel/src/elements/sidebar.js
index bb32543..e9114ff 100644
--- a/react/admin-panel/src/elements/sidebar.js
+++ b/react/admin-panel/src/elements/sidebar.js
@@ -76,7 +76,7 @@ const StyledDrawer = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'ope
export default function Sidebar(props) {
- const {api, showDialog, theme, children, ...other} = props;
+ const {api, showDialog, hideDialog, theme, info, children, ...other} = props;
const {translate: L, currentLocale, setLanguageByCode} = useContext(LocaleContext);
const [languages, setLanguages] = useState(null);
diff --git a/react/admin-panel/src/views/user/user-edit.js b/react/admin-panel/src/views/user/user-edit.js
index aa0ba31..81e3ed3 100644
--- a/react/admin-panel/src/views/user/user-edit.js
+++ b/react/admin-panel/src/views/user/user-edit.js
@@ -9,7 +9,7 @@ import {
FormControlLabel,
FormLabel, Grid,
TextField,
- FormGroup as MuiFormGroup
+ FormGroup as MuiFormGroup, Autocomplete, Chip
} from "@mui/material";
import {LocaleContext} from "shared/locale";
import * as React from "react";
@@ -42,6 +42,8 @@ export default function UserEditView(props) {
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
const [fetchUser, setFetchUser] = useState(!isNewUser);
const [user, setUser] = useState(isNewUser ? initialUser : null);
+ const [groups, setGroups] = useState([]);
+ const [groupInput, setGroupInput] = useState("");
// ui
const [hasChanged, setChanged] = useState(isNewUser);
@@ -56,17 +58,27 @@ export default function UserEditView(props) {
});
}, [currentLocale]);
+ const onFetchGroups = useCallback(() => {
+ api.searchGroups(groupInput, user?.groups?.map(group => group.id)).then((res) => {
+ if (res.success) {
+ setGroups(res.groups);
+ } else {
+ showDialog(res.msg, L("account.search_groups_error"));
+ }
+ });
+ }, [api, showDialog, user?.groups, groupInput]);
+
const onFetchUser = useCallback((force = false) => {
if (!isNewUser && (force || fetchUser)) {
setFetchUser(false);
api.getUser(userId).then((res) => {
if (!res.success) {
- showDialog(res.msg, L("account.error_user_get"));
+ showDialog(res.msg, L("account.get_user_error"));
if (user === null) {
navigate("/admin/users");
}
} else {
- setUser(res.user);
+ setUser({...res.user, groups: Object.values(res.user.groups)});
}
});
}
@@ -77,15 +89,17 @@ export default function UserEditView(props) {
setUser({...initialUser});
} else {
onFetchUser(true);
+ setChanged(false);
}
}, [isNewUser, onFetchUser]);
const onSaveUser = useCallback(() => {
if (!isSaving) {
+ let groupIds = user.groups.map(group => group.id);
setSaving(true);
if (isNewUser) {
if (sendInvite) {
- api.inviteUser(user.name, user.fullName, user.email).then(res => {
+ api.inviteUser(user.name, user.fullName, user.email, groupIds).then(res => {
setSaving(false);
if (res.success) {
setChanged(false);
@@ -95,7 +109,9 @@ export default function UserEditView(props) {
}
});
} else {
- api.createUser(user.name, user.fullName, user.email, user.password, user.passwordConfirm).then(res => {
+ api.createUser(user.name, user.fullName, user.email, groupIds,
+ user.password, user.passwordConfirm
+ ).then(res => {
setSaving(false);
if (res.success) {
setChanged(false);
@@ -108,7 +124,7 @@ export default function UserEditView(props) {
} else {
api.editUser(
userId, user.name, user.email, user.password,
- user.groups, user.confirmed, user.active
+ groupIds, user.confirmed, user.active
).then(res => {
setSaving(false);
if (res.success) {
@@ -120,7 +136,7 @@ export default function UserEditView(props) {
}
}
- }, [isSaving, sendInvite, isNewUser, userId, showDialog]);
+ }, [isSaving, sendInvite, isNewUser, userId, showDialog, user]);
const onChangeValue = useCallback((name, value) => {
setUser({...user, [name]: value});
@@ -133,6 +149,10 @@ export default function UserEditView(props) {
}
}, []);
+ useEffect(() => {
+ onFetchGroups();
+ }, [groupInput, user?.groups]);
+
if (user === null) {
return
}
@@ -140,7 +160,7 @@ export default function UserEditView(props) {
return Home,
User,
- {isNewUser ? "New" : "Edit"}
+ {isNewUser ? L("general.new") : L("general.edit")}
]}>
@@ -149,7 +169,7 @@ export default function UserEditView(props) {
setUser({...user, name: e.target.value})} />
+ onChange={e => onChangeValue("name", e.target.value)} />
@@ -157,18 +177,44 @@ export default function UserEditView(props) {
setUser({...user, fullName: e.target.value})} />
+ onChange={e => onChangeValue("fullName", e.target.value)} />
{L("account.email")}
setUser({...user, email: e.target.value})} />
+ onChange={e => onChangeValue("email", e.target.value)} />
+
+ {L("account.groups")}
+ group.name}
+ getOptionKey={group => group.id}
+ filterOptions={(options) => options}
+ clearOnBlur={true}
+ clearOnEscape
+ freeSolo
+ multiple
+ value={user.groups}
+ inputValue={groupInput}
+ onChange={(e, v) => onChangeValue("groups", v)}
+ onInputChange={e => setGroupInput((!e || e.target.value === 0) ? "" : e.target.value) }
+ renderTags={(values, props) =>
+ values.map((option, index) => {
+ return
+ })
+ }
+ renderInput={(params) => setGroupInput("")} />}
+ />
+
{ !isNewUser ?
<>
@@ -178,7 +224,7 @@ export default function UserEditView(props) {
value={user.password}
type={"password"}
placeholder={"(" + L("general.unchanged") + ")"}
- onChange={e => setUser({...user, password: e.target.value})} />
+ onChange={e => onChangeValue("password", e.target.value)} />
@@ -210,7 +256,7 @@ export default function UserEditView(props) {
setUser({...user, password: e.target.value})} />
+ onChange={e => onChangeValue("password", e.target.value)} />
@@ -219,7 +265,7 @@ export default function UserEditView(props) {
setUser({...user, passwordConfirm: e.target.value})} />
+ onChange={e => onChangeValue("passwordConfirm", e.target.value)} />
diff --git a/react/shared/api.js b/react/shared/api.js
index 865957f..d236616 100644
--- a/react/shared/api.js
+++ b/react/shared/api.js
@@ -159,13 +159,13 @@ export default class API {
return this.apiCall("user/fetch", { page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder });
}
- async inviteUser(username, fullName, email) {
- return this.apiCall("user/invite", { username: username, fullName: fullName, email: email });
+ async inviteUser(username, fullName, email, groups) {
+ return this.apiCall("user/invite", { username: username, fullName: fullName, email: email, groups: groups });
}
- async createUser(username, fullName, email, password, confirmPassword) {
+ async createUser(username, fullName, email, groups, password, confirmPassword) {
return this.apiCall("user/create", { username: username, email: email,
- fullName: fullName,
+ fullName: fullName, groups: groups,
password: password, confirmPassword: confirmPassword
});
}
@@ -219,6 +219,10 @@ export default class API {
return this.apiCall("groups/fetch", { page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder });
}
+ async searchGroups(query = "", exclude = []) {
+ return this.apiCall("groups/search", { query: query, exclude: exclude });
+ }
+
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 });
}