full
This commit is contained in:
182
utils/googleUserAuth.js
Normal file
182
utils/googleUserAuth.js
Normal file
@@ -0,0 +1,182 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user