add server side filter for server admin

This commit is contained in:
miloschwartz
2026-09-11 11:28:07 -04:00
parent 0b11bdc9ff
commit e73ea120e3
4 changed files with 95 additions and 38 deletions
+7 -7
View File
@@ -527,7 +527,7 @@
"userAbount": "About User Management", "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.", "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", "userServer": "Server Users",
"userSearch": "Search server users...", "userSearch": "Search users...",
"userErrorDelete": "Error deleting user", "userErrorDelete": "Error deleting user",
"userDeleteConfirm": "Confirm Delete User", "userDeleteConfirm": "Confirm Delete User",
"userDeleteServer": "Delete User from Server", "userDeleteServer": "Delete User from Server",
@@ -1429,17 +1429,17 @@
"promoteServerAdmin": "Promote to Server admin", "promoteServerAdmin": "Promote to Server admin",
"promoteServerAdminTitle": "Promote to Server Admin", "promoteServerAdminTitle": "Promote to Server Admin",
"promoteServerAdminQuestion": "Are you sure you want to promote {selectedUser} 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.", "promoteServerAdminMessage": "Server admins have the highest privileges and can manage the server.",
"promoteServerAdminWarning": "This can be undone at any time by demoting the user from this page.", "promoteServerAdminWarning": "This can be undone at any time by demoting the user.",
"promoteServerAdminConfirm": "Promote to server admin", "promoteServerAdminConfirm": "Promote to Server Admin",
"promoteServerAdminSuccess": "User promoted", "promoteServerAdminSuccess": "User Promoted",
"promoteServerAdminSuccessDescription": "{selectedUser} is now a server admin.", "promoteServerAdminSuccessDescription": "{selectedUser} is now a server admin.",
"promoteServerAdminError": "Failed to promote user", "promoteServerAdminError": "Failed to promote user",
"demoteServerAdmin": "Demote from Server admin", "demoteServerAdmin": "Demote from Server admin",
"demoteServerAdminTitle": "Demote from Server Admin", "demoteServerAdminTitle": "Demote from Server Admin",
"demoteServerAdminQuestion": "Are you sure you want to demote {selectedUser} 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.", "demoteServerAdminMessage": "{selectedUser} will lose all server admin privileges.",
"demoteServerAdminWarning": "This can be undone at any time by promoting the user from this page.", "demoteServerAdminWarning": "This can be undone at any time by promoting the user.",
"demoteServerAdminConfirm": "Demote from server admin", "demoteServerAdminConfirm": "Demote from server admin",
"demoteServerAdminSuccess": "User demoted", "demoteServerAdminSuccess": "User demoted",
"demoteServerAdminSuccessDescription": "{selectedUser} is no longer a server admin.", "demoteServerAdminSuccessDescription": "{selectedUser} is no longer a server admin.",
+15 -1
View File
@@ -88,6 +88,15 @@ const listUsersSchema = z.strictObject({
type: "boolean", type: "boolean",
description: description:
"Filter by 2FA state matching: enabled if twoFactorEnabled or twoFactorSetupRequested" "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, sort_by,
order, order,
idp_id, idp_id,
two_factor: twoFactorFilter two_factor: twoFactorFilter,
server_admin: serverAdminFilter
} = parsedQuery.data; } = parsedQuery.data;
if (typeof idp_id === "number") { 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 whereClause = and(...conditions);
const countQuery = db.$count( const countQuery = db.$count(
+14 -5
View File
@@ -31,7 +31,8 @@ const AdminSetServerAdminResponseDataSchema = z.object({
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/user/{userId}/server-admin", 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], tags: [OpenAPITags.User],
request: { request: {
params: setServerAdminParamsSchema, params: setServerAdminParamsSchema,
@@ -63,9 +64,7 @@ export async function adminSetServerAdmin(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedParams = setServerAdminParamsSchema.safeParse( const parsedParams = setServerAdminParamsSchema.safeParse(req.params);
req.params
);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
@@ -91,7 +90,8 @@ export async function adminSetServerAdmin(
const [existingUser] = await db const [existingUser] = await db
.select({ .select({
userId: users.userId, userId: users.userId,
serverAdmin: users.serverAdmin serverAdmin: users.serverAdmin,
type: users.type
}) })
.from(users) .from(users)
.where(eq(users.userId, userId)) .where(eq(users.userId, userId))
@@ -101,6 +101,15 @@ export async function adminSetServerAdmin(
return next(createHttpError(HttpCode.NOT_FOUND, "User not found")); 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) { if (!serverAdmin && req.user?.userId === userId) {
return next( return next(
createHttpError( createHttpError(
+59 -25
View File
@@ -44,7 +44,6 @@ import {
CredenzaClose CredenzaClose
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import CopyToClipboard from "@app/components/CopyToClipboard"; import CopyToClipboard from "@app/components/CopyToClipboard";
import { Badge } from "./ui/badge";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -123,6 +122,11 @@ export default function UsersTable({
.optional() .optional()
.catch(undefined); .catch(undefined);
const serverAdminFilterSchema = z
.enum(["true", "false"])
.optional()
.catch(undefined);
function handleFilterChange( function handleFilterChange(
column: string, column: string,
value: string | undefined | null value: string | undefined | null
@@ -310,19 +314,16 @@ export default function UsersTable({
</> </>
)} )}
{row.original.serverAdmin && ( {row.original.serverAdmin && (
<> <TooltipProvider>
<TooltipProvider> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <ShieldUserIcon className="text-primary size-4 flex-none" />
<ShieldUserIcon className="text-primary size-5 flex-none" /> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>
<TooltipContent> {t("serverAdmin")}
{t("serverAdmin")} </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> </TooltipProvider>
</TooltipProvider>
{/* <Badge>{t("serverAdmin")}</Badge> */}
</>
)} )}
</span> </span>
) )
@@ -431,6 +432,37 @@ export default function UsersTable({
); );
} }
}, },
{
accessorKey: "serverAdmin",
friendlyName: t("serverAdmin"),
header: () => (
<ColumnFilterButton
options={[
{ value: "true", label: t("yes") },
{ value: "false", label: t("no") }
]}
selectedValue={serverAdminFilterSchema.parse(
searchParams.get("server_admin") ?? undefined
)}
onValueChange={(value) =>
handleFilterChange("server_admin", value)
}
searchPlaceholder={t("searchPlaceholder")}
emptyMessage={t("emptySearchOptions")}
label={t("serverAdmin")}
className="p-3"
/>
),
cell: ({ row }) => (
<span>
{row.original.serverAdmin ? (
<span>{t("yes")}</span>
) : (
<span>{t("no")}</span>
)}
</span>
)
},
{ {
id: "actions", id: "actions",
enableHiding: false, enableHiding: false,
@@ -459,7 +491,7 @@ export default function UsersTable({
{t("generatePasswordResetCode")} {t("generatePasswordResetCode")}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{!r.serverAdmin && ( {r.type === "internal" && !r.serverAdmin && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setPromoting(r); setPromoting(r);
@@ -469,16 +501,18 @@ export default function UsersTable({
{t("promoteServerAdmin")} {t("promoteServerAdmin")}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{r.serverAdmin && r.id !== user.user.userId && ( {r.type === "internal" &&
<DropdownMenuItem r.serverAdmin &&
onClick={() => { r.id !== user.user.userId && (
setDemoting(r); <DropdownMenuItem
setIsDemoteModalOpen(true); onClick={() => {
}} setDemoting(r);
> setIsDemoteModalOpen(true);
{t("demoteServerAdmin")} }}
</DropdownMenuItem> >
)} {t("demoteServerAdmin")}
</DropdownMenuItem>
)}
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setSelected(r); setSelected(r);