Compare commits

..

13 Commits

Author SHA1 Message Date
payacom
0cfec31f52 fix: parse multipart uploads for shop-chat files and chat-room images
Same root cause as the shop logo fix: these two routes still expected
req.files.<field> (the removed express-form-data pattern) with no
multer middleware in front of them, so:
- POST /shop-chat/:shopId/messages/file always rejected with "فایل
  الزامی است" even when a valid image/video was sent.
- POST /chat-rooms room creation silently dropped room_image, and
  would lose all text fields (title, visibility, ...) whenever the
  request was multipart (i.e. whenever an image was attached).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-28 17:01:43 +03:30
payacom
6e25e1a38a fix(shops): parse multipart logo upload on PATCH /shops/:id/logo
req.files.logo relied on an express-form-data style parser that was
removed app-wide, so the route never had a multipart parser and every
logo upload failed with "لطفاً لوگوی فروشگاه را انتخاب کنید" even
though the client had already picked and sent an image. Add the same
multer-based pattern already used for profile_image and shop-products
image uploads.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-28 16:46:04 +03:30
payacom
e5dcd0f1b2 reels 2026-10-09 15:44:47 +03:30
payacom
367416d419 pay 2026-10-06 17:08:44 +03:30
payacom
fade8543e2 e2ee 2026-10-02 18:13:34 +03:30
payacom
d4ebc20aa8 reels 2026-10-01 21:12:05 +03:30
payacom
806471241e full app 2026-09-24 21:03:27 +03:30
payacom
7f0ddfde70 full app 2026-09-24 21:00:07 +03:30
payacom
1954b2661d Add shop-chat push notifications and a "send shop to users" endpoint
- shop-chat messages (buyer <-> shop owner) never sent any push notification
  at all — sendShopChatPushNotification (chatHelpers.js) now fires on every
  send, mirroring the existing regular-chat push path; data payload carries
  shop_id/buyer_id/sender_role so clients can route directly to the thread
- New POST /shops/:id/send, mirroring the existing shop-product send
  endpoint — lets a user share a shop itself (not just one product) into a
  regular chat message as a sharedShop payload

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 14:05:19 +03:30
payacom
2e14db1ccd Add Pinterest tag/filter for shop products
- New is_pinterest boolean on ShopProductListingModel, settable via the
  existing create/update product endpoints (multipart form field)
- /shop-products/discover accepts ?pinterest=true to show only
  Pinterest-tagged products across shops; skips the tagged-posts merge
  under this filter since those posts aren't Pinterest-tagged themselves

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 10:22:37 +03:30
payacom
e1abb40cc6 Reduce media bandwidth: compress uploads, add long-lived cache headers, gzip API
- Resize/re-encode every uploaded image server-side (posts, avatars, stories,
  shop products/logo, chat messages) via a shared sharp-based optimizer —
  caps dimensions to what's actually displayed and re-compresses in place,
  cutting a typical raw camera photo by ~90% with no visible quality loss
- Cap uploaded video resolution/bitrate (re-encode only when above 1280px/
  ~2.5Mbps) alongside the existing faststart step; apply faststart+cap to
  story and chat videos too (previously stored completely raw)
- Add long-lived Cache-Control to all /storage static routes so clients stop
  re-downloading unchanged media on every request
- Add gzip/brotli compression for JSON API responses (feed/explore/chat)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 17:55:09 +03:30
payacom
26979057d7 feat: mask Iranian phone numbers in user-generated text fields
کاربران نتونن با گذاشتن شماره موبایل/تلفن توی بیو، کپشن پست/ریلز، توضیحاتِ
فروشگاه/کالا/پروژه/دوره/پروفایلِ آموزشگاه، بیرون از پلتفرم تماس بدن. یک
util مشترک (utils/phoneNumberMasking.js) شماره‌های ایرانی رو حتی وقتی
رقم‌ها با فاصله/خط‌فاصله/نقطه از هم جدا شده باشن تشخیص می‌ده و فقط همون
بخش رو با * جایگزین می‌کنه — بدون اینکه به قیمت/سایز/کد سفارش و بقیه‌ی
اعدادِ عادی دست بزنه. موقعِ ذخیره (نه نمایش) اعمال می‌شه، پس هیچ endpointِ
خوندنی نیاز به تغییر نداره.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 15:54:26 +03:30
payacom
5f99516078 fix: shop profile product count — approved-only for visitors, total for owner
توی پروفایل عمومی فروشگاه تعداد کالا فقط شاملِ کالاهای تاییدشده (active) می‌شه؛
خودِ مالک فروشگاه توی پروفایلِ خودش تعداد کل کالاها (هر وضعیتی) رو می‌بینه.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 13:20:41 +03:30
212 changed files with 10665 additions and 11309 deletions

View File

@@ -1,74 +0,0 @@
const AcademyCategoryModel = require('../../../models/AcademyCategoryModel');
// GET all categories
exports.getAll = async (req, res) => {
try {
const categories = await AcademyCategoryModel.find({}).sort({ createdAt: -1 });
res.status(200).json({ success: true, categories });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
// CREATE category
exports.create = async (req, res) => {
try {
const { title, status } = req.body;
if (!title) {
return res.status(400).json({ success: false, message: "title is required" });
}
const newCategory = new AcademyCategoryModel({
title,
status: status ?? true,
});
await newCategory.save();
res.status(201).json({ success: true, category: newCategory });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
// UPDATE category
exports.update = async (req, res) => {
try {
const { id } = req.params;
const { title, status } = req.body;
const updated = await AcademyCategoryModel.findByIdAndUpdate(
id,
{ title, status },
{ new: true, runValidators: true }
);
if (!updated) {
return res.status(404).json({ success: false, message: "category not found" });
}
res.status(200).json({ success: true, category: updated });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
// DELETE
exports.remove = async (req, res) => {
try {
const { id } = req.params;
const deleted = await AcademyCategoryModel.findByIdAndDelete(id);
if (!deleted) {
return res.status(404).json({ success: false, message: "category not found" });
}
res.status(200).json({
success: true,
message: 'category deleted successfully'
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};

View File

@@ -1,80 +0,0 @@
const moment = require('moment-jalaali')
const MessageModel = require('../models/MessageModel')
const UserModel = require('../models/UserModel')
function formatTimeOnly(date) {
return moment(date).locale('fa').format('HH:mm:ss')
}
async function enrichMessage(message) {
const doc = message._doc ? { ...message._doc } : { ...message }
let replyTo = null
if (doc.replyToId) {
const parent = await MessageModel.findById(doc.replyToId).lean()
if (parent) {
const parentUser =
String(parent.senderId) === String(doc.senderId)
? await UserModel.findById(doc.senderId).select('first_name last_name user_name').lean()
: await UserModel.findById(parent.senderId).select('first_name last_name user_name').lean()
replyTo = {
_id: parent._id,
content: (parent.content || 'پیام').slice(0, 200),
senderName: parentUser
? `${parentUser.first_name || ''} ${parentUser.last_name || ''}`.trim() || parentUser.user_name
: 'کاربر'
}
}
}
return {
...doc,
createdAt: formatTimeOnly(doc.createdAt),
createdAtIso: doc.createdAt ? new Date(doc.createdAt).toISOString() : undefined,
expiresAt: doc.expiresAt ? new Date(doc.expiresAt).toISOString() : undefined,
viewOnce: Boolean(doc.viewOnce),
viewOnceLocked: Boolean(doc.viewOnceLocked),
viewOnceExpired: Boolean(doc.viewOnceExpired),
viewOnceOpenedAt: doc.viewOnceOpenedAt
? new Date(doc.viewOnceOpenedAt).toISOString()
: undefined,
replyTo,
forwardedFrom: doc.forwardedFrom || undefined
}
}
function emitChatEvents(io, message, senderId, receiverId) {
const payload = message
const roomA = `chat:${senderId}:${receiverId}`
const roomB = `chat:${receiverId}:${senderId}`
io.to(roomA).to(roomB).emit('newMessage', payload)
io.to(`user:${senderId}`).to(`user:${receiverId}`).emit('chatListUpdate', {
senderId,
receiverId,
message: payload
})
}
function parseForwardedContent(content) {
if (!content) return { body: '', forwardedFrom: null }
try {
const line = content.split('\n')[0]
const j = JSON.parse(line)
if (j.forwardedFrom) {
return {
forwardedFrom: j.forwardedFrom,
body: content.replace(/^\{.*\}\n?/, '').trim()
}
}
} catch (_) {
/* not json */
}
return { body: content, forwardedFrom: null }
}
module.exports = {
formatTimeOnly,
enrichMessage,
emitChatEvents,
parseForwardedContent
}

View File

@@ -1,129 +0,0 @@
const path = require('path')
const fs = require('fs-extra')
const MessageModel = require('../models/MessageModel')
const ALLOWED_SELF_DESTRUCT_SECONDS = [10, 30, 60, 300, 3600]
function parseSelfDestructSeconds(raw) {
if (raw == null || raw === '' || raw === '0' || raw === 0) return null
const n = parseInt(String(raw), 10)
if (!Number.isFinite(n) || !ALLOWED_SELF_DESTRUCT_SECONDS.includes(n)) return null
return n
}
function buildExpiresAt(seconds) {
if (!seconds) return undefined
return new Date(Date.now() + seconds * 1000)
}
function activeMessageFilter(baseFilter) {
return {
$and: [
baseFilter,
{
$or: [
{ expiresAt: { $exists: false } },
{ expiresAt: null },
{ expiresAt: { $gt: new Date() } }
]
}
]
}
}
async function removeMessageFiles(messages) {
for (const msg of messages) {
if (!msg.file) continue
const relative = String(msg.file).replace(/^\//, '')
const filePath = path.join(__dirname, '../storage', relative)
if (fs.existsSync(filePath)) {
await fs.remove(filePath).catch(() => {})
}
}
}
async function deleteMessagesAndNotify(io, messages, options = {}) {
if (!messages?.length) return []
await removeMessageFiles(messages)
const deletedIds = messages.map((m) => String(m._id))
await MessageModel.deleteMany({ _id: { $in: deletedIds } })
if (io) {
const byThread = new Map()
messages.forEach((m) => {
const s = String(m.senderId)
const r = String(m.receiverId)
const key = [s, r].sort().join(':')
if (!byThread.has(key)) byThread.set(key, { s, r, ids: [] })
byThread.get(key).ids.push(String(m._id))
})
byThread.forEach(({ s, r, ids }) => {
io.to(`chat:${s}:${r}`)
.to(`chat:${r}:${s}`)
.emit('messagesDeleted', {
messageIds: ids,
deletedBy: options.deletedBy,
reason: options.reason || (options.deletedBy ? 'manual' : 'expired')
})
io.to(`user:${s}`).to(`user:${r}`).emit('chatListUpdate', { senderId: s, receiverId: r })
})
}
return deletedIds
}
async function purgeExpiredMessages(io) {
try {
const expired = await MessageModel.find({ expiresAt: { $lte: new Date() } })
if (!expired.length) return []
return deleteMessagesAndNotify(io, expired)
} catch (err) {
console.error('purgeExpiredMessages error:', err)
return []
}
}
const scheduledTimers = new Map()
function scheduleMessageExpiry(io, message) {
if (!message?.expiresAt || !message._id) return
const id = String(message._id)
if (scheduledTimers.has(id)) {
clearTimeout(scheduledTimers.get(id))
}
const ms = new Date(message.expiresAt).getTime() - Date.now()
if (ms <= 0) {
MessageModel.findById(id)
.then((doc) => doc && deleteMessagesAndNotify(io, [doc]))
.catch(() => {})
return
}
const timer = setTimeout(async () => {
scheduledTimers.delete(id)
try {
const doc = await MessageModel.findById(id)
if (doc && doc.expiresAt && new Date(doc.expiresAt) <= new Date()) {
await deleteMessagesAndNotify(io, [doc])
}
} catch (err) {
console.error('scheduleMessageExpiry error:', err)
}
}, ms)
scheduledTimers.set(id, timer)
}
module.exports = {
ALLOWED_SELF_DESTRUCT_SECONDS,
parseSelfDestructSeconds,
buildExpiresAt,
activeMessageFilter,
purgeExpiredMessages,
scheduleMessageExpiry,
deleteMessagesAndNotify
}

View File

@@ -1,104 +0,0 @@
const MessageModel = require('../models/MessageModel')
const { deleteMessagesAndNotify } = require('./expiredMessages')
function parseViewOnce(raw) {
if (raw == null || raw === '' || raw === false || raw === 'false' || raw === 0) {
return false
}
return raw === true || raw === 'true' || raw === 1 || raw === '1'
}
const VIEW_ONCE_MEDIA = new Set(['image', 'video', 'voice'])
function isViewOnceMediaType(fileType) {
return VIEW_ONCE_MEDIA.has(fileType)
}
/** Hide file URL for receiver until they open view-once media */
function sanitizeMessageForViewer(message, viewerId) {
const doc = message._doc ? { ...message._doc } : { ...message }
const viewer = String(viewerId)
const sender = String(doc.senderId)
const receiver = String(doc.receiverId)
if (!doc.viewOnce || !isViewOnceMediaType(doc.fileType)) {
return doc
}
if (viewer === sender) {
return doc
}
if (viewer === receiver) {
if (doc.viewOnceOpenedAt) {
return {
...doc,
file: '',
viewOnceExpired: true
}
}
return {
...doc,
file: '',
viewOnceLocked: true
}
}
return doc
}
async function openViewOnceMessage(messageId, userId) {
const msg = await MessageModel.findById(messageId)
if (!msg || !msg.viewOnce) {
return { error: 'پیام یافت نشد', status: 404 }
}
if (String(msg.receiverId) !== String(userId)) {
return { error: 'دسترسی مجاز نیست', status: 403 }
}
if (msg.viewOnceOpenedAt) {
return { error: 'این پیام قبلاً باز شده است', status: 410 }
}
msg.viewOnceOpenedAt = new Date()
await msg.save()
return {
message: msg.toObject(),
file: msg.file,
fileType: msg.fileType
}
}
async function completeViewOnceMessage(io, messageId, userId) {
const msg = await MessageModel.findById(messageId)
if (!msg || !msg.viewOnce) {
return { error: 'پیام یافت نشد', status: 404 }
}
const uid = String(userId)
const isReceiver = String(msg.receiverId) === uid
const isSender = String(msg.senderId) === uid
if (!isReceiver && !isSender) {
return { error: 'دسترسی مجاز نیست', status: 403 }
}
if (isReceiver && !msg.viewOnceOpenedAt) {
return { error: 'پیام هنوز باز نشده', status: 400 }
}
await deleteMessagesAndNotify(io, [msg], {
deletedBy: userId,
reason: 'viewOnce'
})
return { deletedIds: [String(msg._id)] }
}
module.exports = {
parseViewOnce,
isViewOnceMediaType,
sanitizeMessageForViewer,
openViewOnceMessage,
completeViewOnceMessage
}

View File

@@ -1,357 +0,0 @@
const express = require('express');
const app = express();
const path = require('path');
const http = require('http').createServer(app);
const cors = require('cors');
const moment = require('moment-jalaali');
const MessageModel = require('./models/MessageModel');
const fs = require('fs-extra');
const cron = require('node-cron');
const UserModel = require('./models/UserModel');
const blockCheck = require('./middlewares/blockCheck');
const rateLimit = require('express-rate-limit');
const { default: axios } = require('axios');
const allowedOrigins = [
'http://localhost:3000',
'http://localhost:3001', // اضافه شد — مهم برای توسعه فرانت
'http://localhost:3002',
'http://localhost:3003',
'http://localhost',
'https://modstagram.com',
'https://www.modstagram.com',
'https://api.modstagram.com',
'https://panel.modstagram.com',
'https://www.panel.modstagram.com',
'https://panel.modstagram.ir',
'https://www.panel.modstagram.ir',
'ionic://localhost',
'capacitor://localhost',
// IPهای محلی (برای تست در شبکه داخلی)
/^http:\/\/192\.168\.\d{1,3}\.\d{1,3}:\d+$/,
/^http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$/,
];
app.use(cors({
origin: (origin, callback) => {
// در محیط توسعه، همه originها مجاز باشن
if (process.env.NODE_ENV !== 'production') {
return callback(null, true);
}
// اگر origin وجود نداشته باشه (مثل درخواست از موبایل یا Postman) اجازه بده
if (!origin) {
return callback(null, true);
}
// چک لیست یا RegExp
const isAllowed = allowedOrigins.some(item => {
if (typeof item === 'string') return item === origin;
if (item instanceof RegExp) return item.test(origin);
return false;
});
if (isAllowed) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-no-toast'],
credentials: true,
optionsSuccessStatus: 200 // برای مرورگرهای قدیمی
}));
// Socket.IO CORS
const io = require('socket.io')(http, {
cors: {
origin: (origin, callback) => {
if (process.env.NODE_ENV !== 'production') {
return callback(null, true);
}
if (!origin) {
return callback(null, true);
}
const isAllowed = allowedOrigins.some(item => {
if (typeof item === 'string') return item === origin;
if (item instanceof RegExp) return item.test(origin);
return false;
});
if (isAllowed) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
credentials: true
}
});
// افزایش limit برای Base64 (1000mb کافیه)
app.use(express.json({ limit: '1000mb' }));
app.use(express.urlencoded({ limit: '1000mb', extended: true }));
app.use('/storage', express.static(path.join(__dirname, 'storage')));
app.use('/storage/profiles', express.static(path.join(__dirname, '../storage/profiles')));
app.use('/storage/carts', express.static(path.join(__dirname, '../storage/carts')));
app.use('/storage/posts', express.static(path.join(__dirname, '../storage/posts')));
app.use('/storage/messages', express.static(path.join(__dirname, '../storage/messages')));
app.use('/storage/tickets', express.static(path.join(__dirname, '../storage/tickets')));
app.use('/storage/advertising', express.static(path.join(__dirname, '../storage/advertising')));
app.use('/storage/services', express.static(path.join(__dirname, '../storage/services')));
require('./boot');
// غیرفعال کردن پارس‌کننده‌های بدنه برای روت‌های آپلود فایل
app.use((req, res, next) => {
if (req.path === '/api/v1/posts/create' && req.method === 'POST') {
console.log('DEBUG: Skipping body parsers for /api/v1/posts/create');
return next();
}
express.json()(req, res, next);
});
app.use((req, res, next) => {
if (req.path === '/api/v1/posts/create' && req.method === 'POST') {
return next();
}
express.urlencoded({ extended: true })(req, res, next);
});
require('./middlewares')(app);
const limiter = rateLimit({
windowMs: 3 * 60 * 1000,
max: 3000,
standardHeaders: true,
legacyHeaders: false,
message: 'تعداد درخواست‌های شما بیشتر از حد مجاز است، لطفاً بعداً دوباره تلاش کنید.',
handler: (req, res) => {
const retryAfter = Math.ceil((req.rateLimit.resetTime - Date.now()) / 1000);
res.set('Retry-After', retryAfter);
res.status(429).json({
error: `شما بیش از حد مجاز درخواست ارسال کرده‌اید. لطفاً بعد از ${retryAfter} ثانیه دوباره تلاش کنید.`
});
}
});
app.use(limiter);
app.set('trust proxy', 'loopback');
require('./routes')(app);
const { enrichMessage, emitChatEvents, parseForwardedContent } = require('./helpers/chatHelpers')
app.get('/api/v1/chat', async (req, res) => {
try {
const { senderId, receiverId, limit, page } = req.query
const pageNumber = parseInt(page) || 1
const limitPerPage = parseInt(limit) || 40
const filter = {
$or: [
{ senderId, receiverId },
{ senderId: receiverId, receiverId: senderId }
]
}
const totalMessagesCount = await MessageModel.countDocuments(filter)
const totalPages = Math.ceil(totalMessagesCount / limitPerPage) || 1
const skip = (pageNumber - 1) * limitPerPage
const messages = await MessageModel.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limitPerPage)
await MessageModel.updateMany(
{ _id: { $in: messages.map((m) => m._id) }, receiverId: senderId },
{ $set: { readStatus: 1 } }
)
const enriched = await Promise.all(messages.map((m) => enrichMessage(m)))
res.json({ messages: enriched.reverse(), totalPages })
} catch (error) {
console.error('Error fetching messages:', error)
res.status(500).json({ error: 'Server error' })
}
})
app.post('/api/v1/chat', [blockCheck], async (req, res) => {
try {
const { senderId, receiverId, content, replyToId, forwardedFrom: fwdBody } = req.body
const { body, forwardedFrom: fwdParsed } = parseForwardedContent(content)
const payload = {
senderId,
receiverId,
content: body || content,
replyToId: replyToId || undefined,
forwardedFrom: fwdBody || fwdParsed || undefined
}
const newMessage = new MessageModel(payload)
await newMessage.save()
const formatted = await enrichMessage(newMessage)
res.status(201).json({ data: formatted })
emitChatEvents(io, formatted, senderId, receiverId)
} catch (error) {
console.error('Error sending message:', error)
res.status(500).json({ error: 'Server error' })
}
})
app.get('/api/v1/chat/read', async (req, res) => {
try {
const { senderId, receiverId } = req.query // شناسه فرستنده و گیرنده از درخواست دریافت شود
// پیدا کردن تمام پیام‌هایی که کاربر دوم مشاهده کرده است و کاربر اول آنها را ارسال کرده است
const messages = await MessageModel.find(
{ senderId: receiverId, receiverId: senderId } // برای مواقعی که شناسه‌ها معکوس باشند
)
// به روزرسانی وضعیت خوانده شده یا نشده بودن پیام‌ها به "خوانده شده"
await MessageModel.updateMany({ _id: { $in: messages.map(message => message._id) } }, { $set: { readStatus: 1 } })
res.status(200).json({ message: 'Message read status updated successfully' })
} catch (error) {
console.error('Error updating message read status:', error)
res.status(500).json({ error: 'Server error' })
}
})
app.post("/api/v1/notification/send-sms", async (req, res) => {
const { receiverId, message } = req.body;
const user = await UserModel.findById(receiverId);
if (!user || !user.mobile) return res.status(404).json({ error: "User not found" });
console.log("3");
const mobile = user.mobile
const user_name = user.user_name
const data = JSON.stringify({
mobile,
templateId: '876533',
parameters: [
{ name: 'USER', value: user_name }
]
})
const config = {
method: 'post',
url: 'https://api.sms.ir/v1/send/verify',
headers: {
'Content-Type': 'application/json',
Accept: 'text/plain',
'x-api-key': process.env.SMS_IR_API_KEY
},
data
}
axios(config)
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error)
})
res.status(200).json({ success: true });
});
app.post('/api/v1/chat/file', [blockCheck], async (req, res) => {
try {
const { senderId, receiverId, content, replyToId, fileType } = req.body
const { file } = req.files
let fileUrl = null
if (file) {
const uploadDir = path.join(__dirname, '../storage/messages')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
const uniqueFileName = `${Date.now()}-${file.name}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(file.path, filePath)
fileUrl = `/messages/${uniqueFileName}`
}
const newMessage = new MessageModel({
senderId,
receiverId,
content: content || '',
file: fileUrl,
fileType: fileType || undefined,
replyToId: replyToId || undefined
})
await newMessage.save()
const formatted = await enrichMessage(newMessage)
res.status(201).json({ data: formatted })
emitChatEvents(io, formatted, senderId, receiverId)
} catch (error) {
console.error('Error sending message:', error)
res.status(500).json({ error: 'Server error' })
}
})
// Errors
require('./middlewares/exception')(app)
require('./middlewares/404')(app)
io.on('connection', (socket) => {
socket.on('joinUser', ({ userId }) => {
if (userId) socket.join(`user:${userId}`)
})
socket.on('joinChat', ({ userId, receiverId }) => {
if (userId && receiverId) {
socket.join(`chat:${userId}:${receiverId}`)
socket.join(`chat:${receiverId}:${userId}`)
}
})
socket.on('typing', ({ senderId, receiverId }) => {
io.to(`chat:${receiverId}:${senderId}`).emit('userTyping', { userId: senderId })
})
socket.on('stopTyping', ({ senderId, receiverId }) => {
io.to(`chat:${receiverId}:${senderId}`).emit('userStoppedTyping', { userId: senderId })
})
socket.on('messageSeen', async ({ messageId, senderId, receiverId }) => {
try {
if (messageId) {
await MessageModel.findByIdAndUpdate(messageId, { readStatus: 1 })
io.to(`chat:${receiverId}:${senderId}`).emit('messageStatusUpdate', {
messageId,
status: 'seen'
})
}
} catch (e) {
console.error('messageSeen error', e)
}
})
})
// Free Project
require('./services/AdvertisingCron')
require('./services/ProjectCron')
cron.schedule('0 0 1 * *', async () => {
try {
await UserModel.updateMany({}, {
daily_free_request: 1,
last_free_request_date: new Date(),
monthly_free_offer: 1,
last_free_offer_date: new Date()
})
console.log('Monthly free requests reset for all users.')
} catch (error) {
console.error('Error resetting daily free requests:', error)
}
}, {
scheduled: true,
timezone: 'Asia/Tehran'
})
module.exports = (port) => {
http.listen(port, () => {
console.log(`HTTP server is running on port ${port}`)
})
}

View File

@@ -1,18 +0,0 @@
[Wed Mar 13 00:31:27 2024] [Token] {"TokenKey":"OVlma3FmZmJOTjNoY1A3bmxRbE5odHBHTTNZbGpjcGxvcVVJTmZIMDFHdjkvWDNYaG55WlB0cXNpaWRmclVCeHlXNFVrd2xxZThZQktIVEdGak9Xd29QTGNhMWtpYXlWUmwwNXcvZUdxQjhmbHFyenpPNVlDVk9ZbGZsUmplQ3NjUHdHNE5ma0hGdHpQV040MkV0OUx0M285SkdBcGp6OFA5SGpoNHZHUzFEajkwc0ZWQWRWSlNBNFlOMWxxOC9k","IsSuccessful":true,"Message":" درخواست Token با موفقیت انجام شد"}
[Wed Mar 13 00:31:27 2024] [VerificationCode] {"VerificationCodeId":634458365,"IsSuccessful":true,"Message":"your verification code is sent"}
[Wed Mar 13 00:34:14 2024] [Token] {"TokenKey":"OVlma3FmZmJOTjNoY1A3bmxRbE5odHBHTTNZbGpjcGxvcVVJTmZIMDFHdjkvWDNYaG55WlB0cXNpaWRmclVCeHlXNFVrd2xxZThZQktIVEdGak9Xd29QTGNhMWtpYXlWUmwwNXcvZUdxQjhmbHFyenpPNVlDVk9ZbGZsUmplQ3M0WTRVeG9SeVNtN0xnMWUzQWo4WG1Oam9mNUw2SkJPV04rMGVOMUJ6Njhpb1RXMTl2LzFEdGRESXhZazBwaHBk","IsSuccessful":true,"Message":" درخواست Token با موفقیت انجام شد"}
[Wed Mar 13 00:34:15 2024] [VerificationCode] {"VerificationCodeId":634459857,"IsSuccessful":true,"Message":"your verification code is sent"}
[Wed Mar 13 00:34:48 2024] [Token] {"TokenKey":"OVlma3FmZmJOTjNoY1A3bmxRbE5odHBHTTNZbGpjcGxvcVVJTmZIMDFHdjkvWDNYaG55WlB0cXNpaWRmclVCeHlXNFVrd2xxZThZQktIVEdGak9Xd29QTGNhMWtpYXlWUmwwNXcvZUdxQjhmbHFyenpPNVlDVk9ZbGZsUmplQ3NGN0NVV3V1K01VRTg3TGY1by9PcmtlSVpWaFpBa0l4RXNCOERHMzIxMDRkL1RYbWRhOXc5ZmovT0lFV0FDamJs","IsSuccessful":true,"Message":" درخواست Token با موفقیت انجام شد"}
[Wed Mar 13 00:34:48 2024] [VerificationCode] {"VerificationCodeId":634460165,"IsSuccessful":true,"Message":"your verification code is sent"}
[Wed Mar 13 00:34:57 2024] [Token] {"TokenKey":"OVlma3FmZmJOTjNoY1A3bmxRbE5odHBHTTNZbGpjcGxvcVVJTmZIMDFHdjkvWDNYaG55WlB0cXNpaWRmclVCeHlXNFVrd2xxZThZQktIVEdGak9Xd29QTGNhMWtpYXlWUmwwNXcvZUdxQjhmbHFyenpPNVlDVk9ZbGZsUmplQ3NyNmxiRjUwVlF6UUlkSStnOWpOc0lLSyt5VHJjSjFRa0xsTkk0dFB2L3VSaE1PTDdQREY4WGlzcUFQeE01QWRP","IsSuccessful":true,"Message":" درخواست Token با موفقیت انجام شد"}
[Wed Mar 13 00:34:57 2024] [VerificationCode] {"VerificationCodeId":634460241,"IsSuccessful":true,"Message":"your verification code is sent"}
[Wed Mar 13 00:39:39 2024] [Token] {"TokenKey":"OVlma3FmZmJOTjNoY1A3bmxRbE5odHBHTTNZbGpjcGxvcVVJTmZIMDFHdjkvWDNYaG55WlB0cXNpaWRmclVCeHlXNFVrd2xxZThZQktIVEdGak9Xd29QTGNhMWtpYXlWUmwwNXcvZUdxQjhmbHFyenpPNVlDVk9ZbGZsUmplQ3N6MHErTmJEVzMydjZRcVJQaHNLb0R3MzJKcWk5bGJYNTh5UzFmSEpSSUY0b0Q4MFppdDRoeFlxWTZZSEQrL1hL","IsSuccessful":true,"Message":" درخواست Token با موفقیت انجام شد"}
[Wed Mar 13 00:39:39 2024] [VerificationCode] {"VerificationCodeId":634462768,"IsSuccessful":true,"Message":"your verification code is sent"}
[Wed Mar 13 00:40:18 2024] [Token] {"TokenKey":"OVlma3FmZmJOTjNoY1A3bmxRbE5odHBHTTNZbGpjcGxvcVVJTmZIMDFHdjkvWDNYaG55WlB0cXNpaWRmclVCeHlXNFVrd2xxZThZQktIVEdGak9Xd29QTGNhMWtpYXlWUmwwNXcvZUdxQjhmbHFyenpPNVlDVk9ZbGZsUmplQ3NLRlVYejdBcTJxMGl6dXJVSTRsQkNiOGh2M241WTZrSGI3Z0JCY3kxeFZvUzVCTnRkdmVWalFzd25KQzIzV21I","IsSuccessful":true,"Message":" درخواست Token با موفقیت انجام شد"}
[Wed Mar 13 00:40:18 2024] [VerificationCode] {"VerificationCodeId":634463121,"IsSuccessful":true,"Message":"your verification code is sent"}
[Wed Mar 13 00:41:38 2024] [Token] {"TokenKey":"OVlma3FmZmJOTjNoY1A3bmxRbE5odHBHTTNZbGpjcGxvcVVJTmZIMDFHdjkvWDNYaG55WlB0cXNpaWRmclVCeHlXNFVrd2xxZThZQktIVEdGak9Xd29QTGNhMWtpYXlWUmwwNXcvZUdxQjhmbHFyenpPNVlDVk9ZbGZsUmplQ3NOMUs3ZVN3eDU0OVBZdXR1VU1Yd3dhS2lyWmMrWkpjTGpUVVkzWDQ4ZU10VDBTWnFZc0gwYlVSZHArS1FBU2ZL","IsSuccessful":true,"Message":" درخواست Token با موفقیت انجام شد"}
[Wed Mar 13 00:41:38 2024] [VerificationCode] {"VerificationCodeId":634463845,"IsSuccessful":true,"Message":"your verification code is sent"}
[Wed Mar 13 01:11:21 2024] [Token] {"TokenKey":"OVlma3FmZmJOTjNoY1A3bmxRbE5odHBHTTNZbGpjcGxvcVVJTmZIMDFHdjkvWDNYaG55WlB0cXNpaWRmclVCeHlXNFVrd2xxZThZQktIVEdGak9Xd29QTGNhMWtpYXlWUmwwNXcvZUdxQjhmbHFyenpPNVlDVk9ZbGZsUmplQ3M4em1oSG05eU9XbVlna0U1TDRjRWdlRi8zTE10YnAzYVZia0hRTmQ3bWV4cEUyUUl4TCtPMncvWStrNi9TSjNt","IsSuccessful":true,"Message":" درخواست Token با موفقیت انجام شد"}
[Wed Mar 13 01:11:21 2024] [VerificationCode] {"VerificationCodeId":634470192,"IsSuccessful":true,"Message":"your verification code is sent"}
[Wed Mar 13 02:55:34 2024] [Token] {"TokenKey":"OVlma3FmZmJOTjNoY1A3bmxRbE5odHBHTTNZbGpjcGxvcVVJTmZIMDFHdjkvWDNYaG55WlB0cXNpaWRmclVCeHlXNFVrd2xxZThZQktIVEdGak9Xd29QTGNhMWtpYXlWUmwwNXcvZUdxQjhmbHFyenpPNVlDVk9ZbGZsUmplQ3MwSFZidjNtY0p1T3RwQVFlbkxJdjFHZ0xUaFpqZW9CdllCR21oNFNyaXRUczFaVUVSemZ3Rzk0TW0zRG50Q3VU","IsSuccessful":true,"Message":" درخواست Token با موفقیت انجام شد"}
[Wed Mar 13 02:55:34 2024] [VerificationCode] {"VerificationCodeId":634481796,"IsSuccessful":true,"Message":"your verification code is sent"}

View File

@@ -1,9 +0,0 @@
module.exports = (app) => {
app.use((req, res, next) => {
res.status(404).send({
code: 'Not Found',
status: 404,
message: 'requested resource could not be found!'
})
})
}

View File

@@ -1,55 +0,0 @@
const AdminModel = require('../models/AdminModel')
const jwt = require('jsonwebtoken')
module.exports = async (req, res, next) => {
if (!('authorization' in req.headers)) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'you are not authorized!'
})
}
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
let decodedToken
try {
decodedToken = jwt.verify(token, process.env.APP_SECRET)
} catch (err) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!',
error: err.message
})
}
if (!decodedToken) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!'
})
}
try {
// پیدا کردن کاربر با استفاده از اطلاعات موجود در توکن
const admin = await AdminModel.findOne({ _id: decodedToken.id })
if (!admin) {
return res.status(403).send({
status: 'error',
code: 403,
message: 'Access denied!'
})
}
// ادامه مسیر در صورتی که کاربر ادمین باشد
req.admin = admin
next()
} catch (error) {
return res.status(500).send({
status: 'error',
code: 500,
message: 'Internal Server Error',
error: error.message
})
}
}

View File

@@ -1,21 +0,0 @@
const TokenService = require('../services/TokenService')
module.exports = (req, res, next) => {
console.log(1);
if (!('authorization' in req.headers)) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'you are not authorized!'
})
}
const [, tokenValue] = req.headers.authorization.split(' ')
const token = TokenService.verify(tokenValue)
if (!token) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'your token is not valid!'
})
}
next()
}

View File

@@ -1,48 +0,0 @@
const UserModel = require('../models/UserModel')
const jwt = require('jsonwebtoken')
const {
expireBlockSuspensionIfNeeded,
getBlockedAccountResponse
} = require('../utils/blockSuspension')
module.exports = async (req, res, next) => {
try {
if (!('authorization' in req.headers)) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'You are not authorized!'
})
}
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
if (!decodedToken) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!'
})
}
let user = await UserModel.findById(decodedToken.id)
user = await expireBlockSuspensionIfNeeded(user)
const blockResponse = getBlockedAccountResponse(user)
if (blockResponse) {
return res.status(403).send(blockResponse)
}
req.user = user
next()
} catch (error) {
console.error('Error in blockCheck middleware:', error)
res.status(500).send({
status: 'error',
code: 500,
message: 'Server error'
})
}
}

View File

@@ -1,11 +0,0 @@
module.exports = (app) => {
app.use((error, req, res, next) => {
const status = error.status || 500
res.status(500).send({
code: 'Eception',
status,
en_message: error.message,
message: 'خطایی در عملیات مورد نظر رخ داده است.'
})
})
}

View File

@@ -1,21 +0,0 @@
const bodyParser = require('body-parser')
const formData = require('express-form-data')
const session = require('express-session')
const lastOnline = require('./lastOnline')
module.exports = (app) => {
app.use(formData.parse())
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(session({
secret: process.env.SESSION_SECRET || process.env.JWT_SECRET || 'change-me-in-production',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
},
}))
app.use(lastOnline)
}

View File

@@ -1,53 +0,0 @@
const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر
const jwt = require('jsonwebtoken')
module.exports = async (req, res, next) => {
console.log(3);
try {
console.log(31);
if (!('authorization' in req.headers)) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'You are not authorized!'
})
}
console.log(32);
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
if (!decodedToken) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!'
})
}
console.log(33);
const user = await UserModel.findById(decodedToken.id)
if (!user.user_name ||
!user.first_name ||
!user.last_name ||
!user.user_type ||
!user.password) {
return res.status(403).send({
status: 'error',
code: 403,
message: 'لطفا از بخش تنظیمات، ثبت نام خود را کامل کنید',
type: 'not_register'
})
}
console.log(34);
req.user = user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود.
next()
} catch (error) {
console.log(35);
console.error('not_register middleware:', error)
res.status(500).send({
status: 'error',
code: 500,
message: 'Server error'
})
}
}

View File

@@ -1,50 +0,0 @@
const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر
const jwt = require('jsonwebtoken')
module.exports = async (req, res, next) => {
try {
if (!('authorization' in req.headers)) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'You are not authorized!'
})
}
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
if (!decodedToken) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!'
})
}
const user = await UserModel.findById(decodedToken.id)
if (!user.user_name ||
!user.first_name ||
!user.last_name ||
!user.password) {
return res.status(403).send({
status: 'error',
code: 403,
message: 'لطفا از بخش تنظیمات، ثبت نام خود را کامل کنید',
type: 'not_register'
})
}
req.user = user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود.
next()
} catch (error) {
console.error('not_register middleware:', error)
res.status(500).send({
status: 'error',
code: 500,
message: 'Server error'
})
}
}

View File

@@ -1,21 +0,0 @@
const jwt = require('jsonwebtoken')
const UserModel = require('../models/UserModel')
module.exports = async (req, res, next) => {
if (req.header && req.header('Authorization')) {
const token = await req.header('Authorization').split(' ')[1]
const decodedToken = await jwt.verify(token, process.env.APP_SECRET)
const user = await UserModel.findById(decodedToken.id)
if (user) {
try {
user.last_online = new Date()
await user.save() // ذخیره تغییرات در دیتابیس
} catch (error) {
// بررسی و پردازش خطاها
console.error('Error updating last online time:', error)
}
}
}
next()
}

View File

@@ -1,24 +0,0 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs-extra');
const uploadDir = path.join(__dirname, '../storage/posts');
fs.ensureDirSync(uploadDir); // مطمئن میشه مسیر وجود داره
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadDir),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const name = file.originalname.replace(ext, '').replace(/\s/g, '_');
cb(null, `${Date.now()}-${name}${ext}`);
}
});
const fileFilter = (req, file, cb) => {
if (!file.mimetype.startsWith('image/')) return cb(new Error('فقط تصاویر مجاز است'));
cb(null, true);
};
const upload = multer({ storage, fileFilter, limits: { files: 10 } }); // حداکثر ۱۰ فایل
module.exports = upload;

View File

@@ -1,190 +0,0 @@
const mongoose = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');
const timestamp = require('mongoose-timestamp');
const AcademyCategorySchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'عنوان دسته‌بندی الزامی است'],
trim: true,
minLength: [2, 'عنوان باید حداقل ۲ کاراکتر باشد'],
maxLength: [100, 'عنوان باید حداکثر ۱۰۰ کاراکتر باشد'],
unique: true,
index: true,
},
slug: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
index: true,
},
description: {
type: String,
required: false,
trim: true,
maxLength: 500,
default: null,
},
icon: {
type: String,
required: false,
default: null,
},
color: {
type: String,
required: false,
default: '#3B82F6',
match: /^#[0-9A-F]{6}$/i,
},
parent: {
type: mongoose.Schema.Types.ObjectId,
ref: 'AcademyCategory',
required: false,
default: null,
index: true,
},
level: {
type: Number,
required: true,
default: 0,
min: 0,
max: 3,
},
order: {
type: Number,
required: true,
default: 0,
index: true,
},
image: {
type: String,
required: false,
default: null,
},
course_count: {
type: Number,
required: true,
default: 0,
min: 0,
},
status: {
type: String,
enum: ['active', 'inactive', 'deleted'],
required: true,
default: 'active',
index: true,
},
is_featured: {
type: Boolean,
required: true,
default: false,
index: true,
},
seo_title: {
type: String,
required: false,
trim: true,
maxLength: 70,
},
seo_description: {
type: String,
required: false,
trim: true,
maxLength: 160,
},
created_by: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
updated_by: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: false,
},
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Virtual برای زیردسته‌ها
AcademyCategorySchema.virtual('children', {
ref: 'AcademyCategory',
localField: '_id',
foreignField: 'parent',
justOne: false,
options: { sort: { order: 1, title: 1 } }
});
// Virtual برای دوره‌های این دسته
AcademyCategorySchema.virtual('courses', {
ref: 'Course',
localField: '_id',
foreignField: 'category_id',
justOne: false,
match: { status: 'accept' }
});
// Middleware: قبل از save
AcademyCategorySchema.pre('save', async function(next) {
if (this.isModified('title') || this.isNew) {
// تولید slug
this.slug = this.title
.replace(/[^\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C\uFB8E\u0698a-zA-Z0-9\s]/g, '')
.trim()
.replace(/\s+/g, '-')
.toLowerCase();
// اگه parent داره، سطح رو محاسبه کن
if (this.parent) {
const parent = await this.constructor.findById(this.parent);
if (parent) {
this.level = parent.level + 1;
}
} else {
this.level = 0;
}
}
next();
});
// استاتیک متدها
AcademyCategorySchema.statics = {
// دریافت درخت دسته‌بندی‌ها
async getTree() {
const categories = await this.find({ status: 'active', parent: null })
.sort({ order: 1, title: 1 })
.populate({
path: 'children',
match: { status: 'active' },
options: { sort: { order: 1, title: 1 } },
populate: {
path: 'children',
match: { status: 'active' },
options: { sort: { order: 1, title: 1 } }
}
});
return categories;
},
// افزایش تعداد دوره‌ها
async incrementCourseCount(categoryId, increment = 1) {
return this.findByIdAndUpdate(categoryId, {
$inc: { course_count: increment }
});
},
// دریافت دسته‌بندی با اسلاگ
async getBySlug(slug) {
return this.findOne({ slug, status: 'active' })
.populate('children')
.populate('parent');
}
};
AcademyCategorySchema.plugin(timestamp);
AcademyCategorySchema.plugin(mongoosePaginate);
module.exports = mongoose.model('AcademyCategory', AcademyCategorySchema);

View File

@@ -1,71 +0,0 @@
const mongoose = require('mongoose');
const timestamp = require('mongoose-timestamp');
const mongoosePaginate = require('mongoose-paginate-v2');
const modelSchema = new mongoose.Schema({
course_images: {
type: [String], // آرایه‌ای از مسیرهای تصاویر
required: false,
default: [],
},
course_video: {
type: String, // مسیر ویدئو
required: false,
trim: true,
default: null,
},
type: {
type: String, // نوع پست: 'image' یا 'video'
required: true,
enum: ['image', 'video'],
},
files: [
{
path: { type: String, required: false },
type: { type: String, enum: ['image', 'video'], required: false },
}
],
caption: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 1000,
default: null,
},
status: {
type: String,
required: false,
trim: true,
enum: ['pending', 'accept', 'reject'],
default: 'accept',
},
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
}],
comments: {
type: Object,
default: null,
},
courseId: {
type: String,
},
is_free: {
type: Boolean,
},
file_name: {
type: String,
},
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model('AcademyContent', modelSchema);
module.exports = PostModel;

View File

@@ -1,25 +0,0 @@
const mongoose = require("mongoose");
const timestamp = require("mongoose-timestamp");
const mongoosePaginate = require("mongoose-paginate-v2");
const modelSchema = new mongoose.Schema({
user_id: {
type: String,
required: true,
trim: true,
default: 0,
},
course_id: {
type: String,
required: true,
trim: true,
default: 0,
},
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model("Likes", modelSchema);
module.exports = PostModel;

View File

@@ -1,55 +0,0 @@
const mongoose = require("mongoose");
const timestamp = require("mongoose-timestamp");
const mongoosePaginate = require("mongoose-paginate-v2");
const modelSchema = new mongoose.Schema({
academy_image: {
type: String,
required: false,
trim: true,
default: null,
},
academy_name: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null,
},
userId: {
type: String,
required: true,
},
rate: {
type: Number,
trim: true,
default: 0
},
number_of_rate: {
type: Number,
trim: true,
default: 0
},
sheba: {
type: String,
required: false,
default: null
},
bio: {
type: String,
required: false,
default: null
},
tag: {
type: Object,
required: false,
default: null
},
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model("Academy", modelSchema);
module.exports = PostModel;

View File

@@ -1,24 +0,0 @@
const mongoose = require("mongoose");
const timestamp = require("mongoose-timestamp");
const mongoosePaginate = require("mongoose-paginate-v2");
const modelSchema = new mongoose.Schema({
price: { type: Number, required: true }, // قیمت نهایی شامل مالیات
discountAmount: { type: Number, default: 0 }, // مبلغ تخفیف
taxAmount: { type: Number, default: 0 }, // مبلغ مالیات
taxRate: { type: Number, default: 9 }, // درصد مالیات
course_name: { type: String, required: true },
course_id: { type: mongoose.Schema.Types.ObjectId, ref: "Course", required: true },
academy_id: { type: mongoose.Schema.Types.ObjectId, ref: "Academy", required: true },
user_id: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
payment_authority: { type: String },
payment_ref_id: { type: String },
status: { type: String, enum: ["pending", "success", "failed", "settled"], default: "success" },
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model("AcademyPayment", modelSchema);
module.exports = PostModel;

View File

@@ -1,80 +0,0 @@
const mongoose = require('mongoose')
const mongoosePaginate = require('mongoose-paginate-v2')
const timestamp = require('mongoose-timestamp')
const adminSchema = new mongoose.Schema({
mobile: {
type: String,
required: false,
trim: true,
unique: true
},
otp: {
type: String,
required: false,
trim: true,
minLength: 6,
maxLength: 6
},
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
},
profile_image: {
type: String,
required: false,
trim: true
},
gender: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
last_online: {
type: Date, // ذخیره تاریخ و زمان آخرین بازدید
default: Date.now // تنظیم تاریخ به تاریخ فعلی
}
})
adminSchema.plugin(timestamp)
adminSchema.plugin(mongoosePaginate)
const AdminModel = mongoose.model('Admin', adminSchema)
module.exports = AdminModel

View File

@@ -1,23 +0,0 @@
const mongoose = require('mongoose')
const mongoosePaginate = require('mongoose-paginate-v2')
const timestamp = require('mongoose-timestamp')
const advertisingCategorySchema = new mongoose.Schema({
title: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
status: {
type: Boolean,
required: false,
default: true
}
})
advertisingCategorySchema.plugin(timestamp)
advertisingCategorySchema.plugin(mongoosePaginate)
const AdvertisingCategoryModel = mongoose.model('AdvertisingCategory', advertisingCategorySchema)
module.exports = AdvertisingCategoryModel

View File

@@ -1,32 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const mongoosePaginate = require('mongoose-paginate-v2')
const advertisingCommentSchema = new mongoose.Schema({
advertisingId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Advertising',
required: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
text: {
type: String,
required: true,
trim: true
},
status: {
type: String,
enum: ['pending', 'accepted', 'rejected'],
default: 'pending'
}
})
advertisingCommentSchema.plugin(timestamp)
advertisingCommentSchema.plugin(mongoosePaginate)
const AdvertisingComment = mongoose.model('AdvertisingComment', advertisingCommentSchema)
module.exports = AdvertisingComment

View File

@@ -1,27 +0,0 @@
const mongoose = require('mongoose')
const mongoosePaginate = require('mongoose-paginate-v2')
const timestamp = require('mongoose-timestamp')
const advertisingFeaturesSchema = new mongoose.Schema({
title: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
status: {
type: Boolean,
required: false,
default: true
},
type: {
type: String,
default: 'boolean'
}
})
advertisingFeaturesSchema.plugin(timestamp)
advertisingFeaturesSchema.plugin(mongoosePaginate)
const AdvertisingFeaturesModel = mongoose.model('AdvertisingFeatures', advertisingFeaturesSchema)
module.exports = AdvertisingFeaturesModel

View File

@@ -1,20 +0,0 @@
const mongoose = require('mongoose')
const advertisingLikeSchema = new mongoose.Schema({
advertisingId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Advertising',
required: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
}
})
// تعریف مدل AdvertisingLikeModel
const AdvertisingLikeModel = mongoose.model('AdvertisingLike', advertisingLikeSchema)
module.exports = AdvertisingLikeModel

View File

@@ -1,230 +0,0 @@
const mongoose = require('mongoose')
const mongoosePaginate = require('mongoose-paginate-v2')
const timestamp = require('mongoose-timestamp')
const advertisingSchema = new mongoose.Schema({
title: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
category: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
description: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 1024,
default: null
},
province: {
type: Object,
default: null
},
city: {
type: Object,
default: null
},
neighbourhood: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
address: {
type: String,
trim: true,
minLength: 1,
maxLength: 512,
default: null
},
lat: {
type: String,
trim: true,
minLength: 1,
maxLength: 256,
default: null
},
lng: {
type: String,
trim: true,
minLength: 1,
maxLength: 256,
default: null
},
images: [{
type: String,
required: false,
trim: true,
default: null
}],
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
}
}],
features: [{
title: {
type: String,
required: false,
trim: true
},
value: {
type: Boolean,
required: true
}
}],
contactInfo: {
phone: {
type: String,
required: false,
trim: true,
default: null
},
mobile: {
type: String,
required: false,
trim: true,
default: null
},
telegramLink: {
type: String,
required: false,
trim: true,
default: null
},
whatsappNumber: {
type: String,
required: false,
trim: true,
default: null
},
instagramLink: {
type: String,
required: false,
trim: true,
default: null
},
saveInfoForNextAds: {
type: Boolean,
required: true,
default: false
}
},
type: {
type: String,
enum: ['free', 'normal', 'special', 'highlight'],
default: 'normal'
},
status: {
type: String,
enum: ['pre_payment', 'paid', 'accepted', 'rejected', 'expired'],
default: 'pre_payment'
},
payment_status: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
creator_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
reject_reason: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 1024,
default: null
},
mostDiscountPercentage: {
type: Number,
required: false
},
showDiscount: {
type: Boolean,
required: false,
default: false
},
likesCount: {
type: Number,
default: 0
},
commentsCount: {
type: Number,
default: 0
},
ratings: [{
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
rating: {
type: Number,
required: true,
min: 1,
max: 5
}
}],
averageRating: {
type: Number,
default: 0
},
ratingsCount: {
type: Number,
default: 0
},
viewCount: {
type: Number,
default: 0
},
acceptedAt: {
type: Date,
default: null
}
})
advertisingSchema.plugin(timestamp)
advertisingSchema.plugin(mongoosePaginate)
const AdvertisingModel = mongoose.model('Advertising', advertisingSchema)
module.exports = AdvertisingModel

View File

@@ -1,113 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const mongoosePaginate = require('mongoose-paginate-v2')
const advertisingProfileSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
profile_image: {
type: String,
required: false,
trim: true,
default: null
},
image_count: {
type: Number,
default: 0
},
like_count: {
type: Number,
default: 0
},
comment_count: {
type: Number,
default: 0
},
vitrine_name: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
category: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
about_us: {
type: String,
required: false,
minLength: 1,
maxLength: 1024,
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
},
address: {
type: String,
required: false,
trim: true,
default: null
},
neighbourhood: {
type: String,
required: false,
trim: true,
default: null
},
adTotalRatings: {
type: Number,
default: 0
},
adRatingsCount: {
type: Number,
default: 0
},
adAverageRating: {
type: Number,
default: 0
},
contactInfo: {
phone: String,
mobile: String,
telegramLink: String,
whatsappNumber: String,
instagramLink: String,
saveInfoForNextAds: { type: Boolean, default: false }
}
})
advertisingProfileSchema.plugin(timestamp)
advertisingProfileSchema.plugin(mongoosePaginate)
const AdvertisingProfileModel = mongoose.model('AdvertisingProfile', advertisingProfileSchema)
module.exports = AdvertisingProfileModel

View File

@@ -1,26 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const advertisingRatingSchema = new mongoose.Schema({
advertisingId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Advertising',
required: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
rating: {
type: Number,
required: true,
min: 1,
max: 5
}
})
advertisingRatingSchema.plugin(timestamp)
const AdvertisingRatingModel = mongoose.model('AdvertisingRating', advertisingRatingSchema)
module.exports = AdvertisingRatingModel

View File

@@ -1,25 +0,0 @@
const mongoose = require('mongoose')
const advertisingTypeSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
minLength: 1,
maxLength: 255
},
price: {
type: Number,
required: true,
trim: true,
minLength: 1,
maxLength: 255
},
status: {
type: String,
default: 'active'
}
})
const AdvertisingTypeModel = mongoose.model('AdvertisingType', advertisingTypeSchema)
module.exports = AdvertisingTypeModel

View File

@@ -1,55 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const mongoosePaginate = require('mongoose-paginate-v2')
const commentSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
project: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Project',
required: false
},
offer: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Offer',
required: false
},
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
rating: {
type: Number,
required: false,
default: null
},
comment: {
type: String,
trim: true
},
post: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Post',
required: false
},
comment_for: {
type: String,
enum: ['user', 'project', 'post'],
required: true
},
status: {
type: String,
enum: ['pending', 'accepted', 'rejected'],
default: 'pending'
}
})
commentSchema.plugin(timestamp)
commentSchema.plugin(mongoosePaginate)
const CommentModel = mongoose.model('Comment', commentSchema)
module.exports = CommentModel

View File

@@ -1,40 +0,0 @@
const mongoose = require("mongoose");
const timestamp = require("mongoose-timestamp");
const mongoosePaginate = require("mongoose-paginate-v2");
const modelSchema = new mongoose.Schema({
user_id: {
type: String,
required: true,
trim: true,
},
course_id: {
type: String,
required: true,
trim: true,
},
comment: {
type: String,
required: true,
trim: true,
default: 0,
},
rate: {
type: String,
required: true,
trim: true,
default: 0,
},
status: {
type: String,
enum: ['pending', 'accepted', 'rejected'],
default: 'accepted'
}
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model("CourseComment", modelSchema);
module.exports = PostModel;

View File

@@ -1,89 +0,0 @@
const mongoose = require("mongoose");
const timestamp = require("mongoose-timestamp");
const mongoosePaginate = require("mongoose-paginate-v2");
const modelSchema = new mongoose.Schema({
price: {
type: String,
required: true,
},
cuorse_name: {
type: String,
required: true,
},
category:{
type: String
},
offer: {
type: String
},
academyId:{
type: String
},
caption: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 1000,
default: null,
},
status: {
type: String,
required: false,
trim: true,
enum: ['pending', 'accept', 'reject'],
default: 'accept',
},
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
likes: {
type: Number,
required: false,
trim: true,
default: 0,
},
comments: {
type: Object,
default: null,
},
type: {
type: String,
required: false,
trim: true,
enum: ['normal', 'pro', 'legend'],
default: "normal",
},
course_time: {
type: String
},
number_of_course_content: {
type: String
},
teacher_number: {
type: String
},
course_image: {
type: String
},
teacher_name: {
type: String
},
is_free: {
type: Boolean,
default: false,
},
contactInfo: {
mobile: { type: String, default: null },
telegramLink: { type: String, default: null },
whatsappNumber: { type: String, default: null },
instagramLink: { type: String, default: null },
},
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model("Course", modelSchema);
module.exports = PostModel;

View File

@@ -1,22 +0,0 @@
const mongoose = require("mongoose");
const timestamp = require("mongoose-timestamp");
const mongoosePaginate = require("mongoose-paginate-v2");
const modelSchema = new mongoose.Schema({
price: {
type: String,
required: true,
},
cuorse_type: {
type: String,
required: false,
trim: true,
enum: ['normal', 'pro', 'legend'],
},
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model("CoursePayment", modelSchema);
module.exports = PostModel;

View File

@@ -1,47 +0,0 @@
// const mongoose = require('mongoose')
// const timestamp = require('mongoose-timestamp')
// const subExpertiseSchema = new mongoose.Schema({
// id: {
// type: mongoose.Schema.Types.ObjectId,
// ref: 'ExpertiseModel'
// },
// name: {
// type: String
// }
// })
// const expertiseSchema = new mongoose.Schema({
// expertise: {
// type: String,
// unique: true
// },
// sub_expertise: [subExpertiseSchema]
// })
// expertiseSchema.plugin(timestamp)
// const ExpertiseModel = mongoose.model('Expertise', expertiseSchema)
// module.exports = ExpertiseModel
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const subExpertiseSchema = new mongoose.Schema({
name: {
type: String
}
})
const expertiseSchema = new mongoose.Schema({
expertise: {
type: String,
unique: true
},
sub_expertise: [subExpertiseSchema]
})
expertiseSchema.plugin(timestamp)
const ExpertiseModel = mongoose.model('Expertise', expertiseSchema)
module.exports = ExpertiseModel

View File

@@ -1,54 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const exploreInteractionSchema = new mongoose.Schema({
viewerId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
targetType: {
type: String,
enum: ['post', 'profile', 'academy'],
required: true
},
targetId: {
type: mongoose.Schema.Types.ObjectId,
required: true
},
authorId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null
},
contentType: {
type: String,
enum: ['image', 'video', 'academy'],
default: 'image'
},
action: {
type: String,
enum: [
'view',
'profile_visit',
'share',
'like',
'comment',
'offer',
'watch_complete',
'skip',
'dwell'
],
default: 'view'
},
dwellMs: {
type: Number,
default: null
}
})
exploreInteractionSchema.index({ viewerId: 1, createdAt: -1 })
exploreInteractionSchema.plugin(timestamp)
module.exports = mongoose.model('ExploreInteraction', exploreInteractionSchema)

View File

@@ -1,15 +0,0 @@
const mongoose = require("mongoose");
const timestamp = require("mongoose-timestamp");
const mongoosePaginate = require("mongoose-paginate-v2");
const modelSchema = new mongoose.Schema({
user_id: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
course_id: { type: mongoose.Schema.Types.ObjectId, ref: "Course", required: true },
price_paid: { type: Number, required: true }, // قیمت پرداخت شده
purchased_at: { type: Date, default: Date.now },
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model("IsPaymentCourse", modelSchema);
module.exports = PostModel;

View File

@@ -1,25 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const likeSchema = new mongoose.Schema({
postId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Post',
required: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
})
// تعریف مدل LikeModel
likeSchema.plugin(timestamp)
const LikeModel = mongoose.model('Like', likeSchema)
module.exports = LikeModel

View File

@@ -1,66 +0,0 @@
const mongoose = require('mongoose')
const { Schema } = mongoose
const messageSchema = new Schema({
senderId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
receiverId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
content: {
type: String,
required: false,
trim: true
},
file: {
type: String,
required: false
},
fileType: {
type: String,
enum: ['image', 'video', 'file', 'voice', 'location'],
required: false
},
replyToId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Message',
required: false
},
forwardedFrom: {
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
userName: String,
displayName: String
},
createdAt: {
type: Date,
default: Date.now
},
readStatus: {
type: Number,
default: 0
},
/** Auto-delete after this time (self-destruct messages) */
expiresAt: {
type: Date,
required: false,
index: true
},
/** View-once photo / video / voice */
viewOnce: {
type: Boolean,
default: false
},
viewOnceOpenedAt: {
type: Date,
required: false
}
})
const MessageModel = mongoose.model('Message', messageSchema)
module.exports = MessageModel

View File

@@ -1,37 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const mongoosePaginate = require('mongoose-paginate-v2')
const notificationSchema = new mongoose.Schema({
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // مرجع به مدل کاربر
required: true
},
project_post_id: {
type: mongoose.Schema.Types.ObjectId,
required: false
},
type: {
type: String,
required: true
},
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
read: {
type: Boolean,
default: false // وضعیت خوانده شده یا نشده
}
})
notificationSchema.plugin(timestamp)
notificationSchema.plugin(mongoosePaginate)
const NotificationModel = mongoose.model('Notification', notificationSchema)
module.exports = NotificationModel

View File

@@ -1,32 +0,0 @@
const mongoose = require('mongoose')
const mongoosePaginate = require('mongoose-paginate-v2')
const timestamp = require('mongoose-timestamp')
const offerSchema = new mongoose.Schema({
sender: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
receiver: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
status: {
type: String,
enum: ['pending', 'accepted', 'rejected'], // وضعیت درخواست
default: 'pending'
},
transaction_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Payment',
default: null
}
})
offerSchema.plugin(timestamp)
offerSchema.plugin(mongoosePaginate)
const OfferModel = mongoose.model('Offer', offerSchema)
module.exports = OfferModel

View File

@@ -1,21 +0,0 @@
const mongoose = require('mongoose')
const offerTypeModelSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
minLength: 1,
maxLength: 255
},
price: {
type: Number,
required: true,
trim: true,
minLength: 1,
maxLength: 255
}
})
const OfferTypeModel = mongoose.model('OfferTypeModel', offerTypeModelSchema)
module.exports = OfferTypeModel

View File

@@ -1,46 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const mongoosePaginate = require('mongoose-paginate-v2')
const paymentSchema = new mongoose.Schema({
amount: {
type: Number,
required: true
},
status: {
type: String,
enum: ['pending', 'successful', 'failed'],
default: 'pending'
},
authority: {
type: String,
required: false
},
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
project_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Project'
},
advertising_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Advertising'
},
type: {
type: String,
required: true
},
installment_step: {
type: String,
required: false,
default: null
}
})
paymentSchema.plugin(timestamp)
paymentSchema.plugin(mongoosePaginate)
const PaymentModel = mongoose.model('Payment', paymentSchema)
module.exports = PaymentModel

View File

@@ -1,60 +0,0 @@
const mongoose = require('mongoose');
const timestamp = require('mongoose-timestamp');
const mongoosePaginate = require('mongoose-paginate-v2');
const modelSchema = new mongoose.Schema({
post_images: {
type: [String], // آرایه‌ای از مسیرهای تصاویر
required: false,
default: [],
},
post_video: {
type: String, // مسیر ویدئو
required: false,
trim: true,
default: null,
},
type: {
type: String, // نوع پست: 'image' یا 'video'
required: true,
enum: ['image', 'video'],
},
files: [
{
path: { type: String, required: true },
type: { type: String, enum: ['image', 'video'], required: true },
}
],
caption: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 1000,
default: null,
},
status: {
type: String,
required: false,
trim: true,
enum: ['pending', 'accept', 'reject'],
default: 'pending',
},
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
}],
comments: {
type: Object,
default: null,
},
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model('Post', modelSchema);
module.exports = PostModel;

View File

@@ -1,186 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const mongoosePaginate = require('mongoose-paginate-v2')
const projectSchema = new mongoose.Schema({
title: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
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
},
gender: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
age: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
conversation_projects: {
type: Boolean,
required: false,
default: null
},
province: {
type: Object,
default: null
},
city: {
type: Object,
default: null
},
offer_time: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
offer_price: {
type: Number,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
description: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 1024,
default: null
},
project_type: {
type: String,
enum: ['free', 'normal', 'force', 'highlight'],
default: 'normal'
},
payment_status: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 255,
default: null
},
// فیلد برای وضعیت پروژه
status: {
type: String,
enum: ['pre_payment', 'paid', 'accepted', 'rejected', 'done', 'cancled', 'ongoing'],
default: 'pre_payment'
},
public_status: {
type: String,
enum: ['public', 'private'],
default: 'public'
},
created_for_user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null
},
creator_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
requested_users: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
selected_user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null
},
final_price: {
type: Number,
required: false,
default: null
},
final_time: {
type: Number,
required: false,
default: null
},
installments: [{
installment_number: {
type: Number,
required: true
},
amount: {
type: Number,
required: true
},
due_date: {
type: Date,
required: true
}
}],
reject_reason: {
type: String,
required: false,
trim: true,
minLength: 1,
maxLength: 1024,
default: null
},
ratings: [{
creator_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
rating: {
type: Number,
required: true
},
comment: {
type: String,
trim: true
}
}],
acceptedAt: {
type: Date,
default: null
},
isExpired: {
type: Boolean,
default: false
}
})
projectSchema.plugin(timestamp)
// اضافه کردن پیجینیشن به مدل
projectSchema.plugin(mongoosePaginate)
const ProjectModel = mongoose.model('Project', projectSchema)
module.exports = ProjectModel

View File

@@ -1,33 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const requestSchema = new mongoose.Schema({
project: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Project',
required: true
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
time: {
type: Number,
required: true
},
price: {
type: Number,
required: true
},
status: {
type: String,
enum: ['pending', 'accepted', 'rejected'],
default: 'pending'
}
})
requestSchema.plugin(timestamp)
const RequestModel = mongoose.model('Request', requestSchema)
module.exports = RequestModel

View File

@@ -1,19 +0,0 @@
// models/Setting.js
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const settingSchema = new mongoose.Schema({
key: {
type: String,
required: true,
unique: true
},
value: {
type: String,
required: true
}
})
settingSchema.plugin(timestamp)
const SettingModel = mongoose.model('Setting', settingSchema)
module.exports = SettingModel

View File

@@ -1,19 +0,0 @@
const mongoose = require('mongoose')
const provincesSchema = new mongoose.Schema({
id: Number,
name: String,
slug: String
})
const citiesSchema = new mongoose.Schema({
id: Number,
name: String,
slug: String,
province_id: Number
})
const ProvinceModel = mongoose.model('Province', provincesSchema)
const CityModel = mongoose.model('City', citiesSchema)
module.exports = { ProvinceModel, CityModel }

View File

@@ -1,41 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const storySchema = new mongoose.Schema({
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
media_path: {
type: String,
required: true
},
media_type: {
type: String,
enum: ['image', 'video'],
required: true
},
expires_at: {
type: Date,
required: true,
index: true
},
viewers: [{
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
viewed_at: {
type: Date,
default: Date.now
}
}]
})
storySchema.index({ expires_at: 1 }, { expireAfterSeconds: 0 })
storySchema.plugin(timestamp)
module.exports = mongoose.model('Story', storySchema)

View File

@@ -1,15 +0,0 @@
const mongoose = require("mongoose");
const timestamp = require("mongoose-timestamp");
const mongoosePaginate = require("mongoose-paginate-v2");
const modelSchema = new mongoose.Schema({
type: { type: String, enum: ["course", "product", "service"], required: true },
tax: { type: Number, required: true, default: 9 }, // درصد مالیات
is_active: { type: Boolean, default: true },
});
modelSchema.plugin(timestamp);
modelSchema.plugin(mongoosePaginate);
const PostModel = mongoose.model("Tax", modelSchema);
module.exports = PostModel;

View File

@@ -1,20 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const mongoosePaginate = require('mongoose-paginate-v2')
const ticketMessageSchema = new mongoose.Schema({
text: { type: String, required: false },
file: {
type: String, // مسیر فایل در ذخیره شود
required: false // فایل اختیاری است
},
senderType: { type: String, enum: ['User', 'Admin'], required: true },
senderId: { type: mongoose.Schema.Types.ObjectId, refPath: 'senderType', required: true },
ticket: { type: mongoose.Schema.Types.ObjectId, ref: 'Ticket', required: true }
})
ticketMessageSchema.plugin(timestamp)
// اضافه کردن پیجینیشن به مدل
ticketMessageSchema.plugin(mongoosePaginate)
const TicketMessageModel = mongoose.model('TicketMessage', ticketMessageSchema)
module.exports = TicketMessageModel

View File

@@ -1,24 +0,0 @@
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const mongoosePaginate = require('mongoose-paginate-v2')
const ticketSchema = new mongoose.Schema({
title: { type: String, required: true },
status: { type: String, enum: ['Pending', 'Answered', 'Customer Response', 'Closed'], default: 'Pending' },
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
new_message: {
type: Boolean,
default: false // وضعیت خوانده شده یا نشده
}
})
ticketSchema.pre('save', function (next) {
this.updatedAt = Date.now()
next()
})
ticketSchema.plugin(timestamp)
// اضافه کردن پیجینیشن به مدل
ticketSchema.plugin(mongoosePaginate)
const TicketModel = mongoose.model('Ticket', ticketSchema)
module.exports = TicketModel

View File

@@ -1,21 +0,0 @@
const mongoose = require('mongoose')
const typeAndPriceSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
minLength: 1,
maxLength: 255
},
price: {
type: Number,
required: true,
trim: true,
minLength: 1,
maxLength: 255
}
})
const TypeAndPriceModel = mongoose.model('TypeAndPrice', typeAndPriceSchema)
module.exports = TypeAndPriceModel

View File

@@ -1,391 +0,0 @@
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
},
otp: {
type: String,
required: false,
trim: true,
minLength: 6,
maxLength: 6,
default: null
},
otpSentAt: {
type: Date,
required: false,
default: null
},
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'
},
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
},
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
},
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
},
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
},
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,
default: null
},
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
},
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
}
// 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)
const UserModel = mongoose.model('User', userSchema)
module.exports = UserModel

View File

@@ -1,12 +0,0 @@
// models/VersionModel.js
const mongoose = require('mongoose')
const versionSchema = new mongoose.Schema({
version: { type: String, required: true },
mandatory: { type: Boolean, required: true },
releaseNotes: { type: String },
updateUrls: { type: [String], required: true } // لینک‌های آپدیت
})
const VersionModel = mongoose.model('Version', versionSchema)
module.exports = VersionModel

View File

@@ -1,14 +0,0 @@
const mongoose = require('mongoose')
const mongoosePaginate = require('mongoose-paginate-v2')
const timestamp = require('mongoose-timestamp')
const license = new mongoose.Schema({
userId : String,
Confirmation : { type : Boolean, default : false},
licenseImg : { type : String },
})
license.plugin(timestamp)
license.plugin(mongoosePaginate)
const LicenseMoldel = mongoose.model('license', license)
module.exports = LicenseMoldel

View File

@@ -1,423 +0,0 @@
/* eslint-disable camelcase */
const jwt = require('jsonwebtoken')
const jMoment = require('moment-jalaali')
const OfferModel = require('../../../models/OfferModel')
const UserModel = require('../../../models/UserModel')
const OfferTypeModel = require('../../../models/OfferTypeModel')
const PaymentModel = require('../../../models/PaymentModel')
const NotificationModel = require('../../../models/NotificationModel')
const { default: axios } = require('axios')
const CommentModel = require('../../../models/CommentModel')
const {
resolveRatingForComment,
recalculateUserRating
} = require('../../../utils/commentRating')
const getOfferPriceFromDatabase = async (itemName) => {
const item = await OfferTypeModel.findOne({ name: itemName })
if (!item) {
throw new Error('Item not found in database')
}
return item.price
}
const getOfferTypes = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
const user = await UserModel.findById(userId)
if (!user) {
return res.status(422).json({
error: true,
message: 'کاربر یافت نشد'
})
}
let offerTypes = await OfferTypeModel.find({}, 'name price')
// بررسی برای مواردی مانند پیدا نشدن انواع پروژه
if (!offerTypes) {
return res.status(404).json({ message: 'انواع پروژه یافت نشد.' })
}
// اگر کاربر درخواست ماهانه نداشته باشد، نوع پروژه ماهانه را حذف کنید
if (user.monthly_free_offer <= 0) {
offerTypes = offerTypes.filter(projectType => projectType.name !== 'free')
}
// ارسال انواع پروژه به کاربر
res.status(200).json({ offerTypes })
} catch (error) {
// در صورت بروز خطا، ارسال پیام خطا به کاربر
console.error('Error in getOfferTypes:', error)
res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' })
}
}
const getUserOffers = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
const { page = 1, limit = 10, status_filter, id } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست
let filter = {}
let filterbyid = {receiver: id}
let filterbyidsender = {sender: id}
if (status_filter === 'درخواست') {
filter = { sender: userId }
} else {
filter = { receiver: userId }
}
// تنظیم گزینه‌های صفحه‌بندی
const options = {
page: parseInt(page), // تبدیل صفحه به عدد صحیح
limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح
sort: { createdAt: -1 }, // بر اساس زمان ایجاد (createdAt) مرتب کنید (نزولی)
populate: [
{
path: 'sender',
select: 'profile_image user_level first_name last_name user_name is_verified user_score rate'
},
{
path: 'receiver',
select: 'profile_image user_level first_name last_name user_name is_verified user_score rate'
}
]
}
// دریافت آفرهای صفحه‌بندی شده
let offer = await OfferModel.paginate(filter, options)
let offerbyid = await OfferModel.paginate(filterbyid, options)
let offerbyidsender = await OfferModel.paginate(filterbyidsender, options)
const totalPages = offer.totalPages
const totalItems = offer.totalDocs
// فرمت کردن تاریخ و ارسال پاسخ
offer = offer?.docs.map(offer => {
const jDate = jMoment(offer.createdAt).format('jYYYY-jMM-jDD HH:mm')
return {
...offer._doc,
createdAt: jDate
}
})
res.json({
offerbyidsender,
offerbyid,
offer,
totalPages, // ارسال تعداد کل صفحات
totalItems // ارسال تعداد کل آیتم‌ها
})
} catch (error) {
next(error)
}
}
const updateOfferStatus = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id // ID کاربری که درخواست را به‌روزرسانی می‌کند
const reciverUser = await UserModel.findById(userId)
const { offerId, action } = req.body // آیدی آفر و اکشن (accept یا reject)
// بررسی اعتبار ورودی‌ها
if (!offerId || !['accept', 'reject'].includes(action)) {
return res.status(400).json({ error: 'Invalid input' })
}
// یافتن آفر موردنظر
const offer = await OfferModel.findById(offerId)
const senderUser = await UserModel.findById(offer?.sender)
if (!offer) {
return res.status(404).json({ error: 'Offer not found' })
}
// بررسی اینکه آیا کاربر گیرنده این آفر است یا خیر
if (offer.receiver.toString() !== userId) {
return res.status(403).json({ error: 'You are not authorized to update this offer' })
}
// بررسی اینکه آیا آفر قبلاً به‌روزرسانی شده یا نه
if (offer.status !== 'pending') {
return res.status(400).json({ error: 'Offer has already been updated', message: 'وضعیت این درخواست قبلا تغییر کرده است.' })
}
// به‌روزرسانی وضعیت آفر بر اساس اکشن
offer.status = action === 'accept' ? 'accepted' : 'rejected'
await offer.save()
if (action === 'accept') {
const data = JSON.stringify({
mobile: senderUser?.mobile,
templateId: '633448',
parameters: [
{ name: 'USER', value: reciverUser?.first_name + ' ' + reciverUser?.last_name },
{ name: 'EMPLOYER', value: senderUser?.first_name + ' ' + senderUser?.last_name }
]
})
const config = {
method: 'post',
url: 'https://api.sms.ir/v1/send/verify',
headers: {
'Content-Type': 'application/json',
Accept: 'text/plain',
'x-api-key': process.env.SMS_IR_API_KEY
},
data
}
axios(config)
.then(function (response) {
})
.catch(function (error) {
console.log(error)
})
} else {
const data = JSON.stringify({
mobile: senderUser?.mobile,
templateId: '682678',
parameters: [
{ name: 'USER', value: reciverUser?.first_name + ' ' + reciverUser?.last_name },
{ name: 'EMPLOYER', value: senderUser?.first_name + ' ' + senderUser?.last_name }
]
})
const config = {
method: 'post',
url: 'https://api.sms.ir/v1/send/verify',
headers: {
'Content-Type': 'application/json',
Accept: 'text/plain',
'x-api-key': process.env.SMS_IR_API_KEY
},
data
}
axios(config)
.then(function (response) {
})
.catch(function (error) {
console.log(error)
})
}
res.status(200).json({
message: `Offer has been ${offer.status} successfully`,
offerId: offer._id,
status: offer.status
})
} catch (error) {
next(error)
}
}
// Controller function for handling successful advertising payment
const handleSuccessfulOfferPayment = async (req, res) => {
try {
const { reciverId, offerType } = req.body
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
// Check user's free offer eligibility
const user = await UserModel.findById(userId)
if (!user) return res.status(404).json({ error: 'User not found' })
// Check user's free offer eligibility
const reciverUser = await UserModel.findById(reciverId)
if (!user) return res.status(404).json({ error: 'User not found' })
let price
if (user.monthly_free_offer > 0 && offerType === 'free') {
// Use free offer and decrement counter
price = 0
user.monthly_free_offer -= 1
await user.save()
} else {
// Retrieve offer price based on type
price = await getOfferPriceFromDatabase(offerType)
// Create a new payment entry
const payment = new PaymentModel({
amount: Number(price),
status: 'successful',
authority: '', // Add authority if available
user_id: userId,
type: 'offer',
installment_step: null
})
await payment.save()
}
// Create a new payment entry
const payment = new PaymentModel({
amount: Number(price),
status: 'successful',
authority: '', // Add authority if available
user_id: userId,
type: 'offer',
installment_step: null
})
await payment.save()
// Create a new offer
const offer = new OfferModel({
sender: userId,
receiver: reciverId,
transaction_id: payment._id // Link the payment to the offer
})
await offer.save()
console.log(reciverUser?.first_name)
// Send notification to selected user
const notification = new NotificationModel({
user_id: reciverId,
project_post_id: offer?._id,
type: 'new_offer',
title: 'درخواست همکاری جدید',
description: `${reciverUser?.first_name} عزیز یک درخواست همکاری برای شما در مجموعه مدستاگرام ثبت شد`
})
await notification.save()
// Send Sms
const data = JSON.stringify({
mobile: reciverUser?.mobile,
templateId: '569006',
parameters: [
{ name: 'FIRSTNAME', value: reciverUser?.first_name },
{ name: 'LASTNAME', value: reciverUser?.last_name }
]
})
const config = {
method: 'post',
url: 'https://api.sms.ir/v1/send/verify',
headers: {
'Content-Type': 'application/json',
Accept: 'text/plain',
'x-api-key': process.env.SMS_IR_API_KEY
},
data
}
axios(config)
.then(function (response) {
})
.catch(function (error) {
console.log(error)
})
// Send event based on payment status
// Emit a success event
if ('eventEmitter' in req.app) {
req.app.get('eventEmitter').emit('paymentSuccess', {
message: 'Payment and offer created successfully',
offerId: offer._id
})
} else {
console.error('Error: eventEmitter is not defined or not properly configured.')
}
res.status(200).json({
message: 'Payment and offer recorded successfully',
offerId: offer._id,
price
})
} catch (error) {
console.error('Error occurred:', error)
res.status(500).json({ error: 'Internal server error' })
}
}
// Controller function for handling failed advertising payment
const handleFailedOfferPayment = async (req, res) => {
try {
const { offerType } = req.body
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
const price = await getOfferPriceFromDatabase(offerType)
// Create a new payment entry
const payment = new PaymentModel({
amount: Number(price),
status: 'failed',
authority: '', // Add authority if available
user_id: userId,
type: 'offer',
installment_step: null
})
await payment.save()
// Emit a failure event
if ('eventEmitter' in req.app) {
req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment failed' })
} else {
console.error('Error: eventEmitter is not defined or not properly configured.')
}
res.status(200).json({ message: 'Payment failed', price })
} catch (error) {
console.error('Error occurred:', error)
res.status(500).json({ error: 'Internal server error' })
}
}
const createOfferComment = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
const { offerId, comment, rate, user_id } = req.body
if (!offerId || !comment || !user_id) {
return res.status(400).send({ message: 'All fields are required' })
}
const { ratingValue, isNewRating, requiresRating } = await resolveRatingForComment({
creatorId: userId,
targetUserId: user_id,
rate
})
if (requiresRating) {
return res.status(400).send({ message: 'Rating must be between 1 and 5' })
}
const newComment = new CommentModel({
user: user_id,
offer: offerId,
creator: userId,
rating: ratingValue,
comment,
comment_for: 'user',
status: 'pending'
})
await newComment.save()
if (isNewRating) {
await recalculateUserRating(user_id)
}
res.status(200).json({ message: 'نظر شما با موفقیت ثبت شد' })
} catch (error) {
next(error)
}
}
module.exports = {
getUserOffers,
getOfferTypes,
handleSuccessfulOfferPayment,
handleFailedOfferPayment,
updateOfferStatus,
createOfferComment
}

View File

@@ -1,389 +0,0 @@
const express = require("express");
const router = express.Router();
const academyController = require("../../../controllers/application/academy/academyControllers");
const multer = require("multer");
const path = require("path");
const fs = require("fs");
// const academyCategoryController = require("../../../controllers/application/academy/academyCategoryController");
// ==========================================
// ⚙️ تنظیمات اولیه و دایرکتوری‌ها
// ==========================================
// اطمینان از وجود دایرکتوری‌ها
const ensureDir = (dir) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
console.log("DEBUG: Created directory:", dir);
}
};
const BASE_DIR = process.cwd();
const courseVideoDir = path.join(BASE_DIR, "storage", "courses", "videos");
const courseImageDir = path.join(BASE_DIR, "storage", "courses", "images");
console.log("DEBUG: BASE_DIR:", BASE_DIR);
console.log("DEBUG: courseVideoDir:", courseVideoDir);
console.log("DEBUG: courseImageDir:", courseImageDir);
ensureDir(courseVideoDir);
ensureDir(courseImageDir);
// ==========================================
// 📁 تنظیمات آپلود فایل با Multer
// ==========================================
// تنظیمات ذخیره‌سازی فایل‌ها
const storage = multer.diskStorage({
destination: (req, file, cb) => {
console.log("DEBUG: Multer destination - fieldname:", file.fieldname);
// استفاده از دایرکتوری موقت برای آپلود اولیه
const tempDir = "/tmp/modstagram-uploads";
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
cb(null, tempDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
const filename = uniqueSuffix + path.extname(file.originalname);
console.log("DEBUG: Generated filename:", filename);
cb(null, filename);
},
});
// تنظیمات Multer با محدودیت حجم 500 مگابایت
const upload = multer({
storage: storage,
limits: {
fileSize: 500 * 1024 * 1024, // 500 MB
},
});
// ==========================================
// 🏢 مسیرهای مربوط به آکادمی
// ==========================================
// دریافت اطلاعات آکادمی کاربر جاری
router.get("/", academyController.findAcademy);
// یافتن آکادمی با شناسه
router.post("/findAcademyById", academyController.findAcademyById);
// تکمیل پروفایل آکادمی
router.post("/academy/profile", academyController.academyProfile);
// ==========================================
// 📚 مسیرهای مدیریت دوره‌ها (Courses)
// ==========================================
// ایجاد دوره جدید
router.post("/academy/creat/course", academyController.creatCourse);
// ویرایش دوره
router.patch("/academy/update/course", academyController.updateCourse);
// حذف دوره (تغییر وضعیت به reject)
router.delete("/academy/delete/course", academyController.deleteCourse);
// دریافت لیست دوره‌های آکادمی جاری
router.get("/academy/get/course", academyController.getCourse);
// دریافت لیست همه دوره‌ها با فیلتر و صفحه‌بندی
router.get("/academy/get/curses", academyController.getCourses);
// محتوای رایگان آموزشگاه برای اکسپلور
router.get(
"/academy/explore/free-content",
academyController.getFreeAcademyExploreContent
);
// دریافت دوره‌های یک آکادمی خاص
router.get("/academy/get/getAcademyCourse/:Id", academyController.getAcademyCourse);
// ==========================================
// 💳 مسیرهای پرداخت و مالی
// ==========================================
// پرداخت برای اشتراک پرو
router.post("/academy/course/payment", academyController.proPayment);
// کال‌بک درگاه پرداخت زرین‌پال (اشتراک پرو)
router.get("/academy/course/pro", academyController.handleCoursePaymentCallback);
// پرداخت برای خرید دوره
router.post("/academy/course/payment-web", academyController.coursePayment);
// کال‌بک درگاه پرداخت زرین‌پال (خرید دوره)
router.get("/academy/course/payment/verify", academyController.CoursePaymentCallback);
// ==========================================
// 🎬 مسیرهای محتوای دوره (ویدیوها و فایل‌ها)
// ==========================================
// دریافت محتوای دوره (ویدیوها)
router.get("/academy/course/content", academyController.getCourseContent);
// حذف ویدیو از دوره
router.delete("/academy/course/content/video", academyController.deleteCourseVideo);
// ==========================================
// 📹 مسیرهای آپلود ویدیو (۳ روش مختلف)
// ==========================================
// روش 1: آپلود معمولی با Multipart (با استفاده از Multer)
router.post(
"/academy/course/creat/video",
(req, res, next) => {
console.log("===== STEP 1: Route hit =====");
console.log("Content-Type:", req.headers["content-type"]);
next();
},
(req, res, next) => {
console.log("===== STEP 2: Before multer =====");
next();
},
(req, res, next) => {
// اجرای manual multer با try-catch
upload.single("video")(req, res, (err) => {
if (err) {
console.log("===== MULTER ERROR =====");
console.log("Error name:", err.name);
console.log("Error message:", err.message);
console.log("Full error:", err);
return res.status(400).json({
success: false,
message: `خطا در آپلود: ${err.message}`,
error: err.toString(),
});
}
console.log("===== STEP 3: Multer completed successfully =====");
next();
});
},
(req, res, next) => {
console.log("===== STEP 4: After multer =====");
console.log("req.file:", req.file);
console.log("req.body:", req.body);
if (!req.file) {
console.log("ERROR: No file in req.file!");
return res.status(400).json({
success: false,
message: "فایلی دریافت نشد",
});
}
next();
},
academyController.creatCourseVideo
);
// روش 2: آپلود با Base64 (برای فایل‌های کوچک)
router.post(
"/academy/course/creat/video-base64",
academyController.creatCourseVideoBase64
);
// روش 3: آپلود تکه‌تکه (Chunk Upload) - مناسب برای فایل‌های بزرگ
// دریافت هر تکه از ویدیو
router.post(
"/academy/course/creat/video-chunk",
academyController.creatCourseVideoChunk
);
// ادغام تکه‌ها بعد از اتمام آپلود
router.post(
"/academy/course/creat/video-chunk/merge",
academyController.mergeVideoChunks
);
// بررسی وضعیت آپلود (برای قابلیت Resume)
router.get("/academy/course/upload-status", academyController.getUploadStatus);
// ==========================================
// ❤️ مسیرهای لایک و تعامل
// ==========================================
// لایک کردن محتوای دوره
router.post("/academy/course/like", academyController.likeCourseContent);
// حذف لایک (دیسلایک)
router.post("/academy/course/dislike", academyController.dontLikeCourseContent);
// بررسی وضعیت لایک کاربر
router.post("/academy/course/is-like", academyController.isLikeCourseContent);
// دریافت تعداد لایک‌های یک دوره
router.get(
"/academy/course/likes-count/:courseId",
academyController.getCourseLikesCount
);
// دریافت لیست دوره‌های لایک شده توسط کاربر
router.get(
"/academy/user/liked-courses",
academyController.getUserLikedCourses
);
// ==========================================
// 💬 مسیرهای کامنت و نظرات
// ==========================================
// دریافت تعداد کل کامنت‌های یک دوره
router.get(
"/course/:courseId/comments/count",
academyController.getTotalCommentsCount
);
// دریافت لیست کامنت‌های یک دوره
router.get("/course/:courseId/comments", academyController.getCourseComments);
// ایجاد کامنت جدید
router.post("/comment/create", academyController.createComment);
// تغییر وضعیت کامنت (تایید/رد)
router.patch(
"/comment/:commentId/status",
academyController.updateCommentStatus
);
// حذف کامنت
router.delete("/comment/:commentId/delete", academyController.deleteComment);
// ==========================================
// 🛒 مسیرهای دوره‌های خریداری شده
// ==========================================
// دریافت دوره‌های خریداری شده توسط کاربر جاری
router.get(
"/course/getUserPurchasedCourses",
academyController.getUserPurchasedCourses
);
// دریافت دوره‌های خریداری شده (با populate - روش بهینه)
router.get(
"/course/getUserPurchasedCoursesWithPopulate",
academyController.getUserPurchasedCoursesWithPopulate
);
// دریافت همه پرداخت‌های آکادمی
router.get(
"/course/getAllAcademyPayments",
academyController.getAllAcademyPayments
);
// بررسی خرید دوره توسط کاربر
router.get(
"/course/checkCoursePurchase/:courseId",
academyController.checkCoursePurchase
);
// ==========================================
// 📊 مسیرهای آمار و گزارشات (STATIC)
// ==========================================
// آمار پرداخت‌ها
router.get("/payments/stats", academyController.getPaymentStats);
// آمار دوره‌ها
router.get("/courses/stats", academyController.getCourseStats);
// آمار دوره‌های خریداری شده
router.get("/purchased/stats", academyController.getPurchasedStats);
// آمار آکادمی‌ها
router.get("/academies/stats", academyController.getAcademyStats);
// ==========================================
// ⚙️ مسیرهای تنظیمات (Settings)
// ==========================================
// دریافت همه تنظیمات
router.get("/settings/all", academyController.getAllSettings);
router.get("/settings", academyController.getAllSettings);
// به‌روزرسانی همه تنظیمات
router.put("/settings/all", academyController.updateAllSettings);
router.put("/settings", academyController.updateAllSettings);
// دریافت و ویرایش قیمت دوره‌ها
router.get("/settings/course-prices", academyController.getCoursePrices);
router.put("/settings/course-price", academyController.updateCoursePrice);
// دریافت و ویرایش مالیات
router.get("/settings/tax", academyController.getTax);
router.put("/settings/tax", academyController.updateTax);
// ==========================================
// 🏷️ مسیرهای دسته‌بندی آکادمی (Categories)
// ==========================================
// دریافت لیست دسته‌بندی‌ها (با فیلتر و صفحه‌بندی)
router.get('/categories', academyController.getAll);
// دریافت درخت دسته‌بندی (ساختار سلسله‌مراتبی)
router.get('/categories/tree', academyController.getTree);
// دریافت دسته‌بندی‌های ویژه (برای نمایش در هوم پیج)
router.get('/categories/featured', academyController.getFeatured);
// دریافت دسته‌بندی با اسلاگ (برای سئو و لینک‌های زیبا)
router.get('/categories/slug/:slug', academyController.getBySlug);
// دریافت یک دسته‌بندی با شناسه
router.get('/categories/:id', academyController.getOne);
// ایجاد دسته‌بندی جدید
router.post('/categories', academyController.create);
// ویرایش دسته‌بندی (هم PUT و هم PATCH پشتیبانی می‌شود)
router.put('/categories/:id', academyController.update);
router.patch('/categories/:id', academyController.update);
// حذف دسته‌بندی (سافت دیلیت - فقط غیرفعال می‌شود)
router.delete('/categories/:id', academyController.remove);
// حذف فیزیکی دسته‌بندی (به همراه query string permanent=true)
router.delete('/categories/:id/permanent', (req, res) => {
req.query.permanent = 'true';
academyController.remove(req, res);
});
// تغییر وضعیت دسته‌بندی (فعال/غیرفعال)
router.patch('/categories/:id/status', academyController.toggleStatus);
// افزایش تعداد دوره‌های دسته‌بندی (هر بار یک دوره جدید اضافه می‌شود)
router.post('/categories/:id/increment-count', academyController.incrementCourseCount);
// ==========================================
// 👑 مسیرهای مدیریتی (پنل ادمین)
// ==========================================
// ------ مدیریت پرداخت‌ها ------
router.get("/payments", academyController.getAllPayments);
router.put("/payments/status", academyController.updatePaymentStatus);
// ------ مدیریت دوره‌ها ------
router.get("/courses", academyController.getAllCourses);
router.get("/courses/:id", academyController.getCourseById);
router.put("/courses/:id", academyController.updateCourse);
router.post("/courses", academyController.createCourse);
router.put("/academy/courses/status", academyController.updateCourseStatus);
router.delete("/courses/:courseId", academyController.admindeleteCourse);
// ------ مدیریت دوره‌های خریداری شده ------
router.get("/purchased", academyController.getAllPurchasedCourses);
// ------ مدیریت آکادمی‌ها ------
router.get("/academies", academyController.getAllAcademies);
router.get("/academies/:id", academyController.getAcademyById);
router.put("/academies/:id", academyController.updateAcademy);
router.post("/academies", academyController.createAcademy);
router.put("/academies/status", academyController.updateAcademyStatus);
router.delete("/academies/:id", academyController.deleteAcademy);
// ==========================================
// 📤 خروجی روت‌ها
// ==========================================
module.exports = router;

View File

@@ -1,45 +0,0 @@
const express = require('express')
const blockCheck = require('../../../middlewares/blockCheck')
const { createAdvertisingValidationRules, createAdvertising, getAdvertisingTypes, getAdvertisingCategory, getSingleAdvertising, getAdvertisingFeatures, getAdvertisings, toggleLike, addComment, getComments, rateAdvertising, getUserAdvertisings, editAdvertising, getUserSingleAdvertising, getUserAdvertisingProfile, updateUserAdvertisingProfile, getAdvertisingsWeb, getSingleAdvertisingWeb, getUserAdvertisingProfileWeb } = require('../../../controllers/application/advertising/advertisingController')
const auth = require('../../../middlewares/auth')
const { initiateAdvertisingPayment, handleAdvertisingPaymentCallback, handleAdvertisingRepublishPaymentCallback, republishAdvertisingPayment, handleSuccessfulAdvertisingPayment, handleFailedAdvertisingPayment, handleFailedAdvertisingRepublishPayment, handleSuccessfulAdvertisingRepublishPayment } = require('../../../controllers/application/payment/paymentController')
const { initiateAdvertisingPaymentWeb, handleAdvertisingPaymentCallbackWeb, republishAdvertisingPaymentWeb, handleAdvertisingRepublishPaymentCallbackWeb } = require('../../../controllers/application/payment/paymentControllerWeb')
const router = express.Router()
router.get('/', [auth], getAdvertisings)
router.get('/web', getAdvertisingsWeb)
router.get('/profile', [auth], getUserAdvertisingProfile)
router.get('/profile-web', getUserAdvertisingProfileWeb)
router.patch('/profile', [auth], [blockCheck], updateUserAdvertisingProfile)
router.get('/user-advertising', [auth], getUserAdvertisings)
router.post('/create', [auth], [blockCheck], createAdvertisingValidationRules(), createAdvertising)
router.get('/types', [auth], getAdvertisingTypes)
router.get('/categories', getAdvertisingCategory)
router.get('/features', [auth], getAdvertisingFeatures)
router.get('/get/:advertisingId', [auth], getSingleAdvertising)
router.get('/get/web/:advertisingId', getSingleAdvertisingWeb)
router.get('/get-detail/:advertisingId', [auth], getUserSingleAdvertising)
router.post('/initiate-payment', [auth], [blockCheck], initiateAdvertisingPayment)
router.post('/initiate-payment-web', [auth], [blockCheck], initiateAdvertisingPaymentWeb)
router.post('/republish-payment', [auth], [blockCheck], republishAdvertisingPayment)
router.post('/republish-payment-web', [auth], [blockCheck], republishAdvertisingPaymentWeb)
router.get('/payment', handleAdvertisingPaymentCallback)
router.get('/payment-web', handleAdvertisingPaymentCallbackWeb)
router.get('/republish-verify-payment', handleAdvertisingRepublishPaymentCallback)
router.get('/republish-verify-payment-web', handleAdvertisingRepublishPaymentCallbackWeb)
router.post('/like', [blockCheck], [auth], toggleLike)
router.post('/comment', [blockCheck], [auth], addComment)
router.get('/comments', getComments)
router.post('/rate', [blockCheck], [auth], rateAdvertising)
router.post('/edit/:id', [blockCheck], [auth], editAdvertising)
// Route for handling successful payment
router.post('/payment-success', handleSuccessfulAdvertisingPayment)
// Route for handling failed payment
router.post('/payment-failed', handleFailedAdvertisingPayment)
// Route for handling successful payment
router.post('/republish-payment-success', handleSuccessfulAdvertisingRepublishPayment)
// Route for handling failed payment
router.post('/republish-payment-failed', handleFailedAdvertisingRepublishPayment)
module.exports = router
module.exports = router

View File

@@ -1,7 +0,0 @@
const express = require('express')
const router = express.Router()
const citysController = require('../../../controllers/application/citysController')
router.get('/:id', citysController.getCities)
module.exports = router

View File

@@ -1,6 +0,0 @@
const express = require('express')
const { getExpertise } = require('../../../controllers/application/expertise/expertiseController')
const router = express.Router()
router.get('/', getExpertise)
module.exports = router

View File

@@ -1,6 +0,0 @@
const express = require('express')
const { getFinancial } = require('../../../controllers/application/financial/financialController')
const router = express.Router()
router.get('/', getFinancial)
module.exports = router

View File

@@ -1,14 +0,0 @@
const express = require('express')
const router = express.Router()
const { loginValidationRules, loginUser } = require('../../../controllers/application/login/loginController')
const { verifyUser } = require('../../../controllers/application/login/verifyController')
const { changePasswordUser, changePasswordValidationRules, editChangePasswordUser } = require('../../../controllers/application/login/changePasswordController')
const { loginWithUserName } = require('../../../controllers/application/login/loginWithUserName')
router.post('/', loginValidationRules(), loginUser)
router.post('/verify', verifyUser)
router.post('/change_password', changePasswordValidationRules(), changePasswordUser)
router.patch('/change_password', changePasswordValidationRules(), editChangePasswordUser)
router.post('/login_username', loginWithUserName)
module.exports = router

View File

@@ -1,6 +0,0 @@
const express = require('express')
const { getMessages, getUnreadMessages } = require('../../../controllers/application/messages/messageController')
const router = express.Router()
router.get('/', getMessages)
router.get('/unread', getUnreadMessages)
module.exports = router

View File

@@ -1,6 +0,0 @@
const express = require('express')
const { getNotifications, unreadNotifications } = require('../../../controllers/application/notification/notificationController')
const router = express.Router()
router.get('/', getNotifications)
router.get('/unread', unreadNotifications)
module.exports = router

View File

@@ -1,18 +0,0 @@
const express = require('express')
const { getUserOffers, getOfferTypes, handleSuccessfulOfferPayment, handleFailedOfferPayment, updateOfferStatus, createOfferComment } = require('../../../controllers/application/offer/offerController')
const { initiateOfferPaymentWeb, handleOfferPaymentCallback } = require('../../../controllers/application/payment/paymentControllerWeb')
const blockCheck = require('../../../middlewares/blockCheck')
const isRegister = require('../../../middlewares/isRegister')
const auth = require('./../../../middlewares/auth')
const router = express.Router()
router.get('/', getUserOffers)
router.get('/types', [auth], getOfferTypes)
router.post('/update-status', [auth], updateOfferStatus)
router.post('/payment-success', [auth], handleSuccessfulOfferPayment)
router.post('/payment-failed', [auth], handleFailedOfferPayment)
router.post('/comment', [auth], createOfferComment)
router.post('/initiate-payment-web', [auth], [blockCheck], initiateOfferPaymentWeb)
router.get('/payment-web', handleOfferPaymentCallback)
module.exports = router

View File

@@ -1,205 +0,0 @@
const express = require('express');
const { createPostValidationRules, createPost, getUserPosts, getUserPostsWeb, getPostByIdWeb } = require('../../../controllers/application/posts/postController');
const { toggleLike } = require('../../../controllers/application/posts/likeController');
const blockCheck = require('../../../middlewares/blockCheck');
const auth = require('../../../middlewares/auth');
const isHalfRegister = require('../../../middlewares/isHalfRegister');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
console.log('DEBUG: Starting to load routes/posts.js');
const ensureDir = (dir) => {
console.log('DEBUG: Ensuring directory exists:', dir);
if (!fs.existsSync(dir)) {
console.log('DEBUG: Directory does not exist, creating:', dir);
fs.mkdirSync(dir, { recursive: true });
} else {
console.log('DEBUG: Directory already exists:', dir);
}
};
const imageDir = path.join(__dirname, '../../../storage/posts/images');
const videoDir = path.join(__dirname, '../../../storage/posts/videos');
console.log('DEBUG: imageDir path:', imageDir);
console.log('DEBUG: videoDir path:', videoDir);
ensureDir(imageDir);
ensureDir(videoDir);
console.log('DEBUG: Setting up multer storage');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const dir = file.mimetype.startsWith('video/') ? videoDir : imageDir;
console.log('DEBUG: multer destination for file', file.originalname, ':', dir);
cb(null, dir);
},
filename: (req, file, cb) => {
const filename = `${Date.now()}-${file.originalname}`;
console.log('DEBUG: Generated filename for', file.originalname, ':', filename);
cb(null, filename);
},
});
console.log('DEBUG: Setting up multer instance with limits and filter');
const upload = multer({
storage,
limits: {
fileSize: 100 * 1024 * 1024, // حداکثر 100MB
files: 10, // حداکثر 10 فایل
fieldSize: 1024 * 1024,
fieldNameSize: 100,
},
fileFilter: (req, file, cb) => {
console.log('DEBUG: Checking file filter for', file.originalname, 'mimetype:', file.mimetype);
if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) {
console.log('DEBUG: File accepted:', file.originalname);
cb(null, true);
} else {
console.log('DEBUG: File rejected:', file.originalname);
cb(new Error('فقط تصاویر یا ویدئوها مجاز هستند'), false);
}
},
}).array('post_files', 10);
const multerErrorHandler = (err, req, res, next) => {
console.log('DEBUG: Request headers:', JSON.stringify(req.headers, null, 2));
console.log('DEBUG: Request body (raw):', req.body);
console.log('DEBUG: Request files:', req.files || 'No files received');
if (err instanceof multer.MulterError) {
console.error('DEBUG: Multer error:', err.message, err.code, err.field);
return res.status(400).json({
error: true,
message: `خطای آپلود: ${err.message} (${err.code})`,
field: err.field,
});
} else if (err) {
console.error('DEBUG: File filter error:', err.message);
return res.status(400).json({
error: true,
message: err.message,
});
}
console.log('DEBUG: Multer processing completed, files:', req.files || 'No files');
console.log('DEBUG: Request body after multer:', req.body);
next();
};
console.log('DEBUG: Setting up express router');
const router = express.Router();
// روت اصلی با multipart/form-data
router.post(
'/create',
(req, res, next) => {
console.log('DEBUG: reached auth');
console.log('DEBUG: Raw request headers:', JSON.stringify(req.headers, null, 2));
next();
},
auth,
(req, res, next) => {
console.log('DEBUG: reached blockCheck');
next();
},
blockCheck,
(req, res, next) => {
console.log('DEBUG: reached isHalfRegister');
next();
},
isHalfRegister,
(req, res, next) => {
console.log('DEBUG: reached multer');
console.log('DEBUG: Content-Length:', req.headers['content-length']);
console.log('DEBUG: Content-Type:', req.headers['content-type']);
next();
},
upload,
multerErrorHandler,
(req, res, next) => {
console.log('DEBUG: reached validation');
console.log('DEBUG: Parsed files:', req.files || 'No files');
console.log('DEBUG: Parsed body:', req.body);
next();
},
createPostValidationRules(),
(req, res, next) => {
console.log('DEBUG: reached controller');
next();
},
createPost
);
// روت جدید برای Base64
router.post(
'/create-base64',
(req, res, next) => {
console.log('DEBUG: reached auth for base64');
console.log('DEBUG: Raw request headers:', JSON.stringify(req.headers, null, 2));
next();
},
auth,
(req, res, next) => {
console.log('DEBUG: reached blockCheck for base64');
next();
},
blockCheck,
(req, res, next) => {
console.log('DEBUG: reached isHalfRegister for base64');
next();
},
isHalfRegister,
(req, res, next) => {
console.log('DEBUG: Request body (base64):', JSON.stringify(req.body, null, 2));
next();
},
createPostValidationRules(),
async (req, res) => {
try {
const { files, caption } = req.body;
console.log('DEBUG: Received files (base64):', files.length);
console.log('DEBUG: Received caption:', caption);
// تبدیل Base64 به فایل
const savedFiles = await Promise.all(
files.map(async (file, index) => {
const buffer = Buffer.from(file.data.split(',')[1], 'base64');
const dir = file.type.startsWith('video/') ? videoDir : imageDir;
const filename = `${Date.now()}-${index}-${file.name}`;
const filePath = path.join(dir, filename);
console.log('DEBUG: Saving file:', filePath);
await fs.promises.writeFile(filePath, buffer);
return {
path: filePath,
filename,
mimetype: file.type,
};
})
);
// فراخوانی createPost با فرمت مشابه
req.files = savedFiles;
req.body = { caption };
console.log('DEBUG: Prepared files for createPost:', savedFiles);
console.log('DEBUG: Prepared body for createPost:', req.body);
await createPost(req, res);
} catch (err) {
console.error('DEBUG: Base64 processing error:', err.message);
res.status(400).json({
error: true,
message: `خطای پردازش فایل‌ها: ${err.message}`,
});
}
}
);
router.post('/like', [auth, blockCheck], toggleLike);
router.get('/web/:postId', getPostByIdWeb);
router.get('/user-posts', [auth], getUserPosts);
router.get('/user-posts/web', getUserPostsWeb);
console.log('DEBUG: Finished loading routes/posts.js');
module.exports = router;

View File

@@ -1,8 +0,0 @@
const express = require('express')
const { getProfile , getUserById} = require('../../../controllers/application/profile/profileController')
const auth = require('../../../middlewares/auth')
const router = express.Router()
router.get('/',[auth], getProfile)
router.get('/:id', getUserById)
module.exports = router

View File

@@ -1,44 +0,0 @@
const express = require('express')
const { createProject, createProjectValidationRules } = require('../../../controllers/application/projects/createProjectController')
const { getProjects, getSingleProjects, getSingleProjectsWeb, getUserProjects, getUserProjectsWeb } = require('../../../controllers/application/projects/getProjectController')
const { getProjectTypes } = require('../../../controllers/application/projects/projectTypesController')
const { initiatePayment, handlePaymentCallback, paymentRequestStepOne, paymentRequestStepTwo, paymentRequestStepAll, paymentRequestStepOneCallback, paymentRequestStepTwoCallback, paymentRequestStepAllCallback, handleSuccessfulPayment, handleFailedPayment } = require('../../../controllers/application/payment/paymentController')
const auth = require('../../../middlewares/auth')
const { requestProjectValidationRules, requestProject, acceptProject, editRequestProject } = require('../../../controllers/application/projects/requestProjectController')
const { editProject, doneProject, cancleProject, editProjectValidationRules, setRateProject, doneProjectWeb } = require('../../../controllers/application/projects/updateProjectController')
const blockCheck = require('../../../middlewares/blockCheck')
const isRegister = require('../../../middlewares/isRegister')
const { initiatePaymentWeb, handlePaymentCallbackWeb, paymentRequestAcceptWeb } = require('../../../controllers/application/payment/paymentControllerWeb')
const router = express.Router()
router.get('/', getProjects)
router.get('/user-projects', [auth], getUserProjects)
router.get('/user-projects/web', getUserProjectsWeb)
router.get('/types', [auth], getProjectTypes)
router.get('/get/:projectId', [auth], getSingleProjects)
router.get('/get/web/:projectId', getSingleProjectsWeb)
router.get('/payment', handlePaymentCallback)
router.get('/payment-web', handlePaymentCallbackWeb)
router.post('/create', [blockCheck], [auth], createProjectValidationRules(), createProject)
router.post('/edit', [blockCheck], [auth], editProjectValidationRules(), editProject)
router.post('/rate', [blockCheck], [auth], [isRegister], setRateProject)
router.post('/initiate-payment', [blockCheck], [isRegister], [auth], initiatePayment)
router.post('/initiate-payment-web', [blockCheck], [isRegister], [auth], initiatePaymentWeb)
router.post('/request', [blockCheck], [isRegister], [auth], requestProjectValidationRules(), requestProject)
router.post('/edit-request', [isRegister], [blockCheck], [auth], requestProjectValidationRules(), editRequestProject)
router.post('/payment-request-step-one', [isRegister], [blockCheck], [auth], paymentRequestStepOne)
router.post('/payment-request-step-one/web', [isRegister], [blockCheck], [auth], paymentRequestAcceptWeb)
router.post('/payment-request-step-two', [isRegister], [blockCheck], [auth], paymentRequestStepTwo)
router.post('/payment-request-all', [isRegister], [blockCheck], [auth], paymentRequestStepAll)
router.get('/payment-request-step-one-callback', paymentRequestStepOneCallback)
router.get('/payment-request-step-two-callback', paymentRequestStepTwoCallback)
router.get('/payment-request-step-all-callback', paymentRequestStepAllCallback)
router.post('/accept', [isRegister], [blockCheck], [auth], acceptProject)
router.post('/done', [isRegister], [auth], doneProject)
router.post('/done/web', [isRegister], [auth], doneProjectWeb)
router.post('/cancle', [isRegister], [blockCheck], [auth], cancleProject)
// Route for handling successful payment
router.post('/payment-success', handleSuccessfulPayment)
// Route for handling failed payment
router.post('/payment-failed', handleFailedPayment)
module.exports = router

View File

@@ -1,7 +0,0 @@
const express = require('express')
const router = express.Router()
const provincesController = require('../../../controllers/application/provincesController')
router.get('/', provincesController.getProvinces)
module.exports = router

View File

@@ -1,16 +0,0 @@
const express = require('express')
const router = express.Router()
const { registerValidationRules, registerUser } = require('../../../controllers/application/register/registerController')
const { verifyUser } = require('../../../controllers/application/register/verifyController')
const { setUserName, updateUserName } = require('../../../controllers/application/register/usernameController')
const { setUserFullName, setUserFullNameValidationRules } = require('../../../controllers/application/register/userFullNameController')
const { setUserType } = require('../../../controllers/application/register/userTypeController')
router.post('/', registerValidationRules(), registerUser)
router.post('/verify', verifyUser)
router.post('/username', setUserName)
router.patch('/username', updateUserName)
router.post('/fullname', setUserFullNameValidationRules(), setUserFullName)
router.post('/user_type', setUserType)
module.exports = router

View File

@@ -1,6 +0,0 @@
const express = require('express')
const { getSearch } = require('../../../controllers/application/search-web/searchController')
const router = express.Router()
router.get('/', getSearch)
module.exports = router

View File

@@ -1,6 +0,0 @@
const express = require('express')
const { getSearch } = require('../../../controllers/application/search/searchController')
const router = express.Router()
router.get('/', getSearch)
module.exports = router

View File

@@ -1,6 +0,0 @@
const express = require('express')
const { getSetting } = require('../../../controllers/application/settings/settingsController')
const router = express.Router()
router.get('/:key', getSetting)
module.exports = router

View File

@@ -1,19 +0,0 @@
const express = require('express')
const auth = require('../../../middlewares/auth')
const blockCheck = require('../../../middlewares/blockCheck')
const isHalfRegister = require('../../../middlewares/isHalfRegister')
const {
createStoryBase64,
getStoriesFeed,
markStoryViewed,
deleteStory
} = require('../../../controllers/application/stories/storyController')
const router = express.Router()
router.get('/feed', getStoriesFeed)
router.post('/create-base64', auth, blockCheck, isHalfRegister, createStoryBase64)
router.post('/view', auth, blockCheck, markStoryViewed)
router.delete('/:storyId', auth, blockCheck, deleteStory)
module.exports = router

View File

@@ -1,9 +0,0 @@
const express = require('express')
const { getUserTickets, createTicket, addUserMessageToTicket, getTicketMessages } = require('../../../controllers/application/tickets/ticketsController')
const router = express.Router()
router.get('/', getUserTickets)
router.get('/messages', getTicketMessages)
router.post('/create', createTicket)
router.post('/message', addUserMessageToTicket)
module.exports = router

View File

@@ -1,23 +0,0 @@
const express = require('express')
const { getUsers, getSingleUser, blockUser, unblockUser, getComments,createLicense, getAllLicenses,updateLicenseConfirmation, getLicenseByUserId, inviteEmployer, getUserContactInfo, getPostsWeb, getSingleUserWeb, createUserComment, recordExploreInteraction } = require('../../../controllers/application/users/getUserController')
const blockCheck = require('../../../middlewares/blockCheck')
const isRegister = require('../../../middlewares/isRegister')
const auth = require('../../../middlewares/auth')
const router = express.Router()
router.get('/', [auth], getUsers)
router.get('/web', getPostsWeb)
router.get('/get', [auth], getSingleUser)
router.get('/get/web', getSingleUserWeb)
router.post('/block', [auth], [isRegister], [blockCheck], blockUser)
router.post('/unblock', [auth], [isRegister], [blockCheck], unblockUser)
router.get('/comments', [auth], getComments)
router.post('/comments', [auth], [isRegister], [blockCheck], createUserComment)
router.post('/explore/interaction', [auth], recordExploreInteraction)
router.get('/invite-employer', [auth], [isRegister], [blockCheck], inviteEmployer)
router.get('/contact-info', [auth], getUserContactInfo)
router.get('/License', getAllLicenses)
router.get('/License/:userId', getLicenseByUserId)
router.post('/License', createLicense)
router.put('/License/:userId', updateLicenseConfirmation)
module.exports = router

View File

@@ -1,40 +0,0 @@
const express = require('express')
const { setProfileImage, updateProfileImage } = require('../../../controllers/application/verify/setProfileImageController')
const { setExpertise, updateExpertise } = require('../../../controllers/application/verify/expertiseController')
const { setGender } = require('../../../controllers/application/verify/genderController')
const { setSizes, updateSizes } = require('../../../controllers/application/verify/sizesController')
const { setColors, updateColors } = require('../../../controllers/application/verify/colorsController')
const { setCooperationType, updateCooperationType } = require('../../../controllers/application/verify/cooperationTypeController')
const { setConversation, updateConversation } = require('../../../controllers/application/verify/conversationController')
const { saveAuth, saveAuthValidationRules, updateShaba } = require('../../../controllers/application/verify/authController')
const { setAddress, updateAddress } = require('../../../controllers/application/verify/addressController')
const { setNationalCardImage } = require('../../../controllers/application/verify/setNationalCardImageController')
const { completeRegistration } = require('../../../controllers/application/verify/completeRegistrationController')
const blockCheck = require('../../../middlewares/blockCheck')
const auth = require('../../../middlewares/auth')
const { setServices, updateServices } = require('../../../controllers/application/verify/servicesController')
const router = express.Router()
router.post('/profile_image', setProfileImage)
router.patch('/profile_image', [blockCheck], [auth], updateProfileImage)
router.post('/expertise', setExpertise)
router.patch('/expertise', [blockCheck], [auth], updateExpertise)
router.post('/gender', setGender)
router.post('/sizes', setSizes)
router.patch('/sizes', [blockCheck], [auth], updateSizes)
router.post('/services', setServices)
router.patch('/services', [blockCheck], [auth], updateServices)
router.post('/colors', setColors)
router.patch('/colors', [blockCheck], [auth], updateColors)
router.post('/cooperation_type', setCooperationType)
router.patch('/cooperation_type', [blockCheck], [auth], updateCooperationType)
router.post('/conversation', setConversation)
router.patch('/conversation', [blockCheck], [auth], updateConversation)
router.post('/auth', saveAuthValidationRules(), saveAuth)
router.patch('/shaba', [blockCheck], [auth], updateShaba)
router.post('/address', setAddress)
router.patch('/address', [blockCheck], [auth], updateAddress)
router.post('/national_card_image', setNationalCardImage)
router.post('/complete', completeRegistration)
module.exports = router

View File

@@ -1,6 +0,0 @@
const express = require('express')
const { getVersion } = require('../../../controllers/application/version/versionController')
const router = express.Router()
router.get('/', getVersion)
module.exports = router

View File

@@ -1,8 +0,0 @@
const express = require('express')
const { getProjects, getSingleProject, getProjectRequests } = require('../../../controllers/application/workroom/workroomController')
const router = express.Router()
router.get('/', getProjects)
router.get('/get', getSingleProject)
router.get('/get-requests/:projectId', getProjectRequests)
module.exports = router

View File

@@ -1,79 +0,0 @@
const registerRouter = require('./application/register')
const loginRouter = require('./application/login')
const verifyRouter = require('./application/verify')
const provincesRouter = require('./application/provinces')
const citiesRouter = require('./application/city')
const projectsRouter = require('./application/projects')
const usersRouter = require('./application/users')
const postsRouter = require('./application/posts')
const expertiseRouter = require('./application/expertise')
const profileRouter = require('./application/profile')
const workroomRouter = require('./application/workroom')
const messagesRouter = require('./application/messages')
const searchRouter = require('./application/search')
const searchRouterWeb = require('./application/search-web')
const financialRouter = require('./application/financial')
const notificationRouter = require('./application/notification')
const ticketsRouter = require('./application/tickets')
const advertisingRouter = require('./application/advertising')
const settingsRouter = require('./application/settings')
const versionRouter = require('./application/version')
const offerRouter = require('./application/offers')
const academyRoute = require('./application/academy')
const storiesRouter = require('./application/stories')
const projectsPanelRouter = require('./panel/projects')
const postsPanelRouter = require('./panel/posts')
const expertisePanelRouter = require('./panel/expertise')
const loginPanelRouter = require('./panel/login')
const usersPanelRouter = require('./panel/users')
const ticketsPanelRouter = require('./panel/tickets')
const financialPanelRouter = require('./panel/financial')
const commentsPanelRouter = require('./panel/comments')
const advertisingPanelRouter = require('./panel/advertising')
const shopsPanelRouter = require('./panel/shops')
const settingsPanelRouter = require('./panel/settings')
const versionPanelRouter = require('./panel/version')
const auth = require('../middlewares/auth')
const adminAuth = require('../middlewares/adminAuth')
module.exports = (app) => {
// App
app.use('/api/v1/register', registerRouter)
app.use('/api/v1/login', loginRouter)
app.use('/api/v1/verify', [auth], verifyRouter)
app.use('/api/v1/provinces', provincesRouter)
app.use('/api/v1/cities', citiesRouter)
app.use('/api/v1/projects', projectsRouter)
app.use('/api/v1/users', usersRouter)
app.use('/api/v1/posts', postsRouter)
app.use('/api/v1/expertise', expertiseRouter)
app.use('/api/v1/profile', profileRouter)
app.use('/api/v1/workroom', [auth], workroomRouter)
app.use('/api/v1/messages', [auth], messagesRouter)
app.use('/api/v1/notifications', [auth], notificationRouter)
app.use('/api/v1/search', [auth], searchRouter)
app.use('/api/v1/search-web', searchRouterWeb)
app.use('/api/v1/financial', [auth], financialRouter)
app.use('/api/v1/tickets', [auth], ticketsRouter)
app.use('/api/v1/offers', offerRouter)
app.use('/api/v1/advertising', advertisingRouter)
app.use('/api/v1/settings', settingsRouter)
app.use('/api/v1/version', versionRouter)
app.use('/api/v1/academy', academyRoute)
app.use('/api/v1/stories', storiesRouter)
// Panel
app.use('/api/v1/panel/login', loginPanelRouter)
app.use('/api/v1/panel/users', usersPanelRouter)
app.use('/api/v1/panel/projects', [adminAuth], projectsPanelRouter)
app.use('/api/v1/panel/posts', [adminAuth], postsPanelRouter)
app.use('/api/v1/panel/expertise', [adminAuth], expertisePanelRouter)
app.use('/api/v1/panel/tickets', [adminAuth], ticketsPanelRouter)
app.use('/api/v1/panel/financial', [adminAuth], financialPanelRouter)
app.use('/api/v1/panel/comments', [adminAuth], commentsPanelRouter)
app.use('/api/v1/panel/advertising', [adminAuth], advertisingPanelRouter)
app.use('/api/v1/panel/shops', [adminAuth], shopsPanelRouter)
app.use('/api/v1/panel/settings', [adminAuth], settingsPanelRouter)
app.use('/api/v1/panel/version', [adminAuth], versionPanelRouter)
}

View File

@@ -1,22 +0,0 @@
const express = require('express')
const { getAdvertisings, getSingleAdvertising, acceptAdvertising, rejectAdvertising, getAdvertisingCategories, getAdvertisingCategoryById,createAdvertisingCategory , deleteAdvertisingCategory } = require('../../../controllers/panel/advertising/advertisingController')
const { getAdvertisingComments, getAdvertisingCommentDetail, acceptAdvertisingComment } = require('../../../controllers/panel/comments/advertisingCommentController')
const router = express.Router()
router.get('/', getAdvertisings)
router.get('/detail', getSingleAdvertising)
router.post('/accept', acceptAdvertising)
router.post('/reject', rejectAdvertising)
router.get('/comments', getAdvertisingComments)
router.get('/comments/detail', getAdvertisingCommentDetail)
router.post('/comments/accept', acceptAdvertisingComment)
router.post('/advertising-categories', createAdvertisingCategory)
router.delete('/advertising-categories/:id', deleteAdvertisingCategory)
// لیست دسته‌بندی‌ها (پشتیبانی از query params)
router.get('/advertising-categories', getAdvertisingCategories);
// گرفتن یک دسته‌بندی با id
router.get('/advertising-categories/:id', getAdvertisingCategoryById);
module.exports = router

View File

@@ -1,8 +0,0 @@
const express = require('express')
const { acceptComment, getComments, getCommentDetail } = require('../../../controllers/panel/comments/commentController')
const router = express.Router()
router.get('/', getComments)
router.get('/detail', getCommentDetail)
router.post('/accept', acceptComment)
module.exports = router

View File

@@ -1,9 +0,0 @@
const express = require('express')
const { createExpertise, createSubExpertise, getExpertise, editExpertise } = require('../../../controllers/panel/expertise/expertiseController')
const router = express.Router()
router.get('/', getExpertise)
router.post('/create', createExpertise)
router.post('/create/sub', createSubExpertise)
router.put('/update/:id', editExpertise)
module.exports = router

View File

@@ -1,7 +0,0 @@
const express = require('express')
const { getFinancial, getFinancialDetail } = require('../../../controllers/panel/financial/financialController')
const router = express.Router()
router.get('/', getFinancial)
router.get('/detail', getFinancialDetail)
module.exports = router

View File

@@ -1,8 +0,0 @@
const express = require('express')
const router = express.Router()
const { adminLogin, changePassword } = require('../../../controllers/panel/login/loginController')
const adminAuth = require('../../../middlewares/adminAuth')
router.post('/', adminLogin)
router.post('/change-password', [adminAuth], changePassword)
module.exports = router

View File

@@ -1,8 +0,0 @@
const express = require('express')
const { acceptPost, getPosts, getPostDetail } = require('../../../controllers/panel/posts/postController')
const router = express.Router()
router.get('/', getPosts)
router.get('/detail', getPostDetail)
router.post('/accept', acceptPost)
module.exports = router

View File

@@ -1,11 +0,0 @@
const express = require('express')
const { createProjectTypes, getProjectTypes } = require('../../../controllers/panel/projects/projectTypesController')
const { getProjects, getProjectDetails, acceptProject, rejectProject } = require('../../../controllers/panel/projects/projectController')
const router = express.Router()
router.get('/', getProjects)
router.get('/detail', getProjectDetails)
router.post('/accept', acceptProject)
router.post('/reject', rejectProject)
router.get('/types', getProjectTypes)
router.post('/types/create', createProjectTypes)
module.exports = router

View File

@@ -1,7 +0,0 @@
const express = require('express')
const { getSetting, updateSetting } = require('../../../controllers/panel/settings/settingsController')
const router = express.Router()
router.get('/:key', getSetting)
router.put('/:key', updateSetting)
module.exports = router

View File

@@ -1,8 +0,0 @@
const express = require('express')
const { getShops, getShopDetail, updateShop } = require('../../../controllers/panel/shops/shopsController')
const router = express.Router()
router.get('/', getShops)
router.get('/detail', getShopDetail)
router.put('/update', updateShop)
module.exports = router

View File

@@ -1,9 +0,0 @@
const express = require('express')
const { addAdminMessageToTicket, getAllTickets, getTicketMessages, closeTicket } = require('../../../controllers/panel/tickets/ticketsController')
const router = express.Router()
router.get('/', getAllTickets)
router.get('/messages', getTicketMessages)
router.post('/message', addAdminMessageToTicket)
router.post('/close', closeTicket)
module.exports = router

View File

@@ -1,12 +0,0 @@
const express = require('express')
const { getUsers, getUserDetail,changePass, updateUser, verifyUser, rejectUser, updateUserStatus } = require('../../../controllers/panel/users/usersController')
const router = express.Router()
router.get('/', getUsers)
router.post('/change-password', changePass)
router.get('/detail', getUserDetail)
router.put('/update', updateUser)
router.post('/verify', verifyUser)
router.post('/reject', rejectUser)
router.post('/update-status', updateUserStatus);
module.exports = router

View File

@@ -1,7 +0,0 @@
const express = require('express')
const { getVersion, upsertVersion } = require('../../../controllers/panel/version/versionController')
const router = express.Router()
router.get('/', getVersion)
router.post('/', upsertVersion)
module.exports = router

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
const mongoose = require('mongoose')
const crypto = require('crypto')
const bcrypt = require('bcryptjs')
const UserModel = require('../../../models/UserModel')
const DeviceLinkSessionModel = require('../../../models/DeviceLinkSessionModel')
@@ -154,34 +154,26 @@ const pollDeviceLink = async (req, res, next) => {
}
/**
* رمزِ خودکارِ per-account برای رمزنگاری/بازگشاییِ خودکارِ بک‌آپِ کلیدِ E2EE — به‌جای
* PIN دستیِ کاربر. با یه درخواستِ احرازهویت‌شده به همین اکانت (از هر دستگاهی) قابل‌
* دریافته، پس بازیابیِ هویت روی دستگاهِ جدید بدون هیچ دخالتِ کاربری انجام می‌شه.
* اولین‌بار که خواسته بشه lazily ساخته می‌شه؛ با یه آپدیتِ شرطی (فقط اگه هنوز null
* باشه) از race بینِ دو دستگاهی که هم‌زمان اولین‌بار درخواستش می‌کنن جلوگیری می‌شه.
* کلیدِ E2EE حالا از رمزِ عبورِ خودِ کاربر مشتق می‌شه (نه یه رمزِ خودکارِ ساخته‌شده‌ی
* سرور) — این اندپوینت فقط رمزِ واردشده رو با bcrypt در برابرِ هشِ ذخیره‌شده تایید
* می‌کنه (عینِ loginWithUserName.js)، تا کلاینت قبل از اجرای PBKDF2ِ ۲۵۰هزار-تکراریِ
* محلی (برای decryptPrivateKeyFromBackup) یه فیدبکِ سریع برای «رمزِ اشتباه» داشته
* باشه؛ خودِ مشتق‌سازیِ کلید کاملاً سمتِ کلاینت انجام می‌شه، سرور هیچ‌وقت کلید رو نمی‌بینه
*/
const getOrCreateAutoBackupSecret = async (req, res, next) => {
const verifyPassword = async (req, res, next) => {
try {
const userId = req.user._id
const existing = await UserModel.findById(userId).select('e2ee_auto_backup_secret').lean()
if (existing?.e2ee_auto_backup_secret) {
return res.json({ secret: existing.e2ee_auto_backup_secret })
const { password } = req.body
if (!password || typeof password !== 'string') {
return res.status(422).json({ error: 'password is required' })
}
const candidate = crypto.randomBytes(32).toString('base64')
const updated = await UserModel.findOneAndUpdate(
{ _id: userId, e2ee_auto_backup_secret: null },
{ $set: { e2ee_auto_backup_secret: candidate } },
{ new: true }
).select('e2ee_auto_backup_secret').lean()
const user = await UserModel.findById(req.user._id).select('password').lean()
if (!user?.password) {
return res.json({ valid: false })
}
// اگه updated خالی برگشت یعنی یه درخواستِ هم‌زمانِ دیگه (دستگاهِ دیگه) بین
// خوندنِ بالا و همین آپدیت، خودش رمز رو ساخته بود — بخونش تا هر دو دستگاه دقیقاً
// یه رمزِ یکسان بگیرن، نه دوتا رمزِ متفاوت
const secret = updated?.e2ee_auto_backup_secret ||
(await UserModel.findById(userId).select('e2ee_auto_backup_secret').lean())?.e2ee_auto_backup_secret
res.json({ secret })
const valid = await bcrypt.compare(password, user.password)
res.json({ valid })
} catch (error) {
next(error)
}
@@ -243,7 +235,7 @@ module.exports = {
getDeviceLinkSession,
approveDeviceLink,
pollDeviceLink,
getOrCreateAutoBackupSecret,
verifyPassword,
putKeyBackup,
getKeyBackup,
deleteKeyBackup

View File

@@ -0,0 +1,129 @@
const UserModel = require('../../../models/UserModel')
const { OTP_VALID_MS, isOtpExpired } = require('../../../utils/otpExpiry')
const { sendSmsIrVerify, getSmsIrTemplateId } = require('../../../utils/smsIr')
function normalizeMobile (value) {
return String(value || '').replace(/\D/g, '')
}
function generateOTP () {
return Math.floor(100000 + Math.random() * 900000)
}
// کاربرانی که با گوگل/اینستاگرام ثبت‌نام کردن و شماره موبایل ندارن، از همین‌جا
// (نه فلوی لاگین) می‌تونن یه بار برای همیشه شماره‌شون رو ثبت کنن. بعد از ثبت،
// دیگه هیچ مسیرِ ویرایشی براش نیست — طبق خواسته‌ی کاربر.
const requestMobileOtp = async (req, res, next) => {
try {
if (req.user.mobile) {
return res.status(422).json({
error: true,
message: 'شماره موبایل شما قبلاً ثبت شده و قابل تغییر نیست'
})
}
const mobile = normalizeMobile(req.body.mobile)
if (!mobile || !/^09[0-9]{9}$/.test(mobile)) {
return res.status(422).json({
error: true,
message: 'فرمت شماره موبایل صحیح نیست'
})
}
const existing = await UserModel.findOne({ mobile })
if (existing) {
return res.status(422).json({
error: true,
message: 'این شماره موبایل قبلاً توسط کاربر دیگری ثبت شده است'
})
}
const otp = generateOTP()
const otpSentAt = new Date()
await UserModel.updateOne(
{ _id: req.user._id },
{ $set: { pending_mobile: mobile, otp: String(otp), otpSentAt } }
)
setTimeout(() => {
UserModel.updateOne(
{ _id: req.user._id, otp: String(otp) },
{ $set: { otp: null, otpSentAt: null, pending_mobile: null } }
).catch((error) => console.error('[requestMobileOtp] clear OTP failed:', error))
}, OTP_VALID_MS)
const smsResult = await sendSmsIrVerify({
mobile,
otp,
templateId: getSmsIrTemplateId('930719')
})
if (!smsResult.ok) {
return res.status(502).json({
error: true,
message: 'ارسال پیامک ناموفق بود. لطفاً کمی بعد دوباره تلاش کنید.'
})
}
return res.json({ success: true, message: 'کد تایید ارسال شد' })
} catch (error) {
next(error)
}
}
const verifyMobileOtp = async (req, res, next) => {
try {
const { otp } = req.body
if (!otp) {
return res.status(422).json({ error: true, message: 'کد تایید الزامی است' })
}
const user = await UserModel.findById(req.user._id)
if (!user.pending_mobile || !user.otp) {
return res.status(422).json({
error: true,
message: 'درخواستی برای ثبت شماره موبایل یافت نشد. دوباره تلاش کنید'
})
}
if (isOtpExpired(user)) {
return res.status(422).json({
error: true,
message: 'کد تایید منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید'
})
}
if (String(user.otp) !== String(otp)) {
return res.status(422).json({ error: true, message: 'کد را اشتباه وارد کردید' })
}
// بینِ درخواستِ کد و تاییدش، ممکنه یه کاربرِ دیگه دقیقاً همین شماره رو گرفته باشه
const conflict = await UserModel.findOne({ mobile: user.pending_mobile })
if (conflict) {
return res.status(422).json({
error: true,
message: 'این شماره موبایل قبلاً توسط کاربر دیگری ثبت شده است'
})
}
user.mobile = user.pending_mobile
user.pending_mobile = null
user.otp = null
user.otpSentAt = null
await user.save()
return res.json({
success: true,
message: 'شماره موبایل با موفقیت ثبت شد',
mobile: user.mobile
})
} catch (error) {
next(error)
}
}
module.exports = {
requestMobileOtp,
verifyMobileOtp
}

View File

@@ -0,0 +1,389 @@
const axios = require('axios')
const mongoose = require('mongoose')
const AdModel = require('../../../models/AdModel')
const PostModel = require('../../../models/PostModel')
const StoryModel = require('../../../models/StoryModel')
const UserModel = require('../../../models/UserModel')
const { resolveAdPrice } = require('../../../utils/adPricing')
const { resolveWalletSplit, debitWalletForPurchase } = require('../../../utils/walletSplit')
const { deliverNotificationAd, trackAdEvent, findActiveAd, buildAdLinkedContentItem } = require('../../../utils/adDelivery')
const { resolveLinkedEntity } = require('../../../utils/resolveLinkedEntity')
const { buildPaymentPageUrl } = require('../../../utils/zarinpalPaymentPage')
const SPONSORED_TYPES = ['project', 'product', 'shop', 'billboard']
const MERCHANT_ID = process.env.ZARINPAL_MERCHANT_ID || process.env.MERCHENT_CODE
const AD_CAMPAIGN_DURATION_DAYS = 7
const AD_TYPES = AdModel.AD_TYPES
function toRial (toman) {
return Math.round(Number(toman) * 10)
}
function normalizeTargeting (body) {
// برای POST (createAd) یه آرایه‌ی واقعیه (JSON body)؛ برای GET (getAdPricing) کوئری‌استرینگ
// که فرانت با کاما جدا می‌فرسته — هر دو شکل رو قبول می‌کنیم
let subExpertise = []
if (Array.isArray(body.target_sub_expertise)) {
subExpertise = body.target_sub_expertise.filter(Boolean)
} else if (typeof body.target_sub_expertise === 'string' && body.target_sub_expertise) {
subExpertise = body.target_sub_expertise.split(',').map((s) => s.trim()).filter(Boolean)
}
return {
target_country_id: body.target_country_id != null && body.target_country_id !== '' ? Number(body.target_country_id) : null,
target_province_id: body.target_province_id != null && body.target_province_id !== '' ? Number(body.target_province_id) : null,
target_city_id: body.target_city_id != null && body.target_city_id !== '' ? Number(body.target_city_id) : null,
target_expertise: body.target_expertise ? String(body.target_expertise).trim() : null,
target_sub_expertise: subExpertise,
target_gender: ['female', 'male'].includes(body.target_gender) ? body.target_gender : 'any'
}
}
/** GET /ads/pricing — کاربر با هر انتخابی توی ویزارد، قیمتِ همون سناریو رو زنده می‌بینه */
const getAdPricing = async (req, res, next) => {
try {
const { ad_type: adType } = req.query
if (!AD_TYPES.includes(adType)) {
return res.status(422).json({ error: true, message: 'نوع تبلیغ نامعتبر است' })
}
const targeting = normalizeTargeting(req.query)
const price = await resolveAdPrice(adType, {
provinceId: targeting.target_province_id,
gender: targeting.target_gender === 'any' ? null : targeting.target_gender,
expertise: targeting.target_expertise,
subExpertise: targeting.target_sub_expertise
})
return res.json({ price })
} catch (error) {
next(error)
}
}
async function activateAd (ad) {
ad.status = 'active'
ad.started_at = new Date()
ad.expires_at = new Date(Date.now() + AD_CAMPAIGN_DURATION_DAYS * 24 * 60 * 60 * 1000)
await ad.save()
if (ad.ad_type === 'notification') {
deliverNotificationAd(ad).catch((error) => console.error('deliverNotificationAd error:', error.message))
}
}
/** POST /ads — ساخت تبلیغ؛ عین سفارش فروشگاه/دوره، اول کیف پول رو چک می‌کنه */
const createAd = async (req, res, next) => {
try {
const { ad_type: adType, useWallet } = req.body
if (!AD_TYPES.includes(adType)) {
return res.status(422).json({ error: true, message: 'نوع تبلیغ نامعتبر است' })
}
let contentPostId = null
let contentStoryId = null
let contentMediaPath = null
let contentMediaType = null
let contentOverlays = null
let contentProjectId = null
let contentShopProductId = null
let contentShopId = null
let contentBillboardId = null
let notificationTitle = null
let notificationBody = null
// نگاشتِ ad_type → {کلیدِ ورودی، تابعِ resolveLinkedEntity، پیامِ خطا} برای چهار
// نوعِ «تبلیغِ محتوای موجود» (پروژه/کالا/فروشگاه/بیلبورد) — همون منطقِ مالکیت+وضعیتی
// که ثبتِ پست (resolveLinkedEntity.js) از قبل داره، دوباره‌نویسی نشده
const LINKED_CONTENT_TYPES = {
project: { bodyKey: 'content_project_id', kind: 'project', message: 'پروژه‌ای برای تبلیغ انتخاب نشده است', ownerMessage: 'شما مالک این پروژه نیستید' },
product: { bodyKey: 'content_shop_product_id', kind: 'shopproduct', message: 'کالایی برای تبلیغ انتخاب نشده است', ownerMessage: 'شما مالک این کالا نیستید' },
shop: { bodyKey: 'content_shop_id', kind: 'shop', message: 'فروشگاهی برای تبلیغ انتخاب نشده است', ownerMessage: 'شما مالک این فروشگاه نیستید' },
billboard: { bodyKey: 'content_billboard_id', kind: 'billboard', message: 'بیلبوردی برای تبلیغ انتخاب نشده است', ownerMessage: 'شما مالک این بیلبورد نیستید' }
}
if (adType === 'first_post') {
const postId = req.body.content_post_id
if (!postId || !mongoose.Types.ObjectId.isValid(postId)) {
return res.status(422).json({ error: true, message: 'پستی برای تبلیغ انتخاب نشده است' })
}
const post = await PostModel.findById(postId).select('user_id').lean()
if (!post || String(post.user_id) !== String(req.user._id)) {
return res.status(403).json({ error: true, message: 'شما مالک این پست نیستید' })
}
contentPostId = postId
} else if (adType === 'first_story') {
const storyId = req.body.content_story_id
if (!storyId || !mongoose.Types.ObjectId.isValid(storyId)) {
return res.status(422).json({ error: true, message: 'استوری‌ای برای تبلیغ انتخاب نشده است' })
}
// مدیا/اورلی‌ها همینجا اسنپ‌شات می‌شن (نه فقط رفرنس) — استوریِ اصلی معمولاً ۲۴ساعته
// منقضی می‌شه، ولی کمپینِ تبلیغاتی باید کاملِ مدتش (activateAd) زنده بمونه
const story = await StoryModel.findById(storyId).select('user_id media_path media_type overlays').lean()
if (!story || String(story.user_id) !== String(req.user._id)) {
return res.status(403).json({ error: true, message: 'شما مالک این استوری نیستید' })
}
contentStoryId = storyId
contentMediaPath = story.media_path
contentMediaType = story.media_type
contentOverlays = story.overlays || []
} else if (adType === 'notification') {
notificationTitle = String(req.body.notification_title || '').trim()
notificationBody = String(req.body.notification_body || '').trim()
if (!notificationTitle || !notificationBody) {
return res.status(422).json({ error: true, message: 'عنوان و متن نوتیفیکیشن الزامی است' })
}
} else if (LINKED_CONTENT_TYPES[adType]) {
const { bodyKey, kind, message, ownerMessage } = LINKED_CONTENT_TYPES[adType]
const rawId = req.body[bodyKey]
if (!rawId || !mongoose.Types.ObjectId.isValid(rawId)) {
return res.status(422).json({ error: true, message })
}
const resolvedId = await resolveLinkedEntity(kind, rawId, req.user._id)
if (!resolvedId) {
return res.status(403).json({ error: true, message: ownerMessage })
}
if (adType === 'project') contentProjectId = resolvedId
else if (adType === 'product') contentShopProductId = resolvedId
else if (adType === 'shop') contentShopId = resolvedId
else if (adType === 'billboard') contentBillboardId = resolvedId
}
const targeting = normalizeTargeting(req.body)
const price = await resolveAdPrice(adType, {
provinceId: targeting.target_province_id,
gender: targeting.target_gender === 'any' ? null : targeting.target_gender,
expertise: targeting.target_expertise,
subExpertise: targeting.target_sub_expertise
})
if (!price || price <= 0) {
return res.status(422).json({ error: true, message: 'قیمتی برای این سناریوی تبلیغ تعریف نشده است' })
}
const { walletAmount, gatewayAmount, fullyCovered } = await resolveWalletSplit(
req.user._id,
price,
useWallet
)
const adPayload = {
user_id: req.user._id,
ad_type: adType,
content_post_id: contentPostId,
content_story_id: contentStoryId,
content_project_id: contentProjectId,
content_shop_product_id: contentShopProductId,
content_shop_id: contentShopId,
content_billboard_id: contentBillboardId,
content_media_path: contentMediaPath,
content_media_type: contentMediaType,
content_overlays: contentOverlays,
notification_title: notificationTitle,
notification_body: notificationBody,
...targeting,
price,
walletAmountUsed: walletAmount
}
if (fullyCovered) {
const ad = await AdModel.create(adPayload)
await debitWalletForPurchase(req.user._id, walletAmount, {
referenceType: 'payment',
referenceId: ad._id,
description: 'ثبت تبلیغ از کیف پول'
})
await activateAd(ad)
return res.status(201).json({ success: true, paidFromWallet: true, adId: ad._id })
}
const callbackBase = process.env.APP_SITE_CallBack || process.env.APP_URL
if (!callbackBase) {
return res.status(500).json({ error: true, message: 'آدرس بازگشت پرداخت تنظیم نشده است' })
}
const ad = await AdModel.create({ ...adPayload, status: 'pending_payment' })
const response = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
{
merchant_id: MERCHANT_ID,
amount: toRial(gatewayAmount),
description: `ثبت تبلیغ (${adType})`,
callback_url: `${callbackBase}/payment/ads/callback?adId=${ad._id}`
},
{ headers: { 'Content-Type': 'application/json' } }
)
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
ad.payment_authority = result.authority
await ad.save()
return res.status(201).json({
success: true,
adId: ad._id,
authority: result.authority,
paymentUrl: `${buildPaymentPageUrl(result.authority)}`
})
}
await AdModel.deleteOne({ _id: ad._id })
console.error('ZarinPal initiate error:', response.data)
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
} catch (error) {
console.error('createAd error:', error?.response?.data || error.message)
next(error)
}
}
/** GET /ads/payment/verify — کال‌بک زرین‌پال */
const verifyAdPayment = async (req, res, next) => {
const { adId, Authority, Status } = req.query
const failRedirect = `${process.env.APP_SITE}/settings/ads?payment=failed`
const successRedirect = `${process.env.APP_SITE}/settings/ads?payment=success`
try {
const ad = await AdModel.findById(adId)
if (!ad || ad.status !== 'pending_payment') {
return res.redirect(failRedirect)
}
if (Status !== 'OK') {
ad.status = 'failed'
await ad.save()
return res.redirect(failRedirect)
}
const walletAmount = ad.walletAmountUsed || 0
const gatewayAmount = ad.price - walletAmount
const verify = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/verify.json',
{
merchant_id: MERCHANT_ID,
authority: Authority,
amount: toRial(gatewayAmount)
},
{ headers: { 'Content-Type': 'application/json' } }
)
const data = verify.data?.data
if (data?.code === 100 || data?.code === 101) {
await debitWalletForPurchase(ad.user_id, walletAmount, {
referenceType: 'payment',
referenceId: ad._id,
description: 'ثبت تبلیغ از کیف پول'
})
await activateAd(ad)
return res.redirect(successRedirect)
}
ad.status = 'failed'
await ad.save()
return res.redirect(failRedirect)
} catch (error) {
console.error('verifyAdPayment error:', error?.response?.data || error.message)
return res.redirect(failRedirect)
}
}
/** GET /ads/mine */
const getMyAds = async (req, res, next) => {
try {
const ads = await AdModel.find({ user_id: req.user._id })
.sort({ createdAt: -1 })
.lean()
const now = Date.now()
const withEffectiveStatus = ads.map((ad) => ({
...ad,
effective_status:
ad.status === 'active' && ad.expires_at && new Date(ad.expires_at).getTime() < now
? 'expired'
: ad.status
}))
return res.json({ ads: withEffectiveStatus })
} catch (error) {
next(error)
}
}
/** GET /ads/:id — جزئیات + آنالیتیکس */
const getAdDetail = async (req, res, next) => {
try {
const ad = await AdModel.findById(req.params.id).lean()
if (!ad || String(ad.user_id) !== String(req.user._id)) {
return res.status(404).json({ error: true, message: 'تبلیغ یافت نشد' })
}
const now = Date.now()
const effectiveStatus =
ad.status === 'active' && ad.expires_at && new Date(ad.expires_at).getTime() < now
? 'expired'
: ad.status
let content = null
if (ad.ad_type === 'first_post' && ad.content_post_id) {
content = await PostModel.findById(ad.content_post_id).select('files caption type').lean()
} else if (ad.ad_type === 'first_story') {
// اسنپ‌شاتِ خودِ AdModel — نه لوکاپِ زنده روی StoryModel، چون استوریِ اصلی معمولاً
// ۲۴ساعته منقضی می‌شه ولی این جزئیات باید کاملِ عمرِ تبلیغ در دسترس بمونه
content = { media_path: ad.content_media_path, media_type: ad.content_media_type }
} else if (SPONSORED_TYPES.includes(ad.ad_type)) {
content = await buildAdLinkedContentItem(ad)
}
return res.json({ ad: { ...ad, effective_status: effectiveStatus }, content })
} catch (error) {
next(error)
}
}
/** GET /ads/sponsored?ad_type=project|product|shop|billboard — لیستِ مربوطه (اسپشیالیست‌ها/
* کالاهای فروشگاه/فروشگاه‌ها/بیلبوردها) این رو صدا می‌زنه و اگه چیزی برگشت، اولِ لیستِ خودش
* نشونش می‌ده. برخلافِ first_post/first_story که مستقیم توی fید/استوری تزریق می‌شن، این چهار
* نوع endpoint جدا دارن — چون fetch لیست‌شون توی کنترلرهای پرترافیک/حساسِ جداگانه‌ست و
* دست‌نخورده موندنشون امن‌تره */
const getSponsoredAd = async (req, res, next) => {
try {
const { ad_type: adType } = req.query
if (!SPONSORED_TYPES.includes(adType)) {
return res.status(422).json({ error: true, message: 'نوع تبلیغ نامعتبر است' })
}
const viewer = await UserModel.findById(req.user._id)
.select('country province city gender expertise sub_expertise')
.lean()
const ad = await findActiveAd(adType, { ...viewer, _id: req.user._id })
if (!ad) return res.json({ ad: null })
const item = await buildAdLinkedContentItem(ad)
if (!item) return res.json({ ad: null })
trackAdEvent(ad._id, 'view').catch(() => {})
return res.json({ ad: item })
} catch (error) {
next(error)
}
}
/** POST /ads/:id/track — {type:'view'|'click'} — بیننده صدا می‌زنه، نیازی به مالکیت نیست */
const trackAdEventEndpoint = async (req, res, next) => {
try {
const { type } = req.body
if (!['view', 'click'].includes(type)) {
return res.status(422).json({ error: true, message: 'نوع رویداد نامعتبر است' })
}
await trackAdEvent(req.params.id, type)
return res.json({ success: true })
} catch (error) {
next(error)
}
}
module.exports = {
getAdPricing,
createAd,
verifyAdPayment,
getMyAds,
getAdDetail,
getSponsoredAd,
trackAdEventEndpoint
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable eqeqeq */
/* eslint-disable camelcase */
const mongoose = require('mongoose')
const jwt = require('jsonwebtoken')
const { check, validationResult } = require('express-validator')
const { billboardPublicStatusFilter } = require('../../../utils/moderationStatus')
@@ -25,6 +26,8 @@ const {
createRatingNotification
} = require('../../../utils/commentNotification')
const { buildUnifiedInterestScore, blendFreshIntoRanked } = require('../../../utils/exploreAlgorithm')
const { resolveStoryFlags } = require('../../../utils/storyRingFlags')
const { findActiveAd, buildAdBillboardItem, trackAdEvent } = require('../../../utils/adDelivery')
/** Billboards matching the viewer's cross-content interest profile, for blending into the default (unsorted, unfiltered) listing. */
async function getInterestMatchedAds (userId, filter, excludeIds, limit = 5) {
@@ -57,12 +60,13 @@ const createAdvertisingValidationRules = () => {
check('city').notEmpty().withMessage('شهر نمی‌تواند خالی باشد')
]
}
// تابع کمکی برای ذخیره تصاویر
// تابع کمکی برای ذخیره تصاویر — file یه آبجکتِ multer‌ه (originalname/path)، نه
// express-fileupload (name/path)
const saveFile = async (file, folder, userName) => {
const uploadDir = path.join(__dirname, '../../../../storage', folder)
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true })
const uniqueFileName = `${userName}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(file.name)}`
const uniqueFileName = `${userName}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(file.originalname)}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(file.path, filePath)
return `/${folder}/${uniqueFileName}`
@@ -126,15 +130,16 @@ const createAdvertising = async (req, res, next) => {
return res.status(422).json({ error: true, message: 'فرمت خدمات اشتباه است' })
}
// multer (routes/application/advertising/index.js) با .any() همه‌ی فایل‌ها رو
// به‌صورتِ آرایه می‌ده (چون serviceImages[id] اسمِ فیلدش به‌ازای هر خدمت پویاست)
const uploadedFiles = Array.isArray(req.files) ? req.files : []
const servicesWithImages = []
for (const service of parsedServices) {
const serviceImages = []
const serviceId = service.id
const serviceFiles = req.files?.serviceImages?.[serviceId]
const imageFiles = Array.isArray(serviceFiles) ? serviceFiles : serviceFiles ? [serviceFiles] : []
for (const file of imageFiles) {
const imageUrl = await saveFile(file, 'services', user.user_name)
const serviceFile = uploadedFiles.find((f) => f.fieldname === `serviceImages[${service.id}]`)
if (serviceFile) {
const imageUrl = await saveFile(serviceFile, 'services', user.user_name)
serviceImages.push(imageUrl)
service.image = imageUrl
}
@@ -142,7 +147,7 @@ const createAdvertising = async (req, res, next) => {
}
// ✅ مدیریت تصاویر تبلیغ
const imageFiles = Array.isArray(req.files?.images) ? req.files.images : req.files?.images ? [req.files.images] : []
const imageFiles = uploadedFiles.filter((f) => f.fieldname === 'images')
const images = []
for (const file of imageFiles) {
const imageUrl = await saveFile(file, 'advertising', user.user_name)
@@ -540,6 +545,24 @@ const getAdvertisingsWeb = async (req, res, next) => {
} catch {
/* non-blocking: personalization is a bonus, never break the listing */
}
// تبلیغِ نوع billboard — عینِ الگوی first_post (adDelivery.js): فقط صفحه‌ی اول،
// فقط برای کاربرِ واردشده، خطاش هیچ‌وقت نباید لیستِ بیلبوردها رو بشکنه
try {
const viewer = await UserModel.findById(userId)
.select('country province city gender expertise sub_expertise')
.lean()
const sponsoredAd = await findActiveAd('billboard', { ...viewer, _id: userId })
if (sponsoredAd) {
const adBillboard = await buildAdBillboardItem(sponsoredAd)
if (adBillboard) {
newAds = [adBillboard, ...newAds]
trackAdEvent(sponsoredAd._id, 'view').catch(() => {})
}
}
} catch (adError) {
console.error('billboard ad injection error:', adError.message)
}
}
res.status(200).json({
@@ -831,12 +854,26 @@ const addComment = async (req, res, next) => {
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
const user = await UserModel.findById(decodedToken.id)
// مثل شاپ‌پروداکت/آموزشگاه: امتیاز فقط روی اولین کامنتِ هر کاربر روی این آگهی مجازه
let rateValue = null
const requestedRate = Number(req.body.rate)
if (requestedRate >= 1 && requestedRate <= 5) {
const existingRating = await AdvertisingComment.findOne({
advertisingId,
userId,
rate: { $ne: null, $gt: 0 }
}).select('_id')
if (!existingRating) rateValue = requestedRate
}
const newComment = await saveCommentWithModerationStatus(
AdvertisingComment,
{
advertisingId,
userId,
text
text,
rate: rateValue
},
'text',
user?.verify_badge
@@ -872,11 +909,62 @@ const getComments = async (req, res, next) => {
populate: [{ path: 'userId', select: '_id profile_image user_type user_name first_name last_name is_verified verify_badge' }]
}
const comments = await AdvertisingComment.paginate({ advertisingId, status: commentStatusFilter() }, options)
const [comments, ratingAgg] = await Promise.all([
AdvertisingComment.paginate({ advertisingId, status: commentStatusFilter() }, options),
AdvertisingComment.aggregate([
{ $match: { advertisingId: new mongoose.Types.ObjectId(advertisingId), rate: { $ne: null, $gt: 0 } } },
{ $group: { _id: null, average: { $avg: '$rate' }, count: { $sum: 1 } } }
])
])
// بررسی وجود توکن (اختیاری — لیست کامنت‌ها بدون لاگین هم قابل مشاهده‌ست)
let currentUserId = null
const authHeader = req.header('Authorization')
if (authHeader) {
const tokenParts = authHeader.split(' ')
if (tokenParts.length === 2) {
try {
currentUserId = jwt.verify(tokenParts[1], process.env.APP_SECRET).id
} catch (err) {
console.error('Invalid Token:', err.message)
}
}
}
let hasRated = false
if (currentUserId) {
const existingRating = await AdvertisingComment.findOne({
advertisingId,
userId: currentUserId,
rate: { $ne: null, $gt: 0 }
}).select('_id')
hasRated = Boolean(existingRating)
}
const commenterIds = comments.docs.map((c) => c.userId?._id).filter(Boolean)
const storyFlags = await resolveStoryFlags(commenterIds, currentUserId)
const commentsWithStoryFlags = comments.docs.map((c) => {
const obj = c.toObject ? c.toObject() : c
if (obj.userId) {
const flags = storyFlags[String(obj.userId._id)] || {}
obj.userId = {
...obj.userId,
has_story: flags.has_story || false,
has_unviewed_story: flags.has_unviewed_story || false
}
}
return obj
})
res.status(200).json({
comments: comments.docs,
comments: commentsWithStoryFlags,
totalPages: comments.totalPages,
totalItems: comments.totalDocs
totalItems: comments.totalDocs,
has_rated: hasRated,
rating: {
average: ratingAgg[0]?.average || 0,
count: ratingAgg[0]?.count || 0
}
})
} catch (error) {
next(error)
@@ -1251,6 +1339,10 @@ const editAdvertising = async (req, res, next) => {
const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 })
const cityFind = await CityModel.findOne({ id: city })
// multer (routes/application/advertising/index.js) با .any() همه‌ی فایل‌ها رو
// به‌صورتِ آرایه می‌ده (چون serviceImages[id] اسمِ فیلدش به‌ازای هر خدمت پویاست)
const uploadedFiles = Array.isArray(req.files) ? req.files : []
// مدیریت تصاویر برای خدمات
const servicesWithImages = []
const parsedServices = JSON.parse(services)
@@ -1259,24 +1351,12 @@ const editAdvertising = async (req, res, next) => {
if (typeof service.image === 'string' && service.image.startsWith('/services/')) {
servicesWithImages.push(service)
} else {
const serviceImages = []
const serviceId = service.id
if (req.files && req.files.serviceImages && req.files.serviceImages[serviceId]) {
const imageFiles = Array.isArray(req.files.serviceImages[serviceId]) ? req.files.serviceImages[serviceId] : [req.files.serviceImages[serviceId]]
const uploadDir = path.join(__dirname, '../../../../storage/services')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
for (const imageFile of imageFiles) {
const uniqueFileName = `${user.user_name}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(imageFile.name)}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(imageFile.path, filePath)
const imageUrl = `/services/${uniqueFileName}`
serviceImages.push(imageUrl)
service.image = imageUrl
}
const serviceFile = uploadedFiles.find((f) => f.fieldname === `serviceImages[${service.id}]`)
if (serviceFile) {
const imageUrl = await saveFile(serviceFile, 'services', user.user_name)
service.image = imageUrl
}
servicesWithImages.push({ ...service, image: serviceImages[0] })
servicesWithImages.push(service)
}
}
}
@@ -1293,22 +1373,10 @@ const editAdvertising = async (req, res, next) => {
}
// اضافه کردن تصاویر جدید
if (req.files && req.files.images) {
const imageFiles = Array.isArray(req.files.images) ? req.files.images : [req.files.images]
const uploadDir = path.join(__dirname, '../../../../storage/advertising')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
for (const imageFile of imageFiles) {
// بررسی فرمت تصویر
if (imageFile && typeof imageFile === 'object' && imageFile.path) {
const uniqueFileName = `${user.user_name}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(imageFile.name)}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(imageFile.path, filePath)
const imageUrl = `/advertising/${uniqueFileName}`
images.push(imageUrl)
}
}
const imageFiles = uploadedFiles.filter((f) => f.fieldname === 'images')
for (const file of imageFiles) {
const imageUrl = await saveFile(file, 'advertising', user.user_name)
images.push(imageUrl)
}
const parsedFeatures = JSON.parse(features)
@@ -1335,8 +1403,8 @@ const editAdvertising = async (req, res, next) => {
await advertising.save()
if (contactInfo && contactInfo.saveInfoForNextAds) {
user.contactInfo = contactInfo
await user.save()
// updateOne اتمیک به‌جای user.save() — همون دلیلِ فیکسِ setProfileImageController.js
await UserModel.updateOne({ _id: user._id }, { contactInfo })
}
res.status(200).json({
message: 'ویترین با موفقیت ویرایش شد',
@@ -1560,7 +1628,7 @@ const updateUserAdvertisingProfile = async (req, res, next) => {
})
}
const { profile_image } = req.files
const profile_image = req.file
const {
// profile_image,
vitrine_name,
@@ -1584,7 +1652,7 @@ const updateUserAdvertisingProfile = async (req, res, next) => {
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
const uniqueFileName = `${advertisingProfile?.vitrine_name}-${Date.now()}${path.extname(profile_image.name)}`
const uniqueFileName = `${advertisingProfile?.vitrine_name}-${Date.now()}${path.extname(profile_image.originalname)}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(profile_image.path, filePath)
updateFields.profile_image = `/profiles/${uniqueFileName}`

View File

@@ -0,0 +1,91 @@
const AutoDirectRuleModel = require('../../../models/AutoDirectRuleModel')
function ruleError (res, status, message) {
return res.status(status).json({ error: true, message })
}
async function findOwnedRule (ruleId, userId) {
const rule = await AutoDirectRuleModel.findById(ruleId)
if (!rule) return { error: 'NOT_FOUND' }
if (String(rule.user_id) !== String(userId)) return { error: 'FORBIDDEN' }
return { rule }
}
const getMyAutoDirectRules = async (req, res, next) => {
try {
const rules = await AutoDirectRuleModel.find({ user_id: req.user._id }).sort({ createdAt: -1 })
return res.json({ rules })
} catch (error) {
next(error)
}
}
const createAutoDirectRule = async (req, res, next) => {
try {
const { trigger_text, direct_message, reply_caption } = req.body
if (!trigger_text?.trim() || !direct_message?.trim() || !reply_caption?.trim()) {
return ruleError(res, 422, 'همه‌ی فیلدها الزامی است')
}
const rule = await AutoDirectRuleModel.create({
user_id: req.user._id,
trigger_text: trigger_text.trim(),
direct_message: direct_message.trim(),
reply_caption: reply_caption.trim()
})
return res.status(201).json({ message: 'دایرکت اتوماتیک ثبت شد', rule })
} catch (error) {
next(error)
}
}
const updateAutoDirectRule = async (req, res, next) => {
try {
const { rule, error } = await findOwnedRule(req.params.id, req.user._id)
if (error === 'NOT_FOUND') return ruleError(res, 404, 'دایرکت اتوماتیک یافت نشد')
if (error === 'FORBIDDEN') return ruleError(res, 403, 'شما مالک این قانون نیستید')
const { trigger_text, direct_message, reply_caption, is_active } = req.body
if (trigger_text !== undefined) {
if (!trigger_text?.trim()) return ruleError(res, 422, 'متن کامنت الزامی است')
rule.trigger_text = trigger_text.trim()
}
if (direct_message !== undefined) {
if (!direct_message?.trim()) return ruleError(res, 422, 'متن دایرکت الزامی است')
rule.direct_message = direct_message.trim()
}
if (reply_caption !== undefined) {
if (!reply_caption?.trim()) return ruleError(res, 422, 'متن پاسخ کامنت الزامی است')
rule.reply_caption = reply_caption.trim()
}
if (is_active !== undefined) {
rule.is_active = Boolean(is_active)
}
await rule.save()
return res.json({ message: 'دایرکت اتوماتیک به‌روزرسانی شد', rule })
} catch (error) {
next(error)
}
}
const deleteAutoDirectRule = async (req, res, next) => {
try {
const { rule, error } = await findOwnedRule(req.params.id, req.user._id)
if (error === 'NOT_FOUND') return ruleError(res, 404, 'دایرکت اتوماتیک یافت نشد')
if (error === 'FORBIDDEN') return ruleError(res, 403, 'شما مالک این قانون نیستید')
await rule.deleteOne()
return res.json({ message: 'دایرکت اتوماتیک حذف شد' })
} catch (error) {
next(error)
}
}
module.exports = {
getMyAutoDirectRules,
createAutoDirectRule,
updateAutoDirectRule,
deleteAutoDirectRule
}

View File

@@ -3,6 +3,7 @@ const BookingModel = require('../../../models/BookingModel')
const PaymentModel = require('../../../models/PaymentModel')
const { notifyUser } = require('../../../utils/notifyUser')
const { BOOKING_COMMISSION_RATE, creditBookingShare } = require('../../../utils/bookingCommission')
const { buildPaymentPageUrl } = require('../../../utils/zarinpalPaymentPage')
const apiKey =
process.env.ZARINPAL_MERCHANT_ID ||
@@ -59,7 +60,7 @@ const initiateBookingPayment = async (req, res) => {
await booking.save()
return res.status(200).json({
authority: result.authority,
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
paymentUrl: `${buildPaymentPageUrl(result.authority)}`
})
}

Some files were not shown because too many files have changed in this diff Show More