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 expired = new Date(companyInvite.expires_at) < new Date()
|
||||||
|
|
||||||
const { data: existingUsers } = await serviceClient.auth.admin.listUsers()
|
const { data: alreadyHasAccount } = await serviceClient.rpc('check_email_exists', {
|
||||||
const alreadyHasAccount = existingUsers?.users?.some(
|
email_to_check: companyInvite.email,
|
||||||
(u) => u.email?.toLowerCase() === companyInvite.email.toLowerCase()
|
})
|
||||||
) ?? false
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -135,7 +135,11 @@ export default function InvitePage() {
|
|||||||
await supabase.auth.signOut()
|
await supabase.auth.signOut()
|
||||||
// Keep the invite cookie alive so the next login/register picks it up.
|
// 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}`
|
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) {
|
if (isLoading) {
|
||||||
@@ -209,8 +213,11 @@ export default function InvitePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
) : invite?.alreadyHasAccount && isLoggedInAsInvitee ? (
|
) : isLoggedInAsInvitee ? (
|
||||||
// Already signed in as the invitee — one-click join.
|
// 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">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
@@ -245,7 +252,7 @@ export default function InvitePage() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : invite?.alreadyHasAccount && isLoggedInAsOther ? (
|
) : isLoggedInAsOther ? (
|
||||||
// Signed in as a different user — ask them to sign out first.
|
// Signed in as a different user — ask them to sign out first.
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
@@ -271,8 +278,7 @@ export default function InvitePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : invite?.alreadyHasAccount ? (
|
) : invite?.alreadyHasAccount ? (
|
||||||
// Not signed in yet — current behavior: bounce to /login with
|
// Not signed in — email has an existing account, bounce to login.
|
||||||
// the invite cookie.
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
@@ -297,6 +303,7 @@ export default function InvitePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : invite ? (
|
) : invite ? (
|
||||||
|
// Not signed in, no existing account — register.
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="flex items-start gap-4">
|
<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