Create-user flow now provisions the Neon Auth account
Deploy to route.crispygoat.com / deploy (push) Successful in 4m18s

Previously createAdminUser only inserted a local admin_users row and
emailed the password as plaintext — the user could not sign in because
the password was never set on a Neon Auth account.

This change makes the create flow:
  1. Authorize: only platform_admin can mint new admin users.
  2. Create the Neon Auth user via auth.admin.createUser, falling back
     to the public /sign-up/email + emailVerified=true pattern when
     the caller's Neon Auth session isn't an admin (the common case
     in dev — provision-admin.ts does not set neon_auth.user.role).
  3. Wrap the admin_users INSERT + admin_user_brands link in a
     transaction, returning an error if the local insert fails (the
     Neon Auth user is left orphaned and surfaced in the message).
  4. Send the welcome email best-effort, returning success/failure
     info to the UI.

The CreateUserModal now shows a success state with the temp password
(copy-to-clipboard), the welcome email status, and the auth path
used. The slide-in edit panel surfaces the password via window.alert
as a defense against the dead-code new-user path.

10 new unit tests cover authorization, the admin + signup paths, the
DB-failure orphan case, and the email best-effort behavior.
This commit is contained in:
Tyler
2026-06-17 11:46:39 -06:00
parent 11cd2fd01a
commit d75380eb9a
4 changed files with 936 additions and 77 deletions
+11 -1
View File
@@ -195,7 +195,17 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
}
return;
}
if (res.user) setUsers((prev) => [res.user!, ...prev]);
if (res.user) {
setUsers((prev) => [res.user!, ...prev]);
// Surface the temp password — the slide-in panel has no
// success view. The modal is the primary create flow, but
// we don't want to silently lose the password here.
if (res.tempPassword) {
window.alert(
`User created. Temporary password (share with the user — it will not be shown again):\n\n${res.tempPassword}`,
);
}
}
} else {
const res = await import("@/actions/admin/users").then((m) =>
m.updateAdminUser({