Improve invite flow by replacing user listing with email existence check (#229)

* Improve invite flow by replacing user listing with email existence check

* Refactor invite logic to redirect users based on account status and enhance email existence check permissions
This commit is contained in:
Mattsson
2026-04-13 16:15:20 +02:00
committed by GitHub
parent 7bf7565852
commit 4644642f8a
3 changed files with 34 additions and 9 deletions
+3 -4
View File
@@ -33,10 +33,9 @@ export async function GET(request: NextRequest) {
const expired = new Date(companyInvite.expires_at) < new Date()
const { data: existingUsers } = await serviceClient.auth.admin.listUsers()
const alreadyHasAccount = existingUsers?.users?.some(
(u) => u.email?.toLowerCase() === companyInvite.email.toLowerCase()
) ?? false
const { data: alreadyHasAccount } = await serviceClient.rpc('check_email_exists', {
email_to_check: companyInvite.email,
})
return NextResponse.json({
data: {
+12 -5
View File
@@ -135,7 +135,11 @@ export default function InvitePage() {
await supabase.auth.signOut()
// Keep the invite cookie alive so the next login/register picks it up.
document.cookie = `gnubok-invite-token=${token}; path=/; max-age=3600; samesite=lax${secureCookieFlag}`
router.push('/login')
if (invite?.alreadyHasAccount) {
router.push('/login')
} else {
router.push(`/register?invite=${encodeURIComponent(token)}`)
}
}
if (isLoading) {
@@ -209,8 +213,11 @@ export default function InvitePage() {
</div>
</div>
</Card>
) : invite?.alreadyHasAccount && isLoggedInAsInvitee ? (
) : isLoggedInAsInvitee ? (
// Already signed in as the invitee — one-click join.
// Prioritized over alreadyHasAccount to avoid the broken flow
// where a false-negative from the email check would send a
// logged-in user to /register, which middleware bounces to /.
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-start gap-4">
@@ -245,7 +252,7 @@ export default function InvitePage() {
)}
</Button>
</div>
) : invite?.alreadyHasAccount && isLoggedInAsOther ? (
) : isLoggedInAsOther ? (
// Signed in as a different user — ask them to sign out first.
<div className="space-y-6">
<Card className="p-6">
@@ -271,8 +278,7 @@ export default function InvitePage() {
</Button>
</div>
) : invite?.alreadyHasAccount ? (
// Not signed in yet — current behavior: bounce to /login with
// the invite cookie.
// Not signed in — email has an existing account, bounce to login.
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-start gap-4">
@@ -297,6 +303,7 @@ export default function InvitePage() {
</Button>
</div>
) : invite ? (
// Not signed in, no existing account — register.
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-start gap-4">
@@ -0,0 +1,19 @@
-- Efficient email existence check for invite flow.
-- Replaces the previous approach of listing all auth users (which only
-- returned the first page and broke for instances with >50 users).
CREATE OR REPLACE FUNCTION public.check_email_exists(email_to_check text)
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
SET search_path = ''
AS $$
SELECT EXISTS (
SELECT 1 FROM auth.users WHERE lower(email) = lower(email_to_check)
);
$$;
-- Only callable by service role — prevents email enumeration via PostgREST.
-- Must revoke from PUBLIC first (PostgreSQL grants EXECUTE to PUBLIC by default),
-- then grant explicitly to service_role.
REVOKE EXECUTE ON FUNCTION public.check_email_exists(text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.check_email_exists(text) TO service_role;