← All posts
Frontend8 min readProject: Auth admin panel

Designing an admin panel for dangerous actions: reveal-once secrets, honest confirmations and the last admin

An auth admin panel is full of buttons that can lock people out of every app in the company. Here is how I designed its dangerous moments: a secret that can only be shown once, confirmations that state real consequences with real numbers, type-to-confirm for the irreversible, and actions that are disabled before the backend has to refuse them.

#UX#React#Next.js#Admin Panel#shadcn/ui#Accessibility

The problem

Admin actions in the auth panel could lose an unrecoverable API key, remove access for dozens of people, or lock out the last administrator, behind generic "Are you sure?" dialogs.

The result

Every dangerous action now explains its real effect with real numbers, irreversible ones need deliberate confirmation, and impossible ones are disabled with an explanation.

Stack

Next.jsReactTypeScriptshadcn/uiRadix UITanStack Query

The problem

Our central auth service has an admin panel: the place where the Root Admin registers applications, organizes departments into groups, ticks permissions, and manages users. It's a small app with a big blast radius. A single click in it can:

  • generate a secret that can never be shown again,
  • remove a whole department's access to an application,
  • permanently delete an app, its permission catalog and every activation,
  • remove the only person who can manage the system.

Most admin panels treat all of those the same way: a red button and a dialog that says "Are you sure?". People click "Yes" on autopilot, because the dialog doesn't tell them anything they can actually evaluate.

I wanted every dangerous moment in this panel to be designed for what can go wrong at that exact moment.

The details

Four kinds of danger

Looking at every destructive or risky action, they fell into four groups:

KindExampleWhat can go wrong
Unrecoverable informationthe API key shown after registering an appit's lost forever if not copied
Wide effectdeleting a group, saving permissionsmany people are affected, often more than the admin thinks
Irreversiblehard-deleting an applicationno undo
Impossibleremoving the last Root Adminthe backend will refuse, after the admin already decided

Each kind needs a different answer. A single generic confirmation can't serve all four.

The challenges

  • Dialogs are dismissed too easily. Esc, clicking outside and the close button are all great defaults, except when closing means losing a secret.
  • Admins underestimate impact. "Delete group" sounds small until you realize 40 people lose access to 3 apps.
  • "Are you sure?" trains people to click yes. If every confirmation looks the same, none of them is read.
  • Backend errors arrive too late. A 409 after the admin already filled a form and clicked is frustrating, even if the message is good.

The solution

1. Unrecoverable information: a dialog you can't close by accident

When an application is registered, the backend returns its API key in plain text, once. There's no endpoint to see it again or rotate it. Lose it, and the app has to be registered again.

So that dialog is deliberately blocking:

<Dialog open={application !== null}>
  <DialogContent
    showCloseButton={false}
    onEscapeKeyDown={(event) => event.preventDefault()}
    onPointerDownOutside={(event) => event.preventDefault()}
    onInteractOutside={(event) => event.preventDefault()}
  >
    <DialogTitle>Application registered</DialogTitle>
    <DialogDescription>Copy this API key now. It will never be shown again.</DialogDescription>

    <code className="select-all break-all font-mono">{application?.apiKey}</code>
    <CopyButton value={application?.apiKey ?? ''} />

    <Checkbox id="saved" checked={saved} onCheckedChange={(c) => setSaved(c === true)} />
    <Label htmlFor="saved">I have saved this key</Label>

    <Button disabled={!saved} onClick={done}>Done</Button>
  </DialogContent>
</Dialog>
  • No close button, no Esc, no click outside. The only way out is "Done".
  • "Done" is disabled until a checkbox is ticked: "I have saved this key". It's a tiny speed bump that forces one conscious moment.
  • select-all on the key, so one click selects the whole value, plus a copy button.
  • The dialog also says where the key goes (which header the app must send it in), so the admin knows what to do with it.

Normally, blocking dialogs are bad UX. Here, the cost of an accidental close is re-registering an application, so friction is the feature.

2. Wide effects: confirmations that state real consequences

Instead of "Are you sure you want to delete this group?", the delete-group dialog lists what will actually happen, with numbers from the data already loaded:

<ConfirmDialog
  title={`Delete "${group.name}"?`}
  confirmLabel="Delete group"
  destructive
  description={
    <>
      <p>This also removes:</p>
      <ul>
        <li>access to {apps.length} application(s) — {apps.map((a) => a.name).join(', ')}</li>
        <li>every permission ticked in those applications</li>
        <li>the group of {memberCount} user(s)</li>
      </ul>
      {memberCount > 0 && (
        <p>
          Those {memberCount} users end up with no group. A user without a group
          has access to no application at all, until you assign them another one.
        </p>
      )}
    </>
  }
/>

"This removes access to 3 applications (billing, inventory, reports) for 42 users" is something an admin can actually evaluate. "Are you sure?" isn't.

A few rules made these dialogs consistent:

  • The title names the object: Delete "Finance"?, never Delete group?.
  • The confirm button names the action: "Delete group", "Delete permanently", "Save anyway", never "OK" or "Yes".
  • The description lists effects as facts, with counts and names.
  • The dialog also says what is not affected when that's a reasonable worry: "Groups and users themselves are not affected."

3. Saving that would silently lock people out

Some saves are technically valid but almost certainly a mistake. Unticking every permission a group has in an app is one of them. Members can still enter the app (the group is still activated), but every endpoint will answer 403.

So that save gets its own confirmation, explaining the precise outcome and the better alternative:

This leaves "Finance" with no permission in billing Its 42 members can still enter billing (the access is still there) but every endpoint answers 403. To shut them out completely, remove the group from billing instead.

[ Save anyway ]

The admin learns the difference between "no permissions" and "no access" exactly when it matters, instead of in a support ticket the next morning.

4. Irreversible: type the name to confirm

Hard-deleting an application can't be undone: its catalog, every group activation and every tick are removed. That dialog asks the admin to type the application's name before the button activates:

<ConfirmDialog
  title={`Permanently delete "${application.name}"?`}
  confirmLabel="Delete permanently"
  destructive
  typeToConfirm={application.name}
  description={
    <>
      <p><strong>This cannot be undone.</strong> It removes the application,
        its {stats.permissionCount} catalog entries, and the {stats.groupCount} group
        activations for it with every permission ticked inside them.</p>
      <p>Groups and users themselves are not affected.</p>
    </>
  }
/>

Typing a name is slow on purpose. It's impossible to do on autopilot, and it guarantees the admin is deleting the app they think they're deleting.

And it's only offered at the end of a two-step path: "Delete permanently" only appears on a disabled application. The everyday action, "Disable", is reversible and keeps everything.

5. Impossible: disable it before the backend says no

The backend refuses to remove or disable the last active Root Admin: there would be nobody left to manage the system. A good backend error ("Cannot remove the last Root Admin") is still a bad experience if it arrives after the admin opened a menu, chose an action and confirmed a dialog.

So the panel computes it up front:

const rootAdminCount = useActiveRootAdminCount();

// The backend refuses to orphan the last Root Admin. Don't wait for that
// error: the best error is the one the user never sees.
const isLastRootAdmin = user.isRootAdmin && user.status === 'ACTIVE' && rootAdminCount <= 1;

<LastRootAdminGuard blocked={isLastRootAdmin}>
  <DropdownMenuItem disabled={isLastRootAdmin} onSelect={() => onAction('revoke-root-admin')}>
    Remove Root Admin
  </DropdownMenuItem>
</LastRootAdminGuard>

The menu items are disabled with a tooltip explaining why. The admin sees the rule before trying to break it. The backend still enforces it, because the UI is a convenience, not the security layer.

6. Visual rules that keep danger readable

Consistency is what makes warnings noticeable:

  • Red means destructive, only. Destructive buttons, destructive confirmations.
  • Amber means warning: something is misconfigured or risky, but nothing is being destroyed.
  • Green is only for success toasts.
  • Badges are outlined, never filled, so a filled red block is always an action, never a label.
  • No animations, except a 150 ms fade on dialogs, menus and sheets. Nothing distracts from reading.

If red appears for everything, nothing looks dangerous. Reserving it makes the truly dangerous buttons stand out.

Lessons learned

  • Classify danger before designing for it. Unrecoverable, wide, irreversible and impossible actions need different treatments.
  • Friction is a tool. A dialog you can't close by accident is the right choice when closing loses something forever.
  • Show consequences, not questions. Real names and real counts turn "Are you sure?" into an informed decision.
  • Name the object and the action. Delete "Finance"? with a "Delete group" button is read; "Are you sure? OK" isn't.
  • Catch valid-but-wrong saves. Explain the outcome and offer the better alternative.
  • Make irreversible actions slow. Type-to-confirm can't be done on autopilot.
  • Disable impossible actions early, with a reason. The best error message is the one the user never needs.
  • Reserve colors. Red only for destruction keeps destruction visible.

FAQ

When should a dialog prevent closing with Esc or clicking outside?

When closing it would lose something that can't be recovered, like a secret shown only once. In that case, require an explicit action (and ideally a confirmation like "I have saved this key") before closing.

How do I write a better confirmation dialog than "Are you sure?"

Name the object in the title, describe the concrete consequences with real numbers and names, say what isn't affected, and label the confirm button with the action itself ("Delete group"), not "OK".

What is type-to-confirm and when should I use it?

The user must type the name of the item before the destructive button activates. Use it for irreversible actions with a large impact, such as permanently deleting a project, an application or a database.

Should the frontend enforce business rules like "keep at least one admin"?

The backend must enforce them. The frontend should also anticipate them when it can, by disabling impossible actions with an explanation, so users don't discover the rule through an error.

Facing a similar problem?

I help teams fix performance, backend and reliability issues in production.

Let's talk
Welcome to My WebSite please enjoy and have funWelcome to My WebSite please enjoy and have funWelcome to My WebSite please enjoy and have funWelcome to My WebSite please enjoy and have fun

Componentity

Thanks for visiting my portfolio. Every project here reflects my passion for building and learning. If you’re interested in working together, have a question, or just want to connect—don’t hesitate to reach out. I'm always open to meaningful conversations and new ideas.

© 2026 Zakariya Zouazou. All Rights Reserved.

Contact Us