This commit is contained in:
payacom
2026-09-13 21:32:36 +03:30
parent 90c5f94b57
commit c302aea177
49 changed files with 4063 additions and 920 deletions

View File

@@ -71,6 +71,23 @@ const adminSchema = new mongoose.Schema({
last_online: {
type: Date, // ذخیره تاریخ و زمان آخرین بازدید
default: Date.now // تنظیم تاریخ به تاریخ فعلی
},
// super_admin: به همه‌چیز دسترسی داره (بدون نیاز به لیست permissions). staff: فقط به
// مواردی که توی permissions هست. پیش‌فرض super_admin تا ادمین‌های از قبل موجود بعد
// از این تغییر دسترسیشون رو از دست ندن
role: {
type: String,
enum: ['super_admin', 'staff'],
default: 'super_admin'
},
permissions: {
type: [String],
default: []
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Admin',
default: null
}
})

View File

@@ -0,0 +1,66 @@
const mongoose = require('mongoose')
// رویدادهای خام رفتار کاربر (بازدید صفحه، کلیک، شروع/پایان نشست) — برای بخش
// «آنالیتیکس» پنل. هر رکورد سبک نگه داشته می‌شه چون حجم بالایی داره؛ تجمیع/گزارش‌گیری
// توی کنترلرهای panel/analytics انجام می‌شه، نه اینجا
const AnalyticsEventSchema = new mongoose.Schema(
{
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null,
index: true
},
sessionId: {
type: String,
required: true,
index: true
},
type: {
type: String,
enum: ['session_start', 'page_view', 'click'],
required: true,
index: true
},
// page_view/click: مسیر صفحه‌ای که رویداد توش رخ داده
path: {
type: String,
default: null
},
// فقط page_view: چند میلی‌ثانیه کاربر روی این صفحه مونده (موقع رفتن به صفحه‌ی
// بعدی یا بستن تب محاسبه و ارسال می‌شه)
durationMs: {
type: Number,
default: null
},
// فقط click: شناسه‌ی قابل‌خوندنِ عنصر کلیک‌شده (data-analytics-id یا برچسب/متن آن)
label: {
type: String,
default: null
},
platform: {
type: String,
enum: ['web', 'android', 'ios'],
required: true,
index: true
},
device: {
type: String,
default: null
},
browser: {
type: String,
default: null
},
os: {
type: String,
default: null
}
},
{ timestamps: true }
)
AnalyticsEventSchema.index({ createdAt: -1 })
AnalyticsEventSchema.index({ type: 1, createdAt: -1 })
module.exports = mongoose.model('AnalyticsEvent', AnalyticsEventSchema)

44
models/AuditLogModel.js Normal file
View File

@@ -0,0 +1,44 @@
const mongoose = require('mongoose')
// تاریخچه‌ی سیستم — هر عملِ نوشتاری (POST/PUT/PATCH/DELETE) که یه ادمین توی پنل انجام
// می‌ده خودکار اینجا ثبت می‌شه (middlewares/auditLogger.js)، بدون نیاز به لاگ دستی
// توی تک‌تکِ کنترلرها
const AuditLogSchema = new mongoose.Schema(
{
adminId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Admin',
required: true,
index: true
},
adminUserName: {
type: String,
default: null
},
method: {
type: String,
required: true
},
path: {
type: String,
required: true,
index: true
},
statusCode: {
type: Number,
default: null
},
// خلاصه‌ی بدنه‌ی درخواست (پسورد/توکن حذف می‌شه) — برای اینکه معلوم باشه دقیقاً
// چه تغییری داده شده
body: {
type: mongoose.Schema.Types.Mixed,
default: null
}
},
{ timestamps: true }
)
AuditLogSchema.index({ createdAt: -1 })
AuditLogSchema.index({ adminId: 1, createdAt: -1 })
module.exports = mongoose.model('AuditLog', AuditLogSchema)

View File

@@ -23,7 +23,7 @@ const offerSchema = new mongoose.Schema({
ref: 'Payment',
default: null
},
// سهم ۳۰٪ گیرنده که فقط با پاسخ‌گویی ظرف ۲۴ ساعت به اولین پیام دایرکت واریز می‌شود
// سهم ۳۰٪ گیرنده که فقط با پاسخ‌گویی ظرف ۱۲ ساعت به اولین پیام دایرکت واریز می‌شود —
pendingCommissionAmount: {
type: Number,
default: 0
@@ -43,6 +43,13 @@ const offerSchema = new mongoose.Schema({
commissionCredited: {
type: Boolean,
default: false
},
// وقتی گیرنده ظرف مهلت مقرر به اولین پیام دایرکت پاسخ نداد، سهم ۳۰٪ به‌جای گیرنده
// به کیف پول فرستنده (کسی که پرداخت کرده) برمی‌گردد — این فلگ از بازگشتِ دوباره
// در اجراهای بعدیِ cron جلوگیری می‌کند
commissionRefunded: {
type: Boolean,
default: false
}
})

View File

@@ -5,11 +5,21 @@ const timestamp = require('mongoose-timestamp')
// type مشخص می‌کنه اورلی متنیه یا لینکِ یکی از موارد قابل‌لینک (پکیج/پروژه/بیلبورد/فروشگاه/
// کالا/رزرو، همون‌هایی که توی ثبت پست هم قابل‌لینک‌ان)
const overlaySchema = new mongoose.Schema({
type: { type: String, enum: ['text', 'link'], default: 'text' },
type: { type: String, enum: ['text', 'link', 'mention', 'hashtag', 'location', 'sticker'], default: 'text' },
// text: متن آزاد (type:'text') | متن تگ بدون # (type:'hashtag') | نام مکان (type:'location') |
// خودِ ایموجی (type:'sticker')
text: { type: String, default: '' },
x: { type: Number, default: 0.5 },
y: { type: Number, default: 0.5 },
font_size: { type: Number, default: 0.06 },
// فقط برای type:'text' — کدوم واریانت فونتِ برندِ اپ (Vazir/IRANSansX) استفاده بشه؛
// چون اپ فعلاً فقط یک خانواده‌فونت با وزن‌های مختلف داره (نه چند تایپ‌فیس واقعیِ جدا مثل
// اینستاگرام)، این همون گزینه‌های واقعاً موجودیه
font_family: {
type: String,
enum: ['light', 'regular', 'medium', 'bold', 'black'],
default: 'bold'
},
color: { type: String, default: '#ffffff' },
background: { type: String, default: '' },
rotation: { type: Number, default: 0 },
@@ -17,7 +27,10 @@ const overlaySchema = new mongoose.Schema({
// فقط برای type:'link'
link_kind: { type: String, default: null },
link_id: { type: String, default: null },
link_title: { type: String, default: null }
link_title: { type: String, default: null },
// فقط برای type:'mention'
mention_user_id: { type: String, default: null },
mention_username: { type: String, default: null }
}, { _id: false })
const storySchema = new mongoose.Schema({

View File

@@ -258,7 +258,159 @@ const userSchema = new mongoose.Schema({
},
marital_status: {
type: String,
enum: ['single', 'divorced'],
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
},
@@ -292,7 +444,7 @@ const userSchema = new mongoose.Schema({
},
personal_style: {
type: String,
enum: ['formal', 'sporty'],
enum: ['casual', 'classic', 'minimal', 'formal', 'street', 'romantic'],
required: false,
default: null
},
@@ -306,18 +458,13 @@ const userSchema = new mongoose.Schema({
},
income_range: {
type: String,
enum: ['under_5m', '5m_10m', '10m_20m', 'over_20m'],
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
},
moral_traits: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 1024,
default: null
},
personal_details_public: {
type: Boolean,
required: false,
@@ -618,6 +765,30 @@ const userSchema = new mongoose.Schema({
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,