full
This commit is contained in:
13
middlewares/authRateLimit.js
Normal file
13
middlewares/authRateLimit.js
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
53
middlewares/chatPeerBlock.js
Normal file
53
middlewares/chatPeerBlock.js
Normal 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' })
|
||||
}
|
||||
}
|
||||
@@ -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: 'خطایی در عملیات مورد نظر رخ داده است.',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
54
middlewares/isBasicRegister.js
Normal file
54
middlewares/isBasicRegister.js
Normal 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',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
middlewares/requirePostExpertise.js
Normal file
18
middlewares/requirePostExpertise.js
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user