166 lines
4.7 KiB
JavaScript
166 lines
4.7 KiB
JavaScript
const UserModel = require('../models/UserModel')
|
||
const { downloadImageToProfiles } = require('./downloadRemoteImage')
|
||
|
||
async function findUserByInstagramId (instagramId) {
|
||
if (!instagramId) return null
|
||
return UserModel.findOne({ instagram_id: String(instagramId) })
|
||
}
|
||
|
||
/**
|
||
* Unlike Google, Instagram OAuth never returns an email — the only stable
|
||
* identity we have is instagram_id, so there is no email-based merge path.
|
||
*/
|
||
async function findExistingInstagramUser ({ instagramId }) {
|
||
return findUserByInstagramId(instagramId)
|
||
}
|
||
|
||
async function patchInstagramUser (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 createInstagramUser ({
|
||
instagramId,
|
||
username,
|
||
accountType,
|
||
tokenEnc,
|
||
tokenExpiresAt,
|
||
profilePictureUrl
|
||
}) {
|
||
const now = new Date()
|
||
const doc = {
|
||
instagram_id: String(instagramId),
|
||
instagram_username: username || null,
|
||
instagram_account_type: accountType || null,
|
||
instagram_access_token_enc: tokenEnc || null,
|
||
instagram_token_expires_at: tokenExpiresAt || null,
|
||
instagram_connected_at: now,
|
||
auth_provider: 'instagram',
|
||
createdAt: now,
|
||
updatedAt: now
|
||
}
|
||
|
||
if (profilePictureUrl) {
|
||
try {
|
||
doc.profile_image = await downloadImageToProfiles(profilePictureUrl, `instagram-${instagramId}`)
|
||
} catch (err) {
|
||
// آواتار اینستاگرام صرفاً یک مقدار پیشفرض دلپذیر است — شکست دانلود نباید ثبتنام را متوقف کند
|
||
console.error('[createInstagramUser] avatar download failed:', err.message)
|
||
}
|
||
}
|
||
|
||
try {
|
||
const result = await UserModel.collection.insertOne(doc)
|
||
const created = await UserModel.findById(result.insertedId)
|
||
if (created) return created
|
||
return findExistingInstagramUser({ instagramId })
|
||
} catch (err) {
|
||
if (err?.code !== 11000) throw err
|
||
|
||
const existing = await findExistingInstagramUser({ instagramId })
|
||
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 findExistingInstagramUser({ instagramId })
|
||
if (again) return again
|
||
}
|
||
throw retryErr
|
||
}
|
||
}
|
||
|
||
if (keyValue?.instagram_id) {
|
||
const byInstagram = await findUserByInstagramId(keyValue.instagram_id)
|
||
if (byInstagram) return byInstagram
|
||
}
|
||
|
||
throw err
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Login/register via Instagram:
|
||
* - existing instagram_id → login (refresh token/profile fields)
|
||
* - otherwise create new user
|
||
*
|
||
* No email-based matching (Instagram doesn't provide one), so — unlike Google —
|
||
* there is no EMAIL_LINKED_TO_OTHER conflict case here.
|
||
*/
|
||
async function findOrCreateInstagramUser ({
|
||
instagramId,
|
||
username,
|
||
accountType,
|
||
tokenEnc,
|
||
tokenExpiresAt,
|
||
profilePictureUrl
|
||
}) {
|
||
const normalizedInstagramId = String(instagramId || '').trim()
|
||
|
||
if (!normalizedInstagramId) {
|
||
const err = new Error('MISSING_INSTAGRAM_IDENTITY')
|
||
err.code = 'MISSING_INSTAGRAM_IDENTITY'
|
||
throw err
|
||
}
|
||
|
||
const user = await findUserByInstagramId(normalizedInstagramId)
|
||
|
||
if (user) {
|
||
return (
|
||
(await patchInstagramUser(user._id, {
|
||
instagram_username: username,
|
||
instagram_account_type: accountType,
|
||
instagram_access_token_enc: tokenEnc,
|
||
instagram_token_expires_at: tokenExpiresAt
|
||
})) || user
|
||
)
|
||
}
|
||
|
||
return createInstagramUser({
|
||
instagramId: normalizedInstagramId,
|
||
username,
|
||
accountType,
|
||
tokenEnc,
|
||
tokenExpiresAt
|
||
})
|
||
}
|
||
|
||
module.exports = {
|
||
findUserByInstagramId,
|
||
findExistingInstagramUser,
|
||
patchInstagramUser,
|
||
createInstagramUser,
|
||
findOrCreateInstagramUser
|
||
}
|