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",
"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.",
+15 -1
View File
@@ -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(
+14 -5
View File
@@ -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<any> {
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(
+59 -25
View File
@@ -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 && (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<ShieldUserIcon className="text-primary size-5 flex-none" />
</TooltipTrigger>
<TooltipContent>
{t("serverAdmin")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* <Badge>{t("serverAdmin")}</Badge> */}
</>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<ShieldUserIcon className="text-primary size-4 flex-none" />
</TooltipTrigger>
<TooltipContent>
{t("serverAdmin")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</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",
enableHiding: false,
@@ -459,7 +491,7 @@ export default function UsersTable({
{t("generatePasswordResetCode")}
</DropdownMenuItem>
)}
{!r.serverAdmin && (
{r.type === "internal" && !r.serverAdmin && (
<DropdownMenuItem
onClick={() => {
setPromoting(r);
@@ -469,16 +501,18 @@ export default function UsersTable({
{t("promoteServerAdmin")}
</DropdownMenuItem>
)}
{r.serverAdmin && r.id !== user.user.userId && (
<DropdownMenuItem
onClick={() => {
setDemoting(r);
setIsDemoteModalOpen(true);
}}
>
{t("demoteServerAdmin")}
</DropdownMenuItem>
)}
{r.type === "internal" &&
r.serverAdmin &&
r.id !== user.user.userId && (
<DropdownMenuItem
onClick={() => {
setDemoting(r);
setIsDemoteModalOpen(true);
}}
>
{t("demoteServerAdmin")}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
setSelected(r);