From e73ea120e3ef214bcfbf1b4be900b3ad71415ada Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 11 Sep 2026 11:28:07 -0400 Subject: [PATCH] add server side filter for server admin --- messages/en-US.json | 14 ++-- server/routers/user/adminListUsers.ts | 16 ++++- server/routers/user/adminSetServerAdmin.ts | 19 +++-- src/components/AdminUsersTable.tsx | 84 +++++++++++++++------- 4 files changed, 95 insertions(+), 38 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index c6c8f6820..8398bca71 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -527,7 +527,7 @@ "userAbount": "About User Management", "userAbountDescription": "This table displays all base user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their base user object. They will remain in the system. To completely remove a user from the system, you must delete their base user object using the delete action in this table.", "userServer": "Server Users", - "userSearch": "Search server users...", + "userSearch": "Search users...", "userErrorDelete": "Error deleting user", "userDeleteConfirm": "Confirm Delete User", "userDeleteServer": "Delete User from Server", @@ -1429,17 +1429,17 @@ "promoteServerAdmin": "Promote to Server admin", "promoteServerAdminTitle": "Promote to Server Admin", "promoteServerAdminQuestion": "Are you sure you want to promote {selectedUser} to server admin?", - "promoteServerAdminMessage": "Server admins have full access to every organization, user, and setting on this instance.", - "promoteServerAdminWarning": "This can be undone at any time by demoting the user from this page.", - "promoteServerAdminConfirm": "Promote to server admin", - "promoteServerAdminSuccess": "User promoted", + "promoteServerAdminMessage": "Server admins have the highest privileges and can manage the server.", + "promoteServerAdminWarning": "This can be undone at any time by demoting the user.", + "promoteServerAdminConfirm": "Promote to Server Admin", + "promoteServerAdminSuccess": "User Promoted", "promoteServerAdminSuccessDescription": "{selectedUser} is now a server admin.", "promoteServerAdminError": "Failed to promote user", "demoteServerAdmin": "Demote from Server admin", "demoteServerAdminTitle": "Demote from Server Admin", "demoteServerAdminQuestion": "Are you sure you want to demote {selectedUser} from server admin?", - "demoteServerAdminMessage": "{selectedUser} will lose full access to every organization, user, and setting on this instance.", - "demoteServerAdminWarning": "This can be undone at any time by promoting the user from this page.", + "demoteServerAdminMessage": "{selectedUser} will lose all server admin privileges.", + "demoteServerAdminWarning": "This can be undone at any time by promoting the user.", "demoteServerAdminConfirm": "Demote from server admin", "demoteServerAdminSuccess": "User demoted", "demoteServerAdminSuccessDescription": "{selectedUser} is no longer a server admin.", diff --git a/server/routers/user/adminListUsers.ts b/server/routers/user/adminListUsers.ts index 0a5bad705..4681e79bb 100644 --- a/server/routers/user/adminListUsers.ts +++ b/server/routers/user/adminListUsers.ts @@ -88,6 +88,15 @@ const listUsersSchema = z.strictObject({ type: "boolean", description: "Filter by 2FA state matching: enabled if twoFactorEnabled or twoFactorSetupRequested" + }), + server_admin: z + .enum(["true", "false"]) + .transform((v) => v === "true") + .optional() + .catch(undefined) + .openapi({ + type: "boolean", + description: "Filter by server admin status" }) }); @@ -177,7 +186,8 @@ export async function adminListUsers( sort_by, order, idp_id, - two_factor: twoFactorFilter + two_factor: twoFactorFilter, + server_admin: serverAdminFilter } = parsedQuery.data; if (typeof idp_id === "number") { @@ -233,6 +243,10 @@ export async function adminListUsers( } } + if (typeof serverAdminFilter === "boolean") { + conditions.push(eq(users.serverAdmin, serverAdminFilter)); + } + const whereClause = and(...conditions); const countQuery = db.$count( diff --git a/server/routers/user/adminSetServerAdmin.ts b/server/routers/user/adminSetServerAdmin.ts index 1cc90a084..1fe192b7b 100644 --- a/server/routers/user/adminSetServerAdmin.ts +++ b/server/routers/user/adminSetServerAdmin.ts @@ -31,7 +31,8 @@ const AdminSetServerAdminResponseDataSchema = z.object({ registry.registerPath({ method: "post", path: "/user/{userId}/server-admin", - description: "Promote or demote a user's server admin status (server admin).", + description: + "Promote or demote a user's server admin status (server admin).", tags: [OpenAPITags.User], request: { params: setServerAdminParamsSchema, @@ -63,9 +64,7 @@ export async function adminSetServerAdmin( next: NextFunction ): Promise { try { - const parsedParams = setServerAdminParamsSchema.safeParse( - req.params - ); + const parsedParams = setServerAdminParamsSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( @@ -91,7 +90,8 @@ export async function adminSetServerAdmin( const [existingUser] = await db .select({ userId: users.userId, - serverAdmin: users.serverAdmin + serverAdmin: users.serverAdmin, + type: users.type }) .from(users) .where(eq(users.userId, userId)) @@ -101,6 +101,15 @@ export async function adminSetServerAdmin( return next(createHttpError(HttpCode.NOT_FOUND, "User not found")); } + if (existingUser.type !== "internal") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Server admin status can only be changed for internal users" + ) + ); + } + if (!serverAdmin && req.user?.userId === userId) { return next( createHttpError( diff --git a/src/components/AdminUsersTable.tsx b/src/components/AdminUsersTable.tsx index 8bb141487..34afe463d 100644 --- a/src/components/AdminUsersTable.tsx +++ b/src/components/AdminUsersTable.tsx @@ -44,7 +44,6 @@ import { CredenzaClose } from "@app/components/Credenza"; import CopyToClipboard from "@app/components/CopyToClipboard"; -import { Badge } from "./ui/badge"; import { Tooltip, TooltipContent, @@ -123,6 +122,11 @@ export default function UsersTable({ .optional() .catch(undefined); + const serverAdminFilterSchema = z + .enum(["true", "false"]) + .optional() + .catch(undefined); + function handleFilterChange( column: string, value: string | undefined | null @@ -310,19 +314,16 @@ export default function UsersTable({ )} {row.original.serverAdmin && ( - <> - - - - - - - {t("serverAdmin")} - - - - {/* {t("serverAdmin")} */} - + + + + + + + {t("serverAdmin")} + + + )} ) @@ -431,6 +432,37 @@ export default function UsersTable({ ); } }, + { + accessorKey: "serverAdmin", + friendlyName: t("serverAdmin"), + header: () => ( + + handleFilterChange("server_admin", value) + } + searchPlaceholder={t("searchPlaceholder")} + emptyMessage={t("emptySearchOptions")} + label={t("serverAdmin")} + className="p-3" + /> + ), + cell: ({ row }) => ( + + {row.original.serverAdmin ? ( + {t("yes")} + ) : ( + {t("no")} + )} + + ) + }, { id: "actions", enableHiding: false, @@ -459,7 +491,7 @@ export default function UsersTable({ {t("generatePasswordResetCode")} )} - {!r.serverAdmin && ( + {r.type === "internal" && !r.serverAdmin && ( { setPromoting(r); @@ -469,16 +501,18 @@ export default function UsersTable({ {t("promoteServerAdmin")} )} - {r.serverAdmin && r.id !== user.user.userId && ( - { - setDemoting(r); - setIsDemoteModalOpen(true); - }} - > - {t("demoteServerAdmin")} - - )} + {r.type === "internal" && + r.serverAdmin && + r.id !== user.user.userId && ( + { + setDemoting(r); + setIsDemoteModalOpen(true); + }} + > + {t("demoteServerAdmin")} + + )} { setSelected(r);