This commit is contained in:
payacom
2026-08-31 20:45:41 +03:30
parent fa9f23c61d
commit 3f416a7a60
367 changed files with 621895 additions and 6443 deletions

View File

@@ -0,0 +1,13 @@
const createRateLimiter = require('../utils/createRateLimiter');
const authRateLimit = createRateLimiter('auth', {
windowMs: 15 * 60 * 1000,
max: 40,
standardHeaders: true,
legacyHeaders: false,
message: {
error: 'تعداد درخواست‌های احراز هویت بیش از حد مجاز است. لطفاً چند دقیقه بعد دوباره تلاش کنید.',
},
});
module.exports = authRateLimit;

View File

@@ -28,6 +28,14 @@ module.exports = async (req, res, next) => {
}
let user = await UserModel.findById(decodedToken.id)
if (!user) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'User not found!'
})
}
user = await expireBlockSuspensionIfNeeded(user)
const blockResponse = getBlockedAccountResponse(user)
@@ -39,6 +47,18 @@ module.exports = async (req, res, next) => {
next()
} catch (error) {
console.error('Error in blockCheck middleware:', error)
if (
error?.name === 'JsonWebTokenError' ||
error?.name === 'TokenExpiredError'
) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!'
})
}
res.status(500).send({
status: 'error',
code: 500,

View File

@@ -0,0 +1,53 @@
const mongoose = require('mongoose')
const UserModel = require('../models/UserModel')
const {
viewerBlockedUser,
userBlockedViewer
} = require('../utils/blockVisibility')
function isChatBlockedBetween(userDoc, otherUserId) {
if (!userDoc || !otherUserId) return false
const otherId = String(otherUserId)
return (
viewerBlockedUser(userDoc, otherId) ||
userBlockedViewer(userDoc, otherId)
)
}
module.exports = async (req, res, next) => {
try {
const userId = String(req.user._id)
const receiverId = String(
req.body?.receiverId || req.query?.receiverId || ''
).trim()
if (!receiverId || !mongoose.Types.ObjectId.isValid(receiverId)) {
return next()
}
if (receiverId === userId) {
return res.status(400).json({ message: 'امکان ارسال پیام به خود وجود ندارد' })
}
const [currentUser, peer] = await Promise.all([
UserModel.findById(userId).select('blocked_by blocked_users'),
UserModel.findById(receiverId).select('blocked_by blocked_users')
])
if (!peer) {
return res.status(404).json({ message: 'کاربر یافت نشد' })
}
if (
isChatBlockedBetween(currentUser, receiverId) ||
isChatBlockedBetween(peer, userId)
) {
return res.status(403).json({ message: 'امکان ارسال پیام وجود ندارد' })
}
next()
} catch (error) {
console.error('chatPeerBlock error:', error)
res.status(500).json({ error: 'Server error' })
}
}

View File

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

View File

@@ -1,17 +1,57 @@
const bodyParser = require('body-parser')
const cors = require('cors')
const formData = require('express-form-data')
const session = require('express-session')
const { RedisStore } = require('connect-redis')
const { getRedisClient, getRawClient } = require('../utils/redisClient')
const lastOnline = require('./lastOnline')
const SESSION_OPTIONS = {
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',
},
}
// قبلاً MemoryStoreِ پیش‌فرضِ express-session بود — برای production مناسب نیست (نه
// بین چند اینستنس‌سرور مشترکه، نه محدودیتی داره). الان وقتی Redis وصله از یه store
// مشترک استفاده می‌شه. عین ratedLimiter (createRateLimiter.js): دو نمونه‌ی کاملاً
// جدا ساخته می‌شه و روی *هر* درخواست بر اساسِ آماده‌بودنِ فعلیِ Redis سوییچ می‌شه —
// نه اینکه همیشه به Redis وصل بشه و فقط منتظرِ خطا بمونه. اون روش (client خام،
// بدون چکِ آماده‌بودن) وقتی Redis اصلاً هیچ‌وقت در دسترس نبود (مثلاً یه دیپلویِ
// تازه که هنوز Redis روش ست نشده)، هر درخواست رو تا انقضای connectTimeout کند
// می‌کرد؛ این نسخه اون تأخیر رو صفر می‌کنه چون اصلاً منتظرِ Redis نمی‌مونه
const redisSession = session({
...SESSION_OPTIONS,
store: new RedisStore({ client: getRawClient(), prefix: 'sess:' }),
})
const memorySession = session(SESSION_OPTIONS)
// نکته: اگه Redis وسطِ یه session بیفته (بعد از ساخته‌شدن یه سشنِ Redis-محور)،
// سوییچ به memorySession یعنی اون سشنِ خاص موقتاً دیده نمی‌شه — قابل‌قبوله چون این
// اپ اصلی‌ترین احرازهویتش JWTه، نه سشن (req.session جای دیگه‌ای توی این کدبیس
// استفاده نمی‌شه)
function dispatchSession (req, res, next) {
const store = getRedisClient() ? redisSession : memorySession
store(req, res, (err) => {
if (err) {
// fallback نهایی برای همون ریسِ نادرِ «Redis درست وسطِ همین درخواست قطع شد» —
// به‌جای next(err) (که express-session پیش‌فرضش ۵۰۰-کردنِ کل درخواسته)، فقط
// لاگ می‌کنیم و بدون سشن ادامه می‌دیم
console.error('[session] store error — continuing without session:', err.message)
return next()
}
next()
})
}
module.exports = (app) => {
app.use(cors()) // develop
app.use(formData.parse())
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(session({
secret: 'your_secret_key', // کلید مخفی برای امنیت جلسات
resave: false,
saveUninitialized: false
}))
app.use(dispatchSession)
app.use(lastOnline)
}

View File

@@ -0,0 +1,54 @@
const UserModel = require('../models/UserModel')
const jwt = require('jsonwebtoken')
const {
canUseProjects,
respondProjectProfileIncomplete,
} = require('../utils/projectProfile')
/** حداقل ثبت‌نام برای پروژه: نام کاربری + نام */
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) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!',
})
}
if (!canUseProjects(user)) {
return respondProjectProfileIncomplete(res)
}
req.user = user
next()
} catch (error) {
console.error('isBasicRegister middleware:', error)
res.status(500).send({
status: 'error',
code: 500,
message: 'Server error',
})
}
}

View File

@@ -2,9 +2,7 @@ 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',
@@ -12,7 +10,6 @@ module.exports = async (req, res, next) => {
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)
@@ -24,8 +21,14 @@ module.exports = async (req, res, next) => {
message: 'Your token is not valid!'
})
}
console.log(33);
const user = await UserModel.findById(decodedToken.id)
if (!user) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!'
})
}
if (!user.user_name ||
!user.first_name ||
!user.last_name ||
@@ -38,11 +41,9 @@ module.exports = async (req, res, next) => {
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',

View File

@@ -24,20 +24,26 @@ module.exports = async (req, res, next) => {
}
const user = await UserModel.findById(decodedToken.id)
if (!user) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!'
})
}
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'
})
}
!user.password ||
!user.user_type) {
return res.status(403).send({
status: 'error',
code: 403,
message: 'لطفا از بخش تنظیمات، ثبت نام خود را کامل کنید',
type: 'not_register'
})
}
req.user = user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود.
req.user = user
next()
} catch (error) {
console.error('not_register middleware:', error)

View File

@@ -1,19 +1,49 @@
const jwt = require('jsonwebtoken')
const UserModel = require('../models/UserModel')
const { getRedisClient } = require('../utils/redisClient')
const { markOnline } = require('../utils/presence')
// این میدل‌ور روی *هر* درخواستِ احرازهویت‌شده اجرا می‌شه (app.use سراسری) — قبلاً
// یعنی هر درخواست یه findById+save کاملِ Mongo می‌زد، حتی برای کاربری که پشتِ‌سرِهم
// صدها درخواست در دقیقه می‌فرسته. الان: وضعیتِ «آنلاینِ لحظه‌ای» توی Redis با TTLِ
// کوتاه نگه داشته می‌شه (هر درخواست رفرش می‌شه، تقریباً رایگان — utils/presence.js)،
// و نوشتنِ last_online توی Mongo (برای «آخرین بازدید» که جاهای دیگه‌ی اپ می‌خونن)
// throttle می‌شه به حداکثر یک‌بار در هر ۶۰ ثانیه برای هر کاربر
const MONGO_SYNC_TTL_SECONDS = 60
async function syncLastOnlineToMongo (userId) {
const user = await UserModel.findById(userId).select('ghost_mode account_status')
if (user && !user.ghost_mode && user.account_status !== 'deactivated') {
await UserModel.updateOne({ _id: userId }, { last_online: new Date() })
}
}
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)
try {
const token = req.header('Authorization').split(' ')[1]
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = String(decodedToken.id)
const redis = getRedisClient()
if (redis) {
await markOnline(userId)
const acquiredSyncLock = await redis.set(
`presence:sync:${userId}`,
'1',
{ NX: true, EX: MONGO_SYNC_TTL_SECONDS }
)
if (acquiredSyncLock) {
await syncLastOnlineToMongo(userId)
}
} else {
// Redis در دسترس نیست — همون رفتارِ قبلی، مستقیم Mongo (fallback امن، کندتر
// ولی درست) تا وقتی Redis دوباره وصل بشه
await syncLastOnlineToMongo(userId)
}
} catch (error) {
console.error('Error updating last online time:', error)
}
}

View File

@@ -0,0 +1,18 @@
const { userHasPostExpertise, respondPostExpertiseRequired } = require('../utils/postProfile')
/** برای ثبت پست، تخصص کاربر باید مشخص شده باشد */
module.exports = (req, res, next) => {
const user = req.user
if (!user) {
return res.status(401).json({
error: true,
message: 'You are not authorized!',
})
}
if (!userHasPostExpertise(user)) {
return respondPostExpertiseRequired(res)
}
next()
}