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
@@ -1,24 +1,13 @@
|
||||
import * as schema from '@db/schema';
|
||||
|
||||
import { and, eq, ne } from 'drizzle-orm';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { User } from 'better-auth';
|
||||
import { db } from '@db/drizzle';
|
||||
import { hasVerifiedEmailProvider } from '@db/queries/auth/profile';
|
||||
import { getOrganizationCount } from '@db/queries/organization';
|
||||
import { ssoProvisioningHook } from './sso-provisioning';
|
||||
|
||||
/**
|
||||
* True if the user has any non-credential account (OAuth/SSO). Those providers verify email.
|
||||
*/
|
||||
export async function hasVerifiedEmailProvider(userId: string): Promise<boolean> {
|
||||
const accounts = await db
|
||||
.select({ providerId: schema.account.providerId })
|
||||
.from(schema.account)
|
||||
.where(and(eq(schema.account.userId, userId), ne(schema.account.providerId, 'credential')))
|
||||
.limit(1);
|
||||
return accounts.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when this is a first-time signup on a self-hosted instance (no orgs exist yet).
|
||||
* The first signup creates the only org; we auto-verify their email.
|
||||
@@ -59,6 +48,14 @@ export const createProfileHook = async (user: User, _request?: Request) => {
|
||||
}
|
||||
|
||||
if (existingProfile.length) {
|
||||
if (!existingProfile[0].isEmailVerified && isEmailVerified) {
|
||||
await db.update(schema.user).set({ emailVerified: true }).where(eq(schema.user.id, user.id));
|
||||
await db
|
||||
.update(schema.profile)
|
||||
.set({ isEmailVerified: true, verifiedAt: new Date().toISOString() })
|
||||
.where(eq(schema.profile.id, user.id));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import * as schema from '@db/schema';
|
||||
|
||||
import { User } from 'better-auth';
|
||||
import { db } from '@db/drizzle';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { hasVerifiedEmailProvider } from './create-profile';
|
||||
import { syncProfileEmailVerificationFromAuthUser } from '@db/queries/auth/profile';
|
||||
|
||||
export const syncUserWithProfile = async (user: User) => {
|
||||
console.log('[auth] syncUserWithProfile: running', { userId: user?.id });
|
||||
@@ -13,15 +9,8 @@ export const syncUserWithProfile = async (user: User) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const isEmailVerified = user.emailVerified || (await hasVerifiedEmailProvider(user.id));
|
||||
await db
|
||||
.update(schema.profile)
|
||||
.set({
|
||||
email: user.email,
|
||||
isEmailVerified
|
||||
})
|
||||
.where(eq(schema.profile.id, user.id));
|
||||
await syncProfileEmailVerificationFromAuthUser(user.id);
|
||||
} catch (error) {
|
||||
console.error('Error marking profile as verified:', error);
|
||||
console.error('Error syncing profile email verification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user