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:
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user