From 3a5c203664d805e3bf12b8becadb7b4cbc857316 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Tue, 2 Jun 2026 09:37:04 +0200 Subject: [PATCH] allow admin to see all users even not self created, admin can delete admin accounts --- csharp/App/Backend/Controller.cs | 14 +++++++++++++ .../App/Backend/DataTypes/Methods/Session.cs | 21 ++++++++++++------- csharp/App/Backend/Database/Delete.cs | 10 +++++++++ .../content/dashboards/Users/UsersSearch.tsx | 14 ++++++------- .../src/contexts/AccessContextProvider.tsx | 16 ++++++++++++++ 5 files changed, 61 insertions(+), 14 deletions(-) diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index b1ea257af..fc73909dc 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -498,6 +498,20 @@ public class Controller : ControllerBase .ToList(); } + [HttpGet(nameof(GetAllUsers))] + public ActionResult> GetAllUsers(Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user == null) + return Unauthorized(); + if (user.UserType != 2) // admins only + return Unauthorized(); + + return Db.Users + .Select(u => u.HidePassword()) + .ToList(); + } + [HttpGet(nameof(GetAllInstallationsFromProduct))] public ActionResult> GetAllInstallationsFromProduct(int product,Token authToken) diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index b57b7ce13..5a3edd808 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -367,12 +367,17 @@ public static class SessionMethods { var sessionUser = session?.User; var originalUser = Db.GetUserById(editedUser?.Id); - - return editedUser is not null - && sessionUser is not null - && originalUser is not null - && sessionUser.UserType !=0 - && sessionUser.HasAccessTo(originalUser) + + if (editedUser is null || sessionUser is null || originalUser is null) + return false; + + // email must stay unique; pre-check to avoid hitting the DB [Unique] constraint (500) + var emailOwner = Db.GetUserByEmail(editedUser.Email); + + return sessionUser.UserType != 0 + && originalUser.Id != 0 // never edit the root user + && (sessionUser.UserType == 2 || sessionUser.HasAccessTo(originalUser)) // admins may edit any user + && (emailOwner is null || emailOwner.Id == editedUser.Id) // email not taken by another user && editedUser .WithParentOf(originalUser) // prevent moving .WithPasswordOf(originalUser) @@ -397,7 +402,9 @@ public static class SessionMethods return sessionUser is not null && userToDelete is not null && sessionUser.UserType !=0 - && sessionUser.HasAccessTo(userToDelete) + && userToDelete.Id != 0 // never delete the root user + && userToDelete.Id != sessionUser.Id // never self-delete (avoid lockout) + && (sessionUser.UserType == 2 || sessionUser.HasAccessTo(userToDelete)) // admins may delete any user && Db.Delete(userToDelete); } diff --git a/csharp/App/Backend/Database/Delete.cs b/csharp/App/Backend/Database/Delete.cs index a2f06a600..d80b05dc0 100644 --- a/csharp/App/Backend/Database/Delete.cs +++ b/csharp/App/Backend/Database/Delete.cs @@ -164,8 +164,18 @@ public static partial class Db Boolean DeleteUserAndHisDependencies() { + // Re-parent the deleted user's children up to its own parent so no subtree is orphaned + // (a dangling ParentId would make the children invisible/unmanageable in the tree). + var children = Users.Where(u => u.ParentId == user.Id).ToList(); + foreach (var child in children) + { + child.ParentId = user.ParentId; + Connection.Update(child); + } + FolderAccess .Delete(u => u.UserId == user.Id); InstallationAccess.Delete(u => u.UserId == user.Id); + Sessions .Delete(s => s.UserId == user.Id); // kill the deleted user's login sessions immediately return Users.Delete(u => u.Id == user.Id) > 0; } } diff --git a/typescript/frontend-marios2/src/content/dashboards/Users/UsersSearch.tsx b/typescript/frontend-marios2/src/content/dashboards/Users/UsersSearch.tsx index 1f9666c3b..978c00deb 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Users/UsersSearch.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Users/UsersSearch.tsx @@ -20,8 +20,8 @@ import { UserType } from '../../../interfaces/UserTypes'; function UsersSearch() { const intl = useIntl(); const [searchTerm, setSearchTerm] = useState(''); - const { availableUsers, fetchAvailableUsers } = useContext(AccessContext); - const [filteredData, setFilteredData] = useState(availableUsers); + const { allUsers, fetchAllUsers } = useContext(AccessContext); + const [filteredData, setFilteredData] = useState(allUsers); const [openModal, setOpenModal] = useState(false); const context = useContext(UserContext); const [userCreated, setUserCreated] = useState(false); @@ -29,19 +29,19 @@ function UsersSearch() { const { currentUser } = context; useEffect(() => { - fetchAvailableUsers(); + fetchAllUsers(); }, []); const fetchDataAgain = () => { - fetchAvailableUsers(); + fetchAllUsers(); }; useEffect(() => { - const filtered = availableUsers.filter((item) => + const filtered = allUsers.filter((item) => item.name.toLowerCase().includes(searchTerm.toLowerCase()) ); setFilteredData(filtered); - }, [searchTerm, availableUsers]); + }, [searchTerm, allUsers]); const handleSubmit = () => { setOpenModal(true); @@ -50,7 +50,7 @@ function UsersSearch() { setOpenModal(false); setUserCreated(true); - fetchAvailableUsers(); + fetchAllUsers(); setTimeout(() => { setUserCreated(false); diff --git a/typescript/frontend-marios2/src/contexts/AccessContextProvider.tsx b/typescript/frontend-marios2/src/contexts/AccessContextProvider.tsx index c7a58c1b6..da7318baa 100644 --- a/typescript/frontend-marios2/src/contexts/AccessContextProvider.tsx +++ b/typescript/frontend-marios2/src/contexts/AccessContextProvider.tsx @@ -19,6 +19,8 @@ interface AccessContextProviderProps { accessibleInstallationsForUser: I_Installation[]; availableUsers: InnovEnergyUser[]; fetchAvailableUsers: () => Promise; + allUsers: InnovEnergyUser[]; + fetchAllUsers: () => Promise; usersWithDirectAccess: InnovEnergyUser[]; fetchUsersWithDirectAccessForResource: ( tempresourceType: string, @@ -53,6 +55,10 @@ export const AccessContext = createContext({ fetchAvailableUsers: () => { return Promise.resolve(); }, + allUsers: [], + fetchAllUsers: () => { + return Promise.resolve(); + }, usersWithDirectAccess: [], fetchUsersWithDirectAccessForResource: () => Promise.resolve(), usersWithInheritedAccess: [], @@ -80,6 +86,7 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => { InnovEnergyUser[] >([]); const [availableUsers, setAvailableUsers] = useState([]); + const [allUsers, setAllUsers] = useState([]); const [accessibleInstallationsForUser, setAccessibleInstallationsForUser] = useState([]); @@ -141,6 +148,13 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => { }); }; + // Admin-only: every user in the system (for the Users management page). + const fetchAllUsers = async (): Promise => { + return axiosConfig.get('/GetAllUsers').then((res) => { + setAllUsers(res.data); + }); + }; + const RevokeAccessFromResource = useCallback( async ( resourceType: string, @@ -187,6 +201,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => { accessibleInstallationsForUser, availableUsers, fetchAvailableUsers, + allUsers, + fetchAllUsers, usersWithDirectAccess, fetchUsersWithDirectAccessForResource, usersWithInheritedAccess,