const UserModel = require('../models/UserModel') function escapeRegex (value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } function normalizeEmail (email) { return String(email || '').toLowerCase().trim() } async function findUserByGoogleId (googleId) { if (!googleId) return null return UserModel.findOne({ google_id: String(googleId) }) } async function findUserByEmail (email) { if (!email) return null const normalized = normalizeEmail(email) return UserModel.findOne({ $or: [ { email: normalized }, { email: new RegExp(`^${escapeRegex(normalized)}$`, 'i') }, ], }) } async function findExistingGoogleUser ({ googleId, email }) { const byGoogle = await findUserByGoogleId(googleId) if (byGoogle) return byGoogle return findUserByEmail(email) } async function patchGoogleUser (userId, fields) { // فقط فیلدهای واقعی؛ هرگز uniqueهای sparse را null نکن const $set = {} for (const [key, value] of Object.entries(fields || {})) { if (value === undefined || value === null || value === '') continue $set[key] = value } if (Object.keys($set).length === 0) { return UserModel.findById(userId) } await UserModel.collection.updateOne( { _id: userId }, { $set } ) return UserModel.findById(userId) } function getDuplicateKeyValue (err) { return err?.keyValue && typeof err.keyValue === 'object' ? err.keyValue : null } /** * ساخت کاربر گوگل بدون اعمال defaultهای null روی فیلدهای unique/sparse */ async function createGoogleUser ({ googleId, email }) { const now = new Date() const doc = { google_id: String(googleId), email: normalizeEmail(email), email_verified: true, auth_provider: 'google', createdAt: now, updatedAt: now, } try { const result = await UserModel.collection.insertOne(doc) const created = await UserModel.findById(result.insertedId) if (created) return created return findExistingGoogleUser({ googleId, email }) } catch (err) { if (err?.code !== 11000) throw err const existing = await findExistingGoogleUser({ googleId, email }) if (existing) return existing const keyValue = getDuplicateKeyValue(err) // ایندکس غیر-sparse روی mobile:null — nullها را پاک کن و دوباره بساز if (keyValue && Object.prototype.hasOwnProperty.call(keyValue, 'mobile')) { try { await UserModel.collection.updateMany( { mobile: null }, { $unset: { mobile: '' } } ) const retry = await UserModel.collection.insertOne(doc) return UserModel.findById(retry.insertedId) } catch (retryErr) { if (retryErr?.code === 11000) { const again = await findExistingGoogleUser({ googleId, email }) if (again) return again } throw retryErr } } if (keyValue?.email) { const byEmail = await findUserByEmail(keyValue.email) if (byEmail) { return ( (await patchGoogleUser(byEmail._id, { google_id: String(googleId), email: normalizeEmail(byEmail.email || email), email_verified: true, })) || byEmail ) } } if (keyValue?.google_id) { const byGoogle = await findUserByGoogleId(keyValue.google_id) if (byGoogle) return byGoogle } throw err } } /** * Login/register via Google: * - existing google_id → login * - existing email without google / same google → link + login * - existing email with different google_id → conflict * - otherwise create new user */ async function findOrCreateGoogleUser ({ googleId, email }) { const normalizedEmail = normalizeEmail(email) const normalizedGoogleId = String(googleId || '').trim() if (!normalizedGoogleId || !normalizedEmail) { const err = new Error('MISSING_GOOGLE_IDENTITY') err.code = 'MISSING_GOOGLE_IDENTITY' throw err } let user = await findUserByGoogleId(normalizedGoogleId) if (user) { const patch = {} if (!user.email) patch.email = normalizedEmail if (!user.email_verified) patch.email_verified = true if (Object.keys(patch).length > 0) { return (await patchGoogleUser(user._id, patch)) || user } return user } user = await findUserByEmail(normalizedEmail) if (user) { const existingGoogleId = user.google_id ? String(user.google_id).trim() : '' if (existingGoogleId && existingGoogleId !== normalizedGoogleId) { const conflict = new Error('EMAIL_LINKED_TO_OTHER_GOOGLE') conflict.code = 'EMAIL_LINKED_TO_OTHER_GOOGLE' throw conflict } return ( (await patchGoogleUser(user._id, { google_id: normalizedGoogleId, email: normalizeEmail(user.email || normalizedEmail), email_verified: true, })) || user ) } return createGoogleUser({ googleId: normalizedGoogleId, email: normalizedEmail, }) } module.exports = { findOrCreateGoogleUser, findExistingGoogleUser, }