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