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 <[email protected]>

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

Co-authored-by: Rotimi Best <[email protected]>

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

Co-authored-by: Rotimi Best <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
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
+8 -1
View File
@@ -5,7 +5,12 @@ import {
getOrganizationByProfileId,
hasOrgMemberByProfileIdOrEmail
} from '@cio/db/queries/organization';
import { getProfileByEmail, getProfileById, updateProfile } from '@cio/db/queries/auth';
import {
getProfileByEmail,
getProfileById,
syncProfileEmailVerificationFromAuthUser,
updateProfile
} from '@cio/db/queries/auth';
import type { OrganizationWithMemberAndPlans } from '@cio/db/queries/organization/types';
import { ROLE } from '@cio/utils/constants';
@@ -38,6 +43,8 @@ export async function getAccountData(userId: string): Promise<GetAccountDataResu
throw new AppError(`Account not found for user ID: ${userId}`, ErrorCodes.ACCOUNT_NOT_FOUND, 404);
}
profile = (await syncProfileEmailVerificationFromAuthUser(userId)) ?? profile;
// Self-hosted: auto-add user as student to the single org if they are not a member.
// Skip if they already have membership (by profileId) or a pending invite (by email).
if (env.PUBLIC_IS_SELFHOSTED === 'true' && organizations.length === 0) {