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:
co-authored by
Rotimi Best
Cursor Agent
parent
2fb6b1c477
commit
c0f82d9573
@@ -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();
|
||||
Reference in New Issue
Block a user