183 lines
5.0 KiB
JavaScript
183 lines
5.0 KiB
JavaScript
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,
|
||
}
|