Skip to content

Generated password modal flashes and disappears when creating a user from an empty group's detail page #742

Description

@voron

Generated password modal flashes and disappears when creating a user from an empty group's detail page

Summary

On self-hosted with the embedded IdP, creating a user via Access Control → Groups → (group) → Add User → Create User shows the "User created successfully!" modal containing the generated password for a fraction of a second, then it vanishes before the password can be read or copied.

The user account is created successfully — but its password is displayed exactly once, and once lost there is no recovery path from the dashboard: the account is dead and must be deleted and re-created.

This is what makes the bug more than cosmetic. The only password-change surface in the dashboard is ChangePasswordModal, which is mounted solely in UserDropdown (self-service, for the logged-in user) and PUTs /users/{id}/password with a required old_password — so it cannot help here on two counts: an admin cannot invoke it against another user, and the credential it demands is precisely the one that was just lost. An admin-initiated reset appears to be deliberately absent rather than an oversight, so there is no "just reset it" fallback.

The same modal works correctly from Team → Users → Add User.

Trigger condition: the group must have had zero users before the creation. Creating a user from a group that already has members works fine.

Entry point Group state Create User Invite User
Team → Users n/a ✅ works ✅ works
Group detail → Add User non-empty ✅ works ✅ works
Group detail → Add User empty ❌ modal vanishes ✅ works

Environment

  • Dashboard v2.90.9 (also reproduces by inspection on current main — the two relevant files are unchanged between the tag and main)
  • netbirdio/netbird-server:0.76.1, self-hosted, single box, embedded IdP (embedded_idp_enabled: true, local auth enabled)

Steps to reproduce

  1. Self-hosted instance with the embedded IdP enabled.
  2. Access Control → Groups → create a new group, or pick one with no users assigned.
  3. Open the group → Users tab → Add User.
  4. Choose the Create User tab, fill in name + email, submit.
  5. The success modal with the generated password appears, then disappears on its own within ~a second.

Expected: the modal stays open until "Copy & Close" is clicked (it already sets onEscapeKeyDown/onInteractOutside/onPointerDownOutside to preventDefault, so it is clearly intended to be dismissible only via that button).

Workaround

Until this is fixed, on an empty group either use Invite User instead of Create User (unaffected, and the invitee sets their own password), or create the user from Team → Users and assign the group afterwards. Assigning any user to the group first also avoids it, since the bug requires the group to go from 0 to 1 users.

Cause

UserInviteModal owns the success state that holds the generated password:

https://github.com/netbirdio/dashboard/blob/v2.90.9/src/modules/users/UserInviteModal.tsx#L48-L51

const [open, setOpen] = useState(false);
const [successModal, setSuccessModal] = useState(false);
const [successData, setSuccessData] = useState<SuccessData | null>(null);

The password therefore only survives as long as that component instance stays mounted.

On the group detail page, GroupUsersSection mounts InviteUserButton (which wraps UserInviteModal) in two mutually exclusive branches:

DataTable renders exactly one of those branches depending on whether there is data:

https://github.com/netbirdio/dashboard/blob/v2.90.9/src/components/table/DataTable.tsx#L495-L499

Meanwhile createUser() posts with auto_groups set to the current group and revalidates the user list immediately, before handing the password to onUserCreated:

https://github.com/netbirdio/dashboard/blob/v2.90.9/src/modules/users/UserInviteModal.tsx#L231-L234

.then((user) => {
  mutate("/users?service_user=false");
  onUserCreated && onUserCreated(user);
});

(plus a second refetch 1s later in handleUserCreated, UserInviteModal.tsx#L77-L79).

So the sequence on an empty group is:

  1. successData is set, success modal opens, password is on screen.
  2. The revalidated /users response arrives; the group's user count goes 0 → 1.
  3. DataTable swaps getStartedCard for the table body — unmounting the UserInviteModal instance that holds successData.
  4. The rightSide branch mounts a different UserInviteModal instance with successModal: false, so nothing is shown.

This also explains why Invite User is unaffected from the same button: an invite does not add an entry to /users?service_user=false (pending invites live in /users/invites), so the group's user list does not change, no branch swap happens, and the modal survives.

Suggested fix

Any of:

  • Render a single UserInviteModal instance at GroupUsersSection level, outside the getStartedCard / rightSide branches, and drive it from a lifted open state — the two branches then only render trigger buttons.
  • Or lift successModal/successData out of UserInviteModal into a context/provider that outlives the table's branch switching.
  • Defer the mutate("/users?service_user=false") on L232 until the success modal is dismissed, rather than firing it before onUserCreated. (This alone narrows the race but does not close it, since handleUserCreated also schedules a refetch and other revalidation paths exist — so it is a mitigation, not a fix.)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions