From 9ff2c13f8dd6ab5417b3b4e463c063cc8cea55a7 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 11 May 2026 18:58:31 -0700 Subject: [PATCH] Add functionality to restrict or unrestrict users --- .../users/[userId]/page-client.tsx | 172 +++++++++++------- 1 file changed, 111 insertions(+), 61 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx index 4a241ea48..e45f7e871 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx @@ -46,7 +46,7 @@ import { KnownErrors } from "@stackframe/stack-shared"; import { AppId } from "@stackframe/stack-shared/dist/apps/apps-config"; import { normalizeCountryCode } from "@stackframe/stack-shared/dist/schema-fields"; import { fromNow } from "@stackframe/stack-shared/dist/utils/dates"; -import { captureError, StackAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; +import { StackAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; import { Suspense, useCallback, useMemo, useState, type ReactNode } from "react"; @@ -145,7 +145,7 @@ function UserHeader({ user }: UserHeaderProps) { }] satisfies DesignMenuActionItem[] : [], { id: "restriction", - label: "User restriction", + label: getRestrictionActionLabel(user), onClick: () => { setRestrictionDialogOpen(true); }, }, { @@ -185,6 +185,26 @@ function getRestrictionReasonText(user: ServerUser): string { } } +function getRestrictionActionLabel(user: ServerUser): string { + if (user.restrictedByAdmin) { + return "Edit or remove manual restriction"; + } + if (user.isRestricted) { + return "Add manual restriction"; + } + return "Restrict user"; +} + +function getManualRestrictionStatusText(user: ServerUser): string { + if (user.restrictedByAdmin) { + return "Restricted by admin"; + } + if (user.isRestricted) { + return `Not manually restricted (${getRestrictionReasonText(user)})`; + } + return "Not restricted"; +} + // Restriction dialog for editing restriction details function RestrictionDialog({ user, @@ -195,35 +215,31 @@ function RestrictionDialog({ open: boolean, onOpenChange: (open: boolean) => void, }) { - const restrictedByAdmin = (user as any).restrictedByAdmin ?? false; - const restrictedByAdminReason = (user as any).restrictedByAdminReason ?? null; - const restrictedByAdminPrivateDetails = (user as any).restrictedByAdminPrivateDetails ?? null; - - const [publicReason, setPublicReason] = useState(restrictedByAdminReason ?? ''); - const [privateDetails, setPrivateDetails] = useState(restrictedByAdminPrivateDetails ?? ''); + const [publicReason, setPublicReason] = useState(user.restrictedByAdminReason ?? ''); + const [privateDetails, setPrivateDetails] = useState(user.restrictedByAdminPrivateDetails ?? ''); const [isSaving, setIsSaving] = useState(false); // Reset form when dialog opens const handleOpenChange = (newOpen: boolean) => { if (newOpen) { - setPublicReason(restrictedByAdminReason ?? ''); - setPrivateDetails(restrictedByAdminPrivateDetails ?? ''); + setPublicReason(user.restrictedByAdminReason ?? ''); + setPrivateDetails(user.restrictedByAdminPrivateDetails ?? ''); } onOpenChange(newOpen); }; const handleSaveAndRestrict = async () => { - if (!privateDetails.trim()) { - alert('Please enter the private details for the restriction.'); - return; - } + const trimmedPublicReason = publicReason.trim(); + const trimmedPrivateDetails = privateDetails.trim(); setIsSaving(true); try { - await user.update({ restrictedByAdmin: true, restrictedByAdminReason: publicReason.trim() || null, restrictedByAdminPrivateDetails: privateDetails.trim() || null } as any); + await user.update({ + restrictedByAdmin: true, + restrictedByAdminReason: trimmedPublicReason.length > 0 ? trimmedPublicReason : null, + restrictedByAdminPrivateDetails: trimmedPrivateDetails.length > 0 ? trimmedPrivateDetails : null, + }); onOpenChange(false); - } catch (error) { - captureError(`user-restriction-save-and-restrict-error`, new StackAssertionError(`Failed to save and restrict user ${user.id}`, { cause: error })); } finally { setIsSaving(false); } @@ -236,7 +252,7 @@ function RestrictionDialog({ restrictedByAdmin: false, restrictedByAdminReason: null, restrictedByAdminPrivateDetails: null, - } as any); + }); onOpenChange(false); } finally { setIsSaving(false); @@ -249,7 +265,9 @@ function RestrictionDialog({ User Restriction - Restricted users cannot access your app by default. You can optionally provide a public reason (shown to the user) and private details (for internal notes). + {user.restrictedByAdmin + ? "This user is manually restricted. You can update the notes or remove the manual restriction." + : "Use a manual restriction to block this user from accessing your app by default. You can optionally provide a public reason shown to the user."}
@@ -263,21 +281,20 @@ function RestrictionDialog({ />
- +