From af8e852519997e20af169d351c01fdedf839ad0a Mon Sep 17 00:00:00 2001 From: pavsoss Date: Sun, 21 Jun 2026 20:00:17 +0530 Subject: [PATCH 1/3] fix/email inconsistency --- src/app/actions/profile.ts | 1 + src/lib/db/schema.ts | 1 + supabase/migrations/0022_add_profile_email.sql | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 supabase/migrations/0022_add_profile_email.sql diff --git a/src/app/actions/profile.ts b/src/app/actions/profile.ts index 9cf0b419..7ae16d43 100644 --- a/src/app/actions/profile.ts +++ b/src/app/actions/profile.ts @@ -58,6 +58,7 @@ export async function bootstrapProfile(): Promise> { github_handle: githubHandle, avatar_url: avatarUrl ?? null, display_name: displayName ?? null, + email: user.email ?? null, }, { onConflict: 'id' }, ) diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index a04d245b..ccc22317 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -33,6 +33,7 @@ export const profiles = pgTable( id: uuid('id').primaryKey(), githubId: text('github_id').notNull().unique(), githubHandle: text('github_handle').notNull().unique(), + email: text('email'), displayName: text('display_name'), avatarUrl: text('avatar_url'), role: text('role', { enum: ['contributor', 'maintainer', 'both'] }) diff --git a/supabase/migrations/0022_add_profile_email.sql b/supabase/migrations/0022_add_profile_email.sql new file mode 100644 index 00000000..48d8739c --- /dev/null +++ b/supabase/migrations/0022_add_profile_email.sql @@ -0,0 +1,3 @@ +-- Add email column to profiles +ALTER TABLE profiles +ADD COLUMN IF NOT EXISTS email text; From 2cffd9a2c7fd04ff5d039f4a8be8590c407a28b7 Mon Sep 17 00:00:00 2001 From: pavsoss Date: Wed, 24 Jun 2026 02:23:53 +0530 Subject: [PATCH 2/3] config: rsl --- src/app/actions/profile.ts | 16 +++++++++++- src/inngest/functions/help-dispatch.ts | 18 ++++++++++--- src/lib/db/schema.ts | 10 +++++++- supabase/migrations/0023_profile_emails.sql | 28 +++++++++++++++++++++ 4 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 supabase/migrations/0023_profile_emails.sql diff --git a/src/app/actions/profile.ts b/src/app/actions/profile.ts index e6bd39a2..7a1fcba6 100644 --- a/src/app/actions/profile.ts +++ b/src/app/actions/profile.ts @@ -58,7 +58,6 @@ export async function bootstrapProfile(): Promise> { github_handle: githubHandle, avatar_url: avatarUrl ?? null, display_name: displayName ?? null, - email: user.email ?? null, }, { onConflict: 'id' }, ) @@ -69,6 +68,21 @@ export async function bootstrapProfile(): Promise> { return err('persist_failed', upsertErr?.message ?? 'profile upsert returned nothing'); } + // Upsert the user's email into the private profile_emails table + if (user.email) { + const { error: emailErr } = await service.from('profile_emails').upsert( + { + user_id: user.id, + email: user.email, + }, + { onConflict: 'user_id' }, + ); + + if (emailErr) { + console.error('Failed to upsert profile email', emailErr); + } + } + let auditQueued = false; if (!profile.audit_completed) { diff --git a/src/inngest/functions/help-dispatch.ts b/src/inngest/functions/help-dispatch.ts index fa4d91c3..edcd04a3 100644 --- a/src/inngest/functions/help-dispatch.ts +++ b/src/inngest/functions/help-dispatch.ts @@ -43,7 +43,7 @@ export const helpDispatch = inngest.createFunction( // Pool: all L2+ profiles. In production we'd narrow by recent activity etc. const { data: pool } = await sb .from('profiles') - .select('id, level, primary_language, github_handle, email') + .select('id, level, primary_language, github_handle') .gte('level', 2) .neq('id', userId); @@ -69,6 +69,15 @@ export const helpDispatch = inngest.createFunction( if (targets.length === 0) return { notified: 0 }; + // Fetch emails for the selected targets + const targetUserIds = targets.map((t) => t.userId); + const { data: emailsData } = await sb + .from('profile_emails') + .select('user_id, email') + .in('user_id', targetUserIds); + + const emailMap = new Map(emailsData?.map((row) => [row.user_id, row.email])); + const { data: helpRequest } = await sb .from('help_requests') .select('reason, pr_url') @@ -85,13 +94,14 @@ export const helpDispatch = inngest.createFunction( for (const target of targets) { const mentor = pool?.find((p) => p.id === target.userId); + const mentorEmail = emailMap.get(target.userId); - if (!mentor?.email) continue; + if (!mentorEmail) continue; try { await sendHelpDispatchEmail({ - to: mentor.email, - mentorHandle: mentor.github_handle ?? 'mentor', + to: mentorEmail, + mentorHandle: mentor?.github_handle ?? 'mentor', menteeHandle: mentee?.github_handle ?? 'contributor', prUrl: helpRequest?.pr_url ?? '', helpReason: helpRequest?.reason ?? null, diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 4ef02671..d4351dea 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -33,7 +33,6 @@ export const profiles = pgTable( id: uuid('id').primaryKey(), githubId: text('github_id').notNull().unique(), githubHandle: text('github_handle').notNull().unique(), - email: text('email'), displayName: text('display_name'), avatarUrl: text('avatar_url'), role: text('role', { enum: ['contributor', 'maintainer', 'both'] }) @@ -62,6 +61,15 @@ export const profiles = pgTable( }), ); +export const profileEmails = pgTable('profile_emails', { + userId: uuid('user_id') + .primaryKey() + .references(() => profiles.id, { onDelete: 'cascade' }), + email: text('email').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); + // ---------- GitHub App installations (the gate) ---------- export const githubInstallations = pgTable( diff --git a/supabase/migrations/0023_profile_emails.sql b/supabase/migrations/0023_profile_emails.sql new file mode 100644 index 00000000..f5e33864 --- /dev/null +++ b/supabase/migrations/0023_profile_emails.sql @@ -0,0 +1,28 @@ +-- Migration 0023: Private Profile Emails + +-- Create the private profile_emails table +CREATE TABLE IF NOT EXISTS profile_emails ( + user_id uuid PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE, + email text NOT NULL, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Enable RLS +ALTER TABLE profile_emails ENABLE ROW LEVEL SECURITY; + +-- Add RLS policy for users to read their own email +DROP POLICY IF EXISTS profile_emails_read_own ON profile_emails; +CREATE POLICY profile_emails_read_own ON profile_emails FOR SELECT + USING (auth.uid() = user_id); + +-- Drop the email column from the public profiles table (if it exists) +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'profiles' AND column_name = 'email' + ) THEN + ALTER TABLE profiles DROP COLUMN email; + END IF; +END$$; From 94a27245d03d553c921dbaccd9e92f4e21d6da39 Mon Sep 17 00:00:00 2001 From: pavsoss Date: Wed, 24 Jun 2026 15:20:17 +0530 Subject: [PATCH 3/3] squashed migrations & added test --- src/inngest/functions/help-dispatch.test.ts | 57 +++++++++++++++++++ .../migrations/0022_add_profile_email.sql | 3 - supabase/migrations/0023_profile_emails.sql | 10 ---- 3 files changed, 57 insertions(+), 13 deletions(-) delete mode 100644 supabase/migrations/0022_add_profile_email.sql diff --git a/src/inngest/functions/help-dispatch.test.ts b/src/inngest/functions/help-dispatch.test.ts index 4a30a630..f1a87a4f 100644 --- a/src/inngest/functions/help-dispatch.test.ts +++ b/src/inngest/functions/help-dispatch.test.ts @@ -1,10 +1,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { rankReviewers } from '@/lib/help/dispatch'; +import { sendHelpDispatchEmail } from '@/lib/email'; import { helpDispatch } from './help-dispatch'; import { sb, wire, step } from './__tests__/test-helpers'; vi.mock('@/lib/supabase/service', () => ({ getServiceSupabase: vi.fn() })); vi.mock('@/lib/help/dispatch', () => ({ rankReviewers: vi.fn() })); +vi.mock('@/lib/email', () => ({ sendHelpDispatchEmail: vi.fn() })); vi.mock('../client', () => ({ inngest: { createFunction: (_c: unknown, _t: unknown, h: Function) => h }, })); @@ -93,4 +95,59 @@ describe('helpDispatch', () => { const result = await run({ event: ev(), step }); expect(result).toEqual({ helpRequestId: 123, notified: 0 }); }); + + it('resolves mentor email from profile_emails and sends email', async () => { + wire({ + profiles: sb({ + select: vi.fn().mockReturnThis(), + eq: vi.fn().mockReturnThis(), + gte: vi.fn().mockReturnThis(), + neq: vi.fn().mockResolvedValue({ + data: [{ id: 'm1', level: 2, primary_language: 'TypeScript', github_handle: 'mentor1' }], + }), + maybeSingle: vi.fn().mockResolvedValue({ + data: { level: 1, primary_language: 'TypeScript', github_handle: 'mentee' }, + }), + }), + cohort_members: sb({ + select: vi.fn().mockReturnThis(), + eq: vi.fn().mockReturnThis(), + limit: vi.fn().mockReturnThis(), + maybeSingle: vi.fn().mockResolvedValue({ data: null }), + }), + profile_emails: sb({ + select: vi.fn().mockReturnThis(), + in: vi.fn().mockResolvedValue({ + data: [{ user_id: 'm1', email: 'test_mentor@example.com' }], + }), + }), + help_requests: sb({ + select: vi.fn().mockReturnThis(), + eq: vi.fn().mockReturnThis(), + maybeSingle: vi.fn().mockResolvedValue({ + data: { reason: 'stuck on tests', pr_url: 'https://github.com/foo/bar/pull/1' }, + }), + }), + }); + + vi.mocked(rankReviewers).mockReturnValue([ + { + userId: 'm1', + level: 2, + sameOrgReviewed: false, + sameCohort: false, + languageMatch: true, + }, + ]); + + await run({ event: ev(), step }); + + expect(sendHelpDispatchEmail).toHaveBeenCalledWith({ + to: 'test_mentor@example.com', + mentorHandle: 'mentor1', + menteeHandle: 'mentee', + prUrl: 'https://github.com/foo/bar/pull/1', + helpReason: 'stuck on tests', + }); + }); }); diff --git a/supabase/migrations/0022_add_profile_email.sql b/supabase/migrations/0022_add_profile_email.sql deleted file mode 100644 index 48d8739c..00000000 --- a/supabase/migrations/0022_add_profile_email.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Add email column to profiles -ALTER TABLE profiles -ADD COLUMN IF NOT EXISTS email text; diff --git a/supabase/migrations/0023_profile_emails.sql b/supabase/migrations/0023_profile_emails.sql index f5e33864..b9ec633e 100644 --- a/supabase/migrations/0023_profile_emails.sql +++ b/supabase/migrations/0023_profile_emails.sql @@ -16,13 +16,3 @@ DROP POLICY IF EXISTS profile_emails_read_own ON profile_emails; CREATE POLICY profile_emails_read_own ON profile_emails FOR SELECT USING (auth.uid() = user_id); --- Drop the email column from the public profiles table (if it exists) -DO $$ -BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'profiles' AND column_name = 'email' - ) THEN - ALTER TABLE profiles DROP COLUMN email; - END IF; -END$$;