Files
modstagram-back/models/UserModel.js
payacom d4ebc20aa8 reels
2026-10-01 21:12:05 +03:30

897 lines
20 KiB
JavaScript

const mongoose = require('mongoose')
const mongoosePaginate = require('mongoose-paginate-v2')
const timestamp = require('mongoose-timestamp')
const userSchema = new mongoose.Schema({
mobile: {
type: String,
required: false,
trim: true,
unique: true,
sparse: true
},
otp: {
type: String,
required: false,
trim: true,
minLength: 6,
maxLength: 6,
default: null
},
otpSentAt: {
type: Date,
required: false,
default: null
},
// شماره‌ای که کاربرِ گوگلی/اینستاگرامیِ بدونِ موبایل، در حینِ فرایندِ تک‌مرحله‌ایِ
// «ثبت شماره موبایل» (تنظیمات) وارد کرده و منتظرِ تاییدِ OTP ـه — بعد از تایید،
// به mobile منتقل می‌شه و خودش null می‌شه
pending_mobile: {
type: String,
required: false,
trim: true,
default: null
},
email: {
type: String,
required: false,
trim: true,
lowercase: true,
unique: true,
sparse: true
},
google_id: {
type: String,
required: false,
trim: true,
unique: true,
sparse: true
},
auth_provider: {
type: String,
required: false,
enum: ['mobile', 'google', 'instagram'],
default: 'mobile'
},
instagram_id: {
type: String,
required: false,
trim: true,
unique: true,
sparse: true
},
instagram_username: {
type: String,
required: false,
trim: true,
default: null
},
instagram_account_type: {
type: String,
required: false,
enum: ['BUSINESS', 'MEDIA_CREATOR', null],
default: null
},
instagram_access_token_enc: {
type: String,
required: false,
default: null
},
instagram_token_expires_at: {
type: Date,
required: false,
default: null
},
instagram_connected_at: {
type: Date,
required: false,
default: null
},
instagram_last_synced_at: {
type: Date,
required: false,
default: null
},
instagram_sync_prefs: {
bio: { type: Boolean, default: false },
posts: { type: Boolean, default: false },
reels: { type: Boolean, default: false },
profile_picture: { type: Boolean, default: false },
auto_update: { type: Boolean, default: false }
},
email_verified: {
type: Boolean,
required: false,
default: false
},
totp_secret: {
type: String,
required: false,
default: null
},
totp_pending_secret: {
type: String,
required: false,
default: null
},
totp_enabled: {
type: Boolean,
required: false,
default: false
},
user_name: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
password: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
first_name: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
last_name: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
user_type: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
is_verified: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: 'none'
},
license_verified: {
type: Boolean,
required: false,
default: false
},
gold_badge: {
type: Boolean,
required: false,
default: false
},
verify_badge: {
type: String,
enum: ['none', 'gray', 'blue', 'green', 'gold'],
default: 'none'
},
// کاربرِ زیرمجموعه — یه سندِ UserModel معمولی که به‌جای اکانتِ خودش، اکانتِ
// parent_account_id رو مدیریت می‌کنه؛ فقط با پرمیژن‌های sub_user_permissions
parent_account_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'UserModel',
default: null
},
is_sub_user: {
type: Boolean,
default: false
},
sub_user_active: {
type: Boolean,
default: true
},
sub_user_permissions: {
type: [String],
default: []
},
profile_image: {
type: String,
required: false,
trim: true,
default: null
},
national_card_image: {
type: String,
required: false,
trim: true,
default: null
},
expertise: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
sub_expertise: {
type: [String], // تغییر نوع به آرایه از استرینگ
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
display_sub_expertise: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
gender: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
height: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
weight: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
size: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
eye_color: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
hair_color: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
marital_status: {
type: String,
enum: ['single', 'divorced', 'widowed', 'not_in_relationship', 'prefer_not_to_say'],
required: false,
default: null
},
relationship_goal: {
type: [{
type: String,
enum: [
'friendship',
'serious_partner',
'marriage_intro',
'emotional_relationship',
'travel_event_companion',
'social_networking',
'not_sure'
]
}],
required: false,
default: []
},
seeking_gender: {
type: String,
enum: ['female', 'male', 'both', 'not_important'],
required: false,
default: null
},
desired_age_range: {
type: String,
enum: ['18_24', '25_30', '31_35', '36_40', '41_50', 'over_50', 'not_important'],
required: false,
default: null
},
personality_traits: {
type: [{
type: String,
enum: [
'kind',
'loyal',
'funny',
'calm',
'energetic',
'responsible',
'ambitious',
'emotional',
'logical',
'patient',
'honest',
'romantic',
'independent',
'family_oriented',
'adventurous',
'introvert',
'extrovert'
]
}],
required: false,
default: []
},
lifestyle: {
type: [{
type: String,
enum: [
'active_healthy',
'homebody_calm',
'social_outgoing',
'busy_career',
'traveler',
'night_owl',
'early_riser',
'minimalist',
'luxury_lover'
]
}],
required: false,
default: []
},
interests: {
type: [{
type: String,
enum: [
'movies_series',
'music',
'books_reading',
'sports',
'cooking',
'photography',
'travel',
'nature',
'gaming',
'art_painting',
'fashion',
'pets',
'dancing',
'language_learning'
]
}],
required: false,
default: []
},
values: {
type: [{
type: String,
enum: [
'family',
'career_growth',
'religion_spirituality',
'personal_freedom',
'financial_stability',
'health_fitness',
'personal_growth',
'excitement_new_experiences'
]
}],
required: false,
default: []
},
relationship_view: {
type: String,
enum: [
'traditional',
'modern',
'long_term',
'friendship_first',
'marriage_soon',
'friendship_only_for_now'
],
required: false,
default: null
},
geo_preference: {
type: String,
enum: [
'same_city_only',
'nearby_cities',
'whole_province',
'whole_iran',
'even_abroad',
'online_is_fine'
],
required: false,
default: null
},
children_status: {
type: [{
type: String,
enum: ['no_children', 'has_children', 'open_to_children', 'not_now', 'prefer_not_to_say']
}],
required: false,
default: []
},
smoking_status: {
type: String,
enum: ['none', 'sometimes', 'yes', 'doesnt_matter', 'prefer_not_to_say'],
required: false,
default: null
},
residence_type: {
type: String,
enum: ['owner', 'tenant'],
required: false,
default: null
},
residence_area: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
vehicle_type: {
type: String,
enum: ['none', 'car', 'motorcycle'],
required: false,
default: null
},
vehicle_details: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
personal_style: {
type: String,
enum: ['casual', 'classic', 'minimal', 'formal', 'street', 'romantic'],
required: false,
default: null
},
job: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
income_range: {
type: String,
enum: [
'under_40m', '40m_50m', '50m_60m', '60m_70m', '70m_80m',
'80m_90m', '90m_100m', '100m_150m', '150m_200m', 'over_200m'
],
required: false,
default: null
},
personal_details_public: {
type: Boolean,
required: false,
default: false
},
cooperation_type: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
cooperation_abroad: {
type: Boolean,
required: false,
default: null
},
conversation_projects: {
type: Boolean,
required: false,
default: null
},
bio: {
type: String,
required: false,
minLength: 1,
maxLength: 1024,
default: null
},
national_code: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
birthday: {
type: Date,
required: false,
default: null
},
is_Register: {
type: String,
default: false
},
shaba: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
address: {
type: String,
trim: true,
minLength: 1,
maxLength: 512,
default: null
},
country: {
type: Object,
default: null
},
province: {
type: Object,
default: null
},
city: {
type: Object,
default: null
},
lat: {
type: String,
trim: true,
minLength: 1,
maxLength: 512,
default: null
},
lng: {
type: String,
trim: true,
minLength: 1,
maxLength: 512,
default: null
},
show_location: {
type: Boolean,
required: false,
default: null
},
// وقتی «ریست الگوریتم» زده می‌شه ست می‌شه — buildUserExploreSignals لایک/کامنت/آفر
// قبل از این تاریخ رو نادیده می‌گیره تا کاربر واقعاً مثل تازه‌وارد رفتار ببینه، نه
// فقط کش رتبه‌بندی پاک بشه درحالی‌که سیگنال‌های شخصی‌سازی قدیمی هنوز اثر دارن
algorithm_reset_at: {
type: Date,
default: null
},
rate: {
type: String,
trim: true,
maxLength: 512,
default: null
},
// کد یکتای معرفی‌کننده — با اولین درخواستِ GET /users/referral-code به‌صورت lazy
// ساخته می‌شه (referralController.js)، نه از قبل برای همه‌ی کاربرها. unique+sparse
// چون این کد توی لینکِ عمومیِ اشتراک‌گذاری میره و باید واقعاً غیرتکراری باشه
referral_code: {
type: String,
trim: true,
minLength: 1,
maxLength: 512,
default: null,
unique: true,
sparse: true
},
// کاربری که این کاربر رو با لینکِ معرفی‌اش وارد اپ کرده — فقط توی اولین مرحله‌ی
// ثبت‌نام (POST /register با mobile) ست می‌شه، دیگه هیچ‌جا عوض نمی‌شه
referred_by: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null
},
// جلوگیری از پرداخت دوباره‌ی پاداشِ معرفی — با تکمیلِ ثبت‌نام (userTypeController)
// یک‌بار true می‌شه؛ اگه این مرحله به هر دلیلی دوباره صدا زده بشه، پرداختِ تکراری نمی‌شه
referral_rewarded: {
type: Boolean,
default: false
},
shop_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Shop',
default: null
},
last_online: {
type: Date, // ذخیره تاریخ و زمان آخرین بازدید
default: Date.now // تنظیم تاریخ به تاریخ فعلی
},
business_license: {
type: String,
required: false,
trim: true,
default: null
},
user_level: {
type: String,
trim: true,
minLength: 1,
maxLength: 512,
// پیش‌فرض قبلاً null بود — یعنی تا اولین کامنت امتیازدار روی کاربر (recalculateUserRating
// در utils/commentRating.js)، هیچ سطحی نداشت. الان از همون رشته‌ای که getUserLevel با
// صفر ستاره برمی‌گردونه استفاده می‌کنیم، تا هر کاربر از ابتدا سطح واقعی داشته باشه
default: 'تازه وارد'
},
user_score: {
type: String,
trim: true,
minLength: 1,
maxLength: 512,
default: null
},
last_post: {
type: Object,
default: null
},
// user_ratings: [{
// project_id: {
// type: mongoose.Schema.Types.ObjectId,
// ref: 'Project',
// required: true
// },
// creator_id: {
// type: mongoose.Schema.Types.ObjectId,
// ref: 'User',
// required: true
// },
// rating: {
// type: Number,
// required: true
// },
// comment: {
// type: String,
// trim: true
// }
// }],
daily_free_request: {
type: Number,
default: 1
},
last_free_request_date: {
type: Date,
default: null
},
// تعداد بیلبورد رایگانی که این کاربر تا الان ثبت کرده — سقف کلی (نه روزانه) طبق
// utils/billboardFreeLimit.js، مستقل از daily_free_request که بین پروژه/بیلبورد/
// درخواست مشترکه
free_billboard_count: {
type: Number,
default: 0
},
blocked_by: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
blocked_users: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
block_status: {
type: Boolean,
required: false,
default: null
},
blocked_until: {
type: Date,
required: false,
default: null
},
block_permanent: {
type: Boolean,
required: false,
default: false
},
lifetime_free_creations: {
type: Number,
default: 0
},
advertising_profiles: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'AdvertisingProfile'
}],
services: [{
title: {
type: String,
required: true,
trim: true
},
originalPrice: {
type: String,
required: true
},
discountPrice: {
type: String,
required: false
},
discountPercentage: {
type: Number,
required: false
},
status: {
type: String,
default: "UnderReview",
required: false
},
image: {
type: String,
required: false,
trim: true
}
}],
monthly_free_offer: {
type: Number,
default: 1
},
last_free_offer_date: {
type: Date,
default: null
},
account_status: {
type: String,
enum: ['active', 'deactivated'],
default: 'active',
index: true
},
deactivated_at: {
type: Date,
default: null
},
reactivation_locked_until: {
type: Date,
default: null
},
scheduled_purge_at: {
type: Date,
default: null
},
allow_save_posts: {
type: Boolean,
default: true
},
ghost_mode: {
type: Boolean,
default: false
},
post_location_enabled: {
type: Boolean,
default: false
},
is_private: {
type: Boolean,
default: false
},
// تنظیمات کیف پول — امکان دریافت هدیه (پروفایل + چت) و اینکه آیا درخواست همکاری
// (offer) به این کاربر رایگانه یا طبق قیمت معمول پرداختی می‌شه
gift_enabled: {
type: Boolean,
default: true
},
offer_payment_enabled: {
type: Boolean,
default: true
},
preferred_language: {
type: String,
enum: require('../utils/appLanguages').APP_LANGUAGE_CODES,
default: 'fa'
},
e2ee_public_key: {
type: String,
required: false,
default: null
},
// بک‌آپ کلید خصوصیِ E2EE رمزنگاری‌شده با رمز/PIN انتخابیِ کاربر (سرور هیچ‌وقت PIN یا
// کلید خام رو نمی‌بینه، فقط این بلاکِ رمزنگاری‌شده رو ذخیره می‌کنه) — برای بازیابیِ
// هویتِ رمزنگاری وقتی هیچ دستگاه دیگه‌ای لینک‌شده در دسترس نیست
e2ee_key_backup: {
type: {
salt: String,
iv: String,
ciphertext: String,
iterations: Number
},
required: false,
default: null
},
pinned_post_ids: {
type: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Post'
}],
default: []
},
// توکن‌های FCM دستگاه‌های کاربر — آرایه چون یک کاربر می‌تونه چند دستگاه اندروید داشته باشه
fcm_tokens: {
type: [String],
default: []
},
// توکن‌های FCM Web Push (مرورگر) — جدا از fcm_tokens چون توکن‌های وب و اندروید فرمت/طول عمر متفاوتی دارن
fcm_web_tokens: {
type: [String],
default: []
},
// contactInfo: {
// phone: String,
// mobile: String,
// telegramLink: String,
// whatsappNumber: String,
// instagramLink: String,
// saveInfoForNextAds: { type: Boolean, default: false }
// },
// adTotalRatings: {
// type: Number,
// default: 0
// },
// adRatingsCount: {
// type: Number,
// default: 0
// },
// adAverageRating: {
// type: Number,
// default: 0
// }
})
userSchema.plugin(timestamp)
userSchema.plugin(mongoosePaginate)
// وقتی فیلدهای مرتبط با «یافتن همکار» تغییر می‌کنن (یا ثبت‌نام کامل می‌شه)، جستجوهای
// ذخیره‌شده‌ی فعال رو چک می‌کنه — همه‌ی کنترلرهای ویرایش پروفایل در نهایت save() صدا
// می‌زنن، پس اینجا تک‌نقطه‌ای‌ترین جاییه که بدون تکرار در هر کنترلر جدا کار می‌کنه.
//
// isModified() باید در pre('save') خونده بشه، نه post('save') — مونگوس وضعیت
// «تغییر کرده» رو به‌عنوان بخشی از خود save() پاک می‌کنه، پس تا زمانی که به
// post('save') برسیم isModified() برای همه‌ی فیلدها false برمی‌گرده. مقدار رو
// از طریق $locals بین pre و post منتقل می‌کنیم.
userSchema.pre('save', function (next) {
this.$locals.partnerAlertsRelevant = require('../utils/partnerMatch').shouldCheck(this)
next()
})
userSchema.post('save', function (doc) {
if (doc.$locals.partnerAlertsRelevant) {
require('../utils/partnerMatch').checkPartnerAlerts(doc).catch(() => {})
}
})
const UserModel = mongoose.model('User', userSchema)
module.exports = UserModel