855 lines
18 KiB
JavaScript
855 lines
18 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
|
|
},
|
|
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'
|
|
},
|
|
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
|
|
},
|
|
referral_code: {
|
|
type: String,
|
|
trim: true,
|
|
minLength: 1,
|
|
maxLength: 512,
|
|
default: null
|
|
},
|
|
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
|
|
},
|
|
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
|
|
},
|
|
// یک رشتهی تصادفیِ ۳۲بایتیِ per-account که با درخواستِ احرازهویتشده به همین
|
|
// اکانت، از هر دستگاهی (وب/اندروید) قابلدریافته — بهجای PIN دستی، بهعنوانِ
|
|
// رمزِ رمزنگاریِ e2ee_key_backup استفاده میشه تا بازیابیِ هویتِ E2EE روی دستگاهِ
|
|
// جدید کاملاً خودکار و بدون دخالتِ کاربر انجام بشه. توجه: این یعنی تضمینِ
|
|
// end-to-end دیگه واقعی نیست — هرکسی که به این فیلد (یا سرور) دسترسی پیدا کنه
|
|
// میتونه بکآپ رو باز کنه؛ این تصمیم آگاهانه به نفعِ هیچوقتگمنشدنِ پیامها گرفته شده
|
|
e2ee_auto_backup_secret: {
|
|
type: String,
|
|
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
|