diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..be735eb --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# کپی کنید به .env (بک‌اند) +NODE_ENV=development +APP_PORT=3002 +APP_URL=http://localhost:3002/api/v1 +APP_SITE=http://localhost:3000 +APP_SECRET=your-secret +DATABASE_URL=mongodb://root:PASSWORD@localhost:27017/modstagram?authSource=admin +SMS_SECRET= +SMS_VERIFY_KEY= +MERCHENT_CODE= +ZARINPAL_MERCHANT_ID= + +# روی سرور (production): +# NODE_ENV=production +# APP_PORT=3002 +# APP_URL=https://app.modstagram.ir/api/v1 +# APP_SITE=https://modstagram.com diff --git a/controllers/application/academy/academyControllers.js b/controllers/application/academy/academyControllers.js index 2d465fa..658adb7 100644 --- a/controllers/application/academy/academyControllers.js +++ b/controllers/application/academy/academyControllers.js @@ -15,6 +15,13 @@ const CourseComment = require("../../../models/CourseComentModel"); const IsPaymentCourse = require("../../../models/IsPaymentCourseModel"); const Tax = require("../../../models/TaxModel"); const AcademyCategoryModel = require('../../../models/AcademyCategoryModel'); +const { + createLikeNotification, + resolveCourseOwnerId +} = require('../../../utils/likeNotification'); +const { + createCommentNotification +} = require('../../../utils/commentNotification'); const onServerRestart = async () => { const CoursePayment = await CoursePaymentModel.findOne({ @@ -1178,6 +1185,14 @@ const likeCourseContent = async (req, res, next) => { createdAt: new Date(), }); + const courseOwnerId = await resolveCourseOwnerId(course); + await createLikeNotification({ + ownerId: courseOwnerId, + likerId: userId, + entityId: course._id, + type: 'academy_like' + }); + res.status(200).json({ success: true, message: "دوره با موفقیت لایک شد", @@ -1394,6 +1409,14 @@ const createComment = async (req, res) => { newComment._id ).populate("user_id", "name username"); + const courseOwnerId = await resolveCourseOwnerId(course); + await createCommentNotification({ + ownerId: courseOwnerId, + commenterId: userId, + entityId: course._id, + type: 'academy_comment' + }); + res.status(201).json({ success: true, message: "کامنت با موفقیت ثبت شد", diff --git a/controllers/application/advertising/advertisingController.js b/controllers/application/advertising/advertisingController.js index f8278ee..772da30 100644 --- a/controllers/application/advertising/advertisingController.js +++ b/controllers/application/advertising/advertisingController.js @@ -14,6 +14,11 @@ const AdvertisingLikeModel = require('../../../models/AdvertisingLikeModel') const AdvertisingComment = require('../../../models/AdvertisingCommentModel') const AdvertisingRatingModel = require('../../../models/AdvertisingRatingModel') const AdvertisingProfileModel = require('../../../models/AdvertisingProfile') +const { createLikeNotification } = require('../../../utils/likeNotification') +const { + createCommentNotification, + createRatingNotification +} = require('../../../utils/commentNotification') const createAdvertisingValidationRules = () => { return [ @@ -698,6 +703,14 @@ const toggleLike = async (req, res, next) => { // افزایش تعداد لایک‌ها در تبلیغات advertising.likesCount += 1 await advertising.save() + + await createLikeNotification({ + ownerId: advertising.creator_id, + likerId: userId, + entityId: advertising._id, + type: 'billboard_like' + }) + return res.status(201).json({ message: 'ویترین با موفقیت لایک شد' }) } } catch (error) { @@ -727,8 +740,16 @@ const addComment = async (req, res, next) => { }) await newComment.save() - // افزایش تعداد کامنت‌های تبلیغ - // await AdvertisingModel.findByIdAndUpdate(advertisingId, { $inc: { commentsCount: 1 } }) + + const advertising = await AdvertisingModel.findById(advertisingId) + if (advertising) { + await createCommentNotification({ + ownerId: advertising.creator_id, + commenterId: userId, + entityId: advertising._id, + type: 'billboard_comment' + }) + } res.status(201).json({ message: 'کامنت با موفقیت اضافه شد', comment: newComment }) } catch (error) { @@ -783,6 +804,13 @@ const rateAdvertising = async (req, res, next) => { rating }) await newRating.save() + + await createRatingNotification({ + ownerId: advertising.creator_id, + raterId: userId, + entityId: advertising._id, + type: 'billboard_rating' + }) } // به‌روزرسانی امتیازات کلی کاربر diff --git a/controllers/application/login/loginWithUserName.js b/controllers/application/login/loginWithUserName.js index ded6b6f..e0ffa74 100644 --- a/controllers/application/login/loginWithUserName.js +++ b/controllers/application/login/loginWithUserName.js @@ -2,8 +2,14 @@ const UserModel = require('../../../models/UserModel') const bcrypt = require('bcryptjs') const TokenService = require('../../../services/TokenService') +const { + expireBlockSuspensionIfNeeded, + getBlockedAccountResponse +} = require('../../../utils/blockSuspension') // const jwt = require('jsonwebtoken') +const INVALID_CREDENTIALS = 'نام کاربری یا کلمه عبور را اشتباه وارد کردید' + const loginWithUserName = async (req, res, next) => { try { // اعتبارسنجی ورودی ها @@ -11,30 +17,36 @@ const loginWithUserName = async (req, res, next) => { if (!user_name || !password) { return res.status(422).json({ error: true, - message: 'اطلاعات ارسالی اشتباه است' + message: INVALID_CREDENTIALS }) } // یافتن کاربر با نام کاربری const userLow = user_name.toLowerCase() - const user = await UserModel.findOne({ user_name: userLow }) + let user = await UserModel.findOne({ user_name: userLow }) if (!user) { - return res.status(404).json({ + return res.status(422).json({ error: true, - message: 'کاربری با این نام کاربری یافت نشد' + message: INVALID_CREDENTIALS }) } + + user = await expireBlockSuspensionIfNeeded(user) + const blockResponse = getBlockedAccountResponse(user) + if (blockResponse) { + return res.status(403).json(blockResponse) + } if (!user.password) { return res.status(422).json({ error: true, - message: 'رمز عبور برای این کاربر تنظیم نشده است' + message: INVALID_CREDENTIALS }) } const isPasswordValid = await bcrypt.compare(password, user.password) if (!isPasswordValid) { return res.status(422).json({ error: true, - message: 'نام کاربری یا رمز عبور اشتباه است' + message: INVALID_CREDENTIALS }) } // اگر هم نام کاربری و هم رمز عبور درست بود، ارسال پیام موفقیت آمیز diff --git a/controllers/application/login/verifyController.js b/controllers/application/login/verifyController.js index 1ef37aa..2c57e90 100644 --- a/controllers/application/login/verifyController.js +++ b/controllers/application/login/verifyController.js @@ -1,6 +1,10 @@ const UserModel = require('../../../models/UserModel') const TokenService = require('../../../services/TokenService') const { isOtpExpired } = require('../../../utils/otpExpiry') +const { + expireBlockSuspensionIfNeeded, + getBlockedAccountResponse +} = require('../../../utils/blockSuspension') // const jwt = require('jsonwebtoken') const verifyUser = async (req, res, next) => { @@ -13,13 +17,20 @@ const verifyUser = async (req, res, next) => { }) } - const user = await UserModel.findOne({ mobile }) + let user = await UserModel.findOne({ mobile }) if (!user) { return res.status(404).json({ error: true, message: 'کاربری با این شماره موبایل یافت نشد' }) } + + user = await expireBlockSuspensionIfNeeded(user) + const blockResponse = getBlockedAccountResponse(user) + if (blockResponse) { + return res.status(403).json(blockResponse) + } + const token = TokenService.sign({ id: user._id }) if (!user.otp || isOtpExpired(user)) { @@ -86,7 +97,7 @@ const verifyUser = async (req, res, next) => { return res.status(422).json({ error: true, - message: 'کد تایید اشتباه است' + message: 'کد را اشتباه وارد کردید' }) } catch (error) { next(error) diff --git a/controllers/application/offer/offerController.js b/controllers/application/offer/offerController.js index 7c22be0..9e532f2 100644 --- a/controllers/application/offer/offerController.js +++ b/controllers/application/offer/offerController.js @@ -8,6 +8,10 @@ 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 }) @@ -374,55 +378,35 @@ const createOfferComment = async (req, res, next) => { const userId = decodedToken.id const { offerId, comment, rate, user_id } = req.body - if (!offerId || !comment || !rate || !user_id) { + if (!offerId || !comment || !user_id) { return res.status(400).send({ message: 'All fields are required' }) } - // بررسی اینکه آیا کاربر قبلاً برای این پیشنهاد نظر داده است - const existingComment = await CommentModel.findOne({ - offer: offerId, - creator: userId + + const { ratingValue, isNewRating, requiresRating } = await resolveRatingForComment({ + creatorId: userId, + targetUserId: user_id, + rate }) - if (existingComment) { - return res.status(400).send({ message: 'شما قبلاً نظر خود را برای این درخواست ثبت کرده‌اید' }) + + 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: rate, + rating: ratingValue, comment, comment_for: 'user', status: 'pending' }) await newComment.save() - const user = await UserModel.findById(user_id) - - // پیدا کردن کامنت‌ها و محاسبه‌ی امتیاز کل - const comments = await CommentModel.find({ user: user_id, comment_for: 'user' }) - const totalRating = Number(comments.reduce((acc, comment) => acc + comment.rating, 0)) - - let userLevel - if (user.expertise === 'مدل') { - if (totalRating <= 50) userLevel = 'تازه وارد' - else if (totalRating <= 100) userLevel = 'استاندارد' - else if (totalRating <= 300) userLevel = 'حرفه‌ای' - else userLevel = 'استاد' - } else if (['زیبایی', 'عکاس'].includes(user.expertise)) { - if (totalRating <= 50) userLevel = 'تازه وارد' - else if (totalRating <= 100) userLevel = 'استاندارد' - else if (totalRating <= 300) userLevel = 'حرفه‌ای' - else userLevel = 'استاد' + if (isNewRating) { + await recalculateUserRating(user_id) } - user.user_score = totalRating - user.user_level = userLevel - - const totalComments = await CommentModel.countDocuments({ user: user_id, comment_for: 'user' }) - const averageRating = totalRating / totalComments - user.rate = averageRating.toFixed(1) - await user.save() - res.status(200).json({ message: 'نظر شما با موفقیت ثبت شد' }) } catch (error) { next(error) diff --git a/controllers/application/posts/likeController.js b/controllers/application/posts/likeController.js index 9b511b8..9c2b078 100644 --- a/controllers/application/posts/likeController.js +++ b/controllers/application/posts/likeController.js @@ -3,6 +3,7 @@ const PostModel = require('../../../models/PostModel'); const jwt = require('jsonwebtoken'); const UserModel = require('../../../models/UserModel'); const LikeModel = require('../../../models/LikeModel'); +const { createLikeNotification } = require('../../../utils/likeNotification'); const toggleLike = async (req, res, next) => { try { @@ -48,6 +49,13 @@ const toggleLike = async (req, res, next) => { await post.save(); await LikeModel.create({ postId, userId }); + await createLikeNotification({ + ownerId: post.user_id, + likerId: userId, + entityId: post._id, + type: 'post_like' + }); + return res.status(200).json({ message: 'پست با موفقیت لایک شد', is_liked: true, diff --git a/controllers/application/posts/postController.js b/controllers/application/posts/postController.js index 89db710..8e3d9bd 100644 --- a/controllers/application/posts/postController.js +++ b/controllers/application/posts/postController.js @@ -5,6 +5,7 @@ const { check, validationResult } = require('express-validator'); const jwt = require('jsonwebtoken'); const path = require('path'); const { default: mongoose } = require('mongoose'); +const { viewerIsBlockedBy } = require('../../../utils/blockVisibility'); const createPostValidationRules = () => { console.log(4); @@ -139,6 +140,15 @@ const getUserPostsWeb = async (req, res, next) => { return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) } + if (userReqId && viewerIsBlockedBy(user, userReqId)) { + return res.status(200).json({ + posts: [], + postsCount: 0, + totalPages: 0, + totalItems: 0, + }) + } + const data = {} const options = { page: req.query.page || 1, diff --git a/controllers/application/profile/profileController.js b/controllers/application/profile/profileController.js index bda10aa..2fc0f74 100644 --- a/controllers/application/profile/profileController.js +++ b/controllers/application/profile/profileController.js @@ -35,6 +35,7 @@ const getUserById = async (req, res, next) => { user_name: user.user_name, show_location: user.show_location, is_verified: user.is_verified, + is_Register: user.is_Register, user_score: user.user_score, rate: user.rate, bio: user.bio, diff --git a/controllers/application/projects/updateProjectController.js b/controllers/application/projects/updateProjectController.js index 2343404..41aaeed 100644 --- a/controllers/application/projects/updateProjectController.js +++ b/controllers/application/projects/updateProjectController.js @@ -6,6 +6,46 @@ const { check, validationResult } = require('express-validator') const { ProvinceModel, CityModel } = require('../../../models/StateCity') const NotificationModel = require('../../../models/NotificationModel') const CommentModel = require('../../../models/CommentModel') +const { + resolveRatingForComment, + recalculateUserRating +} = require('../../../utils/commentRating') + +const saveProjectComment = async ({ + targetUserId, + projectId, + creatorId, + comment, + rate, + commentFor +}) => { + const { ratingValue, isNewRating, requiresRating } = await resolveRatingForComment({ + creatorId, + targetUserId, + rate + }) + + if (requiresRating) { + return { error: { status: 400, message: 'Rating must be between 1 and 5' } } + } + + const newComment = new CommentModel({ + user: targetUserId, + project: projectId, + creator: creatorId, + rating: ratingValue, + comment, + comment_for: commentFor, + status: 'pending' + }) + await newComment.save() + + if (isNewRating && commentFor === 'user') { + await recalculateUserRating(targetUserId) + } + + return { ratingValue, isNewRating } +} const doneProject = async (req, res, next) => { try { @@ -16,7 +56,7 @@ const doneProject = async (req, res, next) => { const userId = decodedToken.id const { project_id, comment, rate, user_id } = req.body - if (!project_id || !comment || !rate || !user_id) { + if (!project_id || !comment || !user_id) { return res.status(400).send({ message: 'All fields are required' }) } @@ -42,43 +82,20 @@ const doneProject = async (req, res, next) => { project.status = 'done' await project.save() - const newComment = new CommentModel({ - user: user_id, - project: project_id, - creator: userId, - rating: rate, + const commentResult = await saveProjectComment({ + targetUserId: user_id, + projectId: project_id, + creatorId: userId, comment, - comment_for: 'user', - status: 'pending' + rate, + commentFor: 'user' }) - await newComment.save() - const user = await UserModel.findById(user_id) - - // پیدا کردن کامنت‌ها و محاسبه‌ی امتیاز کل - const comments = await CommentModel.find({ user: user_id, comment_for: 'user' }) - const totalRating = Number(comments.reduce((acc, comment) => acc + comment.rating, 0)) - - let userLevel - if (user.expertise === 'مدل') { - if (totalRating <= 50) userLevel = 'تازه وارد' - else if (totalRating <= 100) userLevel = 'استاندارد' - else if (totalRating <= 300) userLevel = 'حرفه‌ای' - else userLevel = 'استاد' - } else if (['زیبایی', 'عکاس'].includes(user.expertise)) { - if (totalRating <= 50) userLevel = 'تازه وارد' - else if (totalRating <= 100) userLevel = 'استاندارد' - else if (totalRating <= 300) userLevel = 'حرفه‌ای' - else userLevel = 'استاد' + if (commentResult.error) { + return res.status(commentResult.error.status).send({ message: commentResult.error.message }) } - user.user_score = totalRating - user.user_level = userLevel - - const totalComments = await CommentModel.countDocuments({ user: user_id, comment_for: 'user' }) - const averageRating = totalRating / totalComments - user.rate = averageRating.toFixed(1) - await user.save() + const user = await UserModel.findById(user_id) const notification = new NotificationModel({ user_id: user._id, @@ -105,7 +122,7 @@ const doneProjectWeb = async (req, res, next) => { const userId = decodedToken.id const { project_id, comment, rate, user_id } = req.body - if (!project_id || !comment || !rate || !user_id) { + if (!project_id || !comment || !user_id) { return res.status(400).send({ message: 'All fields are required' }) } @@ -117,43 +134,20 @@ const doneProjectWeb = async (req, res, next) => { project.status = 'done' await project.save() - const newComment = new CommentModel({ - user: user_id, - project: project_id, - creator: userId, - rating: rate, + const commentResult = await saveProjectComment({ + targetUserId: user_id, + projectId: project_id, + creatorId: userId, comment, - comment_for: 'user', - status: 'pending' + rate, + commentFor: 'user' }) - await newComment.save() - const user = await UserModel.findById(user_id) - - // پیدا کردن کامنت‌ها و محاسبه‌ی امتیاز کل - const comments = await CommentModel.find({ user: user_id, comment_for: 'user' }) - const totalRating = Number(comments.reduce((acc, comment) => acc + comment.rating, 0)) - - let userLevel - if (user.expertise === 'مدل') { - if (totalRating <= 50) userLevel = 'تازه وارد' - else if (totalRating <= 100) userLevel = 'استاندارد' - else if (totalRating <= 300) userLevel = 'حرفه‌ای' - else userLevel = 'استاد' - } else if (['زیبایی', 'عکاس'].includes(user.expertise)) { - if (totalRating <= 50) userLevel = 'تازه وارد' - else if (totalRating <= 100) userLevel = 'استاندارد' - else if (totalRating <= 300) userLevel = 'حرفه‌ای' - else userLevel = 'استاد' + if (commentResult.error) { + return res.status(commentResult.error.status).send({ message: commentResult.error.message }) } - user.user_score = totalRating - user.user_level = userLevel - - const totalComments = await CommentModel.countDocuments({ user: user_id, comment_for: 'user' }) - const averageRating = totalRating / totalComments - user.rate = averageRating.toFixed(1) - await user.save() + const user = await UserModel.findById(user_id) const notification = new NotificationModel({ user_id: user._id, @@ -182,7 +176,7 @@ const cancleProject = async (req, res, next) => { const { project_id, comment, rate, user_id } = req.body - if (!project_id || !comment || !rate || !user_id) { + if (!project_id || !comment || !user_id) { return res.status(400).send({ message: 'All fields are required' }) } @@ -194,16 +188,18 @@ const cancleProject = async (req, res, next) => { project.status = 'cancled' await project.save() - const newComment = new CommentModel({ - user: user_id, - project: project_id, - creator: userId, - rating: rate, + const commentResult = await saveProjectComment({ + targetUserId: user_id, + projectId: project_id, + creatorId: userId, comment, - comment_for: 'project', - status: 'pending' + rate, + commentFor: 'project' }) - await newComment.save() + + if (commentResult.error) { + return res.status(commentResult.error.status).send({ message: commentResult.error.message }) + } res.status(200).json({ message: 'پروژه با موفقیت تغییر یافت شد' }) } catch (error) { @@ -363,6 +359,14 @@ const setRateProject = async (req, res, next) => { if (!project) { return res.status(403).send({ message: 'Access denied: You are not the creator of this project.' }) } + + const alreadyRated = project.ratings.some((item) => + item.creator_id.toString() === userId.toString() + ) + if (alreadyRated) { + return res.status(400).send({ message: 'شما قبلاً به این پروژه امتیاز داده‌اید' }) + } + project.ratings.push({ project_id, rating: rate, diff --git a/controllers/application/register/verifyController.js b/controllers/application/register/verifyController.js index c1a2d54..a43bb71 100644 --- a/controllers/application/register/verifyController.js +++ b/controllers/application/register/verifyController.js @@ -43,7 +43,7 @@ const verifyUser = async (req, res, next) => { return res.status(422).json({ error: true, - message: 'کد تایید اشتباه است' + message: 'کد را اشتباه وارد کردید' }) } catch (error) { next(error) diff --git a/controllers/application/users/getUserController.js b/controllers/application/users/getUserController.js index 6965266..5a76a9a 100644 --- a/controllers/application/users/getUserController.js +++ b/controllers/application/users/getUserController.js @@ -6,11 +6,22 @@ const jwt = require("jsonwebtoken"); const moment = require("moment-jalaali"); const NotificationModel = require("../../../models/NotificationModel"); const CommentModel = require("../../../models/CommentModel"); +const { + hasCreatorRatedUser, + resolveRatingForComment, + recalculateUserRating, +} = require("../../../utils/commentRating"); +const { createCommentNotification } = require("../../../utils/commentNotification"); const { ProvinceModel, CityModel } = require("../../../models/StateCity"); const OfferModel = require("../../../models/OfferModel"); const PostModel = require("../../../models/PostModel"); const ProjectModel = require("../../../models/ProjectModel"); const LicenseModel = require("../../../models/license") +const { + viewerIsBlockedBy, + blockedProfilePayload, + getBlockerUserIds, +} = require("../../../utils/blockVisibility") const getAllLicenses = async (req, res, next) => { @@ -775,9 +786,19 @@ const getPostsWeb = async (req, res, next) => { .lean(); const userIds = [...new Set(explorePosts.map((p) => String(p.user_id)))]; const usersMap = await UserModel.find({ _id: { $in: userIds } }) - .select('_id user_name first_name last_name expertise province city user_level') + .select('_id user_name first_name last_name expertise province city user_level show_location blocked_users') .lean(); const usersById = Object.fromEntries(usersMap.map((u) => [String(u._id), u])); + + if (decodedToken?.id) { + const blockerIds = new Set( + (await getBlockerUserIds(UserModel, decodedToken.id)).map(String) + ); + explorePosts = explorePosts.filter( + (post) => !blockerIds.has(String(post.user_id)) + ); + } + explorePosts = explorePosts.map((post) => { const user = usersById[String(post.user_id)] || {}; return { @@ -786,8 +807,9 @@ const getPostsWeb = async (req, res, next) => { first_name: user.first_name || '', last_name: user.last_name || '', expertise: user.expertise || '', - province: user.province || {}, - city: user.city || {}, + province: user.show_location ? user.province || {} : {}, + city: user.show_location ? user.city || {} : {}, + show_location: user.show_location, user_level: user.user_level || '', likesCount: post.likes ? post.likes.length : 0, is_liked: false, @@ -817,8 +839,15 @@ const getPostsWeb = async (req, res, next) => { if (userLevel) userFilter.user_level = userLevel; if (_id) userFilter._id = _id; + if (_id && decodedToken?.id) { + const profileOwner = await UserModel.findById(_id).select('blocked_users'); + if (viewerIsBlockedBy(profileOwner, decodedToken.id)) { + return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); + } + } + const users = await UserModel.find(userFilter) - .select("_id user_name first_name last_name expertise province city user_level") + .select("_id user_name first_name last_name expertise province city user_level show_location") .lean(); if (!users || users.length === 0) { @@ -836,6 +865,16 @@ const getPostsWeb = async (req, res, next) => { return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); } + if (decodedToken?.id) { + const blockerIds = new Set( + (await getBlockerUserIds(UserModel, decodedToken.id)).map(String) + ); + posts = posts.filter((post) => !blockerIds.has(String(post.user_id))); + if (!posts.length) { + return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); + } + } + let likeFilter = {}; if (decodedToken) likeFilter.userId = decodedToken.id; const likes = decodedToken @@ -860,8 +899,9 @@ const getPostsWeb = async (req, res, next) => { first_name: user?.first_name || "", last_name: user?.last_name || "", expertise: user?.expertise || "", - province: user?.province || {}, - city: user?.city || {}, + province: user?.show_location ? user?.province || {} : {}, + city: user?.show_location ? user?.city || {} : {}, + show_location: user?.show_location, user_level: user?.user_level || "", likesCount: post.likes ? post.likes.length : 0, is_liked: !!likesMap[post._id.toString()], @@ -918,6 +958,10 @@ const getSingleUser = async (req, res, next) => { const blockedByCurrentUser = user.blocked_by.includes(userReqId); const blockedYou = user.blocked_users.includes(userReqId); + if (blockedYou) { + return res.status(200).json({ user: blockedProfilePayload(user) }); + } + // Allow chat const offerExists = await OfferModel.exists({ $or: [ @@ -937,6 +981,7 @@ const getSingleUser = async (req, res, next) => { user_name: user.user_name, show_location: user.show_location, is_verified: user.is_verified, + is_Register: user.is_Register, user_score: user.user_score, rate: user.rate, bio: user.bio, @@ -948,8 +993,8 @@ const getSingleUser = async (req, res, next) => { profile_image: user.profile_image, lat: user.show_location ? user.lat : null, lng: user.show_location ? user.lng : null, - province: user.province, - city: user.city, + province: user.show_location ? user.province : null, + city: user.show_location ? user.city : null, address: user.show_location ? user.address : null, is_blocked: blockedByCurrentUser, blocked_you: blockedYou, @@ -1016,6 +1061,11 @@ const getSingleUserWeb = async (req, res, next) => { receiver: user._id, }); } + + if (blockedYou && userReqId) { + return res.status(200).json({ user: blockedProfilePayload(user) }); + } + // دریافت تعداد پست‌ها و پروژه‌ها const postsCount = await PostModel.countDocuments({ user_id: user._id, @@ -1043,6 +1093,7 @@ const getSingleUserWeb = async (req, res, next) => { user_name: user.user_name, show_location: user.show_location, is_verified: user.is_verified, + is_Register: user.is_Register, user_score: user.user_score, rate: user.rate, bio: user.bio, @@ -1054,8 +1105,8 @@ const getSingleUserWeb = async (req, res, next) => { profile_image: user.profile_image, lat: user.show_location ? user.lat : null, lng: user.show_location ? user.lng : null, - province: user.province, - city: user.city, + province: user.show_location ? user.province : null, + city: user.show_location ? user.city : null, address: user.show_location ? user.address : null, is_blocked: blockedByCurrentUser, blocked_you: blockedYou, @@ -1099,6 +1150,11 @@ const getUserContactInfo = async (req, res, next) => { next(error); } }; +const { + applyAutoBlockSuspension, + clearAutoBlockSuspension +} = require('../../../utils/blockSuspension') + const blockUser = async (req, res) => { try { const { user_to_block } = req.body; @@ -1107,17 +1163,30 @@ const blockUser = async (req, res) => { const decodedToken = jwt.verify(token, process.env.APP_SECRET); const userReqId = decodedToken.id; - const currentUser = await UserModel.findById(userReqId); // Req - const userToBlock = await UserModel.findById(user_to_block); // To Block - - userToBlock.blocked_by.push(currentUser); - currentUser.blocked_users.push(userToBlock); - - // بررسی تعداد بلاک ها و تنظیم block_status - if (userToBlock.blocked_by.length >= 3) { - userToBlock.block_status = true; + if (userReqId === user_to_block) { + return res.status(400).json({ message: "امکان بلاک خودتان وجود ندارد" }); } + const currentUser = await UserModel.findById(userReqId); + const userToBlock = await UserModel.findById(user_to_block); + + if (!currentUser || !userToBlock) { + return res.status(404).json({ message: "کاربر یافت نشد" }); + } + + const alreadyBlocked = userToBlock.blocked_by.some((id) => + id.equals(currentUser._id) + ); + + if (alreadyBlocked) { + return res.status(400).json({ message: "این کاربر قبلاً بلاک شده است" }); + } + + userToBlock.blocked_by.push(currentUser._id); + currentUser.blocked_users.push(userToBlock._id); + + applyAutoBlockSuspension(userToBlock); + await currentUser.save(); await userToBlock.save(); @@ -1136,18 +1205,17 @@ const unblockUser = async (req, res) => { const decodedToken = jwt.verify(token, process.env.APP_SECRET); const userReqId = decodedToken.id; - const currentUser = await UserModel.findById(userReqId); // Req - const userToUnblock = await UserModel.findById(user_to_unblock); // To Unblock + const currentUser = await UserModel.findById(userReqId); + const userToUnblock = await UserModel.findById(user_to_unblock); + + if (!currentUser || !userToUnblock) { + return res.status(404).json({ message: "کاربر یافت نشد" }); + } - // حذف کاربر از لیست blocked_by کاربر فعلی currentUser.blocked_users.pull(userToUnblock._id); - // حذف کاربر از لیست blocked_users کاربر مورد نظر userToUnblock.blocked_by.pull(currentUser._id); - // بررسی تعداد بلاک ها و تنظیم block_status - if (userToUnblock.blocked_by.length < 3) { - userToUnblock.block_status = null; - } + clearAutoBlockSuspension(userToUnblock); await currentUser.save(); await userToUnblock.save(); @@ -1163,6 +1231,7 @@ const getComments = 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 user_id = req.query.user_id; const post_id = req.query.post_id; @@ -1192,27 +1261,53 @@ const getComments = async (req, res, next) => { const comments = await CommentModel.paginate(filter, options); - const formattedComments = comments.docs.map((comment) => ({ - _id: comment._id, - comment: comment.comment, - rating: comment.rating, - createdAt: moment(comment.createdAt).format("jYYYY-jMM-jDD HH:mm"), - status: comment.status, - user: { - profile_image: comment.creator.profile_image, - user_name: comment.creator.user_name, - is_verified: comment.creator.is_verified, - _id: comment.creator._id, - }, - project: comment.project ? comment.project.project_name : null, - post_id: comment.post ? comment.post.toString() : null, - })); + const earliestRatedComments = await CommentModel.find({ + user: user_id, + rating: { $ne: null, $gt: 0 }, + }) + .select("creator _id createdAt") + .sort({ createdAt: 1 }); + + const ratedCreatorCommentIds = new Map(); + for (const ratedComment of earliestRatedComments) { + const creatorId = ratedComment.creator.toString(); + if (!ratedCreatorCommentIds.has(creatorId)) { + ratedCreatorCommentIds.set(creatorId, ratedComment._id.toString()); + } + } + + const formattedComments = comments.docs.map((comment) => { + const creatorId = comment.creator._id.toString(); + const isPrimaryRatingComment = + comment.rating != null && + comment.rating > 0 && + ratedCreatorCommentIds.get(creatorId) === comment._id.toString(); + + return { + _id: comment._id, + comment: comment.comment, + rating: isPrimaryRatingComment ? comment.rating : null, + createdAt: moment(comment.createdAt).format("jYYYY-jMM-jDD HH:mm"), + status: comment.status, + user: { + profile_image: comment.creator.profile_image, + user_name: comment.creator.user_name, + is_verified: comment.creator.is_verified, + _id: comment.creator._id, + }, + project: comment.project ? comment.project.project_name : null, + post_id: comment.post ? comment.post.toString() : null, + }; + }); + + const hasRated = await hasCreatorRatedUser(decodedToken.id, user_id); res.status(200).json({ comments: formattedComments, totalPages: comments.totalPages, totalItems: comments.totalDocs, currentPage: comments.page, + has_rated: hasRated, }); } catch (error) { next(error); @@ -1231,20 +1326,17 @@ const createUserComment = async (req, res, next) => { return res.status(400).send({ message: "All fields are required" }); } - const existingRating = await CommentModel.findOne({ - user: user_id, - creator: creatorId, - comment_for: post_id ? "post" : "user", - rating: { $exists: true, $ne: null } - }); + const { ratingValue, isNewRating, requiresRating } = + await resolveRatingForComment({ + creatorId, + targetUserId: user_id, + rate, + }); - let ratingValue = rate; - if (existingRating) { - ratingValue = existingRating.rating; - } else { - if (!rate || rate < 1 || rate > 5) { - return res.status(400).send({ message: "Rating must be between 1 and 5" }); - } + if (requiresRating) { + return res + .status(400) + .send({ message: "Rating must be between 1 and 5" }); } const newComment = new CommentModel({ @@ -1258,50 +1350,16 @@ const createUserComment = async (req, res, next) => { }); await newComment.save(); - const user = await UserModel.findById(user_id); - - // محاسبه امتیاز جدید کاربر - const comments = await CommentModel.find({ - user: user_id, - comment_for: "user", - }); - const totalRating = comments.reduce( - (acc, comment) => acc + comment.rating, - 0 - ); - const totalComments = comments.length; - const averageRating = totalRating / totalComments; - - // تعیین سطح کاربر - let userLevel; - if (user.expertise === "مدل") { - if (totalRating <= 50) userLevel = "تازه وارد"; - else if (totalRating <= 100) userLevel = "استاندارد"; - else if (totalRating <= 300) userLevel = "حرفه‌ای"; - else userLevel = "استاد"; - } else if (["زیبایی", "عکاس"].includes(user.expertise)) { - if (totalRating <= 50) userLevel = "تازه وارد"; - else if (totalRating <= 100) userLevel = "استاندارد"; - else if (totalRating <= 300) userLevel = "حرفه‌ای"; - else userLevel = "استاد"; + if (isNewRating) { + await recalculateUserRating(user_id); } - // به‌روزرسانی اطلاعات کاربر - user.user_score = totalRating; - user.user_level = userLevel; - user.rate = averageRating.toFixed(1); - await user.save(); - - // ارسال نوتیفیکیشن - const notification = new NotificationModel({ - user_id: user._id, - type: "user_comment", - title: "نظر جدید", - description: `یک کاربر جدید برای شما نظر ثبت کرد: - ${comment} - امتیاز: ${rate} از 5`, + await createCommentNotification({ + ownerId: user_id, + commenterId: creatorId, + entityId: post_id || user_id, + type: post_id ? "post_comment" : "profile_comment", }); - await notification.save(); res.status(201).json({ message: "نظر با موفقیت ثبت شد و بعد از تایید، منتشر میشود.", diff --git a/controllers/application/verify/addressController.js b/controllers/application/verify/addressController.js index baf7330..b8217f9 100644 --- a/controllers/application/verify/addressController.js +++ b/controllers/application/verify/addressController.js @@ -9,7 +9,18 @@ const setAddress = async (req, res, next) => { if (!token) return res.status(401).send('Access Denied') const { province_id, city_id, address, lat, lng, show_location } = req.body // اعتبارسنجی داده‌ها - if (!province_id || !city_id || !address || !lat || !lng || show_location === 'undefined') { + if ( + !province_id || + !city_id || + !address || + lat === undefined || + lat === null || + lat === '' || + lng === undefined || + lng === null || + lng === '' || + show_location === undefined + ) { return res.status(422).json({ error: true, message: 'اطلاعات ارسالی ناقص است' @@ -41,7 +52,8 @@ const setAddress = async (req, res, next) => { user.address = address user.lat = lat user.lng = lng - user.show_location = show_location + user.show_location = + show_location === true || show_location === 'true' await user.save() @@ -60,7 +72,18 @@ const updateAddress = async (req, res, next) => { const { province_id, city_id, address, lat, lng, show_location } = req.body // اعتبارسنجی داده‌ها - if (!province_id || !city_id || !address || !lat || !lng || show_location === undefined) { + if ( + !province_id || + !city_id || + !address || + lat === undefined || + lat === null || + lat === '' || + lng === undefined || + lng === null || + lng === '' || + show_location === undefined + ) { return res.status(422).json({ error: true, message: 'اطلاعات ارسالی ناقص است' @@ -95,7 +118,8 @@ const updateAddress = async (req, res, next) => { user.address = address user.lat = lat user.lng = lng - user.show_location = show_location + user.show_location = + show_location === true || show_location === 'true' await user.save() diff --git a/controllers/application/verify/completeRegistrationController.js b/controllers/application/verify/completeRegistrationController.js new file mode 100644 index 0000000..5555ef5 --- /dev/null +++ b/controllers/application/verify/completeRegistrationController.js @@ -0,0 +1,39 @@ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const completeRegistration = 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 user = await UserModel.findById(decodedToken.id) + + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربر یافت نشد' + }) + } + + user.is_Register = true + + if (!user.is_verified || user.is_verified === 'none') { + user.is_verified = 'pending' + } + + await user.save() + + return res.json({ + message: 'ثبت نام تکمیل شد', + is_verified: user.is_verified, + is_Register: user.is_Register + }) + } catch (error) { + next(error) + } +} + +module.exports = { + completeRegistration +} diff --git a/controllers/application/verify/setNationalCardImageController.js b/controllers/application/verify/setNationalCardImageController.js index 75b3a5a..3547e14 100644 --- a/controllers/application/verify/setNationalCardImageController.js +++ b/controllers/application/verify/setNationalCardImageController.js @@ -38,6 +38,7 @@ const setNationalCardImage = async (req, res, next) => { // ذخیره مسیر فایل در دیتابیس user.national_card_image = `/carts/${uniqueFileName}` + user.is_Register = true user.is_verified = 'pending' await user.save() diff --git a/index.js b/index.js index dcde565..5ad71dc 100644 --- a/index.js +++ b/index.js @@ -303,6 +303,52 @@ app.post('/api/v1/chat/file', [blockCheck], async (req, res) => { res.status(500).json({ error: 'Server error' }) } }) + +app.delete('/api/v1/chat', [blockCheck], async (req, res) => { + try { + const userId = String(req.user._id) + const { messageIds } = req.body + + if (!Array.isArray(messageIds) || messageIds.length === 0) { + return res.status(422).json({ message: 'پیامی انتخاب نشده است' }) + } + + const messages = await MessageModel.find({ + _id: { $in: messageIds }, + senderId: userId + }) + + if (!messages.length) { + return res.status(403).json({ message: 'فقط پیام‌های خودتان قابل حذف هستند' }) + } + + for (const msg of messages) { + if (msg.file) { + const relative = String(msg.file).replace(/^\//, '') + const filePath = path.join(__dirname, '../storage', relative) + if (fs.existsSync(filePath)) { + await fs.remove(filePath).catch(() => {}) + } + } + } + + const deletedIds = messages.map((m) => String(m._id)) + await MessageModel.deleteMany({ _id: { $in: deletedIds } }) + + const receiverIds = [...new Set(messages.map((m) => String(m.receiverId)))] + receiverIds.forEach((receiverId) => { + io.to(`chat:${userId}:${receiverId}`) + .to(`chat:${receiverId}:${userId}`) + .emit('messagesDeleted', { messageIds: deletedIds, deletedBy: userId }) + }) + + res.json({ deletedIds, message: 'پیام‌ها حذف شدند' }) + } catch (error) { + console.error('Error deleting messages:', error) + res.status(500).json({ error: 'Server error' }) + } +}) + // Errors require('./middlewares/exception')(app) require('./middlewares/404')(app) diff --git a/middlewares/blockCheck.js b/middlewares/blockCheck.js index fb46d6f..357a056 100644 --- a/middlewares/blockCheck.js +++ b/middlewares/blockCheck.js @@ -1,8 +1,11 @@ -const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر +const UserModel = require('../models/UserModel') const jwt = require('jsonwebtoken') +const { + expireBlockSuspensionIfNeeded, + getBlockedAccountResponse +} = require('../utils/blockSuspension') module.exports = async (req, res, next) => { - console.log(2); try { if (!('authorization' in req.headers)) { return res.status(401).send({ @@ -24,17 +27,15 @@ module.exports = async (req, res, next) => { }) } - const user = await UserModel.findById(decodedToken.id) - if (user.block_status === true) { - return res.status(403).send({ - status: 'error', - code: 403, - message: 'حساب کاربری شما مسدود شده است، برای اطلاعات بیشتر با پشتیبانی تماس بگیرید!', - type: 'block' - }) + 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 // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود. + req.user = user next() } catch (error) { console.error('Error in blockCheck middleware:', error) diff --git a/models/CommentModel.js b/models/CommentModel.js index 07c4103..30f2cc8 100644 --- a/models/CommentModel.js +++ b/models/CommentModel.js @@ -25,7 +25,8 @@ const commentSchema = new mongoose.Schema({ }, rating: { type: Number, - required: true + required: false, + default: null }, comment: { type: String, diff --git a/models/UserModel.js b/models/UserModel.js index 9e8731b..baeffdc 100644 --- a/models/UserModel.js +++ b/models/UserModel.js @@ -317,6 +317,11 @@ const userSchema = new mongoose.Schema({ required: false, default: null }, + blocked_until: { + type: Date, + required: false, + default: null + }, advertising_profiles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'AdvertisingProfile' diff --git a/routes/application/verify/index.js b/routes/application/verify/index.js index 0bec659..1b9fc5d 100644 --- a/routes/application/verify/index.js +++ b/routes/application/verify/index.js @@ -9,6 +9,7 @@ const { setConversation, updateConversation } = require('../../../controllers/ap 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') @@ -34,5 +35,6 @@ 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 diff --git a/utils/blockSuspension.js b/utils/blockSuspension.js new file mode 100644 index 0000000..0d503e3 --- /dev/null +++ b/utils/blockSuspension.js @@ -0,0 +1,77 @@ +const moment = require('moment-jalaali') + +const BLOCK_THRESHOLD = 5 +const BLOCK_DURATION_MS = 7 * 24 * 60 * 60 * 1000 + +const applyAutoBlockSuspension = (user) => { + if (user.blocked_by.length >= BLOCK_THRESHOLD) { + user.block_status = true + user.blocked_until = new Date(Date.now() + BLOCK_DURATION_MS) + } +} + +const clearAutoBlockSuspension = (user) => { + if (user.blocked_by.length < BLOCK_THRESHOLD && user.blocked_until) { + user.block_status = null + user.blocked_until = null + } +} + +const isUserCurrentlyBlocked = (user) => { + if (!user.block_status) { + return false + } + + if (user.blocked_until) { + if (new Date() >= new Date(user.blocked_until)) { + return false + } + return true + } + + return user.block_status === true +} + +const expireBlockSuspensionIfNeeded = async (user) => { + if (!user.blocked_until) { + return user + } + + if (new Date() >= new Date(user.blocked_until)) { + user.block_status = null + user.blocked_until = null + await user.save() + } + + return user +} + +const getBlockedAccountResponse = (user) => { + if (!isUserCurrentlyBlocked(user)) { + return null + } + + const untilText = user.blocked_until + ? moment(user.blocked_until).format('jYYYY/jMM/jDD') + : null + + return { + status: 'error', + code: 403, + message: untilText + ? `حساب کاربری شما به دلیل بلاک شدن توسط ${BLOCK_THRESHOLD} کاربر تا تاریخ ${untilText} غیرفعال است.` + : 'حساب کاربری شما مسدود شده است، برای اطلاعات بیشتر با پشتیبانی تماس بگیرید!', + type: 'block', + blocked_until: user.blocked_until || null + } +} + +module.exports = { + BLOCK_THRESHOLD, + BLOCK_DURATION_MS, + applyAutoBlockSuspension, + clearAutoBlockSuspension, + isUserCurrentlyBlocked, + expireBlockSuspensionIfNeeded, + getBlockedAccountResponse +} diff --git a/utils/blockVisibility.js b/utils/blockVisibility.js new file mode 100644 index 0000000..ee485c6 --- /dev/null +++ b/utils/blockVisibility.js @@ -0,0 +1,30 @@ +/** Viewer (blocked person) cannot see profile/posts of user who blocked them */ +function viewerIsBlockedBy(user, viewerId) { + if (!user || !viewerId) return false + return user.blocked_users.some((id) => String(id) === String(viewerId)) +} + +function blockedProfilePayload(user) { + return { + _id: user._id, + user_name: user.user_name, + blocked_you: true, + is_self: false, + user_type: user.user_type, + postsCount: 0, + allProjectsCount: 0, + successfulProjectsCount: 0, + } +} + +/** User IDs who have blocked the viewer */ +async function getBlockerUserIds(UserModel, viewerId) { + if (!viewerId) return [] + return UserModel.find({ blocked_users: viewerId }).distinct('_id') +} + +module.exports = { + viewerIsBlockedBy, + blockedProfilePayload, + getBlockerUserIds, +} diff --git a/utils/commentNotification.js b/utils/commentNotification.js new file mode 100644 index 0000000..fe1bf52 --- /dev/null +++ b/utils/commentNotification.js @@ -0,0 +1,57 @@ +const NotificationModel = require('../models/NotificationModel') +const { resolveCourseOwnerId } = require('./likeNotification') + +const COMMENT_TITLE = 'یک کامنت برای شما ثبت شد' +const COMMENT_DESCRIPTION = 'یک کامنت برای شما ثبت شد' +const RATING_TITLE = 'یک امتیاز برای شما ثبت شد' +const RATING_DESCRIPTION = 'یک امتیاز برای شما ثبت شد' + +const createCommentNotification = async ({ + ownerId, + commenterId, + entityId, + type = 'post_comment' +}) => { + try { + if (!ownerId || !commenterId || !entityId) return + if (String(ownerId) === String(commenterId)) return + + await NotificationModel.create({ + user_id: ownerId, + project_post_id: entityId, + type, + title: COMMENT_TITLE, + description: COMMENT_DESCRIPTION + }) + } catch (error) { + console.error('Error creating comment notification:', error) + } +} + +const createRatingNotification = async ({ + ownerId, + raterId, + entityId, + type = 'billboard_rating' +}) => { + try { + if (!ownerId || !raterId || !entityId) return + if (String(ownerId) === String(raterId)) return + + await NotificationModel.create({ + user_id: ownerId, + project_post_id: entityId, + type, + title: RATING_TITLE, + description: RATING_DESCRIPTION + }) + } catch (error) { + console.error('Error creating rating notification:', error) + } +} + +module.exports = { + createCommentNotification, + createRatingNotification, + resolveCourseOwnerId +} diff --git a/utils/commentRating.js b/utils/commentRating.js new file mode 100644 index 0000000..2c917ea --- /dev/null +++ b/utils/commentRating.js @@ -0,0 +1,84 @@ +const CommentModel = require('../models/CommentModel') +const UserModel = require('../models/UserModel') + +const RATED_FILTER = { $ne: null, $gt: 0 } + +const findExistingUserRating = (creatorId, targetUserId) => { + return CommentModel.findOne({ + creator: creatorId, + user: targetUserId, + rating: RATED_FILTER + }) +} + +const hasCreatorRatedUser = async (creatorId, targetUserId) => { + const existing = await findExistingUserRating(creatorId, targetUserId) + return !!existing +} + +const resolveRatingForComment = async ({ creatorId, targetUserId, rate }) => { + const existingRating = await findExistingUserRating(creatorId, targetUserId) + + if (existingRating) { + return { ratingValue: null, isNewRating: false } + } + + if (!rate || rate < 1 || rate > 5) { + return { ratingValue: null, isNewRating: false, requiresRating: true } + } + + return { ratingValue: rate, isNewRating: true } +} + +const getUserLevel = (user, totalRating) => { + if (user.expertise === 'مدل') { + if (totalRating <= 50) return 'تازه وارد' + if (totalRating <= 100) return 'استاندارد' + if (totalRating <= 300) return 'حرفه‌ای' + return 'استاد' + } + + if (['زیبایی', 'عکاس'].includes(user.expertise)) { + if (totalRating <= 50) return 'تازه وارد' + if (totalRating <= 100) return 'استاندارد' + if (totalRating <= 300) return 'حرفه‌ای' + return 'استاد' + } + + return user.user_level +} + +const recalculateUserRating = async (targetUserId) => { + const user = await UserModel.findById(targetUserId) + if (!user) return + + const ratedComments = await CommentModel.find({ + user: targetUserId, + rating: RATED_FILTER + }).sort({ createdAt: 1 }) + + const uniqueRatingsByCreator = new Map() + for (const comment of ratedComments) { + const creatorKey = comment.creator.toString() + if (!uniqueRatingsByCreator.has(creatorKey)) { + uniqueRatingsByCreator.set(creatorKey, comment.rating) + } + } + + const ratings = Array.from(uniqueRatingsByCreator.values()) + const totalRating = ratings.reduce((acc, rating) => acc + rating, 0) + const ratedCount = ratings.length + + user.user_score = totalRating + user.user_level = getUserLevel(user, totalRating) + user.rate = ratedCount > 0 ? (totalRating / ratedCount).toFixed(1) : '0' + await user.save() +} + +module.exports = { + RATED_FILTER, + findExistingUserRating, + hasCreatorRatedUser, + resolveRatingForComment, + recalculateUserRating +} diff --git a/utils/likeNotification.js b/utils/likeNotification.js new file mode 100644 index 0000000..87ad0a6 --- /dev/null +++ b/utils/likeNotification.js @@ -0,0 +1,47 @@ +const NotificationModel = require('../models/NotificationModel') +const AcademyModel = require('../models/AcademyModel') + +const LIKE_TITLE = 'پست شما لایک شد' +const LIKE_DESCRIPTION = 'پست شما لایک شد' + +const createLikeNotification = async ({ + ownerId, + likerId, + entityId, + type = 'post_like' +}) => { + try { + if (!ownerId || !likerId || !entityId) return + if (String(ownerId) === String(likerId)) return + + await NotificationModel.create({ + user_id: ownerId, + project_post_id: entityId, + type, + title: LIKE_TITLE, + description: LIKE_DESCRIPTION + }) + } catch (error) { + console.error('Error creating like notification:', error) + } +} + +const resolveCourseOwnerId = async (course) => { + if (course?.user_id) { + return course.user_id + } + + if (course?.academyId) { + const academy = await AcademyModel.findById(course.academyId) + if (academy?.userId) { + return academy.userId + } + } + + return null +} + +module.exports = { + createLikeNotification, + resolveCourseOwnerId +}