fix(auth): sync profile email verification with auth user (#726)

* fix(auth): sync profile email verification with auth user

Repair stale profile rows where user.email_verified is true but
profile.is_email_verified is false, which blocked the LMS with the
verify-email modal while send-verification-email returned
EMAIL_IS_ALREADY_VERIFIED.

Sync on session login, account fetch, and user update; update the
modal to respect session emailVerified and self-heal on that error.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* chore(db): add backfill script for stale profile email verification

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* docs(db): document profile email verification backfill command

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(auth): never demote profile email verification on sync

Greptile: self-hosted first signup can verify profile before
user.email_verified is set. Sync now only promotes verification.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(auth): promote user.email_verified when profile is verified

Self-hosted bootstrap and other flows can set profile.is_email_verified
before user.email_verified. Sync and createProfileHook now update both
tables when promoting verification.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
Rotimi Best
2026-07-11 06:38:28 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 2fb6b1c477
commit c0f82d9573
10 changed files with 289 additions and 34 deletions
@@ -0,0 +1,105 @@
/**
* Backfills profile.is_email_verified for users already verified in auth
* (user.email_verified or a linked OAuth/SSO account) but stale on profile.
*
* Usage:
* pnpm db:backfill-profile-email-verification # dry run (default)
* pnpm db:backfill-profile-email-verification -- --execute # apply updates
*/
import 'dotenv/config';
import postgres from 'postgres';
const connectionString = process.env.DATABASE_URL ?? process.env.PRIVATE_DATABASE_URL ?? '';
const shouldExecute = process.argv.includes('--execute');
if (!connectionString) {
console.error('DATABASE_URL or PRIVATE_DATABASE_URL environment variable is required');
process.exit(1);
}
const AFFECTED_PROFILES_SQL = `
SELECT
p.id,
u.email,
u.email_verified AS user_email_verified,
p.is_email_verified AS profile_is_email_verified
FROM profile p
JOIN "user" u ON u.id = p.id
WHERE COALESCE(p.is_email_verified, false) = false
AND (
u.email_verified = true
OR EXISTS (
SELECT 1
FROM account a
WHERE a.user_id = u.id
AND a.provider_id <> 'credential'
)
)
`;
async function main() {
const sql = postgres(connectionString);
try {
const affected = await sql.unsafe(AFFECTED_PROFILES_SQL);
console.log(`Found ${affected.length} profile(s) to backfill.`);
if (affected.length === 0) {
console.log('Nothing to do.');
return;
}
if (!shouldExecute) {
console.log('Dry run only. Re-run with --execute to apply updates.');
console.log('Sample (up to 10):');
for (const row of affected.slice(0, 10)) {
console.log(
` ${row.id} ${row.email} user=${row.user_email_verified} profile=${row.profile_is_email_verified}`
);
}
return;
}
const updated = await sql`
UPDATE profile p
SET
is_email_verified = true,
verified_at = COALESCE(p.verified_at, NOW()),
updated_at = NOW()
FROM "user" u
WHERE p.id = u.id
AND COALESCE(p.is_email_verified, false) = false
AND (
u.email_verified = true
OR EXISTS (
SELECT 1
FROM account a
WHERE a.user_id = u.id
AND a.provider_id <> 'credential'
)
)
RETURNING p.id, p.email
`;
const remaining = await sql.unsafe(`SELECT COUNT(*)::int AS count FROM (${AFFECTED_PROFILES_SQL}) AS affected`);
const remainingCount = remaining[0]?.count ?? 0;
console.log(`Updated ${updated.length} profile(s).`);
console.log(`Remaining mismatches: ${remainingCount}`);
if (remainingCount > 0) {
console.warn('Some rows were not updated — inspect manually before retrying.');
process.exit(1);
}
console.log('Backfill complete.');
} catch (error) {
console.error('backfill-profile-email-verification error:', error);
throw error;
} finally {
await sql.end();
}
}
main();