e2ee
This commit is contained in:
@@ -371,7 +371,7 @@ const sendRoomMessage = async (req, res, next) => {
|
||||
}
|
||||
|
||||
const textContent = content != null ? String(content).trim() : ''
|
||||
const uploaded = req.files?.file
|
||||
const uploaded = req.file
|
||||
const resolvedFileType = fileType || undefined
|
||||
|
||||
if (!textContent && !uploaded) {
|
||||
@@ -409,22 +409,16 @@ const sendRoomMessage = async (req, res, next) => {
|
||||
|
||||
let fileUrl = null
|
||||
if (uploaded) {
|
||||
const uploadDir = path.join(__dirname, '../../../storage/chat-rooms/messages')
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true })
|
||||
}
|
||||
// اسمِ فایل هیچوقت مستقیم از ورودیِ کاربر (uploaded.name) گرفته نمیشه — فقط پسوندش
|
||||
const uniqueFileName = `${Date.now()}-${crypto.randomUUID()}${path.extname(uploaded.name || '')}`
|
||||
const filePath = path.join(uploadDir, uniqueFileName)
|
||||
await fs.move(uploaded.path, filePath)
|
||||
if (resolvedFileType === 'image' || (uploaded.type || '').startsWith('image/')) {
|
||||
await optimizeImageFile(filePath)
|
||||
} else if (resolvedFileType === 'video' || (uploaded.type || '').startsWith('video/')) {
|
||||
optimizeVideoFaststart(filePath).catch((err) => {
|
||||
// multer (roomMessageUpload توی routes) از قبل فایل رو مستقیماً روی پوشهی
|
||||
// نهایی نوشته — دیگه نیازی به fs.move جداگانه نیست
|
||||
if (resolvedFileType === 'image' || (uploaded.mimetype || '').startsWith('image/')) {
|
||||
await optimizeImageFile(uploaded.path)
|
||||
} else if (resolvedFileType === 'video' || (uploaded.mimetype || '').startsWith('video/')) {
|
||||
optimizeVideoFaststart(uploaded.path).catch((err) => {
|
||||
console.error('[chatRoomController] video optimize background error:', err.message)
|
||||
})
|
||||
}
|
||||
fileUrl = `/chat-rooms/messages/${uniqueFileName}`
|
||||
fileUrl = `/chat-rooms/messages/${uploaded.filename}`
|
||||
}
|
||||
|
||||
room.lastActivityAt = new Date()
|
||||
|
||||
@@ -14,7 +14,7 @@ const {
|
||||
consumeFreeCreation,
|
||||
freeCreationLimitResponse
|
||||
} = require('../../../utils/freeCreationLimit')
|
||||
const { canUseFreeBillboard, consumeFreeBillboard } = require('../../../utils/billboardFreeLimit')
|
||||
const { tryConsumeFreeBillboard } = require('../../../utils/billboardFreeLimit')
|
||||
const {
|
||||
PROJECT_ACCEPT_RATE,
|
||||
PROJECT_COMPLETION_COMMISSION_RATE,
|
||||
@@ -64,6 +64,18 @@ const initiatePayment = async (req, res) => {
|
||||
const itemName = req.body.item_name // Get item name from request body
|
||||
const price = await getPriceFromDatabase(itemName) // Get price from database
|
||||
const projectId = req.body.projectId // Get project ID from request body
|
||||
|
||||
// IDOR: قبلاً هیچجا چک نمیشد که این پروژه واقعاً متعلق به همین کاربرِ
|
||||
// احرازهویتشدهست — یه projectId دلخواه کافی بود تا کاربرِ دیگهای پرداخت کنه
|
||||
// ولی پروژهی یه کاربرِ کاملاً متفاوت تایید/پرداختشده بشه
|
||||
const projectForOwnerCheck = await ProjectModel.findById(projectId).select('creator_id')
|
||||
if (!projectForOwnerCheck) {
|
||||
return res.status(404).json({ message: 'پروژه یافت نشد' })
|
||||
}
|
||||
if (String(projectForOwnerCheck.creator_id) !== String(userId)) {
|
||||
return res.status(403).json({ message: 'دسترسی به این پروژه ندارید' })
|
||||
}
|
||||
|
||||
// Save project ID for later use in callback URL
|
||||
req.session.projectId = projectId
|
||||
const item = await TypeAndPriceModel.findOne({ name: itemName })
|
||||
@@ -641,15 +653,24 @@ const initiateAdvertisingPayment = async (req, res) => {
|
||||
const advertisingId = req.body.advertisingId // Get project ID from request body
|
||||
const showDiscount = req.body.showDiscount
|
||||
|
||||
// IDOR: قبلاً هیچجا چک نمیشد که این بیلبورد واقعاً متعلق به همین کاربرِ
|
||||
// احرازهویتشدهست — یه advertisingId دلخواه کافی بود تا کاربرِ دیگهای پرداخت
|
||||
// کنه ولی بیلبوردِ یه کاربرِ کاملاً متفاوت تایید/پرداختشده بشه
|
||||
const advertisingForOwnerCheck = await AdvertisingModel.findById(advertisingId).select('creator_id')
|
||||
if (!advertisingForOwnerCheck) {
|
||||
return res.status(404).json({ message: 'بیلبورد یافت نشد' })
|
||||
}
|
||||
if (String(advertisingForOwnerCheck.creator_id) !== String(userId)) {
|
||||
return res.status(403).json({ message: 'دسترسی به این بیلبورد ندارید' })
|
||||
}
|
||||
|
||||
// Save project ID for later use in callback URL
|
||||
req.session.advertisingId = advertisingId
|
||||
const item = await AdvertisingTypeModel.findOne({ name: itemName })
|
||||
const user = await UserModel.findById(userId)
|
||||
|
||||
if (item.name === 'free' && item.price === 0) {
|
||||
if (canUseFreeBillboard(user)) {
|
||||
await consumeFreeBillboard(user)
|
||||
await user.save()
|
||||
const freeResult = await tryConsumeFreeBillboard(UserModel, userId)
|
||||
if (freeResult.ok) {
|
||||
const project = await AdvertisingModel.findById(advertisingId)
|
||||
if (!project) {
|
||||
res.status(404).send('Advertising not found')
|
||||
@@ -660,7 +681,7 @@ const initiateAdvertisingPayment = async (req, res) => {
|
||||
await project.save()
|
||||
res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'سقف ۲۰۰ بیلبورد رایگان شما به پایان رسیده است.' })
|
||||
res.status(403).json({ message: freeResult.message })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post(
|
||||
@@ -796,12 +817,10 @@ const republishAdvertisingPayment = async (req, res) => {
|
||||
// Save project ID for later use in callback URL
|
||||
req.session.advertisingId = advertisingId
|
||||
const item = await AdvertisingTypeModel.findOne({ name: itemName })
|
||||
const user = await UserModel.findById(userId)
|
||||
|
||||
if (item.name === 'free' && item.price === 0) {
|
||||
if (canUseFreeBillboard(user)) {
|
||||
await consumeFreeBillboard(user)
|
||||
await user.save()
|
||||
const freeResult = await tryConsumeFreeBillboard(UserModel, userId)
|
||||
if (freeResult.ok) {
|
||||
const project = await AdvertisingModel.findById(advertisingId)
|
||||
if (!project) {
|
||||
res.status(404).send('Advertising not found')
|
||||
@@ -812,7 +831,7 @@ const republishAdvertisingPayment = async (req, res) => {
|
||||
await project.save()
|
||||
res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'سقف ۲۰۰ بیلبورد رایگان شما به پایان رسیده است.' })
|
||||
res.status(403).json({ message: freeResult.message })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post(
|
||||
|
||||
@@ -9,7 +9,7 @@ const NotificationModel = require('../../../models/NotificationModel')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel')
|
||||
const AdvertisingModel = require('../../../models/AdvertisingModel')
|
||||
const { canUseFreeBillboard, consumeFreeBillboard } = require('../../../utils/billboardFreeLimit')
|
||||
const { tryConsumeFreeBillboard } = require('../../../utils/billboardFreeLimit')
|
||||
const OfferTypeModel = require('../../../models/OfferTypeModel')
|
||||
const OfferModel = require('../../../models/OfferModel')
|
||||
const { default: axios } = require('axios')
|
||||
@@ -92,6 +92,17 @@ const initiatePaymentWeb = async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
// IDOR: قبلاً هیچجا چک نمیشد که این پروژه واقعاً متعلق به همین کاربرِ
|
||||
// احرازهویتشدهست — یه projectId دلخواه (مثلاً از یه لینکِ ساختگی) کافی بود تا
|
||||
// کاربرِ دیگهای پرداخت کنه ولی پروژهی یه کاربرِ کاملاً متفاوت تایید/پرداختشده بشه
|
||||
const projectForOwnerCheck = await ProjectModel.findById(projectId).select('creator_id')
|
||||
if (!projectForOwnerCheck) {
|
||||
return res.status(404).json({ error: true, message: 'پروژه یافت نشد' })
|
||||
}
|
||||
if (String(projectForOwnerCheck.creator_id) !== String(userId)) {
|
||||
return res.status(403).json({ error: true, message: 'دسترسی به این پروژه ندارید' })
|
||||
}
|
||||
|
||||
const price = Number(item.price) || 0
|
||||
const user = await UserModel.findById(userId)
|
||||
if (!user) {
|
||||
@@ -566,16 +577,25 @@ const initiateAdvertisingPaymentWeb = async (req, res) => {
|
||||
const advertisingId = req.body.advertisingId // Get project ID from request body
|
||||
const showDiscount = req.body.showDiscount
|
||||
|
||||
// IDOR: قبلاً هیچجا چک نمیشد که این بیلبورد واقعاً متعلق به همین کاربرِ
|
||||
// احرازهویتشدهست — یه advertisingId دلخواه کافی بود تا کاربرِ دیگهای پرداخت
|
||||
// کنه ولی بیلبوردِ یه کاربرِ کاملاً متفاوت تایید/پرداختشده بشه (رایگان بود، فقط
|
||||
// با مصرفِ سهمیهی رایگانِ همون کاربر هم ممکن بود)
|
||||
const advertisingForOwnerCheck = await AdvertisingModel.findById(advertisingId).select('creator_id')
|
||||
if (!advertisingForOwnerCheck) {
|
||||
return res.status(404).json({ message: 'بیلبورد یافت نشد' })
|
||||
}
|
||||
if (String(advertisingForOwnerCheck.creator_id) !== String(userId)) {
|
||||
return res.status(403).json({ message: 'دسترسی به این بیلبورد ندارید' })
|
||||
}
|
||||
|
||||
// Save project ID for later use in callback URL
|
||||
req.session.advertisingId = advertisingId
|
||||
const item = await AdvertisingTypeModel.findOne({ name: itemName })
|
||||
const user = await UserModel.findById(userId)
|
||||
|
||||
if (item.name === 'free' && item.price === 0) {
|
||||
if (canUseFreeBillboard(user)) {
|
||||
await consumeFreeBillboard(user)
|
||||
// updateOne + $inc بهجای user.save() — رجوع کنید به توضیح مشابه در initiateOfferPaymentWeb
|
||||
await UserModel.updateOne({ _id: userId }, { $inc: { free_billboard_count: 1 } })
|
||||
const freeResult = await tryConsumeFreeBillboard(UserModel, userId)
|
||||
if (freeResult.ok) {
|
||||
const project = await AdvertisingModel.findById(advertisingId)
|
||||
if (!project) {
|
||||
res.status(404).send('Advertising not found')
|
||||
@@ -586,7 +606,7 @@ const initiateAdvertisingPaymentWeb = async (req, res) => {
|
||||
await project.save()
|
||||
res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'سقف ۲۰۰ بیلبورد رایگان شما به پایان رسیده است.' })
|
||||
res.status(403).json({ message: freeResult.message })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post(
|
||||
@@ -721,16 +741,25 @@ const republishAdvertisingPaymentWeb = async (req, res) => {
|
||||
const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database
|
||||
const advertisingId = req.body.advertisingId // Get project ID from request body
|
||||
const showDiscount = req.body.showDiscount
|
||||
// IDOR: قبلاً هیچجا چک نمیشد که این بیلبورد واقعاً متعلق به همین کاربرِ
|
||||
// احرازهویتشدهست — یه advertisingId دلخواه کافی بود تا کاربرِ دیگهای پرداخت
|
||||
// کنه ولی بیلبوردِ یه کاربرِ کاملاً متفاوت تایید/پرداختشده بشه (رایگان بود، فقط
|
||||
// با مصرفِ سهمیهی رایگانِ همون کاربر هم ممکن بود)
|
||||
const advertisingForOwnerCheck = await AdvertisingModel.findById(advertisingId).select('creator_id')
|
||||
if (!advertisingForOwnerCheck) {
|
||||
return res.status(404).json({ message: 'بیلبورد یافت نشد' })
|
||||
}
|
||||
if (String(advertisingForOwnerCheck.creator_id) !== String(userId)) {
|
||||
return res.status(403).json({ message: 'دسترسی به این بیلبورد ندارید' })
|
||||
}
|
||||
|
||||
// Save project ID for later use in callback URL
|
||||
req.session.advertisingId = advertisingId
|
||||
const item = await AdvertisingTypeModel.findOne({ name: itemName })
|
||||
const user = await UserModel.findById(userId)
|
||||
|
||||
if (item.name === 'free' && item.price === 0) {
|
||||
if (canUseFreeBillboard(user)) {
|
||||
await consumeFreeBillboard(user)
|
||||
// updateOne + $inc بهجای user.save() — رجوع کنید به توضیح مشابه در initiateOfferPaymentWeb
|
||||
await UserModel.updateOne({ _id: userId }, { $inc: { free_billboard_count: 1 } })
|
||||
const freeResult = await tryConsumeFreeBillboard(UserModel, userId)
|
||||
if (freeResult.ok) {
|
||||
const project = await AdvertisingModel.findById(advertisingId)
|
||||
if (!project) {
|
||||
res.status(404).send('Advertising not found')
|
||||
@@ -741,7 +770,7 @@ const republishAdvertisingPaymentWeb = async (req, res) => {
|
||||
await project.save()
|
||||
res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'سقف ۲۰۰ بیلبورد رایگان شما به پایان رسیده است.' })
|
||||
res.status(403).json({ message: freeResult.message })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post(
|
||||
|
||||
@@ -69,6 +69,7 @@ const completeSocialSignup = async (req, res, next) => {
|
||||
)
|
||||
return res.status(422).json({
|
||||
error: true,
|
||||
code: 'duplicate_username',
|
||||
field: 'user_name',
|
||||
message: 'نام کاربری تکراری است',
|
||||
suggestions
|
||||
|
||||
@@ -59,6 +59,7 @@ const setUserName = async (req, res, next) => {
|
||||
)
|
||||
return res.status(422).json({
|
||||
error: true,
|
||||
code: 'duplicate_username',
|
||||
message: 'نام کاربری تکراری است',
|
||||
suggestions
|
||||
})
|
||||
@@ -113,6 +114,7 @@ const updateUserName = async (req, res, next) => {
|
||||
)
|
||||
return res.status(422).json({
|
||||
error: true,
|
||||
code: 'duplicate_username',
|
||||
message: 'نام کاربری تکراری است',
|
||||
suggestions
|
||||
})
|
||||
|
||||
49
index.js
49
index.js
@@ -12,6 +12,7 @@ try {
|
||||
console.warn('[index] postVideoProcessing unavailable:', error.message);
|
||||
}
|
||||
const http = require('http').createServer(app);
|
||||
const multer = require('multer');
|
||||
const cors = require('cors');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const moment = require('moment-jalaali');
|
||||
@@ -417,7 +418,31 @@ app.post('/api/v1/notification/send-sms', [blockCheck], async (req, res) => {
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/v1/chat/file', [blockCheck, chatPeerBlock], async (req, res) => {
|
||||
// قبلاً express-form-data بهصورتِ global بدنهی هر درخواستِ multipart رو میخوند و
|
||||
// req.files رو پر میکرد؛ وقتی اون middleware (بهخاطرِ تداخل با multer توی روتهای
|
||||
// دیگه مثل posts) حذف شد، این روت که مستقیماً به همون req.files تکیه داشت بدونِ هیچ
|
||||
// parserِ جایگزینی موند — یعنی req.files همیشه undefined بود و هر ارسالِ عکس/ویدیو/
|
||||
// فایل/ویس توی چت (وب و اندروید، چون هر دو همین یه اندپوینتِ مشترک رو صدا میزنن) با
|
||||
// «Server error» شکست میخورد. همون الگوی multer.diskStorage که posts استفاده میکنه،
|
||||
// اینجا هم (مستقیماً روی پوشهی نهایی، بدونِ نیاز به fs.move جداگانه) وصل شد
|
||||
const chatFileUploadDir = path.join(__dirname, '../storage/messages')
|
||||
if (!fs.existsSync(chatFileUploadDir)) {
|
||||
fs.mkdirSync(chatFileUploadDir, { recursive: true })
|
||||
}
|
||||
const chatFileUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, chatFileUploadDir),
|
||||
filename: (req, file, cb) => {
|
||||
// اسمِ فایل هیچوقت مستقیم از ورودیِ کاربر (file.originalname) گرفته نمیشه —
|
||||
// فقط پسوندش؛ وگرنه یه originalname مثل ../../../../etc/passwd میتونست از
|
||||
// uploadDir بیرون بزنه (path traversal / نوشتنِ فایلِ دلخواه روی سرور)
|
||||
cb(null, `${Date.now()}-${crypto.randomUUID()}${path.extname(file.originalname || '')}`)
|
||||
}
|
||||
}),
|
||||
limits: { fileSize: 200 * 1024 * 1024 }
|
||||
}).single('file')
|
||||
|
||||
app.post('/api/v1/chat/file', [blockCheck, chatFileUpload, chatPeerBlock], async (req, res) => {
|
||||
try {
|
||||
const senderId = String(req.user._id)
|
||||
const { receiverId, content, replyToId, fileType, selfDestructSeconds, viewOnce } = req.body
|
||||
@@ -448,27 +473,17 @@ app.post('/api/v1/chat/file', [blockCheck, chatPeerBlock], async (req, res) => {
|
||||
if (viewOnceFlag && !isViewOnceMediaType(resolvedFileType)) {
|
||||
return res.status(422).json({ error: 'View once is only for image, video, and voice' })
|
||||
}
|
||||
const { file } = req.files
|
||||
const file = req.file
|
||||
let fileUrl = null
|
||||
if (file) {
|
||||
const uploadDir = path.join(__dirname, '../storage/messages')
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true })
|
||||
}
|
||||
// اسمِ فایل هیچوقت مستقیم از ورودیِ کاربر (file.name) گرفته نمیشه — فقط
|
||||
// پسوندش؛ وگرنه یه filename مثل ../../../../etc/passwd میتونست از uploadDir
|
||||
// بیرون بزنه (path traversal / نوشتنِ فایلِ دلخواه روی سرور)
|
||||
const uniqueFileName = `${Date.now()}-${crypto.randomUUID()}${path.extname(file.name || '')}`
|
||||
const filePath = path.join(uploadDir, uniqueFileName)
|
||||
await fs.move(file.path, filePath)
|
||||
if (resolvedFileType === 'image' || (file.type || '').startsWith('image/')) {
|
||||
await optimizeImageFile(filePath)
|
||||
} else if (resolvedFileType === 'video' || (file.type || '').startsWith('video/')) {
|
||||
optimizeVideoFaststart(filePath).catch((err) => {
|
||||
if (resolvedFileType === 'image' || (file.mimetype || '').startsWith('image/')) {
|
||||
await optimizeImageFile(file.path)
|
||||
} else if (resolvedFileType === 'video' || (file.mimetype || '').startsWith('video/')) {
|
||||
optimizeVideoFaststart(file.path).catch((err) => {
|
||||
console.error('[chat/file] video optimize background error:', err.message)
|
||||
})
|
||||
}
|
||||
fileUrl = `/messages/${uniqueFileName}`
|
||||
fileUrl = `/messages/${file.filename}`
|
||||
}
|
||||
|
||||
const newMessage = new MessageModel({
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const crypto = require('crypto')
|
||||
const multer = require('multer')
|
||||
const {
|
||||
listRooms,
|
||||
createRoom,
|
||||
@@ -15,6 +19,23 @@ const requireSubUserPermission = require('../../../middlewares/requireSubUserPer
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// همون دلیلِ /api/v1/chat/file توی index.js: این روت قبلاً بهجای multer به
|
||||
// express-form-data (که بعداً global حذف شد) تکیه داشت، پس req.files همیشه
|
||||
// undefined بود و هر ارسالِ فایل/عکس/ویس/ویدیو توی چتِ روم هم شکست میخورد
|
||||
const roomMessageUploadDir = path.join(__dirname, '../../../storage/chat-rooms/messages')
|
||||
if (!fs.existsSync(roomMessageUploadDir)) {
|
||||
fs.mkdirSync(roomMessageUploadDir, { recursive: true })
|
||||
}
|
||||
const roomMessageUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, roomMessageUploadDir),
|
||||
filename: (req, file, cb) => {
|
||||
cb(null, `${Date.now()}-${crypto.randomUUID()}${path.extname(file.originalname || '')}`)
|
||||
}
|
||||
}),
|
||||
limits: { fileSize: 200 * 1024 * 1024 }
|
||||
}).single('file')
|
||||
|
||||
router.get('/', listRooms)
|
||||
router.post('/', createRoom)
|
||||
router.get('/:roomId', getRoomDetail)
|
||||
@@ -23,7 +44,7 @@ router.post('/:roomId/members', addRoomMembers)
|
||||
router.get('/:roomId/e2ee-keys', getRoomE2eeKeys)
|
||||
router.put('/:roomId/e2ee-keys', putRoomE2eeKeys)
|
||||
router.get('/:roomId/messages', getRoomMessages)
|
||||
router.post('/:roomId/messages', requireSubUserPermission('reply_dm'), sendRoomMessage)
|
||||
router.post('/:roomId/messages', roomMessageUpload, requireSubUserPermission('reply_dm'), sendRoomMessage)
|
||||
router.post('/:roomId/messages/reaction', reactToRoomMessage)
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -9,16 +9,28 @@ function canUseFreeBillboard (user) {
|
||||
return used < BILLBOARD_FREE_LIMIT
|
||||
}
|
||||
|
||||
async function consumeFreeBillboard (user) {
|
||||
if (!canUseFreeBillboard(user)) {
|
||||
/**
|
||||
* نسخهی atomic — چک (free_billboard_count < سقف) و افزایش توی یه عملیاتِ واحدِ
|
||||
* MongoDB (findOneAndUpdate با شرط توی query) انجام میشه، نه در دو مرحلهی جدا
|
||||
* (قبلاً canUseFreeBillboard روی یه user از قبل fetchشده چک میشد و بعد جداگانه
|
||||
* $inc میخورد) — وگرنه دو درخواستِ همزمان (دابلکلیکِ «تلاشِ دوباره» روی صفحهی
|
||||
* پرداختِ ناموفق، که تا امروز گاردِ submitting هم نداشت) هر دو میتونستن همون
|
||||
* user رو با free_billboard_count زیرِ سقف بخونن و هر دو از سقف رد بشن
|
||||
*/
|
||||
async function tryConsumeFreeBillboard (UserModel, userId) {
|
||||
const updated = await UserModel.findOneAndUpdate(
|
||||
{ _id: userId, free_billboard_count: { $lt: BILLBOARD_FREE_LIMIT } },
|
||||
{ $inc: { free_billboard_count: 1 } },
|
||||
{ new: true }
|
||||
)
|
||||
if (!updated) {
|
||||
return { ok: false, message: `سقف ${BILLBOARD_FREE_LIMIT} بیلبورد رایگان به پایان رسیده است.` }
|
||||
}
|
||||
user.free_billboard_count = (user.free_billboard_count || 0) + 1
|
||||
return { ok: true }
|
||||
return { ok: true, user: updated }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BILLBOARD_FREE_LIMIT,
|
||||
canUseFreeBillboard,
|
||||
consumeFreeBillboard
|
||||
tryConsumeFreeBillboard
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ const { getBucketBalance } = require('../controllers/application/wallet/walletCo
|
||||
* اینطوری اگه کاربر پرداختِ درگاه رو نصفه ول کنه، هیچ مبلغی از کیف پول کم نشده که
|
||||
* لازم باشه برگردونده بشه
|
||||
*/
|
||||
// زرینپال حداقل مبلغِ درگاه رو ۱۵٬۰۰۰ ریال (۱٬۵۰۰ تومان) میخواد — اگه بعدِ کسرِ
|
||||
// کیفپول فقط یه باقیموندهی مثبتِ کوچیکتر از این بمونه، کلِ درخواستِ initiate به
|
||||
// زرینپال با خطای 422 رد میشه (نه فقط همون بخشِ کوچیک) — این دقیقاً همون چیزی بود
|
||||
// که باعث میشد «ثبت درخواست» (و هر مسیرِ دیگهای که از این تابع استفاده میکنه) با
|
||||
// بعضی ترکیبهای قیمت/موجودیِ کیفپول روی سرور ارور بده
|
||||
const ZARINPAL_MIN_GATEWAY_TOMAN = 1500
|
||||
|
||||
async function resolveWalletSplit (userId, amount, useWallet) {
|
||||
if (!useWallet) {
|
||||
return { walletAmount: 0, gatewayAmount: amount, fullyCovered: false }
|
||||
@@ -21,8 +28,25 @@ async function resolveWalletSplit (userId, amount, useWallet) {
|
||||
if (!balance || balance <= 0) {
|
||||
return { walletAmount: 0, gatewayAmount: amount, fullyCovered: false }
|
||||
}
|
||||
const walletAmount = Math.min(balance, amount)
|
||||
const gatewayAmount = amount - walletAmount
|
||||
|
||||
let walletAmount = Math.min(balance, amount)
|
||||
let gatewayAmount = amount - walletAmount
|
||||
|
||||
if (gatewayAmount > 0 && gatewayAmount < ZARINPAL_MIN_GATEWAY_TOMAN) {
|
||||
const neededWalletAmount = amount - ZARINPAL_MIN_GATEWAY_TOMAN
|
||||
if (neededWalletAmount >= 0 && balance >= neededWalletAmount) {
|
||||
// از کیفپول کمتر استفاده کن تا باقیمونده دقیقاً به حداقلِ مجازِ درگاه برسه
|
||||
walletAmount = neededWalletAmount
|
||||
gatewayAmount = ZARINPAL_MIN_GATEWAY_TOMAN
|
||||
} else {
|
||||
// یا کلِ مبلغ از حداقلِ درگاه کمتره، یا کیفپول برای رسوندنِ باقیمونده به اون
|
||||
// حداقل کافی نیست — اصلاً از کیفپول استفاده نکن، همه چیز از درگاه. اینطوری
|
||||
// حداقل یه خطای واضحِ «حداقل مبلغ» از خودِ زرینپال میگیریم، نه یه 422ِ گنگ
|
||||
walletAmount = 0
|
||||
gatewayAmount = amount
|
||||
}
|
||||
}
|
||||
|
||||
return { walletAmount, gatewayAmount, fullyCovered: gatewayAmount <= 0 }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user