diff --git a/boot/mongo.js b/boot/mongo.js index 2d4259a..5f57ead 100644 --- a/boot/mongo.js +++ b/boot/mongo.js @@ -9,7 +9,9 @@ // } // module.exports = startMongoDB + const mongoose = require('mongoose') +require('dotenv').config(); mongoose.connection.on('error', (error) => { console.log('mongodb connection failed! ', error.message) }) diff --git a/controllers/application/academy/academyCategoryController.js b/controllers/application/academy/academyCategoryController.js index c33d87b..07bfec8 100644 --- a/controllers/application/academy/academyCategoryController.js +++ b/controllers/application/academy/academyCategoryController.js @@ -1,74 +1,74 @@ -const AcademyCategoryModel = require('../../../models/AcademyCategoryModel'); -// GET all categories -exports.getAll = async (req, res) => { - try { - const categories = await AcademyCategoryModel.find({}).sort({ createdAt: -1 }); - res.status(200).json({ success: true, categories }); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } -}; - -// CREATE category -exports.create = async (req, res) => { - try { - const { title, status } = req.body; - - if (!title) { - return res.status(400).json({ success: false, message: "title is required" }); - } - - const newCategory = new AcademyCategoryModel({ - title, - status: status ?? true, - }); - - await newCategory.save(); - - res.status(201).json({ success: true, category: newCategory }); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } -}; - -// UPDATE category -exports.update = async (req, res) => { - try { - const { id } = req.params; - const { title, status } = req.body; - - const updated = await AcademyCategoryModel.findByIdAndUpdate( - id, - { title, status }, - { new: true, runValidators: true } - ); - - if (!updated) { - return res.status(404).json({ success: false, message: "category not found" }); - } - - res.status(200).json({ success: true, category: updated }); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } -}; - -// DELETE -exports.remove = async (req, res) => { - try { - const { id } = req.params; - - const deleted = await AcademyCategoryModel.findByIdAndDelete(id); - - if (!deleted) { - return res.status(404).json({ success: false, message: "category not found" }); - } - - res.status(200).json({ - success: true, - message: 'category deleted successfully' - }); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } -}; +const AcademyCategoryModel = require('../../../models/AcademyCategoryModel'); +// GET all categories +exports.getAll = async (req, res) => { + try { + const categories = await AcademyCategoryModel.find({}).sort({ createdAt: -1 }); + res.status(200).json({ success: true, categories }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}; + +// CREATE category +exports.create = async (req, res) => { + try { + const { title, status } = req.body; + + if (!title) { + return res.status(400).json({ success: false, message: "title is required" }); + } + + const newCategory = new AcademyCategoryModel({ + title, + status: status ?? true, + }); + + await newCategory.save(); + + res.status(201).json({ success: true, category: newCategory }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}; + +// UPDATE category +exports.update = async (req, res) => { + try { + const { id } = req.params; + const { title, status } = req.body; + + const updated = await AcademyCategoryModel.findByIdAndUpdate( + id, + { title, status }, + { new: true, runValidators: true } + ); + + if (!updated) { + return res.status(404).json({ success: false, message: "category not found" }); + } + + res.status(200).json({ success: true, category: updated }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}; + +// DELETE +exports.remove = async (req, res) => { + try { + const { id } = req.params; + + const deleted = await AcademyCategoryModel.findByIdAndDelete(id); + + if (!deleted) { + return res.status(404).json({ success: false, message: "category not found" }); + } + + res.status(200).json({ + success: true, + message: 'category deleted successfully' + }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}; diff --git a/controllers/application/academy/academyControllers.js b/controllers/application/academy/academyControllers.js index ca84b88..64a1a5a 100644 --- a/controllers/application/academy/academyControllers.js +++ b/controllers/application/academy/academyControllers.js @@ -1,4550 +1,4550 @@ -const jwt = require("jsonwebtoken"); -const fs = require("fs-extra"); -const path = require("path"); -const jMoment = require("moment-jalaali"); -const AcademyContentModel = require("../../../models/AcademyContentModel"); -const AcademyModel = require("../../../models/AcademyModel"); -const AcademyPaymentModel = require("../../../models/AcademyPaymentModel"); -const CourseModel = require("../../../models/CourseModel"); -const CoursePaymentModel = require("../../../models/CoursePaymentModel"); -const UserModel = require("../../../models/UserModel"); -const PaymentModel = require("../../../models/PaymentModel"); -const { default: axios } = require("axios"); -const AcademyLikeModel = require("../../../models/AcademyLikeModel"); -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({ - cuorse_type: "pro", - }); - if (!CoursePayment) { - const newCoursePayment = new CoursePaymentModel({ - price: "60000", - cuorse_type: "pro", - }); - await newCoursePayment.save(); - - return console.log("اشتراک پرو ساخته شد"); - } - - console.log("اشتراک پرو موجود هست"); - const tax = await Tax.findOne({ - type: "course", - }); - if (!tax) { - const newTax = new Tax({ - tax: 8, - type: "course", - }); - await newTax.save(); - - return console.log("تکس ساخته شد"); - } - - console.log(" تکس موجود هست"); -}; -onServerRestart(); - -const findAcademy = async (req, res, next) => { - try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const user = await UserModel.findById(userId); - if (!user) { - return res.status(422).json({ - error: true, - message: "کاربر یافت نشد", - }); - } - const academy = await AcademyModel.findOne({ userId: userId }); - if (!academy) { - const newAcademy = new AcademyModel({ - userId: userId, - }); - await newAcademy.save(); - - return res.status(200).json({ - academy: newAcademy, - }); - } - - res.status(200).json({ - academy: academy, - }); - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error("Error:", error); - res.status(500).json({ message: "err in creat" }); - } -}; -const findAcademyById = async (req, res, next) => { - try { - const { _id } = req.body; - - const academy = await AcademyModel.findById(_id); - if (!academy) { - res.status(500).json({ - academy: "اکادمی موحود نیست", - }); - } - - res.status(200).json({ - academy: academy, - }); - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error("Error:", error); - res.status(500).json({ message: "err in creat" }); - } -}; -const academyProfile = async (req, res, next) => { - try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const user = await UserModel.findById(userId); - if (!user) { - return res.status(422).json({ - error: true, - message: "کاربر یافت نشد", - }); - } - const academy = await AcademyModel.findOne({ userId: userId }); - if (!academy) { - return res.status(200).json({ - message: "اکادمی پیدا نشد", - }); - } - - const { profile_image } = req.files; - const { name, sheba, bio, tag } = req.body; - - if (!profile_image) { - return res.status(422).json({ - error: true, - message: "اطلاعات ارسالی اشتباه است", - }); - } - - const uploadDir = path.join(__dirname, "../../../../storage/profiles"); - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); - } - - const uniqueFileName = `${name}-${Date.now()}${path.extname( - profile_image.name - )}`; - const filePath = path.join(uploadDir, uniqueFileName); - - await fs.move(profile_image.path, filePath); - - academy.academy_image = `/profiles/${uniqueFileName}`; - academy.academy_name = name; - academy.sheba = sheba; - academy.bio = bio; - academy.tag = tag; - await academy.save(); - - res.status(200).json({ - message: "ثبت شد", - }); - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error("Error:", error); - res.status(500).json({ message: "err in update" }); - } -}; - -const creatCourse = async (req, res, next) => { - try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const user = await UserModel.findById(userId); - if (!user) { - return res.status(422).json({ - error: true, - message: "کاربر یافت نشد", - }); - } - - const academy = await AcademyModel.findOne({ userId: userId }); - if (!academy) { - return res.status(200).json({ - message: "اکادمی پیدا نشد", - }); - } - const { - price, - cuorse_name, - category, - offer, - caption, - course_time, - number_of_course_content, - teacher_number, - teacher_name, - } = req.body; - - const { course_image } = req.files; - - if (!course_image) { - return res.status(422).json({ - error: true, - message: "اطلاعات ارسالی اشتباه است", - }); - } - - const uploadDir = path.join(__dirname, "../../../../storage/profiles"); - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); - } - - const uniqueFileName = `${cuorse_name}-${Date.now()}${path.extname( - course_image.name - )}`; - const filePath = path.join(uploadDir, uniqueFileName); - - await fs.move(course_image.path, filePath); - - // ✅ ایجاد رکورد جدید - const newAcademy = new CourseModel({ - course_image: `/profiles/${uniqueFileName}`, - price, - cuorse_name, - category, - offer, - teacher_name, - academyId: academy._id, - caption, - course_time, - number_of_course_content, - teacher_number, - }); - - await newAcademy.save(); - - res.status(200).json({ - message: "ثبت شد", - }); - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error("Error:", error); - res.status(500).json({ message: "err in update" }); - } -}; - -const updateCourse = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { - price, - cuorse_name, - category, - offer, - courseId, - teacher_name, - caption, - course_time, - number_of_course_content, - teacher_number, - } = req.body; - - // اعتبارسنجی courseId - if (!courseId) { - return res.status(422).json({ - error: true, - message: "آیدی دوره الزامی است", - }); - } - - const user = await UserModel.findById(userId); - if (!user) { - return res.status(422).json({ - error: true, - message: "کاربر یافت نشد", - }); - } - - const course = await CourseModel.findById(courseId); - if (!course) { - return res.status(422).json({ - error: true, - message: "پکیجی یافت نشد", - }); - } - - // ========== آپلود عکس جدید (اختیاری) ========== - const { course_image } = req.files || {}; - - if (course_image) { - // فقط اگه عکس جدید آپلود شده بود، آپلود کن - const uploadDir = path.join(__dirname, "../../../../storage/profiles"); - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); - } - - // استفاده از id به جای نام دوره برای نام فایل - const safeFileName = `course-${courseId}-${Date.now()}${path.extname( - course_image.name - )}`; - const filePath = path.join(uploadDir, safeFileName); - - await fs.move(course_image.path, filePath); - course.course_image = `/profiles/${safeFileName}`; - } - - // ========== به روز رسانی فیلدها (فقط فیلدهایی که ارسال شده) ========== - if (price !== undefined) course.price = price; - if (cuorse_name !== undefined) course.cuorse_name = cuorse_name; - if (category !== undefined) course.category = category; - if (offer !== undefined) course.offer = offer; - if (caption !== undefined) course.caption = caption; - if (course_time !== undefined) course.course_time = course_time; - if (number_of_course_content !== undefined) - course.number_of_course_content = number_of_course_content; - if (teacher_number !== undefined) course.teacher_number = teacher_number; - if (teacher_name !== undefined) course.teacher_name = teacher_name; - - await course.save(); - - res.status(200).json({ - success: true, - message: "دوره با موفقیت به روز رسانی شد", - data: { - course: course, - }, - }); - } catch (error) { - console.error("Error in updateCourse:", error); - res.status(500).json({ - success: false, - message: "خطا در به روز رسانی دوره", - error: error.message, - }); - } -}; -const deleteCourse = async (req, res, next) => { - try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - const { courseId } = req.body; - - const user = await UserModel.findById(userId); - if (!user) { - return res.status(422).json({ - error: true, - message: "کاربر یافت نشد", - }); - } - const course = await CourseModel.findById(courseId); - if (!course) { - return res.status(422).json({ - error: true, - message: "پکیجی یافت نشد", - }); - } - - course.status = "reject"; - await course.save(); - - res.status(200).json({ - message: "ثبت شد", - }); - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error("Error:", error); - res.status(500).json({ message: "err in update" }); - } -}; -const getCourse = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - // گرفتن پارامترهای صفحه‌بندی - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; - - const user = await UserModel.findById(userId); - if (!user) { - return res.status(422).json({ - error: true, - message: "کاربر یافت نشد", - }); - } - - const academy = await AcademyModel.findOne({ userId: userId }); - if (!academy) { - return res.status(404).json({ - success: false, - message: "آکادمی پیدا نشد", - }); - } - - // ✅ شرط جستجو - فقط دوره‌های تایید شده - const query = { - academyId: academy._id, - status: "accept", - }; - - // دریافت تعداد کل دوره‌های تایید شده - const totalCourses = await CourseModel.countDocuments(query); - - // دریافت دوره‌های تایید شده با صفحه‌بندی - const courses = await CourseModel.find(query) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit); - - // ✅ اگر دوره‌ای وجود نداشت، خطا نده، بلکه آرایه خالی برگردون - if (!courses || courses.length === 0) { - return res.status(200).json({ - success: true, - message: "هیچ دوره تایید شده‌ای یافت نشد", - data: { - courses: [], - pagination: { - currentPage: page, - totalPages: 0, - totalItems: 0, - itemsPerPage: limit, - hasNextPage: false, - hasPrevPage: false, - nextPage: null, - prevPage: null, - }, - }, - }); - } - - // محاسبات صفحه‌بندی - const totalPages = Math.ceil(totalCourses / limit); - const hasNextPage = page < totalPages; - const hasPrevPage = page > 1; - - res.status(200).json({ - success: true, - message: "لیست پکیج‌ها با موفقیت دریافت شد", - data: { - courses: courses, - pagination: { - currentPage: page, - totalPages: totalPages, - totalItems: totalCourses, - itemsPerPage: limit, - hasNextPage: hasNextPage, - hasPrevPage: hasPrevPage, - nextPage: hasNextPage ? page + 1 : null, - prevPage: hasPrevPage ? page - 1 : null, - }, - }, - }); - } catch (error) { - console.error("Error:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت پکیج‌ها", - error: error.message, - }); - } -}; - -const getCourses = async (req, res, next) => { - try { - // ========== 3. دریافت پارامترهای صفحه‌بندی ========== - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; - - // ========== 4. دریافت پارامترهای جستجو ========== - const search = req.query.search || ""; - const category = req.query.category || ""; - const minPrice = req.query.minPrice ? parseInt(req.query.minPrice) : 0; - const maxPrice = req.query.maxPrice - ? parseInt(req.query.maxPrice) - : 100000000; - const hasOffer = req.query.hasOffer === "true"; // تخفیف دار - const teacherName = req.query.teacherName || ""; - const type = req.query.type || ""; // normal, pro, legend - const status = req.query.status || "all"; // pending, accept, reject - const sortBy = req.query.sortBy || "createdAt"; - const sortOrder = req.query.sortOrder === "asc" ? 1 : -1; - const _id = req.query._id || ""; - // ========== 5. ساخت شرط جستجو (Query) ========== - let query = {}; - - // جستجو در نام دوره و کپشن - if (search) { - query.$or = [ - { cuorse_name: { $regex: search, $options: "i" } }, - { caption: { $regex: search, $options: "i" } }, - { teacher_name: { $regex: search, $options: "i" } }, - ]; - } - - // فیلتر بر اساس دسته بندی - if (category) { - query.category = category; - } - if (_id) { - query._id = _id; - } - // فیلتر بر اساس محدوده قیمت (تبدیل به عدد برای مقایسه) - if (minPrice > 0 || maxPrice < 100000000) { - query.price = { - $gte: minPrice.toString(), - $lte: maxPrice.toString(), - }; - } - - // فیلتر بر اساس تخفیف دار (offer وجود داشته باشد و از 0 بیشتر باشد) - if (hasOffer) { - query.offer = { $exists: true, $ne: "0", $ne: "" }; - } - - // فیلتر بر اساس نام مدرس - if (teacherName) { - query.teacher_name = { $regex: teacherName, $options: "i" }; - } - - // فیلتر بر اساس نوع دوره - if (type) { - query.type = type; - } - - // فیلتر بر اساس وضعیت - if (status !== "all") { - query.status = status; - } - - // ========== 6. دریافت آمار و اطلاعات ========== - // تعداد کل پکیج‌ها (بدون صفحه‌بندی) - const totalCourses = await CourseModel.countDocuments(query); - - // مجموع قیمت‌ها (تبدیل به عدد برای محاسبه) - const priceStats = await CourseModel.aggregate([ - { $match: query }, - { - $group: { - _id: null, - total: { $sum: { $toDouble: "$price" } }, - avg: { $avg: { $toDouble: "$price" } }, - min: { $min: { $toDouble: "$price" } }, - max: { $max: { $toDouble: "$price" } }, - }, - }, - ]); - - // تعداد دوره‌های با تخفیف - const offeredCourses = await CourseModel.countDocuments({ - ...query, - offer: { $exists: true, $ne: "0", $ne: "" }, - }); - - // تعداد دوره‌ها بر اساس نوع - const typeStats = await CourseModel.aggregate([ - { $match: query }, - { - $group: { - _id: "$type", - count: { $sum: 1 }, - }, - }, - ]); - - // ========== 7. دریافت پکیج‌ها با صفحه‌بندی ========== - const courses = await CourseModel.find(query) - .populate("user_id", "name email") // اطلاعات کاربر سازنده - .populate("likes", "name") // اطلاعات لایک‌ها - .sort({ [sortBy]: sortOrder }) - .skip(skip) - .limit(limit) - .lean(); - - // پردازش داده‌ها برای خروجی - const processedCourses = courses.map((course) => ({ - ...course, - priceNumber: parseInt(course.price) || 0, - offerNumber: parseInt(course.offer) || 0, - finalPrice: course.offer - ? (parseInt(course.price) * (100 - parseInt(course.offer))) / 100 - : parseInt(course.price), - likesCount: course.likes?.length || 0, - })); - - // ========== 8. محاسبات صفحه‌بندی ========== - const totalPages = Math.ceil(totalCourses / limit); - const hasNextPage = page < totalPages; - const hasPrevPage = page > 1; - - // محاسبه محدوده نمایش - const startItem = totalCourses === 0 ? 0 : (page - 1) * limit + 1; - const endItem = Math.min(page * limit, totalCourses); - - // ========== 9. ارسال پاسخ ========== - res.status(200).json({ - success: true, - message: "لیست پکیج‌ها با موفقیت دریافت شد", - data: { - courses: processedCourses, - stats: { - totalCourses, - offeredCourses, - averagePrice: Math.round(priceStats[0]?.avg || 0), - minPrice: priceStats[0]?.min || 0, - maxPrice: priceStats[0]?.max || 0, - totalPrice: priceStats[0]?.total || 0, - typeStats: typeStats, - }, - pagination: { - currentPage: page, - totalPages: totalPages, - totalItems: totalCourses, - itemsPerPage: limit, - hasNextPage: hasNextPage, - hasPrevPage: hasPrevPage, - nextPage: hasNextPage ? page + 1 : null, - prevPage: hasPrevPage ? page - 1 : null, - startItem: startItem, - endItem: endItem, - }, - filters: { - search: search, - category: category, - minPrice: minPrice, - maxPrice: maxPrice, - hasOffer: hasOffer, - teacherName: teacherName, - type: type, - sortBy: sortBy, - sortOrder: sortOrder === 1 ? "asc" : "desc", - status: status, - }, - }, - }); - } catch (error) { - console.error("Error in getCourses:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت پکیج‌ها", - error: error.message, - }); - } -}; - -const proPayment = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - const { courseId } = req.body; - - // ========== 1. بررسی وجود CoursePayment ========== - const coursePayment = await CoursePaymentModel.findOne({ - cuorse_type: "pro", - }); - - if (!coursePayment) { - return res.status(404).json({ - error: true, - message: "قیمت‌گذاری برای نوع پرو یافت نشد", - }); - } - - const price = coursePayment.price; - - // ========== 2. اعتبارسنجی قیمت ========== - if (!price || isNaN(Number(price)) || Number(price) <= 0) { - return res.status(422).json({ - error: true, - message: "مبلغ نامعتبر است", - }); - } - - // ========== 3. بررسی وجود دوره ========== - const course = await CourseModel.findById(courseId); - if (!course) { - return res.status(404).json({ - error: true, - message: "پکیجی یافت نشد", - }); - } - - // ========== 4. محاسبه مبلغ ========== - const amount = Number(price) * 10; - - // مبلغ باید حداقل 1000 تومان باشد (طبق قوانین زرین‌پال) - if (amount < 1000) { - return res.status(422).json({ - error: true, - message: "مبلغ باید حداقل 1000 تومان باشد", - }); - } - - const callbackUrl = `https://api.modstagram.com/api/v1/academy/academy/course/pro?courseId=${courseId}&userId=${userId}`; - - // ========== 5. درخواست به زرین‌پال ========== - const zarinpalRes = await axios.post( - "https://api.zarinpal.com/pg/v4/payment/request.json", - { - merchant_id: - process.env.ZARINPAL_MERCHANT_ID || - "c7c41e8a-918f-4741-bcd5-58f3bc51db73", - amount: Math.round(amount), // حتماً عدد صحیح باشد - description: `خرید اشتراک پرو برای ${course.cuorse_name}`, - callback_url: callbackUrl, - metadata: { - user_id: userId, - course_id: courseId, - }, - }, - { - headers: { "Content-Type": "application/json" }, - timeout: 10000, // 10 ثانیه تایم‌اوت - } - ); - - const result = zarinpalRes.data; - - // ========== 6. بررسی پاسخ زرین‌پال ========== - if (result.data && result.data.code === 100) { - // ذخیره اطلاعات پرداخت در دیتابیس (اختیاری) - await PaymentModel.create({ - authority: result.data.authority, - amount: amount, - user_id: userId, - course_id: courseId, - status: "pending", - type: "pro", - createdAt: new Date(), - }); - - return res.status(200).json({ - success: true, - authority: result.data.authority, - paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.data.authority}`, - }); - } - - // خطای زرین‌پال - console.error("ZarinPal error:", result); - return res.status(422).json({ - error: true, - message: result.errors?.message || "خطا در ارتباط با درگاه پرداخت", - details: result.errors, - }); - } catch (error) { - console.error( - "Error in proPayment:", - error.response?.data || error.message - ); - res.status(500).json({ - error: true, - message: "خطا در پردازش پرداخت", - details: error.response?.data || error.message, - }); - } -}; - -const handleCoursePaymentCallback = async (req, res) => { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); - - try { - const { Authority, Status, userId, courseId } = req.query; - - if (Status !== "OK") { - return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`); - } - - // ========== 1. پیدا کردن قیمت ========== - const coursePayment = await CoursePaymentModel.findOne({ - cuorse_type: "pro", - }); - - if (!coursePayment) { - return res.redirect( - `${process.env.APP_SITE}/offer/payment/failed?message=price_not_found` - ); - } - - const price = coursePayment.price; - const amount = Number(price) * 10; - - // ========== 2. تایید پرداخت با زرین‌پال ========== - const verify = await axios.post( - "https://api.zarinpal.com/pg/v4/payment/verify.json", - { - merchant_id: - process.env.ZARINPAL_MERCHANT_ID || - "c7c41e8a-918f-4741-bcd5-58f3bc51db73", - authority: Authority, - amount: Math.round(amount), - }, - { - headers: { "Content-Type": "application/json" }, - timeout: 10000, - } - ); - - const data = verify.data.data; - - // ========== 3. بررسی نتیجه تایید ========== - if (data.code === 100) { - // ثبت پرداخت موفق - await PaymentModel.create({ - amount: amount / 10, - status: "successful", - authority: Authority, - ref_id: data.ref_id, - user_id: userId, - course_id: courseId, - type: "course", - verifiedAt: new Date(), - }); - - // به روز رسانی نوع دوره به pro - const course = await CourseModel.findById(courseId); - if (course) { - course.type = "pro"; - await course.save(); - } - - // به روز رسانی وضعیت کاربر (اشتراک پرو) - await UserModel.findByIdAndUpdate(userId, { - $set: { - "proSubscription.active": true, - "proSubscription.courseId": courseId, - "proSubscription.startDate": new Date(), - "proSubscription.purchaseDate": new Date(), - }, - }); - - return res.redirect( - `${process.env.APP_SITE}/settings/academy/course?payment=success` - ); - } - - // پرداخت ناموفق - return res.redirect( - `${process.env.APP_SITE}/offer/payment/failed?code=${data.code}` - ); - } catch (err) { - console.error("Error in handleCoursePaymentCallback:", err); - return res.redirect( - `${process.env.APP_SITE}/offer/payment/failed?message=verification_error` - ); - } -}; - -const creatCourseVideo = async (req, res, next) => { - console.log("=== New request received ==="); - console.log("Headers:", req.headers); - console.log("Body:", req.body); - console.log("File:", req.file); - - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { courseId, is_free, file_name } = req.body; - const videoFile = req.file; - - if (!videoFile) { - return res.status(422).json({ - error: true, - message: "فایل ویدیو ارسال نشده است", - }); - } - - if (!courseId) { - return res.status(422).json({ - error: true, - message: "شناسه دوره الزامی است", - }); - } - - // مسیر ذخیره‌سازی نهایی (مشابه سیستم پست‌ها) - const finalVideoDir = path.join( - __dirname, - "../../../storage/courses/videos" - ); - const safeFileName = `course-${courseId}-${Date.now()}${path.extname( - videoFile.originalname - )}`; - const filePath = path.join(finalVideoDir, safeFileName); - - // انتقال فایل از محل موقت به محل نهایی - await fs.rename(videoFile.path, filePath); - - // ایجاد رکورد جدید در دیتابیس - const newAcademyContent = new AcademyContentModel({ - course_video: `/storage/posts/videos/${safeFileName}`, - type: "video", - is_free: is_free === "true" || is_free === true, - courseId, - file_name: file_name || videoFile.originalname, - }); - - await newAcademyContent.save(); - - const course = await CourseModel.findById(courseId); - if (!course) { - return res.status(404).json({ - success: false, - message: "پکیج پیدا نشد", - }); - } - - course.number_of_course_content = `${ - Number(course.number_of_course_content) + 1 - }`; - await course.save(); - - res.status(200).json({ - success: true, - message: "ویدیو با موفقیت ثبت شد", - data: newAcademyContent, - }); - } catch (error) { - console.error("Error in creatCourseVideo:", error); - res.status(500).json({ - success: false, - message: "خطا در ثبت ویدیو", - error: error.message, - }); - } -}; - -const creatCourseVideoBase64 = async (req, res, next) => { - console.log("=== Base64 Video Upload ==="); - console.log("Body:", Object.keys(req.body)); - - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { courseId, is_free, file_name, video_base64, mime_type } = req.body; - - // اعتبارسنجی - if (!video_base64) { - return res.status(422).json({ - error: true, - message: "فایل ویدیو ارسال نشده است", - }); - } - - if (!courseId) { - return res.status(422).json({ - error: true, - message: "شناسه دوره الزامی است", - }); - } - - // تبدیل Base64 به بافر - const base64Data = video_base64.split(",")[1] || video_base64; - const buffer = Buffer.from(base64Data, "base64"); - - console.log("Buffer size:", buffer.length, "bytes"); - - // ایجاد دایرکتوری اگر وجود ندارد - const finalVideoDir = path.join( - process.cwd(), - "storage", - "courses", - "videos" - ); - if (!fs.existsSync(finalVideoDir)) { - fs.mkdirSync(finalVideoDir, { recursive: true }); - } - - // تعیین پسوند فایل از mime_type - let extension = ".mp4"; - if (mime_type === "video/mp4") extension = ".mp4"; - else if (mime_type === "video/quicktime") extension = ".mov"; - else if (mime_type === "video/x-msvideo") extension = ".avi"; - else if (mime_type === "video/webm") extension = ".webm"; - - const safeFileName = `course-${courseId}-${Date.now()}${extension}`; - const filePath = path.join(finalVideoDir, safeFileName); - - // ذخیره فایل - await fs.writeFileSync(filePath, buffer); - - console.log("File saved:", filePath); - - // ایجاد رکورد در دیتابیس - const newAcademyContent = new AcademyContentModel({ - course_video: `/storage/courses/videos/${safeFileName}`, - type: "video", - is_free: is_free === "true" || is_free === true, - courseId, - file_name: file_name, - }); - - await newAcademyContent.save(); - - // به روز رسانی تعداد محتواهای دوره - const course = await CourseModel.findById(courseId); - if (course) { - course.number_of_course_content = `${ - Number(course.number_of_course_content) + 1 - }`; - await course.save(); - } - - res.status(200).json({ - success: true, - message: "ویدیو با موفقیت ثبت شد", - data: newAcademyContent, - }); - } catch (error) { - console.error("Error in creatCourseVideoBase64:", error); - res.status(500).json({ - success: false, - message: "خطا در ثبت ویدیو", - error: error.message, - }); - } -}; - -const getCourseContent = async (req, res, next) => { - try { - // ✅ اصلاح: دریافت courseId از query یا params - const courseId = req.query.courseId || req.params.courseId; - - // اعتبارسنجی courseId - if (!courseId) { - return res.status(422).json({ - error: true, - message: "شناسه دوره الزامی است", - }); - } - - // گرفتن پارامترهای صفحه‌بندی - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; - - const academy = await CourseModel.findById(courseId); - if (!academy) { - return res.status(404).json({ - success: false, - message: "پکیج پیدا نشد", - }); - } - - // ✅ شرط جستجو - فقط دوره‌های تایید شده - const query = { - courseId: academy._id, - status: "accept", - }; - - // دریافت تعداد کل دوره‌های تایید شده - const totalCourses = await AcademyContentModel.countDocuments(query); - - // دریافت دوره‌های تایید شده با صفحه‌بندی - const courses = await AcademyContentModel.find(query) - .skip(skip) - .limit(limit); - - // ✅ اگر دوره‌ای وجود نداشت، خطا نده، بلکه آرایه خالی برگردون - if (!courses || courses.length === 0) { - return res.status(200).json({ - success: true, - message: "هیچ محتوایی یافت نشد", - data: { - courses: [], - pagination: { - currentPage: page, - totalPages: 0, - totalItems: 0, - itemsPerPage: limit, - hasNextPage: false, - hasPrevPage: false, - }, - }, - }); - } - - // محاسبات صفحه‌بندی - const totalPages = Math.ceil(totalCourses / limit); - const hasNextPage = page < totalPages; - const hasPrevPage = page > 1; - - res.status(200).json({ - success: true, - message: "لیست محتواها با موفقیت دریافت شد", - data: { - courses: courses, - pagination: { - currentPage: page, - totalPages: totalPages, - totalItems: totalCourses, - itemsPerPage: limit, - hasNextPage: hasNextPage, - hasPrevPage: hasPrevPage, - nextPage: hasNextPage ? page + 1 : null, - prevPage: hasPrevPage ? page - 1 : null, - }, - }, - }); - } catch (error) { - console.error("Error in getCourseContent:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت محتواها", - error: error.message, - }); - } -}; -// لایک کردن محتوا -const likeCourseContent = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - if (!userId) { - return res.status(422).json({ - error: true, - message: "برای لایک کردن وارد حساب کاربری خود شوید", - }); - } - - const { courseId } = req.body; - - // بررسی وجود دوره - const course = await CourseModel.findById(courseId); - if (!course) { - return res.status(404).json({ - success: false, - message: "پکیج پیدا نشد", - }); - } - - // بررسی اینکه آیا قبلاً لایک کرده است - const existingLike = await AcademyLikeModel.findOne({ - user_id: userId, - course_id: courseId, - }); - - if (existingLike) { - return res.status(400).json({ - success: false, - message: "شما قبلاً این دوره را لایک کرده‌اید", - }); - } - - // افزایش تعداد لایک‌های دوره - course.likes = (course.likes || 0) + 1; - await course.save(); - - // ایجاد رکورد لایک - await AcademyLikeModel.create({ - user_id: userId, - course_id: courseId, - 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: "دوره با موفقیت لایک شد", - isLiked: true, - likesCount: course.likes, - }); - } catch (error) { - console.error("Error in likeCourseContent:", error); - res.status(500).json({ - success: false, - message: "خطا در لایک کردن محتوا", - error: error.message, - }); - } -}; - -// دیسلایک کردن محتوا (حذف لایک) -const dontLikeCourseContent = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { courseId } = req.body; - - // بررسی وجود دوره - const course = await CourseModel.findById(courseId); - if (!course) { - return res.status(404).json({ - success: false, - message: "پکیج پیدا نشد", - }); - } - - // پیدا کردن و حذف لایک - const deletedLike = await AcademyLikeModel.findOneAndDelete({ - user_id: userId, - course_id: courseId, - }); - - if (!deletedLike) { - return res.status(404).json({ - success: false, - message: "شما این دوره را لایک نکرده‌اید", - }); - } - - // کاهش تعداد لایک‌های دوره - course.likes = Math.max((course.likes || 0) - 1, 0); // جلوگیری از منفی شدن - await course.save(); - - res.status(200).json({ - success: true, - message: "لایک دوره با موفقیت حذف شد", - isLiked: false, - likesCount: course.likes, - }); - } catch (error) { - console.error("Error in dontLikeCourseContent:", error); - res.status(500).json({ - success: false, - message: "خطا در حذف لایک محتوا", - error: error.message, - }); - } -}; - -// بررسی وضعیت لایک (آیا کاربر لایک کرده است یا نه) -const isLikeCourseContent = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { courseId } = req.body; - - // پیدا کردن لایک - const academyLike = await AcademyLikeModel.findOne({ - user_id: userId, - course_id: courseId, - }); - - res.status(200).json({ - success: true, - isLiked: !!academyLike, // تبدیل به boolean - }); - } catch (error) { - console.error("Error in isLikeCourseContent:", error); - res.status(500).json({ - success: false, - message: "خطا در بررسی وضعیت لایک", - error: error.message, - }); - } -}; - -// دریافت تعداد لایک‌های یک دوره -const getCourseLikesCount = async (req, res, next) => { - try { - const { courseId } = req.params; - - const course = await CourseModel.findById(courseId); - if (!course) { - return res.status(404).json({ - success: false, - message: "دوره پیدا نشد", - }); - } - - const likesCount = await AcademyLikeModel.countDocuments({ - course_id: courseId, - }); - - res.status(200).json({ - success: true, - likesCount: likesCount, - courseLikes: course.likes || 0, - }); - } catch (error) { - console.error("Error in getCourseLikesCount:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت تعداد لایک‌ها", - error: error.message, - }); - } -}; - -// دریافت همه دوره‌هایی که کاربر لایک کرده است -const getUserLikedCourses = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; - - const likes = await AcademyLikeModel.find({ user_id: userId }) - .populate("course_id") - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit); - - const total = await AcademyLikeModel.countDocuments({ user_id: userId }); - - res.status(200).json({ - success: true, - data: likes, - pagination: { - currentPage: page, - totalPages: Math.ceil(total / limit), - totalItems: total, - itemsPerPage: limit, - }, - }); - } catch (error) { - console.error("Error in getUserLikedCourses:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت دوره‌های لایک شده", - error: error.message, - }); - } -}; - -// ==================== ساخت کامنت جدید ==================== -const createComment = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { course_id, comment, rate } = req.body; - - // اعتبارسنجی - if (!course_id || !comment) { - return res.status(422).json({ - error: true, - message: "شناسه دوره و متن کامنت الزامی است", - }); - } - - // بررسی وجود دوره - const course = await CourseModel.findById(course_id); - if (!course) { - return res.status(404).json({ - error: true, - message: "دوره پیدا نشد", - }); - } - - const parsedRate = Number(rate) || 0; - const existingRatedComment = await CourseComment.findOne({ - user_id: userId, - course_id, - rate: { $gt: 0 }, - }); - - if (existingRatedComment && parsedRate > 0) { - return res.status(400).json({ - error: true, - message: "شما قبلاً به این دوره امتیاز داده‌اید.", - }); - } - - const finalRate = existingRatedComment ? 0 : parsedRate; - - // ایجاد کامنت جدید - const newComment = await CourseComment.create({ - user_id: userId, - course_id: course_id, - comment: comment, - rate: finalRate, - status: "accepted", // با توجه به مدل شما که default: 'accepted' است - }); - - // populate کردن اطلاعات کاربر - const populatedComment = await CourseComment.findById( - 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: "کامنت با موفقیت ثبت شد", - data: populatedComment, - }); - } catch (error) { - console.error("Error in createComment:", error); - res.status(500).json({ - success: false, - message: "خطا در ثبت کامنت", - error: error.message, - }); - } -}; - -// ==================== دریافت تعداد کل کامنت‌های یک دوره ==================== -const getTotalCommentsCount = async (req, res) => { - try { - const { courseId } = req.params; - const { status } = req.query; // اختیاری: فقط کامنت‌های با وضعیت خاص - - const query = { course_id: courseId }; - if (status) { - query.status = status; - } - - const totalComments = await CourseComment.countDocuments(query); - - // محاسبه میانگین امتیازات - const averageRate = await CourseComment.aggregate([ - { $match: { course_id: courseId, status: "accepted" } }, - { $group: { _id: null, avgRate: { $avg: "$rate" } } }, - ]); - - res.status(200).json({ - success: true, - data: { - totalComments: totalComments, - averageRate: averageRate[0]?.avgRate || 0, - courseId: courseId, - }, - }); - } catch (error) { - console.error("Error in getTotalCommentsCount:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت تعداد کامنت‌ها", - error: error.message, - }); - } -}; - -// ==================== دریافت کامنت‌ها با فیلترهای مختلف ==================== -const getCourseComments = async (req, res) => { - try { - const { courseId } = req.params; - const { - page = 1, - limit = 10, - status = "accepted", // 'accepted', 'pending', 'rejected', 'all' - minRate, - maxRate, - sortBy = "createdAt", - sortOrder = "desc", - userId, // اختیاری: کامنت‌های یک کاربر خاص - } = req.query; - - // ساخت شرط جستجو - const query = { course_id: courseId }; - - // فیلتر بر اساس وضعیت - if (status !== "all") { - query.status = status; - } - - // فیلتر بر اساس امتیاز - if (minRate || maxRate) { - query.rate = {}; - if (minRate) query.rate.$gte = parseInt(minRate); - if (maxRate) query.rate.$lte = parseInt(maxRate); - } - - // فیلتر بر اساس کاربر خاص - if (userId) { - query.user_id = userId; - } - - // تنظیمات مرتب‌سازی - const sortOptions = {}; - sortOptions[sortBy] = sortOrder === "desc" ? -1 : 1; - - // دریافت کامنت‌ها با pagination - const skip = (parseInt(page) - 1) * parseInt(limit); - - const [comments, totalComments] = await Promise.all([ - CourseComment.find(query) - .sort(sortOptions) - .skip(skip) - .limit(parseInt(limit)) - .populate("user_id", "name username profileImage"), - CourseComment.countDocuments(query), - ]); - - // محاسبه آمار کامنت‌ها - const stats = await CourseComment.aggregate([ - { $match: { course_id: courseId, status: "accepted" } }, - { - $group: { - _id: null, - totalAccepted: { $sum: 1 }, - averageRate: { $avg: "$rate" }, - rateDistribution: { - $push: "$rate", - }, - }, - }, - ]); - - const totalPages = Math.ceil(totalComments / parseInt(limit)); - - res.status(200).json({ - success: true, - message: "لیست کامنت‌ها با موفقیت دریافت شد", - data: { - comments: comments, - pagination: { - currentPage: parseInt(page), - totalPages: totalPages, - totalItems: totalComments, - itemsPerPage: parseInt(limit), - hasNextPage: parseInt(page) < totalPages, - hasPrevPage: parseInt(page) > 1, - }, - stats: { - totalComments: totalComments, - acceptedComments: stats[0]?.totalAccepted || 0, - averageRate: Math.round((stats[0]?.averageRate || 0) * 10) / 10, - rateDistribution: stats[0]?.rateDistribution || [], - }, - }, - }); - } catch (error) { - console.error("Error in getCourseComments:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت کامنت‌ها", - error: error.message, - }); - } -}; -// ==================== ویرایش وضعیت کامنت (برای همه قابل استفاده است) ==================== -const updateCommentStatus = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { commentId } = req.params; - const { status } = req.body; // 'pending', 'accepted', 'rejected' - - // اعتبارسنجی وضعیت - if (!["pending", "accepted", "rejected"].includes(status)) { - return res.status(422).json({ - error: true, - message: - "وضعیت نامعتبر است. وضعیت باید یکی از این موارد باشد: pending, accepted, rejected", - }); - } - - // پیدا کردن کامنت - const comment = await CourseComment.findById(commentId); - if (!comment) { - return res.status(404).json({ - error: true, - message: "کامنت پیدا نشد", - }); - } - - // بررسی دسترسی: یا خود صاحب کامنت است یا هر کاربر دیگری (طبق درخواست شما که همه بتوانند) - // اگر می‌خواهید فقط ادمین بتواند وضعیت را تغییر دهد، این بخش را تغییر دهید - const user = await UserModel.findById(userId); - const isOwner = comment.user_id.toString() === userId; - const isAdmin = user?.role === "admin"; - - if (!isOwner && !isAdmin) { - return res.status(403).json({ - error: true, - message: "شما اجازه تغییر وضعیت این کامنت را ندارید", - }); - } - - // به روز رسانی وضعیت - comment.status = status; - await comment.save(); - - // اگر کامنت تایید شد و امتیاز داشت، میانگین امتیاز دوره را به روز کن - if (status === "accepted" && comment.rate > 0) { - const averageRate = await CourseComment.aggregate([ - { $match: { course_id: comment.course_id, status: "accepted" } }, - { $group: { _id: null, avgRate: { $avg: "$rate" } } }, - ]); - - await CourseModel.findByIdAndUpdate(comment.course_id, { - averageRate: averageRate[0]?.avgRate || 0, - }); - } - - res.status(200).json({ - success: true, - message: `وضعیت کامنت با موفقیت به ${ - status === "accepted" - ? "تایید شده" - : status === "rejected" - ? "رد شده" - : "در انتظار" - } تغییر یافت`, - data: comment, - }); - } catch (error) { - console.error("Error in updateCommentStatus:", error); - res.status(500).json({ - success: false, - message: "خطا در تغییر وضعیت کامنت", - error: error.message, - }); - } -}; - -// ==================== دریافت کامنت‌های یک کاربر خاص ==================== -const getUserComments = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { page = 1, limit = 10, status = "all", courseId } = req.query; - - const query = { user_id: userId }; - - if (status !== "all") { - query.status = status; - } - - if (courseId) { - query.course_id = courseId; - } - - const skip = (parseInt(page) - 1) * parseInt(limit); - - const [comments, totalComments] = await Promise.all([ - CourseComment.find(query) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(parseInt(limit)) - .populate("course_id", "cuorse_name packageImage"), - CourseComment.countDocuments(query), - ]); - - const totalPages = Math.ceil(totalComments / parseInt(limit)); - - res.status(200).json({ - success: true, - data: { - comments: comments, - pagination: { - currentPage: parseInt(page), - totalPages: totalPages, - totalItems: totalComments, - itemsPerPage: parseInt(limit), - }, - }, - }); - } catch (error) { - console.error("Error in getUserComments:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت کامنت‌های کاربر", - error: error.message, - }); - } -}; - -// ==================== حذف کامنت (اختیاری) ==================== -const deleteComment = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { commentId } = req.params; - - const comment = await CourseComment.findById(commentId); - if (!comment) { - return res.status(404).json({ - error: true, - message: "کامنت پیدا نشد", - }); - } - - // بررسی دسترسی: یا خود صاحب کامنت است یا ادمین - const user = await UserModel.findById(userId); - const isOwner = comment.user_id.toString() === userId; - const isAdmin = user?.role === "admin"; - - if (!isOwner && !isAdmin) { - return res.status(403).json({ - error: true, - message: "شما اجازه حذف این کامنت را ندارید", - }); - } - - await comment.deleteOne(); - - res.status(200).json({ - success: true, - message: "کامنت با موفقیت حذف شد", - }); - } catch (error) { - console.error("Error in deleteComment:", error); - res.status(500).json({ - success: false, - message: "خطا در حذف کامنت", - error: error.message, - }); - } -}; -// academyController.js - -const coursePayment = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) { - return res.status(401).json({ - error: true, - message: "لطفا وارد حساب کاربری خود شوید", - }); - } - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - const { courseId } = req.body; - - // ========== 1. بررسی وجود دوره ========== - const course = await CourseModel.findById(courseId); - if (!course) { - return res.status(404).json({ - error: true, - message: "دوره مورد نظر یافت نشد", - }); - } - - // ========== 2. بررسی خرید قبلی ========== - const existingPurchase = await IsPaymentCourse.findOne({ - user_id: userId, - course_id: course._id, - }); - - if (existingPurchase) { - return res.status(400).json({ - error: true, - message: "شما قبلاً این دوره را خریداری کرده‌اید", - }); - } - - // ========== 3. دریافت نرخ مالیات ========== - let taxRate = 9; // پیش‌فرض 9 درصد - const tax = await Tax.findOne({ type: "course" }); - if (tax && tax.tax) { - taxRate = Number(tax.tax); - } - - // ========== 4. محاسبه قیمت ========== - const offerNum = Number(course.offer) || 0; - const priceNum = Number(course.price); - - // قیمت پس از تخفیف - const discountedPrice = (priceNum / 100) * (100 - offerNum); - - // محاسبه مالیات - const taxAmount = discountedPrice * (taxRate / 100); - - // قیمت نهایی با مالیات (تومان) - const finalPrice = discountedPrice; - const finalTaxPrice = (discountedPrice / 100) * (100 - taxRate); - - // ========== 5. اعتبارسنجی قیمت ========== - if (!finalPrice || isNaN(finalPrice) || finalPrice <= 0) { - return res.status(422).json({ - error: true, - message: "مبلغ نامعتبر است", - }); - } - - // تبدیل به ریال (ضرب در 10 چون هر تومان = 10 ریال) - const amountInRials = Math.round(finalPrice * 10); - - // مبلغ باید حداقل 1000 تومان باشد (10000 ریال) - if (amountInRials < 10000) { - return res.status(422).json({ - error: true, - message: "مبلغ باید حداقل 1000 تومان باشد", - }); - } - - // ========== 6. ساخت کال‌بک URL ========== - const callbackUrl = `https://api.modstagram.com/api/v1/academy/academy/course/payment/verify?courseId=${courseId}&userId=${userId}`; - - // ========== 7. درخواست به زرین‌پال ========== - const zarinpalRes = await axios.post( - "https://api.zarinpal.com/pg/v4/payment/request.json", - { - merchant_id: process.env.ZARINPAL_MERCHANT_ID, - amount: amountInRials, - description: `خرید دوره ${course.cuorse_name}`, - callback_url: callbackUrl, - metadata: { - user_id: userId, - course_id: courseId, - price: finalPrice, - tax_rate: taxRate, - }, - }, - { - headers: { "Content-Type": "application/json" }, - timeout: 90000, - } - ); - - const result = zarinpalRes.data; - - // ========== 8. بررسی پاسخ زرین‌پال ========== - if (result.data && result.data.code === 100) { - // ذخیره اطلاعات پرداخت در حالت pending - await AcademyPaymentModel.create({ - price: finalPrice, - discountAmount: priceNum - discountedPrice, - taxAmount: finalTaxPrice, - taxRate: taxRate, - course_name: course.cuorse_name, - course_id: course._id, - academy_id: course.academyId, - user_id: userId, - payment_authority: result.data.authority, - status: "pending", - }); - - return res.status(200).json({ - success: true, - authority: result.data.authority, - paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.data.authority}`, - }); - } - - // خطای زرین‌پال - console.error("ZarinPal error:", result); - return res.status(422).json({ - error: true, - message: result.errors?.message || "خطا در ارتباط با درگاه پرداخت", - details: result.errors, - }); - } catch (error) { - console.error( - "Error in coursePayment:", - error.response?.data || error.message - ); - res.status(500).json({ - error: true, - message: "خطا در پردازش پرداخت", - details: error.response?.data || error.message, - }); - } -}; - -const CoursePaymentCallback = async (req, res) => { - try { - const { Authority, Status, courseId, userId } = req.query; - - // ========== 1. بررسی وضعیت پرداخت ========== - if (Status !== "OK") { - return res.redirect( - `${process.env.APP_SITE}/offer/payment/failed?message=payment_canceled` - ); - } - - // ========== 2. پیدا کردن دوره ========== - const course = await CourseModel.findById(courseId); - if (!course) { - return res.redirect( - `${process.env.APP_SITE}/offer/payment/failed?message=course_not_found` - ); - } - - // ========== 3. پیدا کردن رکورد پرداخت ========== - const paymentRecord = await AcademyPaymentModel.findOne({ - payment_authority: Authority, - course_id: courseId, - user_id: userId, - }); - - if (!paymentRecord) { - return res.redirect( - `${process.env.APP_SITE}/offer/payment/failed?message=payment_not_found` - ); - } - - // ========== 4. محاسبه مبلغ ========== - const amountInRials = Math.round(paymentRecord.price * 10); - - // ========== 5. تایید پرداخت با زرین‌پال ========== - const verify = await axios.post( - "https://api.zarinpal.com/pg/v4/payment/verify.json", - { - merchant_id: process.env.ZARINPAL_MERCHANT_ID, - authority: Authority, - amount: amountInRials, - }, - { - headers: { "Content-Type": "application/json" }, - timeout: 10000, - } - ); - - const data = verify.data.data; - - // ========== 6. بررسی نتیجه تایید ========== - if (data.code === 100) { - // به‌روزرسانی رکورد پرداخت - await AcademyPaymentModel.findByIdAndUpdate(paymentRecord._id, { - status: "success", - payment_ref_id: data.ref_id, - }); - - // ثبت خرید کاربر - await IsPaymentCourse.create({ - user_id: userId, - course_id: course._id, - price_paid: paymentRecord.price, - purchased_at: new Date(), - }); - - // هدایت به صفحه موفقیت - return res.redirect( - `${process.env.APP_SITE}/academy/${course._id}/success?payment=success` - ); - } - - // پرداخت ناموفق - await AcademyPaymentModel.findByIdAndUpdate(paymentRecord._id, { - status: "failed", - }); - - return res.redirect( - `${process.env.APP_SITE}/offer/payment/failed?code=${data.code}` - ); - } catch (err) { - console.error("Error in CoursePaymentCallback:", err); - return res.redirect( - `${process.env.APP_SITE}/offer/payment/failed?message=verification_error` - ); - } -}; -const getUserPurchasedCourses = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const user_id = decodedToken.id; - - const { page = 1, limit = 10 } = req.query; - - // پیدا کردن تمام رکوردهای پرداخت برای این کاربر - const payments = await IsPaymentCourse.find({ user_id: user_id }) - .sort({ createdAt: -1 }) // جدیدترین اول - .skip((page - 1) * limit) - .limit(parseInt(limit)); - - // گرفتن اطلاعات کامل دوره‌ها - const courseIds = payments.map((payment) => payment.course_id); - - const courses = await CourseModel.find({ - _id: { $in: courseIds }, - status: "accept", // فقط دوره‌های تایید شده - }); - - // محاسبه تعداد کل - const total = await IsPaymentCourse.countDocuments({ user_id: user_id }); - - res.status(200).json({ - success: true, - data: { - courses: courses, - pagination: { - total, - page: parseInt(page), - pages: Math.ceil(total / limit), - limit: parseInt(limit), - }, - }, - }); - } catch (error) { - console.error("Error in getUserPurchasedCourses:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت دوره‌های خریداری شده", - error: error.message, - }); - } -}; - -// API با populate (روش بهتر) -const getUserPurchasedCoursesWithPopulate = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const user_id = decodedToken.id; - - const { page = 1, limit = 10 } = req.query; - - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: { createdAt: -1 }, - populate: { - path: "course_id", - model: "Course", - match: { status: "accept" }, // فقط دوره‌های تایید شده - }, - }; - - const result = await IsPaymentCourse.paginate( - { user_id: user_id }, - options - ); - - // فیلتر کردن دوره‌هایی که populate شده‌اند و وجود دارند - const validCourses = result.docs - .filter((doc) => doc.course_id !== null) - .map((doc) => doc.course_id); - - res.status(200).json({ - success: true, - data: { - courses: validCourses, - pagination: { - total: result.totalDocs, - page: result.page, - totalPages: result.totalPages, - limit: result.limit, - hasNextPage: result.hasNextPage, - hasPrevPage: result.hasPrevPage, - }, - }, - }); - } catch (error) { - console.error("Error:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت دوره‌های خریداری شده", - }); - } -}; - -const getAllAcademyPayments = async (req, res) => { - try { - // 1. بررسی توکن - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) { - return res.status(401).json({ - success: false, - message: "دسترسی غیرمجاز", - }); - } - - // 2. verify توکن - let decodedToken; - try { - decodedToken = jwt.verify(token, process.env.APP_SECRET); - } catch (jwtError) { - return res.status(401).json({ - success: false, - message: "توکن نامعتبر است", - }); - } - - const userId = decodedToken.id; - - // 3. پیدا کردن آکادمی - const academy = await AcademyModel.findOne({ userId: userId }); - - if (!academy) { - return res.status(404).json({ - success: false, - message: "آکادمی برای این کاربر یافت نشد", - }); - } - - const academy_id = academy._id; - const { page = 1, limit = 10, status } = req.query; - - // 4. ساخت فیلتر - let filter = { academy_id: academy_id }; - - if ( - status && - ["pending", "success", "failed", "settled"].includes(status) - ) { - filter.status = status; - } - - // 5. تنظیمات صفحه‌بندی - const pageNum = parseInt(page); - const limitNum = parseInt(limit); - const skip = (pageNum - 1) * limitNum; - - const options = { - page: pageNum, - limit: limitNum, - sort: { createdAt: -1 }, - populate: [ - { - path: "course_id", - model: "Course", - select: "cuorse_name price course_image category teacher_name", - }, - { - path: "user_id", - model: "User", - select: "user_name profile_image email phone", - }, - ], - }; - - // 6. دریافت پرداخت‌ها - const result = await AcademyPaymentModel.paginate(filter, options); - - // 7. محاسبه آمار دقیق‌تر - let totalAmount = 0; - let pendingCount = 0; - let acceptedCount = 0; - - if (result.docs && Array.isArray(result.docs)) { - totalAmount = result.docs.reduce((sum, p) => { - const price = parseInt(p.taxAmount) || 0; - return sum + price; - }, 0); - - pendingCount = result.docs.filter((p) => p.status === "pending").length; - acceptedCount = result.docs.filter((p) => p.status === "accept").length; - } - - // 8. پاسخ نهایی - res.status(200).json({ - success: true, - data: { - academy: { - _id: academy._id, - academy_name: academy.academy_name, - academy_image: academy.academy_image, - bio: academy.bio, - }, - payments: result.docs || [], - pagination: { - total: result.totalDocs || 0, - page: result.page || pageNum, - totalPages: result.totalPages || 1, - limit: result.limit || limitNum, - hasNextPage: result.hasNextPage || false, - hasPrevPage: result.hasPrevPage || false, - }, - stats: { - totalAmount: totalAmount, - pendingCount: pendingCount, - acceptedCount: acceptedCount, - }, - }, - }); - } catch (error) { - console.error("Error in getAllAcademyPayments:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت لیست پرداخت‌ها", - error: process.env.NODE_ENV === "development" ? error.message : undefined, - }); - } -}; -const checkCoursePurchase = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - const { courseId } = req.params; - - // بررسی وجود کاربر و دوره - if (!userId || !courseId) { - return res.status(400).json({ - success: false, - message: "userId و courseId الزامی هستند", - }); - } - - // جستجو در دیتابیس - const purchase = await IsPaymentCourse.findOne({ - user_id: userId, - course_id: courseId, - }); - - // نتیجه - if (purchase) { - return res.status(200).json({ - success: true, - isPurchased: true, - message: "این دوره قبلاً خریداری شده است", - purchaseData: purchase, - }); - } else { - return res.status(200).json({ - success: true, - isPurchased: false, - message: "این دوره خریداری نشده است", - }); - } - } catch (error) { - console.error("Error in checkCoursePurchase:", error); - res.status(500).json({ - success: false, - message: "خطا در بررسی خرید دوره", - error: error.message, - }); - } -}; - -const getAcademyCourse = async (req, res, next) => { - try { - const { Id } = req.params; - - // گرفتن پارامترهای صفحه‌بندی - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; - - const academy = await AcademyModel.findById(Id); - if (!academy) { - return res.status(404).json({ - success: false, - message: "آکادمی پیدا نشد", - }); - } - - // ✅ شرط جستجو - فقط دوره‌های تایید شده - const query = { - academyId: academy._id, - status: "accept", - }; - - // دریافت تعداد کل دوره‌های تایید شده - const totalCourses = await CourseModel.countDocuments(query); - - // دریافت دوره‌های تایید شده با صفحه‌بندی - const courses = await CourseModel.find(query) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit); - - // ✅ اگر دوره‌ای وجود نداشت، خطا نده، بلکه آرایه خالی برگردون - if (!courses || courses.length === 0) { - return res.status(200).json({ - success: true, - message: "هیچ دوره تایید شده‌ای یافت نشد", - data: { - courses: [], - pagination: { - currentPage: page, - totalPages: 0, - totalItems: 0, - itemsPerPage: limit, - hasNextPage: false, - hasPrevPage: false, - nextPage: null, - prevPage: null, - }, - }, - }); - } - - // محاسبات صفحه‌بندی - const totalPages = Math.ceil(totalCourses / limit); - const hasNextPage = page < totalPages; - const hasPrevPage = page > 1; - - res.status(200).json({ - success: true, - message: "لیست پکیج‌ها با موفقیت دریافت شد", - data: { - courses: courses, - pagination: { - currentPage: page, - totalPages: totalPages, - totalItems: totalCourses, - itemsPerPage: limit, - hasNextPage: hasNextPage, - hasPrevPage: hasPrevPage, - nextPage: hasNextPage ? page + 1 : null, - prevPage: hasPrevPage ? page - 1 : null, - }, - }, - }); - } catch (error) { - console.error("Error:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت پکیج‌ها", - error: error.message, - }); - } -}; - -// ==================== مدیریت پرداخت‌ها ==================== -const getAllPayments = async (req, res) => { - try { - const { page = 1, limit = 20, status, startDate, endDate } = req.query; - const skip = (parseInt(page) - 1) * parseInt(limit); - - let query = {}; - if (status) query.status = status; - if (startDate && endDate) { - query.createdAt = { - $gte: new Date(startDate), - $lte: new Date(endDate), - }; - } - - const payments = await AcademyPaymentModel.find(query) - .populate("user_id", "user_name profile_image email") - .populate("course_id", "cuorse_name price course_image") - .populate("academy_id", "academy_name academy_image sheba") - .sort({ createdAt: -1 }) - .skip(skip) - .limit(parseInt(limit)); - - const total = await AcademyPaymentModel.countDocuments(query); - - // آمار کلی پرداخت‌ها - const stats = await AcademyPaymentModel.aggregate([ - { $match: query }, - { - $group: { - _id: null, - totalAmount: { $sum: { $toDouble: "$price" } }, - totalCount: { $sum: 1 }, - pendingCount: { - $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, - }, - settledCount: { - $sum: { $cond: [{ $eq: ["$status", "settled"] }, 1, 0] }, - }, - successCount: { - $sum: { $cond: [{ $eq: ["$status", "success"] }, 1, 0] }, - }, - }, - }, - ]); - - res.status(200).json({ - success: true, - data: { - payments, - stats: stats[0] || { - totalAmount: 0, - totalCount: 0, - pendingCount: 0, - settledCount: 0, - successCount: 0, - }, - pagination: { - currentPage: parseInt(page), - totalPages: Math.ceil(total / parseInt(limit)), - totalItems: total, - itemsPerPage: parseInt(limit), - }, - }, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در دریافت پرداخت‌ها" }); - } -}; - -const updatePaymentStatus = async (req, res) => { - try { - const { paymentId, status } = req.body; - - const payment = await AcademyPaymentModel.findByIdAndUpdate( - paymentId, - { status, updatedAt: new Date() }, - { new: true } - ); - - if (!payment) { - return res - .status(404) - .json({ success: false, message: "پرداخت یافت نشد" }); - } - - res.status(200).json({ - success: true, - message: "وضعیت پرداخت با موفقیت تغییر کرد", - data: payment, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در تغییر وضعیت پرداخت" }); - } -}; - -// ==================== مدیریت دوره‌ها ==================== -const getAllCourses = async (req, res) => { - try { - const { page = 1, limit = 20, status, type, category, search } = req.query; - const skip = (parseInt(page) - 1) * parseInt(limit); - - let query = {}; - if (status) query.status = status; - if (type) query.type = type; - if (category) query.category = category; - if (search) { - query.$or = [ - { cuorse_name: { $regex: search, $options: "i" } }, - { caption: { $regex: search, $options: "i" } }, - { teacher_name: { $regex: search, $options: "i" } }, - ]; - } - - const courses = await CourseModel.find(query) - .populate("academyId", "academy_name academy_image") - .populate("user_id", "user_name profile_image") - .sort({ createdAt: -1 }) - .skip(skip) - .limit(parseInt(limit)); - - const total = await CourseModel.countDocuments(query); - - // آمار دوره‌ها - const stats = await CourseModel.aggregate([ - { $match: query }, - { - $group: { - _id: null, - totalCourses: { $sum: 1 }, - totalPrice: { $sum: { $toDouble: "$price" } }, - normalCount: { - $sum: { $cond: [{ $eq: ["$type", "normal"] }, 1, 0] }, - }, - proCount: { $sum: { $cond: [{ $eq: ["$type", "pro"] }, 1, 0] } }, - legendCount: { - $sum: { $cond: [{ $eq: ["$type", "legend"] }, 1, 0] }, - }, - pendingCount: { - $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, - }, - acceptCount: { - $sum: { $cond: [{ $eq: ["$status", "accept"] }, 1, 0] }, - }, - }, - }, - ]); - - res.status(200).json({ - success: true, - data: { - courses, - stats: stats[0] || {}, - pagination: { - currentPage: parseInt(page), - totalPages: Math.ceil(total / parseInt(limit)), - totalItems: total, - itemsPerPage: parseInt(limit), - }, - }, - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت دوره‌ها" }); - } -}; - -const updateCourseStatus = async (req, res) => { - try { - const { courseId, status, type } = req.body; - - console.log(courseId, status, type); - - const course = await CourseModel.findById(courseId); - - if (!course) { - return res.status(404).json({ success: false, message: "دوره یافت نشد" }); - } - - if (status) course.status = status; - if (type) course.type = type; - await course.save(); - - res.status(200).json({ - success: true, - message: "دوره با موفقیت به‌روزرسانی شد", - data: course, - courseId: courseId, - status: status, - type: type, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی دوره" }); - } -}; - -const admindeleteCourse = async (req, res) => { - try { - const { courseId } = req.params; - - const course = await CourseModel.findByIdAndDelete(courseId); - - if (!course) { - return res.status(404).json({ success: false, message: "دوره یافت نشد" }); - } - - // حذف تمام پرداخت‌های مرتبط با این دوره - await AcademyPaymentModel.deleteMany({ course_id: courseId }); - - res.status(200).json({ - success: true, - message: "دوره با موفقیت حذف شد", - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در حذف دوره" }); - } -}; - -// ==================== مدیریت دوره‌های خریداری شده ==================== -const getAllPurchasedCourses = async (req, res) => { - try { - const { page = 1, limit = 20, userId, courseId, status } = req.query; - const skip = (parseInt(page) - 1) * parseInt(limit); - - let query = {}; - if (userId) query.user_id = userId; - if (courseId) query.course_id = courseId; - if (status) query.status = status; - - const purchases = await IsPaymentCourse.find(query) - .populate("user_id", "user_name profile_image email phone") - .populate("course_id", "cuorse_name price course_image teacher_name") - .sort({ createdAt: -1 }) - .skip(skip) - .limit(parseInt(limit)); - - const total = await IsPaymentCourse.countDocuments(query); - - // آمار خریدها - const stats = await IsPaymentCourse.aggregate([ - { $match: query }, - { - $group: { - _id: null, - totalPurchases: { $sum: 1 }, - uniqueUsers: { $addToSet: "$user_id" }, - }, - }, - ]); - - res.status(200).json({ - success: true, - data: { - purchases, - stats: { - totalPurchases: stats[0]?.totalPurchases || 0, - uniqueUsers: stats[0]?.uniqueUsers?.length || 0, - }, - pagination: { - currentPage: parseInt(page), - totalPages: Math.ceil(total / parseInt(limit)), - totalItems: total, - itemsPerPage: parseInt(limit), - }, - }, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در دریافت دوره‌های خریداری شده" }); - } -}; - -// ==================== مدیریت آکادمی‌ها ==================== -const getAllAcademies = async (req, res) => { - try { - const { page = 1, limit = 20, search } = req.query; - const skip = (parseInt(page) - 1) * parseInt(limit); - - let query = {}; - if (search) { - query.academy_name = { $regex: search, $options: "i" }; - } - - const academies = await AcademyModel.find(query) - .populate("userId", "user_name profile_image email") - .sort({ createdAt: -1 }) - .skip(skip) - .limit(parseInt(limit)); - - const total = await AcademyModel.countDocuments(query); - - // آمار آکادمی‌ها - const stats = await AcademyModel.aggregate([ - { - $group: { - _id: null, - totalAcademies: { $sum: 1 }, - totalRate: { $sum: "$rate" }, - avgRate: { $avg: "$rate" }, - }, - }, - ]); - - res.status(200).json({ - success: true, - data: { - academies, - stats: stats[0] || {}, - pagination: { - currentPage: parseInt(page), - totalPages: Math.ceil(total / parseInt(limit)), - totalItems: total, - itemsPerPage: parseInt(limit), - }, - }, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در دریافت آکادمی‌ها" }); - } -}; - -const updateAcademyStatus = async (req, res) => { - try { - const { academyId, status, rate, sheba } = req.body; - - const updateData = {}; - if (status) updateData.status = status; - if (rate) updateData.rate = rate; - if (sheba) updateData.sheba = sheba; - - const academy = await AcademyModel.findByIdAndUpdate( - academyId, - updateData, - { new: true } - ); - - if (!academy) { - return res - .status(404) - .json({ success: false, message: "آکادمی یافت نشد" }); - } - - res.status(200).json({ - success: true, - message: "آکادمی با موفقیت به‌روزرسانی شد", - data: academy, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی آکادمی" }); - } -}; - -// ==================== مدیریت تنظیمات (تکس و قیمت پرو) ==================== -const getSettings = async (req, res) => { - try { - let settings = await SettingModel.findOne(); - - if (!settings) { - settings = await SettingModel.create({ - taxRate: 9, - proPrice: 99000, - legendPrice: 199000, - minWithdrawAmount: 50000, - maxWithdrawAmount: 10000000, - }); - } - - res.status(200).json({ - success: true, - data: settings, - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت تنظیمات" }); - } -}; - -const updateSettings = async (req, res) => { - try { - const { - taxRate, - proPrice, - legendPrice, - minWithdrawAmount, - maxWithdrawAmount, - } = req.body; - - const settings = await SettingModel.findOneAndUpdate( - {}, - { - taxRate, - proPrice, - legendPrice, - minWithdrawAmount, - maxWithdrawAmount, - updatedAt: new Date(), - }, - { new: true, upsert: true } - ); - - res.status(200).json({ - success: true, - message: "تنظیمات با موفقیت به‌روزرسانی شد", - data: settings, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی تنظیمات" }); - } -}; - -const getCoursePrices = async (req, res) => { - try { - const proPrice = await CoursePaymentModel.findOne({ cuorse_type: "pro" }); - - const legendPrice = await CoursePaymentModel.findOne({ - cuorse_type: "legend", - }); - - const normalPrice = await CoursePaymentModel.findOne({ - cuorse_type: "normal", - }); - - res.status(200).json({ - success: true, - data: { - pro: proPrice - ? { price: proPrice.price, _id: proPrice._id } - : { price: "60000" }, - legend: legendPrice - ? { price: legendPrice.price, _id: legendPrice._id } - : { price: "199000" }, - normal: normalPrice - ? { price: normalPrice.price, _id: normalPrice._id } - : { price: "0" }, - }, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در دریافت قیمت دوره‌ها" }); - } -}; - -const updateCoursePrice = async (req, res) => { - try { - const { courseType, price } = req.body; - - if (!courseType || !price) { - return res - .status(400) - .json({ success: false, message: "نوع دوره و قیمت الزامی است" }); - } - - let coursePayment = await CoursePaymentModel.findOne({ - cuorse_type: courseType, - }); - - if (coursePayment) { - coursePayment.price = price.toString(); - await coursePayment.save(); - } else { - coursePayment = new CoursePaymentModel({ - price: price.toString(), - cuorse_type: courseType, - }); - await coursePayment.save(); - } - - res.status(200).json({ - success: true, - message: `قیمت دوره ${ - courseType === "pro" - ? "پرو" - : courseType === "legend" - ? "لجند" - : "معمولی" - } با موفقیت به‌روزرسانی شد`, - data: coursePayment, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی قیمت دوره" }); - } -}; - -const getTax = async (req, res) => { - try { - let tax = await Tax.findOne({ type: "course" }); - - if (!tax) { - tax = new Tax({ tax: 8, type: "course" }); - await tax.save(); - } - - res.status(200).json({ - success: true, - data: { - tax: tax.tax, - _id: tax._id, - type: tax.type, - }, - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت مالیات" }); - } -}; - -const updateTax = async (req, res) => { - try { - const { tax } = req.body; - - if (tax === undefined || tax === null) { - return res - .status(400) - .json({ success: false, message: "مقدار مالیات الزامی است" }); - } - - let taxRecord = await Tax.findOne({ type: "course" }); - - if (taxRecord) { - taxRecord.tax = tax; - await taxRecord.save(); - } else { - taxRecord = new Tax({ tax: tax, type: "course" }); - await taxRecord.save(); - } - - res.status(200).json({ - success: true, - message: "مالیات با موفقیت به‌روزرسانی شد", - data: taxRecord, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی مالیات" }); - } -}; - -const getAllSettings = async (req, res) => { - try { - const proPrice = await CoursePaymentModel.findOne({ cuorse_type: "pro" }); - const legendPrice = await CoursePaymentModel.findOne({ - cuorse_type: "legend", - }); - - let tax = await Tax.findOne({ type: "course" }); - if (!tax) { - tax = new Tax({ tax: 8, type: "course" }); - await tax.save(); - } - - res.status(200).json({ - success: true, - data: { - tax: tax.tax, - prices: { - pro: proPrice ? proPrice.price : "60000", - legend: legendPrice ? legendPrice.price : "199000", - }, - }, - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت تنظیمات" }); - } -}; - -const updateAllSettings = async (req, res) => { - try { - const { tax, proPrice, legendPrice } = req.body; - - if (tax !== undefined) { - let taxRecord = await Tax.findOne({ type: "course" }); - if (taxRecord) { - taxRecord.tax = tax; - await taxRecord.save(); - } else { - taxRecord = new Tax({ tax: tax, type: "course" }); - await taxRecord.save(); - } - } - - if (proPrice !== undefined) { - let proRecord = await CoursePaymentModel.findOne({ cuorse_type: "pro" }); - if (proRecord) { - proRecord.price = proPrice.toString(); - await proRecord.save(); - } else { - proRecord = new CoursePaymentModel({ - price: proPrice.toString(), - cuorse_type: "pro", - }); - await proRecord.save(); - } - } - - if (legendPrice !== undefined) { - let legendRecord = await CoursePaymentModel.findOne({ - cuorse_type: "legend", - }); - if (legendRecord) { - legendRecord.price = legendPrice.toString(); - await legendRecord.save(); - } else { - legendRecord = new CoursePaymentModel({ - price: legendPrice.toString(), - cuorse_type: "legend", - }); - await legendRecord.save(); - } - } - - res.status(200).json({ - success: true, - message: "تنظیمات با موفقیت به‌روزرسانی شد", - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی تنظیمات" }); - } -}; - -const getPaymentStats = async (req, res) => { - try { - const stats = await AcademyPaymentModel.aggregate([ - { - $group: { - _id: null, - totalAmount: { $sum: { $toDouble: "$price" } }, - totalCount: { $sum: 1 }, - pendingCount: { - $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, - }, - settledCount: { - $sum: { $cond: [{ $eq: ["$status", "settled"] }, 1, 0] }, - }, - successCount: { - $sum: { $cond: [{ $eq: ["$status", "success"] }, 1, 0] }, - }, - }, - }, - ]); - - const result = stats[0] || { - totalAmount: 0, - totalCount: 0, - pendingCount: 0, - settledCount: 0, - successCount: 0, - }; - - result.averageAmount = - result.totalCount > 0 - ? Math.round(result.totalAmount / result.totalCount) - : 0; - - res.status(200).json({ - success: true, - data: result, - }); - } catch (error) { - console.error("Error in getPaymentStats:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت آمار پرداخت‌ها", - error: process.env.NODE_ENV === "development" ? error.message : undefined, - }); - } -}; - -const getCourseById = async (req, res) => { - try { - const { id } = req.params; - const course = await CourseModel.findById(id) - .populate("academyId", "academy_name academy_image") - .populate("user_id", "user_name profile_image"); - - if (!course) { - return res.status(404).json({ success: false, message: "دوره یافت نشد" }); - } - - res.status(200).json({ - success: true, - data: course, - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت دوره" }); - } -}; - -const createCourse = async (req, res) => { - try { - const courseData = req.body; - const newCourse = new CourseModel(courseData); - await newCourse.save(); - - res.status(201).json({ - success: true, - message: "دوره با موفقیت ایجاد شد", - data: newCourse, - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در ایجاد دوره" }); - } -}; - -const getCourseStats = async (req, res) => { - try { - const stats = await CourseModel.aggregate([ - { - $group: { - _id: null, - totalCourses: { $sum: 1 }, - normalCount: { - $sum: { $cond: [{ $eq: ["$type", "normal"] }, 1, 0] }, - }, - proCount: { $sum: { $cond: [{ $eq: ["$type", "pro"] }, 1, 0] } }, - legendCount: { - $sum: { $cond: [{ $eq: ["$type", "legend"] }, 1, 0] }, - }, - pendingCount: { - $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, - }, - acceptCount: { - $sum: { $cond: [{ $eq: ["$status", "accept"] }, 1, 0] }, - }, - rejectCount: { - $sum: { $cond: [{ $eq: ["$status", "reject"] }, 1, 0] }, - }, - }, - }, - ]); - - res.status(200).json({ - success: true, - data: stats[0] || {}, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در دریافت آمار دوره‌ها" }); - } -}; - -const getPurchasedStats = async (req, res) => { - try { - const stats = await IsPaymentCourse.aggregate([ - { - $group: { - _id: null, - totalPurchases: { $sum: 1 }, - uniqueUsers: { $addToSet: "$user_id" }, - }, - }, - ]); - - res.status(200).json({ - success: true, - data: { - totalPurchases: stats[0]?.totalPurchases || 0, - uniqueUsers: stats[0]?.uniqueUsers?.length || 0, - }, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در دریافت آمار خریدها" }); - } -}; - -const getAcademyById = async (req, res) => { - try { - const { id } = req.params; - const academy = await AcademyModel.findById(id).populate( - "userId", - "user_name profile_image email" - ); - - if (!academy) { - return res - .status(404) - .json({ success: false, message: "آکادمی یافت نشد" }); - } - - res.status(200).json({ - success: true, - data: academy, - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت آکادمی" }); - } -}; - -const createAcademy = async (req, res) => { - try { - const academyData = req.body; - const newAcademy = new AcademyModel(academyData); - await newAcademy.save(); - - res.status(201).json({ - success: true, - message: "آکادمی با موفقیت ایجاد شد", - data: newAcademy, - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در ایجاد آکادمی" }); - } -}; - -const updateAcademy = async (req, res) => { - try { - const { id } = req.params; - const updateData = req.body; - - const academy = await AcademyModel.findByIdAndUpdate(id, updateData, { - new: true, - }); - - if (!academy) { - return res - .status(404) - .json({ success: false, message: "آکادمی یافت نشد" }); - } - - res.status(200).json({ - success: true, - message: "آکادمی با موفقیت به‌روزرسانی شد", - data: academy, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی آکادمی" }); - } -}; - -const deleteAcademy = async (req, res) => { - try { - const { id } = req.params; - const academy = await AcademyModel.findByIdAndDelete(id); - - if (!academy) { - return res - .status(404) - .json({ success: false, message: "آکادمی یافت نشد" }); - } - - await CourseModel.deleteMany({ academyId: id }); - - res.status(200).json({ - success: true, - message: "آکادمی با موفقیت حذف شد", - }); - } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در حذف آکادمی" }); - } -}; - -const getAcademyStats = async (req, res) => { - try { - const stats = await AcademyModel.aggregate([ - { - $group: { - _id: null, - totalAcademies: { $sum: 1 }, - totalRate: { $sum: "$rate" }, - avgRate: { $avg: "$rate" }, - }, - }, - ]); - - res.status(200).json({ - success: true, - data: stats[0] || { - totalAcademies: 0, - totalRate: 0, - avgRate: 0, - }, - }); - } catch (error) { - console.error(error); - res - .status(500) - .json({ success: false, message: "خطا در دریافت آمار آکادمی‌ها" }); - } -}; -const deleteCourseVideo = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { courseContentId, courseId } = req.body; - const courseContent = await AcademyContentModel.findById(courseContentId); - - if (!courseContent) { - return res.status(404).json({ - error: true, - message: "ویدعو یافت نشد", - }); - } - - courseContent.status = "reject"; - - await courseContent.save(); - - const course = await CourseModel.findById(courseId); - if (course) { - course.number_of_course_content = `${ - Number(course.number_of_course_content) - 1 - }`; - await course.save(); - } - - res.status(200).json({ - success: true, - message: "ویدیو با موفقیت پاک شد", - }); - } catch (error) { - console.error("Error:", error); - res.status(500).json({ - success: false, - message: "خطا در حذف ویدیو", - error: error.message, - }); - } -}; - -const getChunkDir = (uploadKey) => { - return path.join(process.cwd(), "storage", "temp-chunks", uploadKey); -}; - -setInterval(async () => { - const tempDir = path.join(process.cwd(), "storage", "temp-chunks"); - if (await fs.pathExists(tempDir)) { - const dirs = await fs.readdir(tempDir); - const now = Date.now(); - for (const dir of dirs) { - const dirPath = path.join(tempDir, dir); - const stat = await fs.stat(dirPath); - if (now - stat.mtimeMs > 30 * 60 * 1000) { - // 30 دقیقه - await fs.remove(dirPath); - console.log(`Cleaned up old upload: ${dir}`); - } - } - } -}, 60 * 60 * 1000); - -const creatCourseVideoChunk = async (req, res, next) => { - console.log("=== Chunk Video Upload ==="); - console.log("Chunk index:", req.body.chunkIndex); - - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { - courseId, - is_free, - file_name, - chunk, - chunkIndex, - totalChunks, - originalFileName, - fileSize, - mime_type, - } = req.body; - - if (!chunk) { - return res.status(422).json({ - error: true, - message: "تکه فایل ارسال نشده است", - }); - } - - if (!courseId) { - return res.status(422).json({ - error: true, - message: "شناسه دوره الزامی است", - }); - } - - const base64Data = chunk.split(",")[1] || chunk; - const buffer = Buffer.from(base64Data, "base64"); - - const uploadKey = `${courseId}_${originalFileName}`; - const chunkDir = getChunkDir(uploadKey); - - await fs.ensureDir(chunkDir); - - const metaPath = path.join(chunkDir, "meta.json"); - if (chunkIndex === "0" || chunkIndex === 0) { - const metaData = { - totalChunks: parseInt(totalChunks), - file_name: file_name, - originalFileName: originalFileName, - fileSize: fileSize, - mime_type: mime_type, - courseId: courseId, - is_free: is_free, - createdAt: Date.now(), - }; - await fs.writeJson(metaPath, metaData); - } - - if (!(await fs.pathExists(metaPath))) { - return res.status(404).json({ - error: true, - message: "جلسه آپلود یافت نشد", - }); - } - - const chunkPath = path.join(chunkDir, `chunk_${chunkIndex}`); - await fs.writeFile(chunkPath, buffer); - - console.log( - `✅ Chunk ${parseInt(chunkIndex) + 1}/${totalChunks} saved to disk (${ - buffer.length - } bytes)` - ); - - res.status(200).json({ - success: true, - message: `تکه ${ - parseInt(chunkIndex) + 1 - } از ${totalChunks} با موفقیت دریافت شد`, - chunkIndex: chunkIndex, - totalChunks: totalChunks, - }); - } catch (error) { - console.error("Error in creatCourseVideoChunk:", error); - res.status(500).json({ - success: false, - message: "خطا در دریافت تکه ویدیو", - error: error.message, - }); - } -}; - -const mergeVideoChunks = async (req, res, next) => { - console.log("=== Merge Video Chunks ==="); - - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { courseId, originalFileName, totalChunks } = req.body; - - const uploadKey = `${courseId}_${originalFileName}`; - const chunkDir = getChunkDir(uploadKey); - const metaPath = path.join(chunkDir, "meta.json"); - - if (!(await fs.pathExists(chunkDir)) || !(await fs.pathExists(metaPath))) { - return res.status(404).json({ - error: true, - message: "داده‌های آپلود یافت نشد", - }); - } - - const metaData = await fs.readJson(metaPath); - - const existingChunks = await fs.readdir(chunkDir); - const chunkFiles = existingChunks.filter((f) => f.startsWith("chunk_")); - - if (chunkFiles.length !== parseInt(totalChunks)) { - return res.status(422).json({ - error: true, - message: `تعداد تکه‌ها کامل نیست. دریافت شده: ${chunkFiles.length}، مورد نیاز: ${totalChunks}`, - }); - } - - const chunks = []; - for (let i = 0; i < parseInt(totalChunks); i++) { - const chunkPath = path.join(chunkDir, `chunk_${i}`); - if (!(await fs.pathExists(chunkPath))) { - return res.status(422).json({ - error: true, - message: `تکه ${i + 1} پیدا نشد`, - }); - } - const chunkBuffer = await fs.readFile(chunkPath); - chunks.push(chunkBuffer); - } - - const completeBuffer = Buffer.concat(chunks); - console.log("Original file size:", completeBuffer.length, "bytes"); - - const finalVideoDir = path.join( - process.cwd(), - "storage", - "courses", - "videos" - ); - await fs.ensureDir(finalVideoDir); - - let extension = ".mp4"; - if (metaData.mime_type === "video/mp4") extension = ".mp4"; - else if (metaData.mime_type === "video/quicktime") extension = ".mov"; - else if (metaData.mime_type === "video/x-msvideo") extension = ".avi"; - else if (metaData.mime_type === "video/webm") extension = ".webm"; - - const safeFileName = `course-${courseId}-${Date.now()}${extension}`; - const filePath = path.join(finalVideoDir, safeFileName); - - await fs.writeFile(filePath, completeBuffer); - console.log("Original file saved:", filePath); - - const newAcademyContent = new AcademyContentModel({ - course_video: `/storage/courses/videos/${safeFileName}`, - type: "video", - is_free: metaData.is_free === "true" || metaData.is_free === true, - courseId: courseId, - file_name: metaData.file_name || metaData.originalFileName, - }); - - await newAcademyContent.save(); - - const course = await CourseModel.findById(courseId); - if (course) { - course.number_of_course_content = `${ - Number(course.number_of_course_content) + 1 - }`; - await course.save(); - } - - await fs.remove(chunkDir); - - res.status(200).json({ - success: true, - message: "ویدیو با موفقیت ثبت شد (در حال پردازش...)", - data: newAcademyContent, - }); - - if (completeBuffer.length > 20 * 1024 * 1024) { - // اجرای غیرهمزمان بدون await - compressVideoInBackground(filePath, safeFileName, newAcademyContent._id); - } - } catch (error) { - console.error("Error in mergeVideoChunks:", error); - res.status(500).json({ - success: false, - message: "خطا در ترکیب ویدیو", - error: error.message, - }); - } -}; - -const compressVideoInBackground = async (filePath, fileName, contentId) => { - console.log(`Starting background compression for ${fileName}`); - - const dir = path.dirname(filePath); - const tempPath = path.join(dir, `temp-${fileName}`); - const { exec } = require("child_process"); - const util = require("util"); - const execPromise = util.promisify(exec); - - try { - await execPromise( - `ffmpeg -i "${filePath}" -c:v libx264 -crf 28 -preset veryfast -c:a aac -b:a 128k -movflags +faststart "${tempPath}" -y` - ); - - const compressedStat = await fs.stat(tempPath); - const originalStat = await fs.stat(filePath); - - if (compressedStat.size < originalStat.size) { - await fs.rename(tempPath, filePath); - console.log( - `✅ Background compression done: ${( - originalStat.size / - 1024 / - 1024 - ).toFixed(2)}MB → ${(compressedStat.size / 1024 / 1024).toFixed(2)}MB` - ); - } else { - await fs.remove(tempPath); - console.log("Compression didn't reduce size, keeping original"); - } - } catch (error) { - console.error("Background compression failed:", error.message); - if (await fs.pathExists(tempPath)) { - await fs.remove(tempPath); - } - } -}; - -const getUploadStatus = async (req, res) => { - try { - const { courseId, originalFileName } = req.query; - const uploadKey = `${courseId}_${originalFileName}`; - const chunkDir = getChunkDir(uploadKey); - - if (!(await fs.pathExists(chunkDir))) { - return res.status(200).json({ - success: true, - uploadedChunks: [], - totalChunks: 0, - }); - } - - const files = await fs.readdir(chunkDir); - const chunkFiles = files - .filter((f) => f.startsWith("chunk_")) - .map((f) => parseInt(f.replace("chunk_", ""))) - .sort((a, b) => a - b); - - const metaPath = path.join(chunkDir, "meta.json"); - let totalChunks = 0; - if (await fs.pathExists(metaPath)) { - const metaData = await fs.readJson(metaPath); - totalChunks = metaData.totalChunks; - } - - res.status(200).json({ - success: true, - uploadedChunks: chunkFiles, - totalChunks: totalChunks, - }); - } catch (error) { - res.status(500).json({ success: false, error: error.message }); - } -}; - - - -// ایجاد slug یکتا -const generateUniqueSlug = async (title, excludeId = null) => { - let slug = title - .replace(/[^\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C\uFB8E\u0698a-zA-Z0-9\s]/g, '') - .trim() - .replace(/\s+/g, '-') - .toLowerCase(); - - let uniqueSlug = slug; - let counter = 1; - - while (true) { - const query = { slug: uniqueSlug }; - if (excludeId) query._id = { $ne: excludeId }; - const exists = await AcademyCategoryModel.findOne(query); - if (!exists) break; - uniqueSlug = `${slug}-${counter}`; - counter++; - } - - return uniqueSlug; -}; - -// ==================== GET - دریافت لیست دسته‌بندی‌ها ==================== -const getAll = async (req, res) => { - try { - const { - page = 1, - limit = 20, - sortBy = 'order', - sortOrder = 'asc', - status, - parent, - search, - is_featured, - withChildren = 'false' - } = req.query; - - let query = {}; - - if (status) query.status = status; - if (parent !== undefined) query.parent = parent === 'null' ? null : parent; - if (is_featured === 'true') query.is_featured = true; - - if (search) { - query.$or = [ - { title: { $regex: search, $options: 'i' } }, - { description: { $regex: search, $options: 'i' } } - ]; - } - - const sortOptions = {}; - sortOptions[sortBy] = sortOrder === 'asc' ? 1 : -1; - - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: sortOptions, - populate: [ - { path: 'parent', select: 'title slug' }, - { path: 'created_by', select: 'user_name name' }, - { path: 'updated_by', select: 'user_name name' } - ] - }; - - let categories = await AcademyCategoryModel.paginate(query, options); - - // دریافت درخت دسته‌بندی (اختیاری) - if (withChildren === 'true' && !parent) { - const tree = await AcademyCategoryModel.getTree(); - categories = { ...categories, tree }; - } - - res.status(200).json({ - success: true, - message: 'لیست دسته‌بندی‌ها با موفقیت دریافت شد', - data: categories - }); - } catch (error) { - console.error('Error in getAll:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت دسته‌بندی‌ها', - error: error.message - }); - } -}; - -// ==================== GET - دریافت یک دسته‌بندی ==================== -const getOne = async (req, res) => { - try { - const { id } = req.params; - const { includeCourses = 'false' } = req.query; - - let query = AcademyCategoryModel.findById(id); - - query = query.populate('parent', 'title slug icon color'); - query = query.populate('children', 'title slug icon color course_count order'); - query = query.populate('created_by', 'user_name name'); - - if (includeCourses === 'true') { - query = query.populate({ - path: 'courses', - select: 'cuorse_name course_image price offer', - options: { limit: 10 } - }); - } - - const category = await query; - - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } - - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت دریافت شد', - data: category - }); - } catch (error) { - console.error('Error in getOne:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت دسته‌بندی', - error: error.message - }); - } -}; - -// ==================== GET - دریافت درخت دسته‌بندی ==================== -const getTree = async (req, res) => { - try { - const tree = await AcademyCategoryModel.getTree(); - - res.status(200).json({ - success: true, - message: 'درخت دسته‌بندی با موفقیت دریافت شد', - data: tree - }); - } catch (error) { - console.error('Error in getTree:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت درخت دسته‌بندی', - error: error.message - }); - } -}; - -// ==================== GET - دریافت توسط اسلاگ ==================== -const getBySlug = async (req, res) => { - try { - const { slug } = req.params; - const { page = 1, limit = 20 } = req.query; - - const category = await AcademyCategoryModel.getBySlug(slug); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } - - // دریافت دوره‌های این دسته - const courses = await CourseModel.find({ - category_id: category._id, - status: 'accept' - }) - .sort({ createdAt: -1 }) - .skip((page - 1) * limit) - .limit(parseInt(limit)); - - const totalCourses = await CourseModel.countDocuments({ - category_id: category._id, - status: 'accept' - }); - - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت دریافت شد', - data: { - category, - courses: { - data: courses, - pagination: { - page: parseInt(page), - limit: parseInt(limit), - total: totalCourses, - pages: Math.ceil(totalCourses / limit) - } - } - } - }); - } catch (error) { - console.error('Error in getBySlug:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت دسته‌بندی', - error: error.message - }); - } -}; - -// ==================== POST - ایجاد دسته‌بندی جدید ==================== -const create = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { - title, - description, - icon, - color, - parent, - order, - image, - is_featured, - seo_title, - seo_description - } = req.body; - - // اعتبارسنجی - if (!title || title.trim().length < 2) { - return res.status(422).json({ - success: false, - message: 'عنوان دسته‌بندی باید حداقل ۲ کاراکتر باشد' - }); - } - - // بررسی تکراری نبودن عنوان - const existingTitle = await AcademyCategoryModel.findOne({ title }); - if (existingTitle) { - return res.status(409).json({ - success: false, - message: 'این عنوان قبلاً ثبت شده است' - }); - } - - // تولید slug یکتا - const slug = await generateUniqueSlug(title); - - // بررسی وجود parent - let level = 0; - if (parent) { - const parentCategory = await AcademyCategoryModel.findById(parent); - if (!parentCategory) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی والد یافت نشد' - }); - } - level = parentCategory.level + 1; - - if (level > 3) { - return res.status(422).json({ - success: false, - message: 'عمق دسته‌بندی نمی‌تواند بیشتر از ۳ سطح باشد' - }); - } - } - - const newCategory = new AcademyCategoryModel({ - title, - slug, - description: description || null, - icon: icon || null, - color: color || '#3B82F6', - parent: parent || null, - level, - order: order || 0, - image: image || null, - is_featured: is_featured || false, - seo_title: seo_title || title, - seo_description: seo_description || description, - created_by: userId, - status: 'active' - }); - - await newCategory.save(); - - res.status(201).json({ - success: true, - message: 'دسته‌بندی با موفقیت ایجاد شد', - data: newCategory - }); - } catch (error) { - console.error('Error in create:', error); - res.status(500).json({ - success: false, - message: 'خطا در ایجاد دسته‌بندی', - error: error.message - }); - } -}; - -// ==================== PUT/PATCH - ویرایش دسته‌بندی ==================== -const update = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { id } = req.params; - const updateData = req.body; - - const category = await AcademyCategoryModel.findById(id); - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } - - // بررسی تکراری نبودن عنوان - if (updateData.title && updateData.title !== category.title) { - const existingTitle = await AcademyCategoryModel.findOne({ - title: updateData.title, - _id: { $ne: id } - }); - if (existingTitle) { - return res.status(409).json({ - success: false, - message: 'این عنوان قبلاً ثبت شده است' - }); - } - - // آپدیت slug - updateData.slug = await generateUniqueSlug(updateData.title, id); - } - - // بررسی parent - if (updateData.parent) { - if (updateData.parent === id) { - return res.status(422).json({ - success: false, - message: 'یک دسته‌بندی نمی‌تواند والد خودش باشد' - }); - } - - const parentCategory = await AcademyCategoryModel.findById(updateData.parent); - if (!parentCategory) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی والد یافت نشد' - }); - } - - updateData.level = parentCategory.level + 1; - - if (updateData.level > 3) { - return res.status(422).json({ - success: false, - message: 'عمق دسته‌بندی نمی‌تواند بیشتر از ۳ سطح باشد' - }); - } - } - - updateData.updated_by = userId; - updateData.updatedAt = new Date(); - - const updatedCategory = await AcademyCategoryModel.findByIdAndUpdate( - id, - updateData, - { new: true, runValidators: true } - ); - - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت به‌روزرسانی شد', - data: updatedCategory - }); - } catch (error) { - console.error('Error in update:', error); - res.status(500).json({ - success: false, - message: 'خطا در به‌روزرسانی دسته‌بندی', - error: error.message - }); - } -}; - -// ==================== DELETE - حذف (سافت دیلیت) ==================== -const remove = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { id } = req.params; - const { permanent = 'false' } = req.query; - - const category = await AcademyCategoryModel.findById(id); - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } - - // بررسی وجود زیردسته‌ها - const hasChildren = await AcademyCategoryModel.exists({ parent: id }); - if (hasChildren) { - return res.status(422).json({ - success: false, - message: 'این دسته‌بندی دارای زیردسته است. ابتدا زیردسته‌ها را حذف کنید' - }); - } - - // بررسی وجود دوره در این دسته - const hasCourses = await CourseModel.exists({ category_id: id }); - if (hasCourses && permanent === 'true') { - return res.status(422).json({ - success: false, - message: 'این دسته‌بندی دارای دوره است. ابتدا دوره‌ها را جابه‌جا کنید' - }); - } - - if (permanent === 'true') { - // حذف فیزیکی - await category.deleteOne(); - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت حذف شد' - }); - } else { - // سافت دیلیت - category.status = 'deleted'; - category.updated_by = userId; - await category.save(); - - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت غیرفعال شد' - }); - } - } catch (error) { - console.error('Error in remove:', error); - res.status(500).json({ - success: false, - message: 'خطا در حذف دسته‌بندی', - error: error.message - }); - } -}; - -// ==================== POST - تغییر وضعیت (اکتیو/غیراکتیو) ==================== -const toggleStatus = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - - const { id } = req.params; - const { status } = req.body; // 'active' or 'inactive' - - if (!['active', 'inactive'].includes(status)) { - return res.status(422).json({ - success: false, - message: 'وضعیت نامعتبر است' - }); - } - - const category = await AcademyCategoryModel.findByIdAndUpdate( - id, - { - status, - updated_by: userId, - updatedAt: new Date() - }, - { new: true } - ); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } - - res.status(200).json({ - success: true, - message: `وضعیت دسته‌بندی با موفقیت به ${status === 'active' ? 'فعال' : 'غیرفعال'} تغییر کرد`, - data: category - }); - } catch (error) { - console.error('Error in toggleStatus:', error); - res.status(500).json({ - success: false, - message: 'خطا در تغییر وضعیت دسته‌بندی', - error: error.message - }); - } -}; - -// ==================== POST - افزایش تعداد دوره‌ها ==================== -const incrementCourseCount = async (req, res) => { - try { - const { id } = req.params; - const { increment = 1 } = req.body; - - const category = await AcademyCategoryModel.incrementCourseCount(id, increment); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } - - res.status(200).json({ - success: true, - message: 'تعداد دوره‌ها به‌روزرسانی شد', - data: { course_count: category.course_count } - }); - } catch (error) { - console.error('Error in incrementCourseCount:', error); - res.status(500).json({ - success: false, - message: 'خطا در به‌روزرسانی تعداد دوره‌ها', - error: error.message - }); - } -}; - -// ==================== GET - دریافت دسته‌بندی‌های ویژه (featured) ==================== -const getFeatured = async (req, res) => { - try { - const { limit = 10 } = req.query; - - const categories = await AcademyCategoryModel.find({ - is_featured: true, - status: 'active' - }) - .sort({ order: 1 }) - .limit(parseInt(limit)) - .populate('parent', 'title slug'); - - res.status(200).json({ - success: true, - message: 'دسته‌بندی‌های ویژه با موفقیت دریافت شد', - data: categories - }); - } catch (error) { - console.error('Error in getFeatured:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت دسته‌بندی‌های ویژه', - error: error.message - }); - } -}; - - -const getFreeAcademyExploreContent = async (req, res, next) => { - try { - const page = parseInt(req.query.page, 10) || 1; - const limit = parseInt(req.query.limit, 10) || 15; - const skip = (page - 1) * limit; - - const query = { is_free: true, status: "accept" }; - const totalItems = await AcademyContentModel.countDocuments(query); - const contents = await AcademyContentModel.find(query) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit) - .lean(); - - const courseIds = [ - ...new Set(contents.map((item) => item.courseId).filter(Boolean)), - ]; - const courses = courseIds.length - ? await CourseModel.find({ _id: { $in: courseIds } }) - .select( - "cuorse_name course_image teacher_name academyId user_id caption likes" - ) - .lean() - : []; - const courseMap = Object.fromEntries( - courses.map((course) => [String(course._id), course]) - ); - - const userIds = [ - ...new Set(courses.map((course) => String(course.user_id)).filter(Boolean)), - ]; - const users = userIds.length - ? await UserModel.find({ _id: { $in: userIds } }) - .select("user_name first_name last_name profile_image") - .lean() - : []; - const userMap = Object.fromEntries( - users.map((user) => [String(user._id), user]) - ); - - const items = contents.map((content) => { - const course = courseMap[String(content.courseId)] || {}; - const user = userMap[String(course.user_id)] || {}; - return { - ...content, - course_name: course.cuorse_name || content.file_name || "", - course_image: course.course_image || "", - teacher_name: course.teacher_name || "", - academyId: course.academyId || "", - user_name: user.user_name || "", - first_name: user.first_name || "", - last_name: user.last_name || "", - profile_image: user.profile_image || "", - likesCount: Array.isArray(course.likes) ? course.likes.length : 0, - }; - }); - - const totalPages = Math.ceil(totalItems / limit) || 1; - - return res.status(200).json({ - success: true, - data: { - items, - pagination: { - currentPage: page, - totalPages, - totalItems, - hasNextPage: page < totalPages, - }, - }, - }); - } catch (error) { - console.error("Error in getFreeAcademyExploreContent:", error); - next(error); - } -}; - - -module.exports = { - // ========================================== - // 🏢 ماژول آکادمی (Academy) - // ========================================== - - // پیدا کردن آکادمی کاربر جاری - findAcademy, - - // پیدا کردن آکادمی با شناسه - findAcademyById, - - // تکمیل و ویرایش پروفایل آکادمی - academyProfile, - - // تغییر وضعیت آکادمی (فعال/غیرفعال - فقط ادمین) - updateAcademyStatus, - - // دریافت اطلاعات یک آکادمی با شناسه - getAcademyById, - - // ایجاد آکادمی جدید (دستی - معمولاً خودکار ایجاد می‌شود) - createAcademy, - - // ویرایش اطلاعات آکادمی - updateAcademy, - - // حذف آکادمی (به همراه تمام دوره‌های آن) - deleteAcademy, - - // دریافت آمار کلی آکادمی‌ها (تعداد، میانگین امتیاز و...) - getAcademyStats, - - // ========================================== - // 📚 ماژول دوره‌ها (Course) - // ========================================== - - // ایجاد دوره جدید - creatCourse, - - // ویرایش اطلاعات دوره - updateCourse, - - // حذف دوره (تغییر وضعیت به reject) - deleteCourse, - - // دریافت لیست دوره‌های آکادمی جاری - getCourse, - - // دریافت لیست همه دوره‌ها با فیلتر و جستجو - getCourses, - - // محتوای رایگان آموزشگاه برای اکسپلور - getFreeAcademyExploreContent, - - // دریافت دوره‌های یک آکادمی خاص - getAcademyCourse, - - // آپلود ویدیو با روش Multipart - creatCourseVideo, - - // آپلود ویدیو با روش Base64 (برای فایل‌های کوچک) - creatCourseVideoBase64, - - // ========================================== - // 💳 ماژول پرداخت (Payment) - // ========================================== - - // پرداخت برای اشتراک پرو - proPayment, - - // کال‌بک درگاه پرداخت زرین‌پال (اشتراک پرو) - handleCoursePaymentCallback, - - // پرداخت برای خرید دوره - coursePayment, - - // کال‌بک درگاه پرداخت زرین‌پال (خرید دوره) - CoursePaymentCallback, - - // ========================================== - // 🎬 ماژول محتوای دوره (Course Content) - // ========================================== - - // دریافت محتوای دوره (لیست ویدیوها و فایل‌ها) - getCourseContent, - - // حذف ویدیو از دوره - deleteCourseVideo, - - // لایک کردن محتوا - likeCourseContent, - - // حذف لایک (دیسلایک) - dontLikeCourseContent, - - // دریافت تعداد لایک‌های یک دوره - getCourseLikesCount, - - // دریافت لیست دوره‌های لایک شده توسط کاربر - getUserLikedCourses, - - // بررسی وضعیت لایک کاربر (آیا لایک کرده یا نه) - isLikeCourseContent, - - // ========================================== - // 💬 ماژول کامنت‌ها (Comments) - // ========================================== - - // ایجاد کامنت جدید - createComment, - - // دریافت تعداد کل کامنت‌های یک دوره - getTotalCommentsCount, - - // دریافت لیست کامنت‌های یک دوره (با فیلتر و صفحه‌بندی) - getCourseComments, - - // تغییر وضعیت کامنت (تایید/رد/در انتظار) - updateCommentStatus, - - // دریافت کامنت‌های یک کاربر خاص - getUserComments, - - // حذف کامنت - deleteComment, - - // ========================================== - // 🛒 ماژول دوره‌های خریداری شده (Purchased) - // ========================================== - - // دریافت دوره‌های خریداری شده توسط کاربر جاری - getUserPurchasedCourses, - - // دریافت دوره‌های خریداری شده (با populate - روش بهینه) - getUserPurchasedCoursesWithPopulate, - - // دریافت همه پرداخت‌های آکادمی - getAllAcademyPayments, - - // بررسی خرید یک دوره توسط کاربر (برای صفحه دوره) - checkCoursePurchase, - - // ========================================== - // 👑 ماژول مدیریتی (Admin Panel) - // ========================================== - - // ------ مدیریت پرداخت‌ها ------ - - // دریافت همه پرداخت‌های سیستم (ادمین) - getAllPayments, - - // تغییر وضعیت پرداخت (pending/success/settled/failed) - updatePaymentStatus, - - // ------ مدیریت دوره‌ها ------ - - // دریافت همه دوره‌های سیستم (ادمین) - getAllCourses, - - // تغییر وضعیت دوره (تایید/رد/در انتظار) - updateCourseStatus, - - // حذف کامل دوره از سیستم (ادمین) - admindeleteCourse, - - // ------ مدیریت دوره‌های خریداری شده ------ - - // دریافت همه خریدهای سیستم (ادمین) - getAllPurchasedCourses, - - // ------ مدیریت آکادمی‌ها ------ - - // دریافت همه آکادمی‌های سیستم (ادمین) - getAllAcademies, - - // ------ مدیریت تنظیمات ------ - - // دریافت تنظیمات (تکس، قیمت‌ها و...) - getSettings, - - // به‌روزرسانی تنظیمات - updateSettings, - - // دریافت قیمت دوره‌ها (پرو، لجند، معمولی) - getCoursePrices, - - // به‌روزرسانی قیمت یک نوع دوره - updateCoursePrice, - - // دریافت درصد مالیات - getTax, - - // به‌روزرسانی درصد مالیات - updateTax, - - // دریافت همه تنظیمات یکجا - getAllSettings, - - // به‌روزرسانی همه تنظیمات یکجا - updateAllSettings, - - // ========================================== - // 📊 ماژول آمار (Stats) - // ========================================== - - // دریافت آمار پرداخت‌ها - getPaymentStats, - - // دریافت اطلاعات یک دوره با شناسه - getCourseById, - - // ایجاد دوره جدید (ادمین) - createCourse, - - // ویرایش دوره (ادمین) - updateCourse, - - // دریافت آمار کلی دوره‌ها - getCourseStats, - - // دریافت آمار دوره‌های خریداری شده - getPurchasedStats, - - // ========================================== - // 📹 ماژول آپلود تکه‌تکه (Chunk Upload) - // ========================================== - - // دریافت تکه ویدیو (هر بار یک تکه) - creatCourseVideoChunk, - - // ادغام تکه‌ها بعد از اتمام آپلود - mergeVideoChunks, - - // بررسی وضعیت آپلود (برای ادامه از جای قطع شده) - getUploadStatus, - - // ========================================== - // 🏷️ ماژول دسته‌بندی آکادمی (Category) - // ========================================== - - // دریافت لیست دسته‌بندی‌ها (با فیلتر و صفحه‌بندی) - getAll, - - // دریافت یک دسته‌بندی با شناسه - getOne, - - // دریافت درخت دسته‌بندی (ساختار سلسله‌مراتبی والد-فرزند) - getTree, - - // دریافت دسته‌بندی با اسلاگ (برای سئو و لینک‌های زیبا) - getBySlug, - - // ایجاد دسته‌بندی جدید - create, - - // ویرایش دسته‌بندی - update, - - // حذف دسته‌بندی (سافت دیلیت - فقط غیرفعال می‌شود) - remove, - - // تغییر وضعیت دسته‌بندی (فعال/غیرفعال) - toggleStatus, - - // افزایش تعداد دوره‌های دسته‌بندی - incrementCourseCount, - - // دریافت دسته‌بندی‌های ویژه (برای نمایش در صفحه اصلی) - getFeatured +const jwt = require("jsonwebtoken"); +const fs = require("fs-extra"); +const path = require("path"); +const jMoment = require("moment-jalaali"); +const AcademyContentModel = require("../../../models/AcademyContentModel"); +const AcademyModel = require("../../../models/AcademyModel"); +const AcademyPaymentModel = require("../../../models/AcademyPaymentModel"); +const CourseModel = require("../../../models/CourseModel"); +const CoursePaymentModel = require("../../../models/CoursePaymentModel"); +const UserModel = require("../../../models/UserModel"); +const PaymentModel = require("../../../models/PaymentModel"); +const { default: axios } = require("axios"); +const AcademyLikeModel = require("../../../models/AcademyLikeModel"); +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({ + cuorse_type: "pro", + }); + if (!CoursePayment) { + const newCoursePayment = new CoursePaymentModel({ + price: "60000", + cuorse_type: "pro", + }); + await newCoursePayment.save(); + + return console.log("اشتراک پرو ساخته شد"); + } + + console.log("اشتراک پرو موجود هست"); + const tax = await Tax.findOne({ + type: "course", + }); + if (!tax) { + const newTax = new Tax({ + tax: 8, + type: "course", + }); + await newTax.save(); + + return console.log("تکس ساخته شد"); + } + + console.log(" تکس موجود هست"); +}; +onServerRestart(); + +const findAcademy = async (req, res, next) => { + try { + const token = req.header("Authorization").split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const user = await UserModel.findById(userId); + if (!user) { + return res.status(422).json({ + error: true, + message: "کاربر یافت نشد", + }); + } + const academy = await AcademyModel.findOne({ userId: userId }); + if (!academy) { + const newAcademy = new AcademyModel({ + userId: userId, + }); + await newAcademy.save(); + + return res.status(200).json({ + academy: newAcademy, + }); + } + + res.status(200).json({ + academy: academy, + }); + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error("Error:", error); + res.status(500).json({ message: "err in creat" }); + } +}; +const findAcademyById = async (req, res, next) => { + try { + const { _id } = req.body; + + const academy = await AcademyModel.findById(_id); + if (!academy) { + res.status(500).json({ + academy: "اکادمی موحود نیست", + }); + } + + res.status(200).json({ + academy: academy, + }); + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error("Error:", error); + res.status(500).json({ message: "err in creat" }); + } +}; +const academyProfile = async (req, res, next) => { + try { + const token = req.header("Authorization").split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const user = await UserModel.findById(userId); + if (!user) { + return res.status(422).json({ + error: true, + message: "کاربر یافت نشد", + }); + } + const academy = await AcademyModel.findOne({ userId: userId }); + if (!academy) { + return res.status(200).json({ + message: "اکادمی پیدا نشد", + }); + } + + const { profile_image } = req.files; + const { name, sheba, bio, tag } = req.body; + + if (!profile_image) { + return res.status(422).json({ + error: true, + message: "اطلاعات ارسالی اشتباه است", + }); + } + + const uploadDir = path.join(__dirname, "../../../../storage/profiles"); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + const uniqueFileName = `${name}-${Date.now()}${path.extname( + profile_image.name + )}`; + const filePath = path.join(uploadDir, uniqueFileName); + + await fs.move(profile_image.path, filePath); + + academy.academy_image = `/profiles/${uniqueFileName}`; + academy.academy_name = name; + academy.sheba = sheba; + academy.bio = bio; + academy.tag = tag; + await academy.save(); + + res.status(200).json({ + message: "ثبت شد", + }); + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error("Error:", error); + res.status(500).json({ message: "err in update" }); + } +}; + +const creatCourse = async (req, res, next) => { + try { + const token = req.header("Authorization").split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const user = await UserModel.findById(userId); + if (!user) { + return res.status(422).json({ + error: true, + message: "کاربر یافت نشد", + }); + } + + const academy = await AcademyModel.findOne({ userId: userId }); + if (!academy) { + return res.status(200).json({ + message: "اکادمی پیدا نشد", + }); + } + const { + price, + cuorse_name, + category, + offer, + caption, + course_time, + number_of_course_content, + teacher_number, + teacher_name, + } = req.body; + + const { course_image } = req.files; + + if (!course_image) { + return res.status(422).json({ + error: true, + message: "اطلاعات ارسالی اشتباه است", + }); + } + + const uploadDir = path.join(__dirname, "../../../../storage/profiles"); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + const uniqueFileName = `${cuorse_name}-${Date.now()}${path.extname( + course_image.name + )}`; + const filePath = path.join(uploadDir, uniqueFileName); + + await fs.move(course_image.path, filePath); + + // ✅ ایجاد رکورد جدید + const newAcademy = new CourseModel({ + course_image: `/profiles/${uniqueFileName}`, + price, + cuorse_name, + category, + offer, + teacher_name, + academyId: academy._id, + caption, + course_time, + number_of_course_content, + teacher_number, + }); + + await newAcademy.save(); + + res.status(200).json({ + message: "ثبت شد", + }); + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error("Error:", error); + res.status(500).json({ message: "err in update" }); + } +}; + +const updateCourse = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { + price, + cuorse_name, + category, + offer, + courseId, + teacher_name, + caption, + course_time, + number_of_course_content, + teacher_number, + } = req.body; + + // اعتبارسنجی courseId + if (!courseId) { + return res.status(422).json({ + error: true, + message: "آیدی دوره الزامی است", + }); + } + + const user = await UserModel.findById(userId); + if (!user) { + return res.status(422).json({ + error: true, + message: "کاربر یافت نشد", + }); + } + + const course = await CourseModel.findById(courseId); + if (!course) { + return res.status(422).json({ + error: true, + message: "پکیجی یافت نشد", + }); + } + + // ========== آپلود عکس جدید (اختیاری) ========== + const { course_image } = req.files || {}; + + if (course_image) { + // فقط اگه عکس جدید آپلود شده بود، آپلود کن + const uploadDir = path.join(__dirname, "../../../../storage/profiles"); + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + // استفاده از id به جای نام دوره برای نام فایل + const safeFileName = `course-${courseId}-${Date.now()}${path.extname( + course_image.name + )}`; + const filePath = path.join(uploadDir, safeFileName); + + await fs.move(course_image.path, filePath); + course.course_image = `/profiles/${safeFileName}`; + } + + // ========== به روز رسانی فیلدها (فقط فیلدهایی که ارسال شده) ========== + if (price !== undefined) course.price = price; + if (cuorse_name !== undefined) course.cuorse_name = cuorse_name; + if (category !== undefined) course.category = category; + if (offer !== undefined) course.offer = offer; + if (caption !== undefined) course.caption = caption; + if (course_time !== undefined) course.course_time = course_time; + if (number_of_course_content !== undefined) + course.number_of_course_content = number_of_course_content; + if (teacher_number !== undefined) course.teacher_number = teacher_number; + if (teacher_name !== undefined) course.teacher_name = teacher_name; + + await course.save(); + + res.status(200).json({ + success: true, + message: "دوره با موفقیت به روز رسانی شد", + data: { + course: course, + }, + }); + } catch (error) { + console.error("Error in updateCourse:", error); + res.status(500).json({ + success: false, + message: "خطا در به روز رسانی دوره", + error: error.message, + }); + } +}; +const deleteCourse = async (req, res, next) => { + try { + const token = req.header("Authorization").split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + const { courseId } = req.body; + + const user = await UserModel.findById(userId); + if (!user) { + return res.status(422).json({ + error: true, + message: "کاربر یافت نشد", + }); + } + const course = await CourseModel.findById(courseId); + if (!course) { + return res.status(422).json({ + error: true, + message: "پکیجی یافت نشد", + }); + } + + course.status = "reject"; + await course.save(); + + res.status(200).json({ + message: "ثبت شد", + }); + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error("Error:", error); + res.status(500).json({ message: "err in update" }); + } +}; +const getCourse = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + // گرفتن پارامترهای صفحه‌بندی + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const skip = (page - 1) * limit; + + const user = await UserModel.findById(userId); + if (!user) { + return res.status(422).json({ + error: true, + message: "کاربر یافت نشد", + }); + } + + const academy = await AcademyModel.findOne({ userId: userId }); + if (!academy) { + return res.status(404).json({ + success: false, + message: "آکادمی پیدا نشد", + }); + } + + // ✅ شرط جستجو - فقط دوره‌های تایید شده + const query = { + academyId: academy._id, + status: "accept", + }; + + // دریافت تعداد کل دوره‌های تایید شده + const totalCourses = await CourseModel.countDocuments(query); + + // دریافت دوره‌های تایید شده با صفحه‌بندی + const courses = await CourseModel.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit); + + // ✅ اگر دوره‌ای وجود نداشت، خطا نده، بلکه آرایه خالی برگردون + if (!courses || courses.length === 0) { + return res.status(200).json({ + success: true, + message: "هیچ دوره تایید شده‌ای یافت نشد", + data: { + courses: [], + pagination: { + currentPage: page, + totalPages: 0, + totalItems: 0, + itemsPerPage: limit, + hasNextPage: false, + hasPrevPage: false, + nextPage: null, + prevPage: null, + }, + }, + }); + } + + // محاسبات صفحه‌بندی + const totalPages = Math.ceil(totalCourses / limit); + const hasNextPage = page < totalPages; + const hasPrevPage = page > 1; + + res.status(200).json({ + success: true, + message: "لیست پکیج‌ها با موفقیت دریافت شد", + data: { + courses: courses, + pagination: { + currentPage: page, + totalPages: totalPages, + totalItems: totalCourses, + itemsPerPage: limit, + hasNextPage: hasNextPage, + hasPrevPage: hasPrevPage, + nextPage: hasNextPage ? page + 1 : null, + prevPage: hasPrevPage ? page - 1 : null, + }, + }, + }); + } catch (error) { + console.error("Error:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت پکیج‌ها", + error: error.message, + }); + } +}; + +const getCourses = async (req, res, next) => { + try { + // ========== 3. دریافت پارامترهای صفحه‌بندی ========== + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const skip = (page - 1) * limit; + + // ========== 4. دریافت پارامترهای جستجو ========== + const search = req.query.search || ""; + const category = req.query.category || ""; + const minPrice = req.query.minPrice ? parseInt(req.query.minPrice) : 0; + const maxPrice = req.query.maxPrice + ? parseInt(req.query.maxPrice) + : 100000000; + const hasOffer = req.query.hasOffer === "true"; // تخفیف دار + const teacherName = req.query.teacherName || ""; + const type = req.query.type || ""; // normal, pro, legend + const status = req.query.status || "all"; // pending, accept, reject + const sortBy = req.query.sortBy || "createdAt"; + const sortOrder = req.query.sortOrder === "asc" ? 1 : -1; + const _id = req.query._id || ""; + // ========== 5. ساخت شرط جستجو (Query) ========== + let query = {}; + + // جستجو در نام دوره و کپشن + if (search) { + query.$or = [ + { cuorse_name: { $regex: search, $options: "i" } }, + { caption: { $regex: search, $options: "i" } }, + { teacher_name: { $regex: search, $options: "i" } }, + ]; + } + + // فیلتر بر اساس دسته بندی + if (category) { + query.category = category; + } + if (_id) { + query._id = _id; + } + // فیلتر بر اساس محدوده قیمت (تبدیل به عدد برای مقایسه) + if (minPrice > 0 || maxPrice < 100000000) { + query.price = { + $gte: minPrice.toString(), + $lte: maxPrice.toString(), + }; + } + + // فیلتر بر اساس تخفیف دار (offer وجود داشته باشد و از 0 بیشتر باشد) + if (hasOffer) { + query.offer = { $exists: true, $ne: "0", $ne: "" }; + } + + // فیلتر بر اساس نام مدرس + if (teacherName) { + query.teacher_name = { $regex: teacherName, $options: "i" }; + } + + // فیلتر بر اساس نوع دوره + if (type) { + query.type = type; + } + + // فیلتر بر اساس وضعیت + if (status !== "all") { + query.status = status; + } + + // ========== 6. دریافت آمار و اطلاعات ========== + // تعداد کل پکیج‌ها (بدون صفحه‌بندی) + const totalCourses = await CourseModel.countDocuments(query); + + // مجموع قیمت‌ها (تبدیل به عدد برای محاسبه) + const priceStats = await CourseModel.aggregate([ + { $match: query }, + { + $group: { + _id: null, + total: { $sum: { $toDouble: "$price" } }, + avg: { $avg: { $toDouble: "$price" } }, + min: { $min: { $toDouble: "$price" } }, + max: { $max: { $toDouble: "$price" } }, + }, + }, + ]); + + // تعداد دوره‌های با تخفیف + const offeredCourses = await CourseModel.countDocuments({ + ...query, + offer: { $exists: true, $ne: "0", $ne: "" }, + }); + + // تعداد دوره‌ها بر اساس نوع + const typeStats = await CourseModel.aggregate([ + { $match: query }, + { + $group: { + _id: "$type", + count: { $sum: 1 }, + }, + }, + ]); + + // ========== 7. دریافت پکیج‌ها با صفحه‌بندی ========== + const courses = await CourseModel.find(query) + .populate("user_id", "name email") // اطلاعات کاربر سازنده + .populate("likes", "name") // اطلاعات لایک‌ها + .sort({ [sortBy]: sortOrder }) + .skip(skip) + .limit(limit) + .lean(); + + // پردازش داده‌ها برای خروجی + const processedCourses = courses.map((course) => ({ + ...course, + priceNumber: parseInt(course.price) || 0, + offerNumber: parseInt(course.offer) || 0, + finalPrice: course.offer + ? (parseInt(course.price) * (100 - parseInt(course.offer))) / 100 + : parseInt(course.price), + likesCount: course.likes?.length || 0, + })); + + // ========== 8. محاسبات صفحه‌بندی ========== + const totalPages = Math.ceil(totalCourses / limit); + const hasNextPage = page < totalPages; + const hasPrevPage = page > 1; + + // محاسبه محدوده نمایش + const startItem = totalCourses === 0 ? 0 : (page - 1) * limit + 1; + const endItem = Math.min(page * limit, totalCourses); + + // ========== 9. ارسال پاسخ ========== + res.status(200).json({ + success: true, + message: "لیست پکیج‌ها با موفقیت دریافت شد", + data: { + courses: processedCourses, + stats: { + totalCourses, + offeredCourses, + averagePrice: Math.round(priceStats[0]?.avg || 0), + minPrice: priceStats[0]?.min || 0, + maxPrice: priceStats[0]?.max || 0, + totalPrice: priceStats[0]?.total || 0, + typeStats: typeStats, + }, + pagination: { + currentPage: page, + totalPages: totalPages, + totalItems: totalCourses, + itemsPerPage: limit, + hasNextPage: hasNextPage, + hasPrevPage: hasPrevPage, + nextPage: hasNextPage ? page + 1 : null, + prevPage: hasPrevPage ? page - 1 : null, + startItem: startItem, + endItem: endItem, + }, + filters: { + search: search, + category: category, + minPrice: minPrice, + maxPrice: maxPrice, + hasOffer: hasOffer, + teacherName: teacherName, + type: type, + sortBy: sortBy, + sortOrder: sortOrder === 1 ? "asc" : "desc", + status: status, + }, + }, + }); + } catch (error) { + console.error("Error in getCourses:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت پکیج‌ها", + error: error.message, + }); + } +}; + +const proPayment = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + const { courseId } = req.body; + + // ========== 1. بررسی وجود CoursePayment ========== + const coursePayment = await CoursePaymentModel.findOne({ + cuorse_type: "pro", + }); + + if (!coursePayment) { + return res.status(404).json({ + error: true, + message: "قیمت‌گذاری برای نوع پرو یافت نشد", + }); + } + + const price = coursePayment.price; + + // ========== 2. اعتبارسنجی قیمت ========== + if (!price || isNaN(Number(price)) || Number(price) <= 0) { + return res.status(422).json({ + error: true, + message: "مبلغ نامعتبر است", + }); + } + + // ========== 3. بررسی وجود دوره ========== + const course = await CourseModel.findById(courseId); + if (!course) { + return res.status(404).json({ + error: true, + message: "پکیجی یافت نشد", + }); + } + + // ========== 4. محاسبه مبلغ ========== + const amount = Number(price) * 10; + + // مبلغ باید حداقل 1000 تومان باشد (طبق قوانین زرین‌پال) + if (amount < 1000) { + return res.status(422).json({ + error: true, + message: "مبلغ باید حداقل 1000 تومان باشد", + }); + } + + const callbackUrl = `https://api.modstagram.com/api/v1/academy/academy/course/pro?courseId=${courseId}&userId=${userId}`; + + // ========== 5. درخواست به زرین‌پال ========== + const zarinpalRes = await axios.post( + "https://api.zarinpal.com/pg/v4/payment/request.json", + { + merchant_id: + process.env.ZARINPAL_MERCHANT_ID || + "c7c41e8a-918f-4741-bcd5-58f3bc51db73", + amount: Math.round(amount), // حتماً عدد صحیح باشد + description: `خرید اشتراک پرو برای ${course.cuorse_name}`, + callback_url: callbackUrl, + metadata: { + user_id: userId, + course_id: courseId, + }, + }, + { + headers: { "Content-Type": "application/json" }, + timeout: 10000, // 10 ثانیه تایم‌اوت + } + ); + + const result = zarinpalRes.data; + + // ========== 6. بررسی پاسخ زرین‌پال ========== + if (result.data && result.data.code === 100) { + // ذخیره اطلاعات پرداخت در دیتابیس (اختیاری) + await PaymentModel.create({ + authority: result.data.authority, + amount: amount, + user_id: userId, + course_id: courseId, + status: "pending", + type: "pro", + createdAt: new Date(), + }); + + return res.status(200).json({ + success: true, + authority: result.data.authority, + paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.data.authority}`, + }); + } + + // خطای زرین‌پال + console.error("ZarinPal error:", result); + return res.status(422).json({ + error: true, + message: result.errors?.message || "خطا در ارتباط با درگاه پرداخت", + details: result.errors, + }); + } catch (error) { + console.error( + "Error in proPayment:", + error.response?.data || error.message + ); + res.status(500).json({ + error: true, + message: "خطا در پردازش پرداخت", + details: error.response?.data || error.message, + }); + } +}; + +const handleCoursePaymentCallback = async (req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + + try { + const { Authority, Status, userId, courseId } = req.query; + + if (Status !== "OK") { + return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`); + } + + // ========== 1. پیدا کردن قیمت ========== + const coursePayment = await CoursePaymentModel.findOne({ + cuorse_type: "pro", + }); + + if (!coursePayment) { + return res.redirect( + `${process.env.APP_SITE}/offer/payment/failed?message=price_not_found` + ); + } + + const price = coursePayment.price; + const amount = Number(price) * 10; + + // ========== 2. تایید پرداخت با زرین‌پال ========== + const verify = await axios.post( + "https://api.zarinpal.com/pg/v4/payment/verify.json", + { + merchant_id: + process.env.ZARINPAL_MERCHANT_ID || + "c7c41e8a-918f-4741-bcd5-58f3bc51db73", + authority: Authority, + amount: Math.round(amount), + }, + { + headers: { "Content-Type": "application/json" }, + timeout: 10000, + } + ); + + const data = verify.data.data; + + // ========== 3. بررسی نتیجه تایید ========== + if (data.code === 100) { + // ثبت پرداخت موفق + await PaymentModel.create({ + amount: amount / 10, + status: "successful", + authority: Authority, + ref_id: data.ref_id, + user_id: userId, + course_id: courseId, + type: "course", + verifiedAt: new Date(), + }); + + // به روز رسانی نوع دوره به pro + const course = await CourseModel.findById(courseId); + if (course) { + course.type = "pro"; + await course.save(); + } + + // به روز رسانی وضعیت کاربر (اشتراک پرو) + await UserModel.findByIdAndUpdate(userId, { + $set: { + "proSubscription.active": true, + "proSubscription.courseId": courseId, + "proSubscription.startDate": new Date(), + "proSubscription.purchaseDate": new Date(), + }, + }); + + return res.redirect( + `${process.env.APP_SITE}/settings/academy/course?payment=success` + ); + } + + // پرداخت ناموفق + return res.redirect( + `${process.env.APP_SITE}/offer/payment/failed?code=${data.code}` + ); + } catch (err) { + console.error("Error in handleCoursePaymentCallback:", err); + return res.redirect( + `${process.env.APP_SITE}/offer/payment/failed?message=verification_error` + ); + } +}; + +const creatCourseVideo = async (req, res, next) => { + console.log("=== New request received ==="); + console.log("Headers:", req.headers); + console.log("Body:", req.body); + console.log("File:", req.file); + + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { courseId, is_free, file_name } = req.body; + const videoFile = req.file; + + if (!videoFile) { + return res.status(422).json({ + error: true, + message: "فایل ویدیو ارسال نشده است", + }); + } + + if (!courseId) { + return res.status(422).json({ + error: true, + message: "شناسه دوره الزامی است", + }); + } + + // مسیر ذخیره‌سازی نهایی (مشابه سیستم پست‌ها) + const finalVideoDir = path.join( + __dirname, + "../../../storage/courses/videos" + ); + const safeFileName = `course-${courseId}-${Date.now()}${path.extname( + videoFile.originalname + )}`; + const filePath = path.join(finalVideoDir, safeFileName); + + // انتقال فایل از محل موقت به محل نهایی + await fs.rename(videoFile.path, filePath); + + // ایجاد رکورد جدید در دیتابیس + const newAcademyContent = new AcademyContentModel({ + course_video: `/storage/posts/videos/${safeFileName}`, + type: "video", + is_free: is_free === "true" || is_free === true, + courseId, + file_name: file_name || videoFile.originalname, + }); + + await newAcademyContent.save(); + + const course = await CourseModel.findById(courseId); + if (!course) { + return res.status(404).json({ + success: false, + message: "پکیج پیدا نشد", + }); + } + + course.number_of_course_content = `${ + Number(course.number_of_course_content) + 1 + }`; + await course.save(); + + res.status(200).json({ + success: true, + message: "ویدیو با موفقیت ثبت شد", + data: newAcademyContent, + }); + } catch (error) { + console.error("Error in creatCourseVideo:", error); + res.status(500).json({ + success: false, + message: "خطا در ثبت ویدیو", + error: error.message, + }); + } +}; + +const creatCourseVideoBase64 = async (req, res, next) => { + console.log("=== Base64 Video Upload ==="); + console.log("Body:", Object.keys(req.body)); + + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { courseId, is_free, file_name, video_base64, mime_type } = req.body; + + // اعتبارسنجی + if (!video_base64) { + return res.status(422).json({ + error: true, + message: "فایل ویدیو ارسال نشده است", + }); + } + + if (!courseId) { + return res.status(422).json({ + error: true, + message: "شناسه دوره الزامی است", + }); + } + + // تبدیل Base64 به بافر + const base64Data = video_base64.split(",")[1] || video_base64; + const buffer = Buffer.from(base64Data, "base64"); + + console.log("Buffer size:", buffer.length, "bytes"); + + // ایجاد دایرکتوری اگر وجود ندارد + const finalVideoDir = path.join( + process.cwd(), + "storage", + "courses", + "videos" + ); + if (!fs.existsSync(finalVideoDir)) { + fs.mkdirSync(finalVideoDir, { recursive: true }); + } + + // تعیین پسوند فایل از mime_type + let extension = ".mp4"; + if (mime_type === "video/mp4") extension = ".mp4"; + else if (mime_type === "video/quicktime") extension = ".mov"; + else if (mime_type === "video/x-msvideo") extension = ".avi"; + else if (mime_type === "video/webm") extension = ".webm"; + + const safeFileName = `course-${courseId}-${Date.now()}${extension}`; + const filePath = path.join(finalVideoDir, safeFileName); + + // ذخیره فایل + await fs.writeFileSync(filePath, buffer); + + console.log("File saved:", filePath); + + // ایجاد رکورد در دیتابیس + const newAcademyContent = new AcademyContentModel({ + course_video: `/storage/courses/videos/${safeFileName}`, + type: "video", + is_free: is_free === "true" || is_free === true, + courseId, + file_name: file_name, + }); + + await newAcademyContent.save(); + + // به روز رسانی تعداد محتواهای دوره + const course = await CourseModel.findById(courseId); + if (course) { + course.number_of_course_content = `${ + Number(course.number_of_course_content) + 1 + }`; + await course.save(); + } + + res.status(200).json({ + success: true, + message: "ویدیو با موفقیت ثبت شد", + data: newAcademyContent, + }); + } catch (error) { + console.error("Error in creatCourseVideoBase64:", error); + res.status(500).json({ + success: false, + message: "خطا در ثبت ویدیو", + error: error.message, + }); + } +}; + +const getCourseContent = async (req, res, next) => { + try { + // ✅ اصلاح: دریافت courseId از query یا params + const courseId = req.query.courseId || req.params.courseId; + + // اعتبارسنجی courseId + if (!courseId) { + return res.status(422).json({ + error: true, + message: "شناسه دوره الزامی است", + }); + } + + // گرفتن پارامترهای صفحه‌بندی + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const skip = (page - 1) * limit; + + const academy = await CourseModel.findById(courseId); + if (!academy) { + return res.status(404).json({ + success: false, + message: "پکیج پیدا نشد", + }); + } + + // ✅ شرط جستجو - فقط دوره‌های تایید شده + const query = { + courseId: academy._id, + status: "accept", + }; + + // دریافت تعداد کل دوره‌های تایید شده + const totalCourses = await AcademyContentModel.countDocuments(query); + + // دریافت دوره‌های تایید شده با صفحه‌بندی + const courses = await AcademyContentModel.find(query) + .skip(skip) + .limit(limit); + + // ✅ اگر دوره‌ای وجود نداشت، خطا نده، بلکه آرایه خالی برگردون + if (!courses || courses.length === 0) { + return res.status(200).json({ + success: true, + message: "هیچ محتوایی یافت نشد", + data: { + courses: [], + pagination: { + currentPage: page, + totalPages: 0, + totalItems: 0, + itemsPerPage: limit, + hasNextPage: false, + hasPrevPage: false, + }, + }, + }); + } + + // محاسبات صفحه‌بندی + const totalPages = Math.ceil(totalCourses / limit); + const hasNextPage = page < totalPages; + const hasPrevPage = page > 1; + + res.status(200).json({ + success: true, + message: "لیست محتواها با موفقیت دریافت شد", + data: { + courses: courses, + pagination: { + currentPage: page, + totalPages: totalPages, + totalItems: totalCourses, + itemsPerPage: limit, + hasNextPage: hasNextPage, + hasPrevPage: hasPrevPage, + nextPage: hasNextPage ? page + 1 : null, + prevPage: hasPrevPage ? page - 1 : null, + }, + }, + }); + } catch (error) { + console.error("Error in getCourseContent:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت محتواها", + error: error.message, + }); + } +}; +// لایک کردن محتوا +const likeCourseContent = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + if (!userId) { + return res.status(422).json({ + error: true, + message: "برای لایک کردن وارد حساب کاربری خود شوید", + }); + } + + const { courseId } = req.body; + + // بررسی وجود دوره + const course = await CourseModel.findById(courseId); + if (!course) { + return res.status(404).json({ + success: false, + message: "پکیج پیدا نشد", + }); + } + + // بررسی اینکه آیا قبلاً لایک کرده است + const existingLike = await AcademyLikeModel.findOne({ + user_id: userId, + course_id: courseId, + }); + + if (existingLike) { + return res.status(400).json({ + success: false, + message: "شما قبلاً این دوره را لایک کرده‌اید", + }); + } + + // افزایش تعداد لایک‌های دوره + course.likes = (course.likes || 0) + 1; + await course.save(); + + // ایجاد رکورد لایک + await AcademyLikeModel.create({ + user_id: userId, + course_id: courseId, + 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: "دوره با موفقیت لایک شد", + isLiked: true, + likesCount: course.likes, + }); + } catch (error) { + console.error("Error in likeCourseContent:", error); + res.status(500).json({ + success: false, + message: "خطا در لایک کردن محتوا", + error: error.message, + }); + } +}; + +// دیسلایک کردن محتوا (حذف لایک) +const dontLikeCourseContent = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { courseId } = req.body; + + // بررسی وجود دوره + const course = await CourseModel.findById(courseId); + if (!course) { + return res.status(404).json({ + success: false, + message: "پکیج پیدا نشد", + }); + } + + // پیدا کردن و حذف لایک + const deletedLike = await AcademyLikeModel.findOneAndDelete({ + user_id: userId, + course_id: courseId, + }); + + if (!deletedLike) { + return res.status(404).json({ + success: false, + message: "شما این دوره را لایک نکرده‌اید", + }); + } + + // کاهش تعداد لایک‌های دوره + course.likes = Math.max((course.likes || 0) - 1, 0); // جلوگیری از منفی شدن + await course.save(); + + res.status(200).json({ + success: true, + message: "لایک دوره با موفقیت حذف شد", + isLiked: false, + likesCount: course.likes, + }); + } catch (error) { + console.error("Error in dontLikeCourseContent:", error); + res.status(500).json({ + success: false, + message: "خطا در حذف لایک محتوا", + error: error.message, + }); + } +}; + +// بررسی وضعیت لایک (آیا کاربر لایک کرده است یا نه) +const isLikeCourseContent = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { courseId } = req.body; + + // پیدا کردن لایک + const academyLike = await AcademyLikeModel.findOne({ + user_id: userId, + course_id: courseId, + }); + + res.status(200).json({ + success: true, + isLiked: !!academyLike, // تبدیل به boolean + }); + } catch (error) { + console.error("Error in isLikeCourseContent:", error); + res.status(500).json({ + success: false, + message: "خطا در بررسی وضعیت لایک", + error: error.message, + }); + } +}; + +// دریافت تعداد لایک‌های یک دوره +const getCourseLikesCount = async (req, res, next) => { + try { + const { courseId } = req.params; + + const course = await CourseModel.findById(courseId); + if (!course) { + return res.status(404).json({ + success: false, + message: "دوره پیدا نشد", + }); + } + + const likesCount = await AcademyLikeModel.countDocuments({ + course_id: courseId, + }); + + res.status(200).json({ + success: true, + likesCount: likesCount, + courseLikes: course.likes || 0, + }); + } catch (error) { + console.error("Error in getCourseLikesCount:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت تعداد لایک‌ها", + error: error.message, + }); + } +}; + +// دریافت همه دوره‌هایی که کاربر لایک کرده است +const getUserLikedCourses = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const skip = (page - 1) * limit; + + const likes = await AcademyLikeModel.find({ user_id: userId }) + .populate("course_id") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit); + + const total = await AcademyLikeModel.countDocuments({ user_id: userId }); + + res.status(200).json({ + success: true, + data: likes, + pagination: { + currentPage: page, + totalPages: Math.ceil(total / limit), + totalItems: total, + itemsPerPage: limit, + }, + }); + } catch (error) { + console.error("Error in getUserLikedCourses:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت دوره‌های لایک شده", + error: error.message, + }); + } +}; + +// ==================== ساخت کامنت جدید ==================== +const createComment = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) + return res.status(401).json({ error: true, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { course_id, comment, rate } = req.body; + + // اعتبارسنجی + if (!course_id || !comment) { + return res.status(422).json({ + error: true, + message: "شناسه دوره و متن کامنت الزامی است", + }); + } + + // بررسی وجود دوره + const course = await CourseModel.findById(course_id); + if (!course) { + return res.status(404).json({ + error: true, + message: "دوره پیدا نشد", + }); + } + + const parsedRate = Number(rate) || 0; + const existingRatedComment = await CourseComment.findOne({ + user_id: userId, + course_id, + rate: { $gt: 0 }, + }); + + if (existingRatedComment && parsedRate > 0) { + return res.status(400).json({ + error: true, + message: "شما قبلاً به این دوره امتیاز داده‌اید.", + }); + } + + const finalRate = existingRatedComment ? 0 : parsedRate; + + // ایجاد کامنت جدید + const newComment = await CourseComment.create({ + user_id: userId, + course_id: course_id, + comment: comment, + rate: finalRate, + status: "accepted", // با توجه به مدل شما که default: 'accepted' است + }); + + // populate کردن اطلاعات کاربر + const populatedComment = await CourseComment.findById( + 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: "کامنت با موفقیت ثبت شد", + data: populatedComment, + }); + } catch (error) { + console.error("Error in createComment:", error); + res.status(500).json({ + success: false, + message: "خطا در ثبت کامنت", + error: error.message, + }); + } +}; + +// ==================== دریافت تعداد کل کامنت‌های یک دوره ==================== +const getTotalCommentsCount = async (req, res) => { + try { + const { courseId } = req.params; + const { status } = req.query; // اختیاری: فقط کامنت‌های با وضعیت خاص + + const query = { course_id: courseId }; + if (status) { + query.status = status; + } + + const totalComments = await CourseComment.countDocuments(query); + + // محاسبه میانگین امتیازات + const averageRate = await CourseComment.aggregate([ + { $match: { course_id: courseId, status: "accepted" } }, + { $group: { _id: null, avgRate: { $avg: "$rate" } } }, + ]); + + res.status(200).json({ + success: true, + data: { + totalComments: totalComments, + averageRate: averageRate[0]?.avgRate || 0, + courseId: courseId, + }, + }); + } catch (error) { + console.error("Error in getTotalCommentsCount:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت تعداد کامنت‌ها", + error: error.message, + }); + } +}; + +// ==================== دریافت کامنت‌ها با فیلترهای مختلف ==================== +const getCourseComments = async (req, res) => { + try { + const { courseId } = req.params; + const { + page = 1, + limit = 10, + status = "accepted", // 'accepted', 'pending', 'rejected', 'all' + minRate, + maxRate, + sortBy = "createdAt", + sortOrder = "desc", + userId, // اختیاری: کامنت‌های یک کاربر خاص + } = req.query; + + // ساخت شرط جستجو + const query = { course_id: courseId }; + + // فیلتر بر اساس وضعیت + if (status !== "all") { + query.status = status; + } + + // فیلتر بر اساس امتیاز + if (minRate || maxRate) { + query.rate = {}; + if (minRate) query.rate.$gte = parseInt(minRate); + if (maxRate) query.rate.$lte = parseInt(maxRate); + } + + // فیلتر بر اساس کاربر خاص + if (userId) { + query.user_id = userId; + } + + // تنظیمات مرتب‌سازی + const sortOptions = {}; + sortOptions[sortBy] = sortOrder === "desc" ? -1 : 1; + + // دریافت کامنت‌ها با pagination + const skip = (parseInt(page) - 1) * parseInt(limit); + + const [comments, totalComments] = await Promise.all([ + CourseComment.find(query) + .sort(sortOptions) + .skip(skip) + .limit(parseInt(limit)) + .populate("user_id", "name username profileImage"), + CourseComment.countDocuments(query), + ]); + + // محاسبه آمار کامنت‌ها + const stats = await CourseComment.aggregate([ + { $match: { course_id: courseId, status: "accepted" } }, + { + $group: { + _id: null, + totalAccepted: { $sum: 1 }, + averageRate: { $avg: "$rate" }, + rateDistribution: { + $push: "$rate", + }, + }, + }, + ]); + + const totalPages = Math.ceil(totalComments / parseInt(limit)); + + res.status(200).json({ + success: true, + message: "لیست کامنت‌ها با موفقیت دریافت شد", + data: { + comments: comments, + pagination: { + currentPage: parseInt(page), + totalPages: totalPages, + totalItems: totalComments, + itemsPerPage: parseInt(limit), + hasNextPage: parseInt(page) < totalPages, + hasPrevPage: parseInt(page) > 1, + }, + stats: { + totalComments: totalComments, + acceptedComments: stats[0]?.totalAccepted || 0, + averageRate: Math.round((stats[0]?.averageRate || 0) * 10) / 10, + rateDistribution: stats[0]?.rateDistribution || [], + }, + }, + }); + } catch (error) { + console.error("Error in getCourseComments:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت کامنت‌ها", + error: error.message, + }); + } +}; +// ==================== ویرایش وضعیت کامنت (برای همه قابل استفاده است) ==================== +const updateCommentStatus = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) + return res.status(401).json({ error: true, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { commentId } = req.params; + const { status } = req.body; // 'pending', 'accepted', 'rejected' + + // اعتبارسنجی وضعیت + if (!["pending", "accepted", "rejected"].includes(status)) { + return res.status(422).json({ + error: true, + message: + "وضعیت نامعتبر است. وضعیت باید یکی از این موارد باشد: pending, accepted, rejected", + }); + } + + // پیدا کردن کامنت + const comment = await CourseComment.findById(commentId); + if (!comment) { + return res.status(404).json({ + error: true, + message: "کامنت پیدا نشد", + }); + } + + // بررسی دسترسی: یا خود صاحب کامنت است یا هر کاربر دیگری (طبق درخواست شما که همه بتوانند) + // اگر می‌خواهید فقط ادمین بتواند وضعیت را تغییر دهد، این بخش را تغییر دهید + const user = await UserModel.findById(userId); + const isOwner = comment.user_id.toString() === userId; + const isAdmin = user?.role === "admin"; + + if (!isOwner && !isAdmin) { + return res.status(403).json({ + error: true, + message: "شما اجازه تغییر وضعیت این کامنت را ندارید", + }); + } + + // به روز رسانی وضعیت + comment.status = status; + await comment.save(); + + // اگر کامنت تایید شد و امتیاز داشت، میانگین امتیاز دوره را به روز کن + if (status === "accepted" && comment.rate > 0) { + const averageRate = await CourseComment.aggregate([ + { $match: { course_id: comment.course_id, status: "accepted" } }, + { $group: { _id: null, avgRate: { $avg: "$rate" } } }, + ]); + + await CourseModel.findByIdAndUpdate(comment.course_id, { + averageRate: averageRate[0]?.avgRate || 0, + }); + } + + res.status(200).json({ + success: true, + message: `وضعیت کامنت با موفقیت به ${ + status === "accepted" + ? "تایید شده" + : status === "rejected" + ? "رد شده" + : "در انتظار" + } تغییر یافت`, + data: comment, + }); + } catch (error) { + console.error("Error in updateCommentStatus:", error); + res.status(500).json({ + success: false, + message: "خطا در تغییر وضعیت کامنت", + error: error.message, + }); + } +}; + +// ==================== دریافت کامنت‌های یک کاربر خاص ==================== +const getUserComments = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) + return res.status(401).json({ error: true, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { page = 1, limit = 10, status = "all", courseId } = req.query; + + const query = { user_id: userId }; + + if (status !== "all") { + query.status = status; + } + + if (courseId) { + query.course_id = courseId; + } + + const skip = (parseInt(page) - 1) * parseInt(limit); + + const [comments, totalComments] = await Promise.all([ + CourseComment.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)) + .populate("course_id", "cuorse_name packageImage"), + CourseComment.countDocuments(query), + ]); + + const totalPages = Math.ceil(totalComments / parseInt(limit)); + + res.status(200).json({ + success: true, + data: { + comments: comments, + pagination: { + currentPage: parseInt(page), + totalPages: totalPages, + totalItems: totalComments, + itemsPerPage: parseInt(limit), + }, + }, + }); + } catch (error) { + console.error("Error in getUserComments:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت کامنت‌های کاربر", + error: error.message, + }); + } +}; + +// ==================== حذف کامنت (اختیاری) ==================== +const deleteComment = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) + return res.status(401).json({ error: true, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { commentId } = req.params; + + const comment = await CourseComment.findById(commentId); + if (!comment) { + return res.status(404).json({ + error: true, + message: "کامنت پیدا نشد", + }); + } + + // بررسی دسترسی: یا خود صاحب کامنت است یا ادمین + const user = await UserModel.findById(userId); + const isOwner = comment.user_id.toString() === userId; + const isAdmin = user?.role === "admin"; + + if (!isOwner && !isAdmin) { + return res.status(403).json({ + error: true, + message: "شما اجازه حذف این کامنت را ندارید", + }); + } + + await comment.deleteOne(); + + res.status(200).json({ + success: true, + message: "کامنت با موفقیت حذف شد", + }); + } catch (error) { + console.error("Error in deleteComment:", error); + res.status(500).json({ + success: false, + message: "خطا در حذف کامنت", + error: error.message, + }); + } +}; +// academyController.js + +const coursePayment = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) { + return res.status(401).json({ + error: true, + message: "لطفا وارد حساب کاربری خود شوید", + }); + } + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + const { courseId } = req.body; + + // ========== 1. بررسی وجود دوره ========== + const course = await CourseModel.findById(courseId); + if (!course) { + return res.status(404).json({ + error: true, + message: "دوره مورد نظر یافت نشد", + }); + } + + // ========== 2. بررسی خرید قبلی ========== + const existingPurchase = await IsPaymentCourse.findOne({ + user_id: userId, + course_id: course._id, + }); + + if (existingPurchase) { + return res.status(400).json({ + error: true, + message: "شما قبلاً این دوره را خریداری کرده‌اید", + }); + } + + // ========== 3. دریافت نرخ مالیات ========== + let taxRate = 9; // پیش‌فرض 9 درصد + const tax = await Tax.findOne({ type: "course" }); + if (tax && tax.tax) { + taxRate = Number(tax.tax); + } + + // ========== 4. محاسبه قیمت ========== + const offerNum = Number(course.offer) || 0; + const priceNum = Number(course.price); + + // قیمت پس از تخفیف + const discountedPrice = (priceNum / 100) * (100 - offerNum); + + // محاسبه مالیات + const taxAmount = discountedPrice * (taxRate / 100); + + // قیمت نهایی با مالیات (تومان) + const finalPrice = discountedPrice; + const finalTaxPrice = (discountedPrice / 100) * (100 - taxRate); + + // ========== 5. اعتبارسنجی قیمت ========== + if (!finalPrice || isNaN(finalPrice) || finalPrice <= 0) { + return res.status(422).json({ + error: true, + message: "مبلغ نامعتبر است", + }); + } + + // تبدیل به ریال (ضرب در 10 چون هر تومان = 10 ریال) + const amountInRials = Math.round(finalPrice * 10); + + // مبلغ باید حداقل 1000 تومان باشد (10000 ریال) + if (amountInRials < 10000) { + return res.status(422).json({ + error: true, + message: "مبلغ باید حداقل 1000 تومان باشد", + }); + } + + // ========== 6. ساخت کال‌بک URL ========== + const callbackUrl = `https://api.modstagram.com/api/v1/academy/academy/course/payment/verify?courseId=${courseId}&userId=${userId}`; + + // ========== 7. درخواست به زرین‌پال ========== + const zarinpalRes = await axios.post( + "https://api.zarinpal.com/pg/v4/payment/request.json", + { + merchant_id: process.env.ZARINPAL_MERCHANT_ID, + amount: amountInRials, + description: `خرید دوره ${course.cuorse_name}`, + callback_url: callbackUrl, + metadata: { + user_id: userId, + course_id: courseId, + price: finalPrice, + tax_rate: taxRate, + }, + }, + { + headers: { "Content-Type": "application/json" }, + timeout: 90000, + } + ); + + const result = zarinpalRes.data; + + // ========== 8. بررسی پاسخ زرین‌پال ========== + if (result.data && result.data.code === 100) { + // ذخیره اطلاعات پرداخت در حالت pending + await AcademyPaymentModel.create({ + price: finalPrice, + discountAmount: priceNum - discountedPrice, + taxAmount: finalTaxPrice, + taxRate: taxRate, + course_name: course.cuorse_name, + course_id: course._id, + academy_id: course.academyId, + user_id: userId, + payment_authority: result.data.authority, + status: "pending", + }); + + return res.status(200).json({ + success: true, + authority: result.data.authority, + paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.data.authority}`, + }); + } + + // خطای زرین‌پال + console.error("ZarinPal error:", result); + return res.status(422).json({ + error: true, + message: result.errors?.message || "خطا در ارتباط با درگاه پرداخت", + details: result.errors, + }); + } catch (error) { + console.error( + "Error in coursePayment:", + error.response?.data || error.message + ); + res.status(500).json({ + error: true, + message: "خطا در پردازش پرداخت", + details: error.response?.data || error.message, + }); + } +}; + +const CoursePaymentCallback = async (req, res) => { + try { + const { Authority, Status, courseId, userId } = req.query; + + // ========== 1. بررسی وضعیت پرداخت ========== + if (Status !== "OK") { + return res.redirect( + `${process.env.APP_SITE}/offer/payment/failed?message=payment_canceled` + ); + } + + // ========== 2. پیدا کردن دوره ========== + const course = await CourseModel.findById(courseId); + if (!course) { + return res.redirect( + `${process.env.APP_SITE}/offer/payment/failed?message=course_not_found` + ); + } + + // ========== 3. پیدا کردن رکورد پرداخت ========== + const paymentRecord = await AcademyPaymentModel.findOne({ + payment_authority: Authority, + course_id: courseId, + user_id: userId, + }); + + if (!paymentRecord) { + return res.redirect( + `${process.env.APP_SITE}/offer/payment/failed?message=payment_not_found` + ); + } + + // ========== 4. محاسبه مبلغ ========== + const amountInRials = Math.round(paymentRecord.price * 10); + + // ========== 5. تایید پرداخت با زرین‌پال ========== + const verify = await axios.post( + "https://api.zarinpal.com/pg/v4/payment/verify.json", + { + merchant_id: process.env.ZARINPAL_MERCHANT_ID, + authority: Authority, + amount: amountInRials, + }, + { + headers: { "Content-Type": "application/json" }, + timeout: 10000, + } + ); + + const data = verify.data.data; + + // ========== 6. بررسی نتیجه تایید ========== + if (data.code === 100) { + // به‌روزرسانی رکورد پرداخت + await AcademyPaymentModel.findByIdAndUpdate(paymentRecord._id, { + status: "success", + payment_ref_id: data.ref_id, + }); + + // ثبت خرید کاربر + await IsPaymentCourse.create({ + user_id: userId, + course_id: course._id, + price_paid: paymentRecord.price, + purchased_at: new Date(), + }); + + // هدایت به صفحه موفقیت + return res.redirect( + `${process.env.APP_SITE}/academy/${course._id}/success?payment=success` + ); + } + + // پرداخت ناموفق + await AcademyPaymentModel.findByIdAndUpdate(paymentRecord._id, { + status: "failed", + }); + + return res.redirect( + `${process.env.APP_SITE}/offer/payment/failed?code=${data.code}` + ); + } catch (err) { + console.error("Error in CoursePaymentCallback:", err); + return res.redirect( + `${process.env.APP_SITE}/offer/payment/failed?message=verification_error` + ); + } +}; +const getUserPurchasedCourses = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) + return res.status(401).json({ error: true, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const user_id = decodedToken.id; + + const { page = 1, limit = 10 } = req.query; + + // پیدا کردن تمام رکوردهای پرداخت برای این کاربر + const payments = await IsPaymentCourse.find({ user_id: user_id }) + .sort({ createdAt: -1 }) // جدیدترین اول + .skip((page - 1) * limit) + .limit(parseInt(limit)); + + // گرفتن اطلاعات کامل دوره‌ها + const courseIds = payments.map((payment) => payment.course_id); + + const courses = await CourseModel.find({ + _id: { $in: courseIds }, + status: "accept", // فقط دوره‌های تایید شده + }); + + // محاسبه تعداد کل + const total = await IsPaymentCourse.countDocuments({ user_id: user_id }); + + res.status(200).json({ + success: true, + data: { + courses: courses, + pagination: { + total, + page: parseInt(page), + pages: Math.ceil(total / limit), + limit: parseInt(limit), + }, + }, + }); + } catch (error) { + console.error("Error in getUserPurchasedCourses:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت دوره‌های خریداری شده", + error: error.message, + }); + } +}; + +// API با populate (روش بهتر) +const getUserPurchasedCoursesWithPopulate = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) + return res.status(401).json({ error: true, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const user_id = decodedToken.id; + + const { page = 1, limit = 10 } = req.query; + + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: { createdAt: -1 }, + populate: { + path: "course_id", + model: "Course", + match: { status: "accept" }, // فقط دوره‌های تایید شده + }, + }; + + const result = await IsPaymentCourse.paginate( + { user_id: user_id }, + options + ); + + // فیلتر کردن دوره‌هایی که populate شده‌اند و وجود دارند + const validCourses = result.docs + .filter((doc) => doc.course_id !== null) + .map((doc) => doc.course_id); + + res.status(200).json({ + success: true, + data: { + courses: validCourses, + pagination: { + total: result.totalDocs, + page: result.page, + totalPages: result.totalPages, + limit: result.limit, + hasNextPage: result.hasNextPage, + hasPrevPage: result.hasPrevPage, + }, + }, + }); + } catch (error) { + console.error("Error:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت دوره‌های خریداری شده", + }); + } +}; + +const getAllAcademyPayments = async (req, res) => { + try { + // 1. بررسی توکن + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) { + return res.status(401).json({ + success: false, + message: "دسترسی غیرمجاز", + }); + } + + // 2. verify توکن + let decodedToken; + try { + decodedToken = jwt.verify(token, process.env.APP_SECRET); + } catch (jwtError) { + return res.status(401).json({ + success: false, + message: "توکن نامعتبر است", + }); + } + + const userId = decodedToken.id; + + // 3. پیدا کردن آکادمی + const academy = await AcademyModel.findOne({ userId: userId }); + + if (!academy) { + return res.status(404).json({ + success: false, + message: "آکادمی برای این کاربر یافت نشد", + }); + } + + const academy_id = academy._id; + const { page = 1, limit = 10, status } = req.query; + + // 4. ساخت فیلتر + let filter = { academy_id: academy_id }; + + if ( + status && + ["pending", "success", "failed", "settled"].includes(status) + ) { + filter.status = status; + } + + // 5. تنظیمات صفحه‌بندی + const pageNum = parseInt(page); + const limitNum = parseInt(limit); + const skip = (pageNum - 1) * limitNum; + + const options = { + page: pageNum, + limit: limitNum, + sort: { createdAt: -1 }, + populate: [ + { + path: "course_id", + model: "Course", + select: "cuorse_name price course_image category teacher_name", + }, + { + path: "user_id", + model: "User", + select: "user_name profile_image email phone", + }, + ], + }; + + // 6. دریافت پرداخت‌ها + const result = await AcademyPaymentModel.paginate(filter, options); + + // 7. محاسبه آمار دقیق‌تر + let totalAmount = 0; + let pendingCount = 0; + let acceptedCount = 0; + + if (result.docs && Array.isArray(result.docs)) { + totalAmount = result.docs.reduce((sum, p) => { + const price = parseInt(p.taxAmount) || 0; + return sum + price; + }, 0); + + pendingCount = result.docs.filter((p) => p.status === "pending").length; + acceptedCount = result.docs.filter((p) => p.status === "accept").length; + } + + // 8. پاسخ نهایی + res.status(200).json({ + success: true, + data: { + academy: { + _id: academy._id, + academy_name: academy.academy_name, + academy_image: academy.academy_image, + bio: academy.bio, + }, + payments: result.docs || [], + pagination: { + total: result.totalDocs || 0, + page: result.page || pageNum, + totalPages: result.totalPages || 1, + limit: result.limit || limitNum, + hasNextPage: result.hasNextPage || false, + hasPrevPage: result.hasPrevPage || false, + }, + stats: { + totalAmount: totalAmount, + pendingCount: pendingCount, + acceptedCount: acceptedCount, + }, + }, + }); + } catch (error) { + console.error("Error in getAllAcademyPayments:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت لیست پرداخت‌ها", + error: process.env.NODE_ENV === "development" ? error.message : undefined, + }); + } +}; +const checkCoursePurchase = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) + return res.status(401).json({ error: true, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + const { courseId } = req.params; + + // بررسی وجود کاربر و دوره + if (!userId || !courseId) { + return res.status(400).json({ + success: false, + message: "userId و courseId الزامی هستند", + }); + } + + // جستجو در دیتابیس + const purchase = await IsPaymentCourse.findOne({ + user_id: userId, + course_id: courseId, + }); + + // نتیجه + if (purchase) { + return res.status(200).json({ + success: true, + isPurchased: true, + message: "این دوره قبلاً خریداری شده است", + purchaseData: purchase, + }); + } else { + return res.status(200).json({ + success: true, + isPurchased: false, + message: "این دوره خریداری نشده است", + }); + } + } catch (error) { + console.error("Error in checkCoursePurchase:", error); + res.status(500).json({ + success: false, + message: "خطا در بررسی خرید دوره", + error: error.message, + }); + } +}; + +const getAcademyCourse = async (req, res, next) => { + try { + const { Id } = req.params; + + // گرفتن پارامترهای صفحه‌بندی + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const skip = (page - 1) * limit; + + const academy = await AcademyModel.findById(Id); + if (!academy) { + return res.status(404).json({ + success: false, + message: "آکادمی پیدا نشد", + }); + } + + // ✅ شرط جستجو - فقط دوره‌های تایید شده + const query = { + academyId: academy._id, + status: "accept", + }; + + // دریافت تعداد کل دوره‌های تایید شده + const totalCourses = await CourseModel.countDocuments(query); + + // دریافت دوره‌های تایید شده با صفحه‌بندی + const courses = await CourseModel.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit); + + // ✅ اگر دوره‌ای وجود نداشت، خطا نده، بلکه آرایه خالی برگردون + if (!courses || courses.length === 0) { + return res.status(200).json({ + success: true, + message: "هیچ دوره تایید شده‌ای یافت نشد", + data: { + courses: [], + pagination: { + currentPage: page, + totalPages: 0, + totalItems: 0, + itemsPerPage: limit, + hasNextPage: false, + hasPrevPage: false, + nextPage: null, + prevPage: null, + }, + }, + }); + } + + // محاسبات صفحه‌بندی + const totalPages = Math.ceil(totalCourses / limit); + const hasNextPage = page < totalPages; + const hasPrevPage = page > 1; + + res.status(200).json({ + success: true, + message: "لیست پکیج‌ها با موفقیت دریافت شد", + data: { + courses: courses, + pagination: { + currentPage: page, + totalPages: totalPages, + totalItems: totalCourses, + itemsPerPage: limit, + hasNextPage: hasNextPage, + hasPrevPage: hasPrevPage, + nextPage: hasNextPage ? page + 1 : null, + prevPage: hasPrevPage ? page - 1 : null, + }, + }, + }); + } catch (error) { + console.error("Error:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت پکیج‌ها", + error: error.message, + }); + } +}; + +// ==================== مدیریت پرداخت‌ها ==================== +const getAllPayments = async (req, res) => { + try { + const { page = 1, limit = 20, status, startDate, endDate } = req.query; + const skip = (parseInt(page) - 1) * parseInt(limit); + + let query = {}; + if (status) query.status = status; + if (startDate && endDate) { + query.createdAt = { + $gte: new Date(startDate), + $lte: new Date(endDate), + }; + } + + const payments = await AcademyPaymentModel.find(query) + .populate("user_id", "user_name profile_image email") + .populate("course_id", "cuorse_name price course_image") + .populate("academy_id", "academy_name academy_image sheba") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)); + + const total = await AcademyPaymentModel.countDocuments(query); + + // آمار کلی پرداخت‌ها + const stats = await AcademyPaymentModel.aggregate([ + { $match: query }, + { + $group: { + _id: null, + totalAmount: { $sum: { $toDouble: "$price" } }, + totalCount: { $sum: 1 }, + pendingCount: { + $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, + }, + settledCount: { + $sum: { $cond: [{ $eq: ["$status", "settled"] }, 1, 0] }, + }, + successCount: { + $sum: { $cond: [{ $eq: ["$status", "success"] }, 1, 0] }, + }, + }, + }, + ]); + + res.status(200).json({ + success: true, + data: { + payments, + stats: stats[0] || { + totalAmount: 0, + totalCount: 0, + pendingCount: 0, + settledCount: 0, + successCount: 0, + }, + pagination: { + currentPage: parseInt(page), + totalPages: Math.ceil(total / parseInt(limit)), + totalItems: total, + itemsPerPage: parseInt(limit), + }, + }, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در دریافت پرداخت‌ها" }); + } +}; + +const updatePaymentStatus = async (req, res) => { + try { + const { paymentId, status } = req.body; + + const payment = await AcademyPaymentModel.findByIdAndUpdate( + paymentId, + { status, updatedAt: new Date() }, + { new: true } + ); + + if (!payment) { + return res + .status(404) + .json({ success: false, message: "پرداخت یافت نشد" }); + } + + res.status(200).json({ + success: true, + message: "وضعیت پرداخت با موفقیت تغییر کرد", + data: payment, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در تغییر وضعیت پرداخت" }); + } +}; + +// ==================== مدیریت دوره‌ها ==================== +const getAllCourses = async (req, res) => { + try { + const { page = 1, limit = 20, status, type, category, search } = req.query; + const skip = (parseInt(page) - 1) * parseInt(limit); + + let query = {}; + if (status) query.status = status; + if (type) query.type = type; + if (category) query.category = category; + if (search) { + query.$or = [ + { cuorse_name: { $regex: search, $options: "i" } }, + { caption: { $regex: search, $options: "i" } }, + { teacher_name: { $regex: search, $options: "i" } }, + ]; + } + + const courses = await CourseModel.find(query) + .populate("academyId", "academy_name academy_image") + .populate("user_id", "user_name profile_image") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)); + + const total = await CourseModel.countDocuments(query); + + // آمار دوره‌ها + const stats = await CourseModel.aggregate([ + { $match: query }, + { + $group: { + _id: null, + totalCourses: { $sum: 1 }, + totalPrice: { $sum: { $toDouble: "$price" } }, + normalCount: { + $sum: { $cond: [{ $eq: ["$type", "normal"] }, 1, 0] }, + }, + proCount: { $sum: { $cond: [{ $eq: ["$type", "pro"] }, 1, 0] } }, + legendCount: { + $sum: { $cond: [{ $eq: ["$type", "legend"] }, 1, 0] }, + }, + pendingCount: { + $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, + }, + acceptCount: { + $sum: { $cond: [{ $eq: ["$status", "accept"] }, 1, 0] }, + }, + }, + }, + ]); + + res.status(200).json({ + success: true, + data: { + courses, + stats: stats[0] || {}, + pagination: { + currentPage: parseInt(page), + totalPages: Math.ceil(total / parseInt(limit)), + totalItems: total, + itemsPerPage: parseInt(limit), + }, + }, + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در دریافت دوره‌ها" }); + } +}; + +const updateCourseStatus = async (req, res) => { + try { + const { courseId, status, type } = req.body; + + console.log(courseId, status, type); + + const course = await CourseModel.findById(courseId); + + if (!course) { + return res.status(404).json({ success: false, message: "دوره یافت نشد" }); + } + + if (status) course.status = status; + if (type) course.type = type; + await course.save(); + + res.status(200).json({ + success: true, + message: "دوره با موفقیت به‌روزرسانی شد", + data: course, + courseId: courseId, + status: status, + type: type, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در به‌روزرسانی دوره" }); + } +}; + +const admindeleteCourse = async (req, res) => { + try { + const { courseId } = req.params; + + const course = await CourseModel.findByIdAndDelete(courseId); + + if (!course) { + return res.status(404).json({ success: false, message: "دوره یافت نشد" }); + } + + // حذف تمام پرداخت‌های مرتبط با این دوره + await AcademyPaymentModel.deleteMany({ course_id: courseId }); + + res.status(200).json({ + success: true, + message: "دوره با موفقیت حذف شد", + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در حذف دوره" }); + } +}; + +// ==================== مدیریت دوره‌های خریداری شده ==================== +const getAllPurchasedCourses = async (req, res) => { + try { + const { page = 1, limit = 20, userId, courseId, status } = req.query; + const skip = (parseInt(page) - 1) * parseInt(limit); + + let query = {}; + if (userId) query.user_id = userId; + if (courseId) query.course_id = courseId; + if (status) query.status = status; + + const purchases = await IsPaymentCourse.find(query) + .populate("user_id", "user_name profile_image email phone") + .populate("course_id", "cuorse_name price course_image teacher_name") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)); + + const total = await IsPaymentCourse.countDocuments(query); + + // آمار خریدها + const stats = await IsPaymentCourse.aggregate([ + { $match: query }, + { + $group: { + _id: null, + totalPurchases: { $sum: 1 }, + uniqueUsers: { $addToSet: "$user_id" }, + }, + }, + ]); + + res.status(200).json({ + success: true, + data: { + purchases, + stats: { + totalPurchases: stats[0]?.totalPurchases || 0, + uniqueUsers: stats[0]?.uniqueUsers?.length || 0, + }, + pagination: { + currentPage: parseInt(page), + totalPages: Math.ceil(total / parseInt(limit)), + totalItems: total, + itemsPerPage: parseInt(limit), + }, + }, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در دریافت دوره‌های خریداری شده" }); + } +}; + +// ==================== مدیریت آکادمی‌ها ==================== +const getAllAcademies = async (req, res) => { + try { + const { page = 1, limit = 20, search } = req.query; + const skip = (parseInt(page) - 1) * parseInt(limit); + + let query = {}; + if (search) { + query.academy_name = { $regex: search, $options: "i" }; + } + + const academies = await AcademyModel.find(query) + .populate("userId", "user_name profile_image email") + .sort({ createdAt: -1 }) + .skip(skip) + .limit(parseInt(limit)); + + const total = await AcademyModel.countDocuments(query); + + // آمار آکادمی‌ها + const stats = await AcademyModel.aggregate([ + { + $group: { + _id: null, + totalAcademies: { $sum: 1 }, + totalRate: { $sum: "$rate" }, + avgRate: { $avg: "$rate" }, + }, + }, + ]); + + res.status(200).json({ + success: true, + data: { + academies, + stats: stats[0] || {}, + pagination: { + currentPage: parseInt(page), + totalPages: Math.ceil(total / parseInt(limit)), + totalItems: total, + itemsPerPage: parseInt(limit), + }, + }, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در دریافت آکادمی‌ها" }); + } +}; + +const updateAcademyStatus = async (req, res) => { + try { + const { academyId, status, rate, sheba } = req.body; + + const updateData = {}; + if (status) updateData.status = status; + if (rate) updateData.rate = rate; + if (sheba) updateData.sheba = sheba; + + const academy = await AcademyModel.findByIdAndUpdate( + academyId, + updateData, + { new: true } + ); + + if (!academy) { + return res + .status(404) + .json({ success: false, message: "آکادمی یافت نشد" }); + } + + res.status(200).json({ + success: true, + message: "آکادمی با موفقیت به‌روزرسانی شد", + data: academy, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در به‌روزرسانی آکادمی" }); + } +}; + +// ==================== مدیریت تنظیمات (تکس و قیمت پرو) ==================== +const getSettings = async (req, res) => { + try { + let settings = await SettingModel.findOne(); + + if (!settings) { + settings = await SettingModel.create({ + taxRate: 9, + proPrice: 99000, + legendPrice: 199000, + minWithdrawAmount: 50000, + maxWithdrawAmount: 10000000, + }); + } + + res.status(200).json({ + success: true, + data: settings, + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در دریافت تنظیمات" }); + } +}; + +const updateSettings = async (req, res) => { + try { + const { + taxRate, + proPrice, + legendPrice, + minWithdrawAmount, + maxWithdrawAmount, + } = req.body; + + const settings = await SettingModel.findOneAndUpdate( + {}, + { + taxRate, + proPrice, + legendPrice, + minWithdrawAmount, + maxWithdrawAmount, + updatedAt: new Date(), + }, + { new: true, upsert: true } + ); + + res.status(200).json({ + success: true, + message: "تنظیمات با موفقیت به‌روزرسانی شد", + data: settings, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در به‌روزرسانی تنظیمات" }); + } +}; + +const getCoursePrices = async (req, res) => { + try { + const proPrice = await CoursePaymentModel.findOne({ cuorse_type: "pro" }); + + const legendPrice = await CoursePaymentModel.findOne({ + cuorse_type: "legend", + }); + + const normalPrice = await CoursePaymentModel.findOne({ + cuorse_type: "normal", + }); + + res.status(200).json({ + success: true, + data: { + pro: proPrice + ? { price: proPrice.price, _id: proPrice._id } + : { price: "60000" }, + legend: legendPrice + ? { price: legendPrice.price, _id: legendPrice._id } + : { price: "199000" }, + normal: normalPrice + ? { price: normalPrice.price, _id: normalPrice._id } + : { price: "0" }, + }, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در دریافت قیمت دوره‌ها" }); + } +}; + +const updateCoursePrice = async (req, res) => { + try { + const { courseType, price } = req.body; + + if (!courseType || !price) { + return res + .status(400) + .json({ success: false, message: "نوع دوره و قیمت الزامی است" }); + } + + let coursePayment = await CoursePaymentModel.findOne({ + cuorse_type: courseType, + }); + + if (coursePayment) { + coursePayment.price = price.toString(); + await coursePayment.save(); + } else { + coursePayment = new CoursePaymentModel({ + price: price.toString(), + cuorse_type: courseType, + }); + await coursePayment.save(); + } + + res.status(200).json({ + success: true, + message: `قیمت دوره ${ + courseType === "pro" + ? "پرو" + : courseType === "legend" + ? "لجند" + : "معمولی" + } با موفقیت به‌روزرسانی شد`, + data: coursePayment, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در به‌روزرسانی قیمت دوره" }); + } +}; + +const getTax = async (req, res) => { + try { + let tax = await Tax.findOne({ type: "course" }); + + if (!tax) { + tax = new Tax({ tax: 8, type: "course" }); + await tax.save(); + } + + res.status(200).json({ + success: true, + data: { + tax: tax.tax, + _id: tax._id, + type: tax.type, + }, + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در دریافت مالیات" }); + } +}; + +const updateTax = async (req, res) => { + try { + const { tax } = req.body; + + if (tax === undefined || tax === null) { + return res + .status(400) + .json({ success: false, message: "مقدار مالیات الزامی است" }); + } + + let taxRecord = await Tax.findOne({ type: "course" }); + + if (taxRecord) { + taxRecord.tax = tax; + await taxRecord.save(); + } else { + taxRecord = new Tax({ tax: tax, type: "course" }); + await taxRecord.save(); + } + + res.status(200).json({ + success: true, + message: "مالیات با موفقیت به‌روزرسانی شد", + data: taxRecord, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در به‌روزرسانی مالیات" }); + } +}; + +const getAllSettings = async (req, res) => { + try { + const proPrice = await CoursePaymentModel.findOne({ cuorse_type: "pro" }); + const legendPrice = await CoursePaymentModel.findOne({ + cuorse_type: "legend", + }); + + let tax = await Tax.findOne({ type: "course" }); + if (!tax) { + tax = new Tax({ tax: 8, type: "course" }); + await tax.save(); + } + + res.status(200).json({ + success: true, + data: { + tax: tax.tax, + prices: { + pro: proPrice ? proPrice.price : "60000", + legend: legendPrice ? legendPrice.price : "199000", + }, + }, + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در دریافت تنظیمات" }); + } +}; + +const updateAllSettings = async (req, res) => { + try { + const { tax, proPrice, legendPrice } = req.body; + + if (tax !== undefined) { + let taxRecord = await Tax.findOne({ type: "course" }); + if (taxRecord) { + taxRecord.tax = tax; + await taxRecord.save(); + } else { + taxRecord = new Tax({ tax: tax, type: "course" }); + await taxRecord.save(); + } + } + + if (proPrice !== undefined) { + let proRecord = await CoursePaymentModel.findOne({ cuorse_type: "pro" }); + if (proRecord) { + proRecord.price = proPrice.toString(); + await proRecord.save(); + } else { + proRecord = new CoursePaymentModel({ + price: proPrice.toString(), + cuorse_type: "pro", + }); + await proRecord.save(); + } + } + + if (legendPrice !== undefined) { + let legendRecord = await CoursePaymentModel.findOne({ + cuorse_type: "legend", + }); + if (legendRecord) { + legendRecord.price = legendPrice.toString(); + await legendRecord.save(); + } else { + legendRecord = new CoursePaymentModel({ + price: legendPrice.toString(), + cuorse_type: "legend", + }); + await legendRecord.save(); + } + } + + res.status(200).json({ + success: true, + message: "تنظیمات با موفقیت به‌روزرسانی شد", + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در به‌روزرسانی تنظیمات" }); + } +}; + +const getPaymentStats = async (req, res) => { + try { + const stats = await AcademyPaymentModel.aggregate([ + { + $group: { + _id: null, + totalAmount: { $sum: { $toDouble: "$price" } }, + totalCount: { $sum: 1 }, + pendingCount: { + $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, + }, + settledCount: { + $sum: { $cond: [{ $eq: ["$status", "settled"] }, 1, 0] }, + }, + successCount: { + $sum: { $cond: [{ $eq: ["$status", "success"] }, 1, 0] }, + }, + }, + }, + ]); + + const result = stats[0] || { + totalAmount: 0, + totalCount: 0, + pendingCount: 0, + settledCount: 0, + successCount: 0, + }; + + result.averageAmount = + result.totalCount > 0 + ? Math.round(result.totalAmount / result.totalCount) + : 0; + + res.status(200).json({ + success: true, + data: result, + }); + } catch (error) { + console.error("Error in getPaymentStats:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت آمار پرداخت‌ها", + error: process.env.NODE_ENV === "development" ? error.message : undefined, + }); + } +}; + +const getCourseById = async (req, res) => { + try { + const { id } = req.params; + const course = await CourseModel.findById(id) + .populate("academyId", "academy_name academy_image") + .populate("user_id", "user_name profile_image"); + + if (!course) { + return res.status(404).json({ success: false, message: "دوره یافت نشد" }); + } + + res.status(200).json({ + success: true, + data: course, + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در دریافت دوره" }); + } +}; + +const createCourse = async (req, res) => { + try { + const courseData = req.body; + const newCourse = new CourseModel(courseData); + await newCourse.save(); + + res.status(201).json({ + success: true, + message: "دوره با موفقیت ایجاد شد", + data: newCourse, + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در ایجاد دوره" }); + } +}; + +const getCourseStats = async (req, res) => { + try { + const stats = await CourseModel.aggregate([ + { + $group: { + _id: null, + totalCourses: { $sum: 1 }, + normalCount: { + $sum: { $cond: [{ $eq: ["$type", "normal"] }, 1, 0] }, + }, + proCount: { $sum: { $cond: [{ $eq: ["$type", "pro"] }, 1, 0] } }, + legendCount: { + $sum: { $cond: [{ $eq: ["$type", "legend"] }, 1, 0] }, + }, + pendingCount: { + $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, + }, + acceptCount: { + $sum: { $cond: [{ $eq: ["$status", "accept"] }, 1, 0] }, + }, + rejectCount: { + $sum: { $cond: [{ $eq: ["$status", "reject"] }, 1, 0] }, + }, + }, + }, + ]); + + res.status(200).json({ + success: true, + data: stats[0] || {}, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در دریافت آمار دوره‌ها" }); + } +}; + +const getPurchasedStats = async (req, res) => { + try { + const stats = await IsPaymentCourse.aggregate([ + { + $group: { + _id: null, + totalPurchases: { $sum: 1 }, + uniqueUsers: { $addToSet: "$user_id" }, + }, + }, + ]); + + res.status(200).json({ + success: true, + data: { + totalPurchases: stats[0]?.totalPurchases || 0, + uniqueUsers: stats[0]?.uniqueUsers?.length || 0, + }, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در دریافت آمار خریدها" }); + } +}; + +const getAcademyById = async (req, res) => { + try { + const { id } = req.params; + const academy = await AcademyModel.findById(id).populate( + "userId", + "user_name profile_image email" + ); + + if (!academy) { + return res + .status(404) + .json({ success: false, message: "آکادمی یافت نشد" }); + } + + res.status(200).json({ + success: true, + data: academy, + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در دریافت آکادمی" }); + } +}; + +const createAcademy = async (req, res) => { + try { + const academyData = req.body; + const newAcademy = new AcademyModel(academyData); + await newAcademy.save(); + + res.status(201).json({ + success: true, + message: "آکادمی با موفقیت ایجاد شد", + data: newAcademy, + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در ایجاد آکادمی" }); + } +}; + +const updateAcademy = async (req, res) => { + try { + const { id } = req.params; + const updateData = req.body; + + const academy = await AcademyModel.findByIdAndUpdate(id, updateData, { + new: true, + }); + + if (!academy) { + return res + .status(404) + .json({ success: false, message: "آکادمی یافت نشد" }); + } + + res.status(200).json({ + success: true, + message: "آکادمی با موفقیت به‌روزرسانی شد", + data: academy, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در به‌روزرسانی آکادمی" }); + } +}; + +const deleteAcademy = async (req, res) => { + try { + const { id } = req.params; + const academy = await AcademyModel.findByIdAndDelete(id); + + if (!academy) { + return res + .status(404) + .json({ success: false, message: "آکادمی یافت نشد" }); + } + + await CourseModel.deleteMany({ academyId: id }); + + res.status(200).json({ + success: true, + message: "آکادمی با موفقیت حذف شد", + }); + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: "خطا در حذف آکادمی" }); + } +}; + +const getAcademyStats = async (req, res) => { + try { + const stats = await AcademyModel.aggregate([ + { + $group: { + _id: null, + totalAcademies: { $sum: 1 }, + totalRate: { $sum: "$rate" }, + avgRate: { $avg: "$rate" }, + }, + }, + ]); + + res.status(200).json({ + success: true, + data: stats[0] || { + totalAcademies: 0, + totalRate: 0, + avgRate: 0, + }, + }); + } catch (error) { + console.error(error); + res + .status(500) + .json({ success: false, message: "خطا در دریافت آمار آکادمی‌ها" }); + } +}; +const deleteCourseVideo = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { courseContentId, courseId } = req.body; + const courseContent = await AcademyContentModel.findById(courseContentId); + + if (!courseContent) { + return res.status(404).json({ + error: true, + message: "ویدعو یافت نشد", + }); + } + + courseContent.status = "reject"; + + await courseContent.save(); + + const course = await CourseModel.findById(courseId); + if (course) { + course.number_of_course_content = `${ + Number(course.number_of_course_content) - 1 + }`; + await course.save(); + } + + res.status(200).json({ + success: true, + message: "ویدیو با موفقیت پاک شد", + }); + } catch (error) { + console.error("Error:", error); + res.status(500).json({ + success: false, + message: "خطا در حذف ویدیو", + error: error.message, + }); + } +}; + +const getChunkDir = (uploadKey) => { + return path.join(process.cwd(), "storage", "temp-chunks", uploadKey); +}; + +setInterval(async () => { + const tempDir = path.join(process.cwd(), "storage", "temp-chunks"); + if (await fs.pathExists(tempDir)) { + const dirs = await fs.readdir(tempDir); + const now = Date.now(); + for (const dir of dirs) { + const dirPath = path.join(tempDir, dir); + const stat = await fs.stat(dirPath); + if (now - stat.mtimeMs > 30 * 60 * 1000) { + // 30 دقیقه + await fs.remove(dirPath); + console.log(`Cleaned up old upload: ${dir}`); + } + } + } +}, 60 * 60 * 1000); + +const creatCourseVideoChunk = async (req, res, next) => { + console.log("=== Chunk Video Upload ==="); + console.log("Chunk index:", req.body.chunkIndex); + + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { + courseId, + is_free, + file_name, + chunk, + chunkIndex, + totalChunks, + originalFileName, + fileSize, + mime_type, + } = req.body; + + if (!chunk) { + return res.status(422).json({ + error: true, + message: "تکه فایل ارسال نشده است", + }); + } + + if (!courseId) { + return res.status(422).json({ + error: true, + message: "شناسه دوره الزامی است", + }); + } + + const base64Data = chunk.split(",")[1] || chunk; + const buffer = Buffer.from(base64Data, "base64"); + + const uploadKey = `${courseId}_${originalFileName}`; + const chunkDir = getChunkDir(uploadKey); + + await fs.ensureDir(chunkDir); + + const metaPath = path.join(chunkDir, "meta.json"); + if (chunkIndex === "0" || chunkIndex === 0) { + const metaData = { + totalChunks: parseInt(totalChunks), + file_name: file_name, + originalFileName: originalFileName, + fileSize: fileSize, + mime_type: mime_type, + courseId: courseId, + is_free: is_free, + createdAt: Date.now(), + }; + await fs.writeJson(metaPath, metaData); + } + + if (!(await fs.pathExists(metaPath))) { + return res.status(404).json({ + error: true, + message: "جلسه آپلود یافت نشد", + }); + } + + const chunkPath = path.join(chunkDir, `chunk_${chunkIndex}`); + await fs.writeFile(chunkPath, buffer); + + console.log( + `✅ Chunk ${parseInt(chunkIndex) + 1}/${totalChunks} saved to disk (${ + buffer.length + } bytes)` + ); + + res.status(200).json({ + success: true, + message: `تکه ${ + parseInt(chunkIndex) + 1 + } از ${totalChunks} با موفقیت دریافت شد`, + chunkIndex: chunkIndex, + totalChunks: totalChunks, + }); + } catch (error) { + console.error("Error in creatCourseVideoChunk:", error); + res.status(500).json({ + success: false, + message: "خطا در دریافت تکه ویدیو", + error: error.message, + }); + } +}; + +const mergeVideoChunks = async (req, res, next) => { + console.log("=== Merge Video Chunks ==="); + + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { courseId, originalFileName, totalChunks } = req.body; + + const uploadKey = `${courseId}_${originalFileName}`; + const chunkDir = getChunkDir(uploadKey); + const metaPath = path.join(chunkDir, "meta.json"); + + if (!(await fs.pathExists(chunkDir)) || !(await fs.pathExists(metaPath))) { + return res.status(404).json({ + error: true, + message: "داده‌های آپلود یافت نشد", + }); + } + + const metaData = await fs.readJson(metaPath); + + const existingChunks = await fs.readdir(chunkDir); + const chunkFiles = existingChunks.filter((f) => f.startsWith("chunk_")); + + if (chunkFiles.length !== parseInt(totalChunks)) { + return res.status(422).json({ + error: true, + message: `تعداد تکه‌ها کامل نیست. دریافت شده: ${chunkFiles.length}، مورد نیاز: ${totalChunks}`, + }); + } + + const chunks = []; + for (let i = 0; i < parseInt(totalChunks); i++) { + const chunkPath = path.join(chunkDir, `chunk_${i}`); + if (!(await fs.pathExists(chunkPath))) { + return res.status(422).json({ + error: true, + message: `تکه ${i + 1} پیدا نشد`, + }); + } + const chunkBuffer = await fs.readFile(chunkPath); + chunks.push(chunkBuffer); + } + + const completeBuffer = Buffer.concat(chunks); + console.log("Original file size:", completeBuffer.length, "bytes"); + + const finalVideoDir = path.join( + process.cwd(), + "storage", + "courses", + "videos" + ); + await fs.ensureDir(finalVideoDir); + + let extension = ".mp4"; + if (metaData.mime_type === "video/mp4") extension = ".mp4"; + else if (metaData.mime_type === "video/quicktime") extension = ".mov"; + else if (metaData.mime_type === "video/x-msvideo") extension = ".avi"; + else if (metaData.mime_type === "video/webm") extension = ".webm"; + + const safeFileName = `course-${courseId}-${Date.now()}${extension}`; + const filePath = path.join(finalVideoDir, safeFileName); + + await fs.writeFile(filePath, completeBuffer); + console.log("Original file saved:", filePath); + + const newAcademyContent = new AcademyContentModel({ + course_video: `/storage/courses/videos/${safeFileName}`, + type: "video", + is_free: metaData.is_free === "true" || metaData.is_free === true, + courseId: courseId, + file_name: metaData.file_name || metaData.originalFileName, + }); + + await newAcademyContent.save(); + + const course = await CourseModel.findById(courseId); + if (course) { + course.number_of_course_content = `${ + Number(course.number_of_course_content) + 1 + }`; + await course.save(); + } + + await fs.remove(chunkDir); + + res.status(200).json({ + success: true, + message: "ویدیو با موفقیت ثبت شد (در حال پردازش...)", + data: newAcademyContent, + }); + + if (completeBuffer.length > 20 * 1024 * 1024) { + // اجرای غیرهمزمان بدون await + compressVideoInBackground(filePath, safeFileName, newAcademyContent._id); + } + } catch (error) { + console.error("Error in mergeVideoChunks:", error); + res.status(500).json({ + success: false, + message: "خطا در ترکیب ویدیو", + error: error.message, + }); + } +}; + +const compressVideoInBackground = async (filePath, fileName, contentId) => { + console.log(`Starting background compression for ${fileName}`); + + const dir = path.dirname(filePath); + const tempPath = path.join(dir, `temp-${fileName}`); + const { exec } = require("child_process"); + const util = require("util"); + const execPromise = util.promisify(exec); + + try { + await execPromise( + `ffmpeg -i "${filePath}" -c:v libx264 -crf 28 -preset veryfast -c:a aac -b:a 128k -movflags +faststart "${tempPath}" -y` + ); + + const compressedStat = await fs.stat(tempPath); + const originalStat = await fs.stat(filePath); + + if (compressedStat.size < originalStat.size) { + await fs.rename(tempPath, filePath); + console.log( + `✅ Background compression done: ${( + originalStat.size / + 1024 / + 1024 + ).toFixed(2)}MB → ${(compressedStat.size / 1024 / 1024).toFixed(2)}MB` + ); + } else { + await fs.remove(tempPath); + console.log("Compression didn't reduce size, keeping original"); + } + } catch (error) { + console.error("Background compression failed:", error.message); + if (await fs.pathExists(tempPath)) { + await fs.remove(tempPath); + } + } +}; + +const getUploadStatus = async (req, res) => { + try { + const { courseId, originalFileName } = req.query; + const uploadKey = `${courseId}_${originalFileName}`; + const chunkDir = getChunkDir(uploadKey); + + if (!(await fs.pathExists(chunkDir))) { + return res.status(200).json({ + success: true, + uploadedChunks: [], + totalChunks: 0, + }); + } + + const files = await fs.readdir(chunkDir); + const chunkFiles = files + .filter((f) => f.startsWith("chunk_")) + .map((f) => parseInt(f.replace("chunk_", ""))) + .sort((a, b) => a - b); + + const metaPath = path.join(chunkDir, "meta.json"); + let totalChunks = 0; + if (await fs.pathExists(metaPath)) { + const metaData = await fs.readJson(metaPath); + totalChunks = metaData.totalChunks; + } + + res.status(200).json({ + success: true, + uploadedChunks: chunkFiles, + totalChunks: totalChunks, + }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}; + + + +// ایجاد slug یکتا +const generateUniqueSlug = async (title, excludeId = null) => { + let slug = title + .replace(/[^\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C\uFB8E\u0698a-zA-Z0-9\s]/g, '') + .trim() + .replace(/\s+/g, '-') + .toLowerCase(); + + let uniqueSlug = slug; + let counter = 1; + + while (true) { + const query = { slug: uniqueSlug }; + if (excludeId) query._id = { $ne: excludeId }; + const exists = await AcademyCategoryModel.findOne(query); + if (!exists) break; + uniqueSlug = `${slug}-${counter}`; + counter++; + } + + return uniqueSlug; +}; + +// ==================== GET - دریافت لیست دسته‌بندی‌ها ==================== +const getAll = async (req, res) => { + try { + const { + page = 1, + limit = 20, + sortBy = 'order', + sortOrder = 'asc', + status, + parent, + search, + is_featured, + withChildren = 'false' + } = req.query; + + let query = {}; + + if (status) query.status = status; + if (parent !== undefined) query.parent = parent === 'null' ? null : parent; + if (is_featured === 'true') query.is_featured = true; + + if (search) { + query.$or = [ + { title: { $regex: search, $options: 'i' } }, + { description: { $regex: search, $options: 'i' } } + ]; + } + + const sortOptions = {}; + sortOptions[sortBy] = sortOrder === 'asc' ? 1 : -1; + + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: sortOptions, + populate: [ + { path: 'parent', select: 'title slug' }, + { path: 'created_by', select: 'user_name name' }, + { path: 'updated_by', select: 'user_name name' } + ] + }; + + let categories = await AcademyCategoryModel.paginate(query, options); + + // دریافت درخت دسته‌بندی (اختیاری) + if (withChildren === 'true' && !parent) { + const tree = await AcademyCategoryModel.getTree(); + categories = { ...categories, tree }; + } + + res.status(200).json({ + success: true, + message: 'لیست دسته‌بندی‌ها با موفقیت دریافت شد', + data: categories + }); + } catch (error) { + console.error('Error in getAll:', error); + res.status(500).json({ + success: false, + message: 'خطا در دریافت دسته‌بندی‌ها', + error: error.message + }); + } +}; + +// ==================== GET - دریافت یک دسته‌بندی ==================== +const getOne = async (req, res) => { + try { + const { id } = req.params; + const { includeCourses = 'false' } = req.query; + + let query = AcademyCategoryModel.findById(id); + + query = query.populate('parent', 'title slug icon color'); + query = query.populate('children', 'title slug icon color course_count order'); + query = query.populate('created_by', 'user_name name'); + + if (includeCourses === 'true') { + query = query.populate({ + path: 'courses', + select: 'cuorse_name course_image price offer', + options: { limit: 10 } + }); + } + + const category = await query; + + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }); + } + + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت دریافت شد', + data: category + }); + } catch (error) { + console.error('Error in getOne:', error); + res.status(500).json({ + success: false, + message: 'خطا در دریافت دسته‌بندی', + error: error.message + }); + } +}; + +// ==================== GET - دریافت درخت دسته‌بندی ==================== +const getTree = async (req, res) => { + try { + const tree = await AcademyCategoryModel.getTree(); + + res.status(200).json({ + success: true, + message: 'درخت دسته‌بندی با موفقیت دریافت شد', + data: tree + }); + } catch (error) { + console.error('Error in getTree:', error); + res.status(500).json({ + success: false, + message: 'خطا در دریافت درخت دسته‌بندی', + error: error.message + }); + } +}; + +// ==================== GET - دریافت توسط اسلاگ ==================== +const getBySlug = async (req, res) => { + try { + const { slug } = req.params; + const { page = 1, limit = 20 } = req.query; + + const category = await AcademyCategoryModel.getBySlug(slug); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }); + } + + // دریافت دوره‌های این دسته + const courses = await CourseModel.find({ + category_id: category._id, + status: 'accept' + }) + .sort({ createdAt: -1 }) + .skip((page - 1) * limit) + .limit(parseInt(limit)); + + const totalCourses = await CourseModel.countDocuments({ + category_id: category._id, + status: 'accept' + }); + + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت دریافت شد', + data: { + category, + courses: { + data: courses, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total: totalCourses, + pages: Math.ceil(totalCourses / limit) + } + } + } + }); + } catch (error) { + console.error('Error in getBySlug:', error); + res.status(500).json({ + success: false, + message: 'خطا در دریافت دسته‌بندی', + error: error.message + }); + } +}; + +// ==================== POST - ایجاد دسته‌بندی جدید ==================== +const create = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { + title, + description, + icon, + color, + parent, + order, + image, + is_featured, + seo_title, + seo_description + } = req.body; + + // اعتبارسنجی + if (!title || title.trim().length < 2) { + return res.status(422).json({ + success: false, + message: 'عنوان دسته‌بندی باید حداقل ۲ کاراکتر باشد' + }); + } + + // بررسی تکراری نبودن عنوان + const existingTitle = await AcademyCategoryModel.findOne({ title }); + if (existingTitle) { + return res.status(409).json({ + success: false, + message: 'این عنوان قبلاً ثبت شده است' + }); + } + + // تولید slug یکتا + const slug = await generateUniqueSlug(title); + + // بررسی وجود parent + let level = 0; + if (parent) { + const parentCategory = await AcademyCategoryModel.findById(parent); + if (!parentCategory) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی والد یافت نشد' + }); + } + level = parentCategory.level + 1; + + if (level > 3) { + return res.status(422).json({ + success: false, + message: 'عمق دسته‌بندی نمی‌تواند بیشتر از ۳ سطح باشد' + }); + } + } + + const newCategory = new AcademyCategoryModel({ + title, + slug, + description: description || null, + icon: icon || null, + color: color || '#3B82F6', + parent: parent || null, + level, + order: order || 0, + image: image || null, + is_featured: is_featured || false, + seo_title: seo_title || title, + seo_description: seo_description || description, + created_by: userId, + status: 'active' + }); + + await newCategory.save(); + + res.status(201).json({ + success: true, + message: 'دسته‌بندی با موفقیت ایجاد شد', + data: newCategory + }); + } catch (error) { + console.error('Error in create:', error); + res.status(500).json({ + success: false, + message: 'خطا در ایجاد دسته‌بندی', + error: error.message + }); + } +}; + +// ==================== PUT/PATCH - ویرایش دسته‌بندی ==================== +const update = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { id } = req.params; + const updateData = req.body; + + const category = await AcademyCategoryModel.findById(id); + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }); + } + + // بررسی تکراری نبودن عنوان + if (updateData.title && updateData.title !== category.title) { + const existingTitle = await AcademyCategoryModel.findOne({ + title: updateData.title, + _id: { $ne: id } + }); + if (existingTitle) { + return res.status(409).json({ + success: false, + message: 'این عنوان قبلاً ثبت شده است' + }); + } + + // آپدیت slug + updateData.slug = await generateUniqueSlug(updateData.title, id); + } + + // بررسی parent + if (updateData.parent) { + if (updateData.parent === id) { + return res.status(422).json({ + success: false, + message: 'یک دسته‌بندی نمی‌تواند والد خودش باشد' + }); + } + + const parentCategory = await AcademyCategoryModel.findById(updateData.parent); + if (!parentCategory) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی والد یافت نشد' + }); + } + + updateData.level = parentCategory.level + 1; + + if (updateData.level > 3) { + return res.status(422).json({ + success: false, + message: 'عمق دسته‌بندی نمی‌تواند بیشتر از ۳ سطح باشد' + }); + } + } + + updateData.updated_by = userId; + updateData.updatedAt = new Date(); + + const updatedCategory = await AcademyCategoryModel.findByIdAndUpdate( + id, + updateData, + { new: true, runValidators: true } + ); + + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت به‌روزرسانی شد', + data: updatedCategory + }); + } catch (error) { + console.error('Error in update:', error); + res.status(500).json({ + success: false, + message: 'خطا در به‌روزرسانی دسته‌بندی', + error: error.message + }); + } +}; + +// ==================== DELETE - حذف (سافت دیلیت) ==================== +const remove = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { id } = req.params; + const { permanent = 'false' } = req.query; + + const category = await AcademyCategoryModel.findById(id); + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }); + } + + // بررسی وجود زیردسته‌ها + const hasChildren = await AcademyCategoryModel.exists({ parent: id }); + if (hasChildren) { + return res.status(422).json({ + success: false, + message: 'این دسته‌بندی دارای زیردسته است. ابتدا زیردسته‌ها را حذف کنید' + }); + } + + // بررسی وجود دوره در این دسته + const hasCourses = await CourseModel.exists({ category_id: id }); + if (hasCourses && permanent === 'true') { + return res.status(422).json({ + success: false, + message: 'این دسته‌بندی دارای دوره است. ابتدا دوره‌ها را جابه‌جا کنید' + }); + } + + if (permanent === 'true') { + // حذف فیزیکی + await category.deleteOne(); + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت حذف شد' + }); + } else { + // سافت دیلیت + category.status = 'deleted'; + category.updated_by = userId; + await category.save(); + + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت غیرفعال شد' + }); + } + } catch (error) { + console.error('Error in remove:', error); + res.status(500).json({ + success: false, + message: 'خطا در حذف دسته‌بندی', + error: error.message + }); + } +}; + +// ==================== POST - تغییر وضعیت (اکتیو/غیراکتیو) ==================== +const toggleStatus = async (req, res) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); + + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userId = decodedToken.id; + + const { id } = req.params; + const { status } = req.body; // 'active' or 'inactive' + + if (!['active', 'inactive'].includes(status)) { + return res.status(422).json({ + success: false, + message: 'وضعیت نامعتبر است' + }); + } + + const category = await AcademyCategoryModel.findByIdAndUpdate( + id, + { + status, + updated_by: userId, + updatedAt: new Date() + }, + { new: true } + ); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }); + } + + res.status(200).json({ + success: true, + message: `وضعیت دسته‌بندی با موفقیت به ${status === 'active' ? 'فعال' : 'غیرفعال'} تغییر کرد`, + data: category + }); + } catch (error) { + console.error('Error in toggleStatus:', error); + res.status(500).json({ + success: false, + message: 'خطا در تغییر وضعیت دسته‌بندی', + error: error.message + }); + } +}; + +// ==================== POST - افزایش تعداد دوره‌ها ==================== +const incrementCourseCount = async (req, res) => { + try { + const { id } = req.params; + const { increment = 1 } = req.body; + + const category = await AcademyCategoryModel.incrementCourseCount(id, increment); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }); + } + + res.status(200).json({ + success: true, + message: 'تعداد دوره‌ها به‌روزرسانی شد', + data: { course_count: category.course_count } + }); + } catch (error) { + console.error('Error in incrementCourseCount:', error); + res.status(500).json({ + success: false, + message: 'خطا در به‌روزرسانی تعداد دوره‌ها', + error: error.message + }); + } +}; + +// ==================== GET - دریافت دسته‌بندی‌های ویژه (featured) ==================== +const getFeatured = async (req, res) => { + try { + const { limit = 10 } = req.query; + + const categories = await AcademyCategoryModel.find({ + is_featured: true, + status: 'active' + }) + .sort({ order: 1 }) + .limit(parseInt(limit)) + .populate('parent', 'title slug'); + + res.status(200).json({ + success: true, + message: 'دسته‌بندی‌های ویژه با موفقیت دریافت شد', + data: categories + }); + } catch (error) { + console.error('Error in getFeatured:', error); + res.status(500).json({ + success: false, + message: 'خطا در دریافت دسته‌بندی‌های ویژه', + error: error.message + }); + } +}; + + +const getFreeAcademyExploreContent = async (req, res, next) => { + try { + const page = parseInt(req.query.page, 10) || 1; + const limit = parseInt(req.query.limit, 10) || 15; + const skip = (page - 1) * limit; + + const query = { is_free: true, status: "accept" }; + const totalItems = await AcademyContentModel.countDocuments(query); + const contents = await AcademyContentModel.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .lean(); + + const courseIds = [ + ...new Set(contents.map((item) => item.courseId).filter(Boolean)), + ]; + const courses = courseIds.length + ? await CourseModel.find({ _id: { $in: courseIds } }) + .select( + "cuorse_name course_image teacher_name academyId user_id caption likes" + ) + .lean() + : []; + const courseMap = Object.fromEntries( + courses.map((course) => [String(course._id), course]) + ); + + const userIds = [ + ...new Set(courses.map((course) => String(course.user_id)).filter(Boolean)), + ]; + const users = userIds.length + ? await UserModel.find({ _id: { $in: userIds } }) + .select("user_name first_name last_name profile_image") + .lean() + : []; + const userMap = Object.fromEntries( + users.map((user) => [String(user._id), user]) + ); + + const items = contents.map((content) => { + const course = courseMap[String(content.courseId)] || {}; + const user = userMap[String(course.user_id)] || {}; + return { + ...content, + course_name: course.cuorse_name || content.file_name || "", + course_image: course.course_image || "", + teacher_name: course.teacher_name || "", + academyId: course.academyId || "", + user_name: user.user_name || "", + first_name: user.first_name || "", + last_name: user.last_name || "", + profile_image: user.profile_image || "", + likesCount: Array.isArray(course.likes) ? course.likes.length : 0, + }; + }); + + const totalPages = Math.ceil(totalItems / limit) || 1; + + return res.status(200).json({ + success: true, + data: { + items, + pagination: { + currentPage: page, + totalPages, + totalItems, + hasNextPage: page < totalPages, + }, + }, + }); + } catch (error) { + console.error("Error in getFreeAcademyExploreContent:", error); + next(error); + } +}; + + +module.exports = { + // ========================================== + // 🏢 ماژول آکادمی (Academy) + // ========================================== + + // پیدا کردن آکادمی کاربر جاری + findAcademy, + + // پیدا کردن آکادمی با شناسه + findAcademyById, + + // تکمیل و ویرایش پروفایل آکادمی + academyProfile, + + // تغییر وضعیت آکادمی (فعال/غیرفعال - فقط ادمین) + updateAcademyStatus, + + // دریافت اطلاعات یک آکادمی با شناسه + getAcademyById, + + // ایجاد آکادمی جدید (دستی - معمولاً خودکار ایجاد می‌شود) + createAcademy, + + // ویرایش اطلاعات آکادمی + updateAcademy, + + // حذف آکادمی (به همراه تمام دوره‌های آن) + deleteAcademy, + + // دریافت آمار کلی آکادمی‌ها (تعداد، میانگین امتیاز و...) + getAcademyStats, + + // ========================================== + // 📚 ماژول دوره‌ها (Course) + // ========================================== + + // ایجاد دوره جدید + creatCourse, + + // ویرایش اطلاعات دوره + updateCourse, + + // حذف دوره (تغییر وضعیت به reject) + deleteCourse, + + // دریافت لیست دوره‌های آکادمی جاری + getCourse, + + // دریافت لیست همه دوره‌ها با فیلتر و جستجو + getCourses, + + // محتوای رایگان آموزشگاه برای اکسپلور + getFreeAcademyExploreContent, + + // دریافت دوره‌های یک آکادمی خاص + getAcademyCourse, + + // آپلود ویدیو با روش Multipart + creatCourseVideo, + + // آپلود ویدیو با روش Base64 (برای فایل‌های کوچک) + creatCourseVideoBase64, + + // ========================================== + // 💳 ماژول پرداخت (Payment) + // ========================================== + + // پرداخت برای اشتراک پرو + proPayment, + + // کال‌بک درگاه پرداخت زرین‌پال (اشتراک پرو) + handleCoursePaymentCallback, + + // پرداخت برای خرید دوره + coursePayment, + + // کال‌بک درگاه پرداخت زرین‌پال (خرید دوره) + CoursePaymentCallback, + + // ========================================== + // 🎬 ماژول محتوای دوره (Course Content) + // ========================================== + + // دریافت محتوای دوره (لیست ویدیوها و فایل‌ها) + getCourseContent, + + // حذف ویدیو از دوره + deleteCourseVideo, + + // لایک کردن محتوا + likeCourseContent, + + // حذف لایک (دیسلایک) + dontLikeCourseContent, + + // دریافت تعداد لایک‌های یک دوره + getCourseLikesCount, + + // دریافت لیست دوره‌های لایک شده توسط کاربر + getUserLikedCourses, + + // بررسی وضعیت لایک کاربر (آیا لایک کرده یا نه) + isLikeCourseContent, + + // ========================================== + // 💬 ماژول کامنت‌ها (Comments) + // ========================================== + + // ایجاد کامنت جدید + createComment, + + // دریافت تعداد کل کامنت‌های یک دوره + getTotalCommentsCount, + + // دریافت لیست کامنت‌های یک دوره (با فیلتر و صفحه‌بندی) + getCourseComments, + + // تغییر وضعیت کامنت (تایید/رد/در انتظار) + updateCommentStatus, + + // دریافت کامنت‌های یک کاربر خاص + getUserComments, + + // حذف کامنت + deleteComment, + + // ========================================== + // 🛒 ماژول دوره‌های خریداری شده (Purchased) + // ========================================== + + // دریافت دوره‌های خریداری شده توسط کاربر جاری + getUserPurchasedCourses, + + // دریافت دوره‌های خریداری شده (با populate - روش بهینه) + getUserPurchasedCoursesWithPopulate, + + // دریافت همه پرداخت‌های آکادمی + getAllAcademyPayments, + + // بررسی خرید یک دوره توسط کاربر (برای صفحه دوره) + checkCoursePurchase, + + // ========================================== + // 👑 ماژول مدیریتی (Admin Panel) + // ========================================== + + // ------ مدیریت پرداخت‌ها ------ + + // دریافت همه پرداخت‌های سیستم (ادمین) + getAllPayments, + + // تغییر وضعیت پرداخت (pending/success/settled/failed) + updatePaymentStatus, + + // ------ مدیریت دوره‌ها ------ + + // دریافت همه دوره‌های سیستم (ادمین) + getAllCourses, + + // تغییر وضعیت دوره (تایید/رد/در انتظار) + updateCourseStatus, + + // حذف کامل دوره از سیستم (ادمین) + admindeleteCourse, + + // ------ مدیریت دوره‌های خریداری شده ------ + + // دریافت همه خریدهای سیستم (ادمین) + getAllPurchasedCourses, + + // ------ مدیریت آکادمی‌ها ------ + + // دریافت همه آکادمی‌های سیستم (ادمین) + getAllAcademies, + + // ------ مدیریت تنظیمات ------ + + // دریافت تنظیمات (تکس، قیمت‌ها و...) + getSettings, + + // به‌روزرسانی تنظیمات + updateSettings, + + // دریافت قیمت دوره‌ها (پرو، لجند، معمولی) + getCoursePrices, + + // به‌روزرسانی قیمت یک نوع دوره + updateCoursePrice, + + // دریافت درصد مالیات + getTax, + + // به‌روزرسانی درصد مالیات + updateTax, + + // دریافت همه تنظیمات یکجا + getAllSettings, + + // به‌روزرسانی همه تنظیمات یکجا + updateAllSettings, + + // ========================================== + // 📊 ماژول آمار (Stats) + // ========================================== + + // دریافت آمار پرداخت‌ها + getPaymentStats, + + // دریافت اطلاعات یک دوره با شناسه + getCourseById, + + // ایجاد دوره جدید (ادمین) + createCourse, + + // ویرایش دوره (ادمین) + updateCourse, + + // دریافت آمار کلی دوره‌ها + getCourseStats, + + // دریافت آمار دوره‌های خریداری شده + getPurchasedStats, + + // ========================================== + // 📹 ماژول آپلود تکه‌تکه (Chunk Upload) + // ========================================== + + // دریافت تکه ویدیو (هر بار یک تکه) + creatCourseVideoChunk, + + // ادغام تکه‌ها بعد از اتمام آپلود + mergeVideoChunks, + + // بررسی وضعیت آپلود (برای ادامه از جای قطع شده) + getUploadStatus, + + // ========================================== + // 🏷️ ماژول دسته‌بندی آکادمی (Category) + // ========================================== + + // دریافت لیست دسته‌بندی‌ها (با فیلتر و صفحه‌بندی) + getAll, + + // دریافت یک دسته‌بندی با شناسه + getOne, + + // دریافت درخت دسته‌بندی (ساختار سلسله‌مراتبی والد-فرزند) + getTree, + + // دریافت دسته‌بندی با اسلاگ (برای سئو و لینک‌های زیبا) + getBySlug, + + // ایجاد دسته‌بندی جدید + create, + + // ویرایش دسته‌بندی + update, + + // حذف دسته‌بندی (سافت دیلیت - فقط غیرفعال می‌شود) + remove, + + // تغییر وضعیت دسته‌بندی (فعال/غیرفعال) + toggleStatus, + + // افزایش تعداد دوره‌های دسته‌بندی + incrementCourseCount, + + // دریافت دسته‌بندی‌های ویژه (برای نمایش در صفحه اصلی) + getFeatured }; \ No newline at end of file diff --git a/controllers/application/academy/helpers/chatHelpers.js b/controllers/application/academy/helpers/chatHelpers.js index f398a6d..53ced98 100644 --- a/controllers/application/academy/helpers/chatHelpers.js +++ b/controllers/application/academy/helpers/chatHelpers.js @@ -1,72 +1,80 @@ -const moment = require('moment-jalaali') -const MessageModel = require('../models/MessageModel') -const UserModel = require('../models/UserModel') - -function formatTimeOnly(date) { - return moment(date).locale('fa').format('HH:mm:ss') -} - -async function enrichMessage(message) { - const doc = message._doc ? { ...message._doc } : { ...message } - let replyTo = null - - if (doc.replyToId) { - const parent = await MessageModel.findById(doc.replyToId).lean() - if (parent) { - const parentUser = - String(parent.senderId) === String(doc.senderId) - ? await UserModel.findById(doc.senderId).select('first_name last_name user_name').lean() - : await UserModel.findById(parent.senderId).select('first_name last_name user_name').lean() - replyTo = { - _id: parent._id, - content: (parent.content || 'پیام').slice(0, 200), - senderName: parentUser - ? `${parentUser.first_name || ''} ${parentUser.last_name || ''}`.trim() || parentUser.user_name - : 'کاربر' - } - } - } - - return { - ...doc, - createdAt: formatTimeOnly(doc.createdAt), - replyTo, - forwardedFrom: doc.forwardedFrom || undefined - } -} - -function emitChatEvents(io, message, senderId, receiverId) { - const payload = message - const roomA = `chat:${senderId}:${receiverId}` - const roomB = `chat:${receiverId}:${senderId}` - io.to(roomA).to(roomB).emit('newMessage', payload) - io.to(`user:${senderId}`).to(`user:${receiverId}`).emit('chatListUpdate', { - senderId, - receiverId, - message: payload - }) -} - -function parseForwardedContent(content) { - if (!content) return { body: '', forwardedFrom: null } - try { - const line = content.split('\n')[0] - const j = JSON.parse(line) - if (j.forwardedFrom) { - return { - forwardedFrom: j.forwardedFrom, - body: content.replace(/^\{.*\}\n?/, '').trim() - } - } - } catch (_) { - /* not json */ - } - return { body: content, forwardedFrom: null } -} - -module.exports = { - formatTimeOnly, - enrichMessage, - emitChatEvents, - parseForwardedContent -} +const moment = require('moment-jalaali') +const MessageModel = require('../models/MessageModel') +const UserModel = require('../models/UserModel') + +function formatTimeOnly(date) { + return moment(date).locale('fa').format('HH:mm:ss') +} + +async function enrichMessage(message) { + const doc = message._doc ? { ...message._doc } : { ...message } + let replyTo = null + + if (doc.replyToId) { + const parent = await MessageModel.findById(doc.replyToId).lean() + if (parent) { + const parentUser = + String(parent.senderId) === String(doc.senderId) + ? await UserModel.findById(doc.senderId).select('first_name last_name user_name').lean() + : await UserModel.findById(parent.senderId).select('first_name last_name user_name').lean() + replyTo = { + _id: parent._id, + content: (parent.content || 'پیام').slice(0, 200), + senderName: parentUser + ? `${parentUser.first_name || ''} ${parentUser.last_name || ''}`.trim() || parentUser.user_name + : 'کاربر' + } + } + } + + return { + ...doc, + createdAt: formatTimeOnly(doc.createdAt), + createdAtIso: doc.createdAt ? new Date(doc.createdAt).toISOString() : undefined, + expiresAt: doc.expiresAt ? new Date(doc.expiresAt).toISOString() : undefined, + viewOnce: Boolean(doc.viewOnce), + viewOnceLocked: Boolean(doc.viewOnceLocked), + viewOnceExpired: Boolean(doc.viewOnceExpired), + viewOnceOpenedAt: doc.viewOnceOpenedAt + ? new Date(doc.viewOnceOpenedAt).toISOString() + : undefined, + replyTo, + forwardedFrom: doc.forwardedFrom || undefined + } +} + +function emitChatEvents(io, message, senderId, receiverId) { + const payload = message + const roomA = `chat:${senderId}:${receiverId}` + const roomB = `chat:${receiverId}:${senderId}` + io.to(roomA).to(roomB).emit('newMessage', payload) + io.to(`user:${senderId}`).to(`user:${receiverId}`).emit('chatListUpdate', { + senderId, + receiverId, + message: payload + }) +} + +function parseForwardedContent(content) { + if (!content) return { body: '', forwardedFrom: null } + try { + const line = content.split('\n')[0] + const j = JSON.parse(line) + if (j.forwardedFrom) { + return { + forwardedFrom: j.forwardedFrom, + body: content.replace(/^\{.*\}\n?/, '').trim() + } + } + } catch (_) { + /* not json */ + } + return { body: content, forwardedFrom: null } +} + +module.exports = { + formatTimeOnly, + enrichMessage, + emitChatEvents, + parseForwardedContent +} diff --git a/controllers/application/academy/helpers/expiredMessages.js b/controllers/application/academy/helpers/expiredMessages.js new file mode 100644 index 0000000..cf9feb2 --- /dev/null +++ b/controllers/application/academy/helpers/expiredMessages.js @@ -0,0 +1,129 @@ +const path = require('path') +const fs = require('fs-extra') +const MessageModel = require('../models/MessageModel') + +const ALLOWED_SELF_DESTRUCT_SECONDS = [10, 30, 60, 300, 3600] + +function parseSelfDestructSeconds(raw) { + if (raw == null || raw === '' || raw === '0' || raw === 0) return null + const n = parseInt(String(raw), 10) + if (!Number.isFinite(n) || !ALLOWED_SELF_DESTRUCT_SECONDS.includes(n)) return null + return n +} + +function buildExpiresAt(seconds) { + if (!seconds) return undefined + return new Date(Date.now() + seconds * 1000) +} + +function activeMessageFilter(baseFilter) { + return { + $and: [ + baseFilter, + { + $or: [ + { expiresAt: { $exists: false } }, + { expiresAt: null }, + { expiresAt: { $gt: new Date() } } + ] + } + ] + } +} + +async function removeMessageFiles(messages) { + for (const msg of messages) { + if (!msg.file) continue + const relative = String(msg.file).replace(/^\//, '') + const filePath = path.join(__dirname, '../storage', relative) + if (fs.existsSync(filePath)) { + await fs.remove(filePath).catch(() => {}) + } + } +} + +async function deleteMessagesAndNotify(io, messages, options = {}) { + if (!messages?.length) return [] + + await removeMessageFiles(messages) + const deletedIds = messages.map((m) => String(m._id)) + await MessageModel.deleteMany({ _id: { $in: deletedIds } }) + + if (io) { + const byThread = new Map() + messages.forEach((m) => { + const s = String(m.senderId) + const r = String(m.receiverId) + const key = [s, r].sort().join(':') + if (!byThread.has(key)) byThread.set(key, { s, r, ids: [] }) + byThread.get(key).ids.push(String(m._id)) + }) + + byThread.forEach(({ s, r, ids }) => { + io.to(`chat:${s}:${r}`) + .to(`chat:${r}:${s}`) + .emit('messagesDeleted', { + messageIds: ids, + deletedBy: options.deletedBy, + reason: options.reason || (options.deletedBy ? 'manual' : 'expired') + }) + io.to(`user:${s}`).to(`user:${r}`).emit('chatListUpdate', { senderId: s, receiverId: r }) + }) + } + + return deletedIds +} + +async function purgeExpiredMessages(io) { + try { + const expired = await MessageModel.find({ expiresAt: { $lte: new Date() } }) + if (!expired.length) return [] + return deleteMessagesAndNotify(io, expired) + } catch (err) { + console.error('purgeExpiredMessages error:', err) + return [] + } +} + +const scheduledTimers = new Map() + +function scheduleMessageExpiry(io, message) { + if (!message?.expiresAt || !message._id) return + + const id = String(message._id) + if (scheduledTimers.has(id)) { + clearTimeout(scheduledTimers.get(id)) + } + + const ms = new Date(message.expiresAt).getTime() - Date.now() + if (ms <= 0) { + MessageModel.findById(id) + .then((doc) => doc && deleteMessagesAndNotify(io, [doc])) + .catch(() => {}) + return + } + + const timer = setTimeout(async () => { + scheduledTimers.delete(id) + try { + const doc = await MessageModel.findById(id) + if (doc && doc.expiresAt && new Date(doc.expiresAt) <= new Date()) { + await deleteMessagesAndNotify(io, [doc]) + } + } catch (err) { + console.error('scheduleMessageExpiry error:', err) + } + }, ms) + + scheduledTimers.set(id, timer) +} + +module.exports = { + ALLOWED_SELF_DESTRUCT_SECONDS, + parseSelfDestructSeconds, + buildExpiresAt, + activeMessageFilter, + purgeExpiredMessages, + scheduleMessageExpiry, + deleteMessagesAndNotify +} diff --git a/controllers/application/academy/helpers/viewOnceMessages.js b/controllers/application/academy/helpers/viewOnceMessages.js new file mode 100644 index 0000000..99754d7 --- /dev/null +++ b/controllers/application/academy/helpers/viewOnceMessages.js @@ -0,0 +1,104 @@ +const MessageModel = require('../models/MessageModel') +const { deleteMessagesAndNotify } = require('./expiredMessages') + +function parseViewOnce(raw) { + if (raw == null || raw === '' || raw === false || raw === 'false' || raw === 0) { + return false + } + return raw === true || raw === 'true' || raw === 1 || raw === '1' +} + +const VIEW_ONCE_MEDIA = new Set(['image', 'video', 'voice']) + +function isViewOnceMediaType(fileType) { + return VIEW_ONCE_MEDIA.has(fileType) +} + +/** Hide file URL for receiver until they open view-once media */ +function sanitizeMessageForViewer(message, viewerId) { + const doc = message._doc ? { ...message._doc } : { ...message } + const viewer = String(viewerId) + const sender = String(doc.senderId) + const receiver = String(doc.receiverId) + + if (!doc.viewOnce || !isViewOnceMediaType(doc.fileType)) { + return doc + } + + if (viewer === sender) { + return doc + } + + if (viewer === receiver) { + if (doc.viewOnceOpenedAt) { + return { + ...doc, + file: '', + viewOnceExpired: true + } + } + return { + ...doc, + file: '', + viewOnceLocked: true + } + } + + return doc +} + +async function openViewOnceMessage(messageId, userId) { + const msg = await MessageModel.findById(messageId) + if (!msg || !msg.viewOnce) { + return { error: 'پیام یافت نشد', status: 404 } + } + if (String(msg.receiverId) !== String(userId)) { + return { error: 'دسترسی مجاز نیست', status: 403 } + } + if (msg.viewOnceOpenedAt) { + return { error: 'این پیام قبلاً باز شده است', status: 410 } + } + + msg.viewOnceOpenedAt = new Date() + await msg.save() + + return { + message: msg.toObject(), + file: msg.file, + fileType: msg.fileType + } +} + +async function completeViewOnceMessage(io, messageId, userId) { + const msg = await MessageModel.findById(messageId) + if (!msg || !msg.viewOnce) { + return { error: 'پیام یافت نشد', status: 404 } + } + + const uid = String(userId) + const isReceiver = String(msg.receiverId) === uid + const isSender = String(msg.senderId) === uid + + if (!isReceiver && !isSender) { + return { error: 'دسترسی مجاز نیست', status: 403 } + } + + if (isReceiver && !msg.viewOnceOpenedAt) { + return { error: 'پیام هنوز باز نشده', status: 400 } + } + + await deleteMessagesAndNotify(io, [msg], { + deletedBy: userId, + reason: 'viewOnce' + }) + + return { deletedIds: [String(msg._id)] } +} + +module.exports = { + parseViewOnce, + isViewOnceMediaType, + sanitizeMessageForViewer, + openViewOnceMessage, + completeViewOnceMessage +} diff --git a/controllers/application/academy/index.js b/controllers/application/academy/index.js index 67ae759..06e62f8 100644 --- a/controllers/application/academy/index.js +++ b/controllers/application/academy/index.js @@ -1,357 +1,357 @@ -const express = require('express'); -const app = express(); -const path = require('path'); -const http = require('http').createServer(app); -const cors = require('cors'); -const moment = require('moment-jalaali'); -const MessageModel = require('./models/MessageModel'); -const fs = require('fs-extra'); -const cron = require('node-cron'); -const UserModel = require('./models/UserModel'); -const blockCheck = require('./middlewares/blockCheck'); -const rateLimit = require('express-rate-limit'); -const { default: axios } = require('axios'); - -const allowedOrigins = [ - 'http://localhost:3000', - 'http://localhost:3001', // اضافه شد — مهم برای توسعه فرانت - 'http://localhost:3002', - 'http://localhost:3003', - 'http://localhost', - 'https://modstagram.com', - 'https://www.modstagram.com', - 'https://api.modstagram.com', - 'https://panel.modstagram.com', - 'https://www.panel.modstagram.com', - 'https://panel.modstagram.ir', - 'https://www.panel.modstagram.ir', - 'ionic://localhost', - 'capacitor://localhost', - // IPهای محلی (برای تست در شبکه داخلی) - /^http:\/\/192\.168\.\d{1,3}\.\d{1,3}:\d+$/, - /^http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$/, -]; - -app.use(cors({ - origin: (origin, callback) => { - // در محیط توسعه، همه originها مجاز باشن - if (process.env.NODE_ENV !== 'production') { - return callback(null, true); - } - - // اگر origin وجود نداشته باشه (مثل درخواست از موبایل یا Postman) اجازه بده - if (!origin) { - return callback(null, true); - } - - // چک لیست یا RegExp - const isAllowed = allowedOrigins.some(item => { - if (typeof item === 'string') return item === origin; - if (item instanceof RegExp) return item.test(origin); - return false; - }); - - if (isAllowed) { - callback(null, true); - } else { - callback(new Error('Not allowed by CORS')); - } - }, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-no-toast'], - credentials: true, - optionsSuccessStatus: 200 // برای مرورگرهای قدیمی -})); - -// Socket.IO CORS -const io = require('socket.io')(http, { - cors: { - origin: (origin, callback) => { - if (process.env.NODE_ENV !== 'production') { - return callback(null, true); - } - - if (!origin) { - return callback(null, true); - } - - const isAllowed = allowedOrigins.some(item => { - if (typeof item === 'string') return item === origin; - if (item instanceof RegExp) return item.test(origin); - return false; - }); - - if (isAllowed) { - callback(null, true); - } else { - callback(new Error('Not allowed by CORS')); - } - }, - methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'], - credentials: true - } -}); - -// افزایش limit برای Base64 (1000mb کافیه) -app.use(express.json({ limit: '1000mb' })); -app.use(express.urlencoded({ limit: '1000mb', extended: true })); - -app.use('/storage', express.static(path.join(__dirname, 'storage'))); -app.use('/storage/profiles', express.static(path.join(__dirname, '../storage/profiles'))); -app.use('/storage/carts', express.static(path.join(__dirname, '../storage/carts'))); -app.use('/storage/posts', express.static(path.join(__dirname, '../storage/posts'))); -app.use('/storage/messages', express.static(path.join(__dirname, '../storage/messages'))); -app.use('/storage/tickets', express.static(path.join(__dirname, '../storage/tickets'))); -app.use('/storage/advertising', express.static(path.join(__dirname, '../storage/advertising'))); -app.use('/storage/services', express.static(path.join(__dirname, '../storage/services'))); - -require('./boot'); - -// غیرفعال کردن پارس‌کننده‌های بدنه برای روت‌های آپلود فایل -app.use((req, res, next) => { - if (req.path === '/api/v1/posts/create' && req.method === 'POST') { - console.log('DEBUG: Skipping body parsers for /api/v1/posts/create'); - return next(); - } - express.json()(req, res, next); -}); -app.use((req, res, next) => { - if (req.path === '/api/v1/posts/create' && req.method === 'POST') { - return next(); - } - express.urlencoded({ extended: true })(req, res, next); -}); - -require('./middlewares')(app); - -const limiter = rateLimit({ - windowMs: 3 * 60 * 1000, - max: 3000, - standardHeaders: true, - legacyHeaders: false, - message: 'تعداد درخواست‌های شما بیشتر از حد مجاز است، لطفاً بعداً دوباره تلاش کنید.', - handler: (req, res) => { - const retryAfter = Math.ceil((req.rateLimit.resetTime - Date.now()) / 1000); - res.set('Retry-After', retryAfter); - res.status(429).json({ - error: `شما بیش از حد مجاز درخواست ارسال کرده‌اید. لطفاً بعد از ${retryAfter} ثانیه دوباره تلاش کنید.` - }); - } -}); -app.use(limiter); -app.set('trust proxy', 'loopback'); - -require('./routes')(app); - -const { enrichMessage, emitChatEvents, parseForwardedContent } = require('./helpers/chatHelpers') - -app.get('/api/v1/chat', async (req, res) => { - try { - const { senderId, receiverId, limit, page } = req.query - const pageNumber = parseInt(page) || 1 - const limitPerPage = parseInt(limit) || 40 - - const filter = { - $or: [ - { senderId, receiverId }, - { senderId: receiverId, receiverId: senderId } - ] - } - - const totalMessagesCount = await MessageModel.countDocuments(filter) - const totalPages = Math.ceil(totalMessagesCount / limitPerPage) || 1 - const skip = (pageNumber - 1) * limitPerPage - - const messages = await MessageModel.find(filter) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limitPerPage) - - await MessageModel.updateMany( - { _id: { $in: messages.map((m) => m._id) }, receiverId: senderId }, - { $set: { readStatus: 1 } } - ) - - const enriched = await Promise.all(messages.map((m) => enrichMessage(m))) - res.json({ messages: enriched.reverse(), totalPages }) - } catch (error) { - console.error('Error fetching messages:', error) - res.status(500).json({ error: 'Server error' }) - } -}) - -app.post('/api/v1/chat', [blockCheck], async (req, res) => { - try { - const { senderId, receiverId, content, replyToId, forwardedFrom: fwdBody } = req.body - const { body, forwardedFrom: fwdParsed } = parseForwardedContent(content) - - const payload = { - senderId, - receiverId, - content: body || content, - replyToId: replyToId || undefined, - forwardedFrom: fwdBody || fwdParsed || undefined - } - - const newMessage = new MessageModel(payload) - await newMessage.save() - const formatted = await enrichMessage(newMessage) - - res.status(201).json({ data: formatted }) - emitChatEvents(io, formatted, senderId, receiverId) - } catch (error) { - console.error('Error sending message:', error) - res.status(500).json({ error: 'Server error' }) - } -}) -app.get('/api/v1/chat/read', async (req, res) => { - try { - const { senderId, receiverId } = req.query // شناسه فرستنده و گیرنده از درخواست دریافت شود - // پیدا کردن تمام پیام‌هایی که کاربر دوم مشاهده کرده است و کاربر اول آنها را ارسال کرده است - const messages = await MessageModel.find( - { senderId: receiverId, receiverId: senderId } // برای مواقعی که شناسه‌ها معکوس باشند - ) - // به روزرسانی وضعیت خوانده شده یا نشده بودن پیام‌ها به "خوانده شده" - await MessageModel.updateMany({ _id: { $in: messages.map(message => message._id) } }, { $set: { readStatus: 1 } }) - res.status(200).json({ message: 'Message read status updated successfully' }) - } catch (error) { - console.error('Error updating message read status:', error) - res.status(500).json({ error: 'Server error' }) - } -}) - -app.post("/api/v1/notification/send-sms", async (req, res) => { - const { receiverId, message } = req.body; - const user = await UserModel.findById(receiverId); - if (!user || !user.mobile) return res.status(404).json({ error: "User not found" }); - console.log("3"); - - const mobile = user.mobile - const user_name = user.user_name - const data = JSON.stringify({ - mobile, - templateId: '876533', - parameters: [ - { name: 'USER', value: user_name } - ] - }) - - const config = { - method: 'post', - url: 'https://api.sms.ir/v1/send/verify', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/plain', - 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' - }, - data - } - - axios(config) - .then(function (response) { - console.log(response); - - }) - .catch(function (error) { - console.log(error) - }) - - res.status(200).json({ success: true }); -}); - -app.post('/api/v1/chat/file', [blockCheck], async (req, res) => { - try { - const { senderId, receiverId, content, replyToId, fileType } = req.body - const { file } = req.files - let fileUrl = null - if (file) { - const uploadDir = path.join(__dirname, '../storage/messages') - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - const uniqueFileName = `${Date.now()}-${file.name}` - const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(file.path, filePath) - fileUrl = `/messages/${uniqueFileName}` - } - - const newMessage = new MessageModel({ - senderId, - receiverId, - content: content || '', - file: fileUrl, - fileType: fileType || undefined, - replyToId: replyToId || undefined - }) - await newMessage.save() - const formatted = await enrichMessage(newMessage) - - res.status(201).json({ data: formatted }) - emitChatEvents(io, formatted, senderId, receiverId) - } catch (error) { - console.error('Error sending message:', error) - res.status(500).json({ error: 'Server error' }) - } -}) -// Errors -require('./middlewares/exception')(app) -require('./middlewares/404')(app) -io.on('connection', (socket) => { - socket.on('joinUser', ({ userId }) => { - if (userId) socket.join(`user:${userId}`) - }) - - socket.on('joinChat', ({ userId, receiverId }) => { - if (userId && receiverId) { - socket.join(`chat:${userId}:${receiverId}`) - socket.join(`chat:${receiverId}:${userId}`) - } - }) - - socket.on('typing', ({ senderId, receiverId }) => { - io.to(`chat:${receiverId}:${senderId}`).emit('userTyping', { userId: senderId }) - }) - - socket.on('stopTyping', ({ senderId, receiverId }) => { - io.to(`chat:${receiverId}:${senderId}`).emit('userStoppedTyping', { userId: senderId }) - }) - - socket.on('messageSeen', async ({ messageId, senderId, receiverId }) => { - try { - if (messageId) { - await MessageModel.findByIdAndUpdate(messageId, { readStatus: 1 }) - io.to(`chat:${receiverId}:${senderId}`).emit('messageStatusUpdate', { - messageId, - status: 'seen' - }) - } - } catch (e) { - console.error('messageSeen error', e) - } - }) -}) - -// Free Project -require('./services/AdvertisingCron') -require('./services/ProjectCron') -cron.schedule('0 0 1 * *', async () => { - try { - await UserModel.updateMany({}, { - daily_free_request: 1, - last_free_request_date: new Date(), - monthly_free_offer: 1, - last_free_offer_date: new Date() - }) - console.log('Monthly free requests reset for all users.') - } catch (error) { - console.error('Error resetting daily free requests:', error) - } -}, { - scheduled: true, - timezone: 'Asia/Tehran' -}) -module.exports = (port) => { - http.listen(port, () => { - console.log(`HTTP server is running on port ${port}`) - }) -} +const express = require('express'); +const app = express(); +const path = require('path'); +const http = require('http').createServer(app); +const cors = require('cors'); +const moment = require('moment-jalaali'); +const MessageModel = require('./models/MessageModel'); +const fs = require('fs-extra'); +const cron = require('node-cron'); +const UserModel = require('./models/UserModel'); +const blockCheck = require('./middlewares/blockCheck'); +const rateLimit = require('express-rate-limit'); +const { default: axios } = require('axios'); + +const allowedOrigins = [ + 'http://localhost:3000', + 'http://localhost:3001', // اضافه شد — مهم برای توسعه فرانت + 'http://localhost:3002', + 'http://localhost:3003', + 'http://localhost', + 'https://modstagram.com', + 'https://www.modstagram.com', + 'https://api.modstagram.com', + 'https://panel.modstagram.com', + 'https://www.panel.modstagram.com', + 'https://panel.modstagram.ir', + 'https://www.panel.modstagram.ir', + 'ionic://localhost', + 'capacitor://localhost', + // IPهای محلی (برای تست در شبکه داخلی) + /^http:\/\/192\.168\.\d{1,3}\.\d{1,3}:\d+$/, + /^http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$/, +]; + +app.use(cors({ + origin: (origin, callback) => { + // در محیط توسعه، همه originها مجاز باشن + if (process.env.NODE_ENV !== 'production') { + return callback(null, true); + } + + // اگر origin وجود نداشته باشه (مثل درخواست از موبایل یا Postman) اجازه بده + if (!origin) { + return callback(null, true); + } + + // چک لیست یا RegExp + const isAllowed = allowedOrigins.some(item => { + if (typeof item === 'string') return item === origin; + if (item instanceof RegExp) return item.test(origin); + return false; + }); + + if (isAllowed) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-no-toast'], + credentials: true, + optionsSuccessStatus: 200 // برای مرورگرهای قدیمی +})); + +// Socket.IO CORS +const io = require('socket.io')(http, { + cors: { + origin: (origin, callback) => { + if (process.env.NODE_ENV !== 'production') { + return callback(null, true); + } + + if (!origin) { + return callback(null, true); + } + + const isAllowed = allowedOrigins.some(item => { + if (typeof item === 'string') return item === origin; + if (item instanceof RegExp) return item.test(origin); + return false; + }); + + if (isAllowed) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'], + credentials: true + } +}); + +// افزایش limit برای Base64 (1000mb کافیه) +app.use(express.json({ limit: '1000mb' })); +app.use(express.urlencoded({ limit: '1000mb', extended: true })); + +app.use('/storage', express.static(path.join(__dirname, 'storage'))); +app.use('/storage/profiles', express.static(path.join(__dirname, '../storage/profiles'))); +app.use('/storage/carts', express.static(path.join(__dirname, '../storage/carts'))); +app.use('/storage/posts', express.static(path.join(__dirname, '../storage/posts'))); +app.use('/storage/messages', express.static(path.join(__dirname, '../storage/messages'))); +app.use('/storage/tickets', express.static(path.join(__dirname, '../storage/tickets'))); +app.use('/storage/advertising', express.static(path.join(__dirname, '../storage/advertising'))); +app.use('/storage/services', express.static(path.join(__dirname, '../storage/services'))); + +require('./boot'); + +// غیرفعال کردن پارس‌کننده‌های بدنه برای روت‌های آپلود فایل +app.use((req, res, next) => { + if (req.path === '/api/v1/posts/create' && req.method === 'POST') { + console.log('DEBUG: Skipping body parsers for /api/v1/posts/create'); + return next(); + } + express.json()(req, res, next); +}); +app.use((req, res, next) => { + if (req.path === '/api/v1/posts/create' && req.method === 'POST') { + return next(); + } + express.urlencoded({ extended: true })(req, res, next); +}); + +require('./middlewares')(app); + +const limiter = rateLimit({ + windowMs: 3 * 60 * 1000, + max: 3000, + standardHeaders: true, + legacyHeaders: false, + message: 'تعداد درخواست‌های شما بیشتر از حد مجاز است، لطفاً بعداً دوباره تلاش کنید.', + handler: (req, res) => { + const retryAfter = Math.ceil((req.rateLimit.resetTime - Date.now()) / 1000); + res.set('Retry-After', retryAfter); + res.status(429).json({ + error: `شما بیش از حد مجاز درخواست ارسال کرده‌اید. لطفاً بعد از ${retryAfter} ثانیه دوباره تلاش کنید.` + }); + } +}); +app.use(limiter); +app.set('trust proxy', 'loopback'); + +require('./routes')(app); + +const { enrichMessage, emitChatEvents, parseForwardedContent } = require('./helpers/chatHelpers') + +app.get('/api/v1/chat', async (req, res) => { + try { + const { senderId, receiverId, limit, page } = req.query + const pageNumber = parseInt(page) || 1 + const limitPerPage = parseInt(limit) || 40 + + const filter = { + $or: [ + { senderId, receiverId }, + { senderId: receiverId, receiverId: senderId } + ] + } + + const totalMessagesCount = await MessageModel.countDocuments(filter) + const totalPages = Math.ceil(totalMessagesCount / limitPerPage) || 1 + const skip = (pageNumber - 1) * limitPerPage + + const messages = await MessageModel.find(filter) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limitPerPage) + + await MessageModel.updateMany( + { _id: { $in: messages.map((m) => m._id) }, receiverId: senderId }, + { $set: { readStatus: 1 } } + ) + + const enriched = await Promise.all(messages.map((m) => enrichMessage(m))) + res.json({ messages: enriched.reverse(), totalPages }) + } catch (error) { + console.error('Error fetching messages:', error) + res.status(500).json({ error: 'Server error' }) + } +}) + +app.post('/api/v1/chat', [blockCheck], async (req, res) => { + try { + const { senderId, receiverId, content, replyToId, forwardedFrom: fwdBody } = req.body + const { body, forwardedFrom: fwdParsed } = parseForwardedContent(content) + + const payload = { + senderId, + receiverId, + content: body || content, + replyToId: replyToId || undefined, + forwardedFrom: fwdBody || fwdParsed || undefined + } + + const newMessage = new MessageModel(payload) + await newMessage.save() + const formatted = await enrichMessage(newMessage) + + res.status(201).json({ data: formatted }) + emitChatEvents(io, formatted, senderId, receiverId) + } catch (error) { + console.error('Error sending message:', error) + res.status(500).json({ error: 'Server error' }) + } +}) +app.get('/api/v1/chat/read', async (req, res) => { + try { + const { senderId, receiverId } = req.query // شناسه فرستنده و گیرنده از درخواست دریافت شود + // پیدا کردن تمام پیام‌هایی که کاربر دوم مشاهده کرده است و کاربر اول آنها را ارسال کرده است + const messages = await MessageModel.find( + { senderId: receiverId, receiverId: senderId } // برای مواقعی که شناسه‌ها معکوس باشند + ) + // به روزرسانی وضعیت خوانده شده یا نشده بودن پیام‌ها به "خوانده شده" + await MessageModel.updateMany({ _id: { $in: messages.map(message => message._id) } }, { $set: { readStatus: 1 } }) + res.status(200).json({ message: 'Message read status updated successfully' }) + } catch (error) { + console.error('Error updating message read status:', error) + res.status(500).json({ error: 'Server error' }) + } +}) + +app.post("/api/v1/notification/send-sms", async (req, res) => { + const { receiverId, message } = req.body; + const user = await UserModel.findById(receiverId); + if (!user || !user.mobile) return res.status(404).json({ error: "User not found" }); + console.log("3"); + + const mobile = user.mobile + const user_name = user.user_name + const data = JSON.stringify({ + mobile, + templateId: '876533', + parameters: [ + { name: 'USER', value: user_name } + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + + axios(config) + .then(function (response) { + console.log(response); + + }) + .catch(function (error) { + console.log(error) + }) + + res.status(200).json({ success: true }); +}); + +app.post('/api/v1/chat/file', [blockCheck], async (req, res) => { + try { + const { senderId, receiverId, content, replyToId, fileType } = req.body + const { file } = req.files + let fileUrl = null + if (file) { + const uploadDir = path.join(__dirname, '../storage/messages') + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + const uniqueFileName = `${Date.now()}-${file.name}` + const filePath = path.join(uploadDir, uniqueFileName) + await fs.move(file.path, filePath) + fileUrl = `/messages/${uniqueFileName}` + } + + const newMessage = new MessageModel({ + senderId, + receiverId, + content: content || '', + file: fileUrl, + fileType: fileType || undefined, + replyToId: replyToId || undefined + }) + await newMessage.save() + const formatted = await enrichMessage(newMessage) + + res.status(201).json({ data: formatted }) + emitChatEvents(io, formatted, senderId, receiverId) + } catch (error) { + console.error('Error sending message:', error) + res.status(500).json({ error: 'Server error' }) + } +}) +// Errors +require('./middlewares/exception')(app) +require('./middlewares/404')(app) +io.on('connection', (socket) => { + socket.on('joinUser', ({ userId }) => { + if (userId) socket.join(`user:${userId}`) + }) + + socket.on('joinChat', ({ userId, receiverId }) => { + if (userId && receiverId) { + socket.join(`chat:${userId}:${receiverId}`) + socket.join(`chat:${receiverId}:${userId}`) + } + }) + + socket.on('typing', ({ senderId, receiverId }) => { + io.to(`chat:${receiverId}:${senderId}`).emit('userTyping', { userId: senderId }) + }) + + socket.on('stopTyping', ({ senderId, receiverId }) => { + io.to(`chat:${receiverId}:${senderId}`).emit('userStoppedTyping', { userId: senderId }) + }) + + socket.on('messageSeen', async ({ messageId, senderId, receiverId }) => { + try { + if (messageId) { + await MessageModel.findByIdAndUpdate(messageId, { readStatus: 1 }) + io.to(`chat:${receiverId}:${senderId}`).emit('messageStatusUpdate', { + messageId, + status: 'seen' + }) + } + } catch (e) { + console.error('messageSeen error', e) + } + }) +}) + +// Free Project +require('./services/AdvertisingCron') +require('./services/ProjectCron') +cron.schedule('0 0 1 * *', async () => { + try { + await UserModel.updateMany({}, { + daily_free_request: 1, + last_free_request_date: new Date(), + monthly_free_offer: 1, + last_free_offer_date: new Date() + }) + console.log('Monthly free requests reset for all users.') + } catch (error) { + console.error('Error resetting daily free requests:', error) + } +}, { + scheduled: true, + timezone: 'Asia/Tehran' +}) +module.exports = (port) => { + http.listen(port, () => { + console.log(`HTTP server is running on port ${port}`) + }) +} diff --git a/controllers/application/academy/middlewares/404.js b/controllers/application/academy/middlewares/404.js index 8c2cb19..fb30b71 100644 --- a/controllers/application/academy/middlewares/404.js +++ b/controllers/application/academy/middlewares/404.js @@ -1,9 +1,9 @@ -module.exports = (app) => { - app.use((req, res, next) => { - res.status(404).send({ - code: 'Not Found', - status: 404, - message: 'requested resource could not be found!' - }) - }) -} +module.exports = (app) => { + app.use((req, res, next) => { + res.status(404).send({ + code: 'Not Found', + status: 404, + message: 'requested resource could not be found!' + }) + }) +} diff --git a/controllers/application/academy/middlewares/adminAuth.js b/controllers/application/academy/middlewares/adminAuth.js index fe58f18..060d854 100644 --- a/controllers/application/academy/middlewares/adminAuth.js +++ b/controllers/application/academy/middlewares/adminAuth.js @@ -1,55 +1,55 @@ -const AdminModel = require('../models/AdminModel') -const jwt = require('jsonwebtoken') - -module.exports = async (req, res, next) => { - if (!('authorization' in req.headers)) { - return res.status(401).send({ - status: 'error', - code: 401, - message: 'you are not authorized!' - }) - } - - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - let decodedToken - try { - decodedToken = jwt.verify(token, process.env.APP_SECRET) - } catch (err) { - return res.status(401).send({ - status: 'error', - code: 401, - message: 'Your token is not valid!', - error: err.message - }) - } - if (!decodedToken) { - return res.status(401).send({ - status: 'error', - code: 401, - message: 'Your token is not valid!' - }) - } - try { - // پیدا کردن کاربر با استفاده از اطلاعات موجود در توکن - const admin = await AdminModel.findOne({ _id: decodedToken.id }) - if (!admin) { - return res.status(403).send({ - status: 'error', - code: 403, - message: 'Access denied!' - }) - } - - // ادامه مسیر در صورتی که کاربر ادمین باشد - req.admin = admin - next() - } catch (error) { - return res.status(500).send({ - status: 'error', - code: 500, - message: 'Internal Server Error', - error: error.message - }) - } -} +const AdminModel = require('../models/AdminModel') +const jwt = require('jsonwebtoken') + +module.exports = async (req, res, next) => { + if (!('authorization' in req.headers)) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'you are not authorized!' + }) + } + + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + let decodedToken + try { + decodedToken = jwt.verify(token, process.env.APP_SECRET) + } catch (err) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'Your token is not valid!', + error: err.message + }) + } + if (!decodedToken) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'Your token is not valid!' + }) + } + try { + // پیدا کردن کاربر با استفاده از اطلاعات موجود در توکن + const admin = await AdminModel.findOne({ _id: decodedToken.id }) + if (!admin) { + return res.status(403).send({ + status: 'error', + code: 403, + message: 'Access denied!' + }) + } + + // ادامه مسیر در صورتی که کاربر ادمین باشد + req.admin = admin + next() + } catch (error) { + return res.status(500).send({ + status: 'error', + code: 500, + message: 'Internal Server Error', + error: error.message + }) + } +} diff --git a/controllers/application/academy/middlewares/auth.js b/controllers/application/academy/middlewares/auth.js index 0dcfce0..d97a1cd 100644 --- a/controllers/application/academy/middlewares/auth.js +++ b/controllers/application/academy/middlewares/auth.js @@ -1,21 +1,21 @@ -const TokenService = require('../services/TokenService') -module.exports = (req, res, next) => { - console.log(1); - if (!('authorization' in req.headers)) { - return res.status(401).send({ - status: 'error', - code: 401, - message: 'you are not authorized!' - }) - } - const [, tokenValue] = req.headers.authorization.split(' ') - const token = TokenService.verify(tokenValue) - if (!token) { - return res.status(401).send({ - status: 'error', - code: 401, - message: 'your token is not valid!' - }) - } - next() -} +const TokenService = require('../services/TokenService') +module.exports = (req, res, next) => { + console.log(1); + if (!('authorization' in req.headers)) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'you are not authorized!' + }) + } + const [, tokenValue] = req.headers.authorization.split(' ') + const token = TokenService.verify(tokenValue) + if (!token) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'your token is not valid!' + }) + } + next() +} diff --git a/controllers/application/academy/middlewares/blockCheck.js b/controllers/application/academy/middlewares/blockCheck.js index fb46d6f..d486024 100644 --- a/controllers/application/academy/middlewares/blockCheck.js +++ b/controllers/application/academy/middlewares/blockCheck.js @@ -1,47 +1,48 @@ -const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر -const jwt = require('jsonwebtoken') - -module.exports = async (req, res, next) => { - console.log(2); - 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.block_status === true) { - return res.status(403).send({ - status: 'error', - code: 403, - message: 'حساب کاربری شما مسدود شده است، برای اطلاعات بیشتر با پشتیبانی تماس بگیرید!', - type: 'block' - }) - } - - req.user = user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود. - next() - } catch (error) { - console.error('Error in blockCheck middleware:', error) - res.status(500).send({ - status: 'error', - code: 500, - message: 'Server error' - }) - } -} +const UserModel = require('../models/UserModel') +const jwt = require('jsonwebtoken') +const { + expireBlockSuspensionIfNeeded, + getBlockedAccountResponse +} = require('../utils/blockSuspension') + +module.exports = async (req, res, next) => { + try { + if (!('authorization' in req.headers)) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'You are not authorized!' + }) + } + + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + + if (!decodedToken) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'Your token is not valid!' + }) + } + + let user = await UserModel.findById(decodedToken.id) + user = await expireBlockSuspensionIfNeeded(user) + + const blockResponse = getBlockedAccountResponse(user) + if (blockResponse) { + return res.status(403).send(blockResponse) + } + + req.user = user + next() + } catch (error) { + console.error('Error in blockCheck middleware:', error) + res.status(500).send({ + status: 'error', + code: 500, + message: 'Server error' + }) + } +} diff --git a/controllers/application/academy/middlewares/exception.js b/controllers/application/academy/middlewares/exception.js index 9f75b03..1d7ddff 100644 --- a/controllers/application/academy/middlewares/exception.js +++ b/controllers/application/academy/middlewares/exception.js @@ -1,11 +1,11 @@ -module.exports = (app) => { - app.use((error, req, res, next) => { - const status = error.status || 500 - res.status(500).send({ - code: 'Eception', - status, - en_message: error.message, - message: 'خطایی در عملیات مورد نظر رخ داده است.' - }) - }) -} +module.exports = (app) => { + app.use((error, req, res, next) => { + const status = error.status || 500 + res.status(500).send({ + code: 'Eception', + status, + en_message: error.message, + message: 'خطایی در عملیات مورد نظر رخ داده است.' + }) + }) +} diff --git a/controllers/application/academy/middlewares/index.js b/controllers/application/academy/middlewares/index.js index 53f4ae7..1192668 100644 --- a/controllers/application/academy/middlewares/index.js +++ b/controllers/application/academy/middlewares/index.js @@ -1,17 +1,17 @@ -const bodyParser = require('body-parser') -const cors = require('cors') -const formData = require('express-form-data') -const session = require('express-session') -const lastOnline = require('./lastOnline') -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(lastOnline) -} +const bodyParser = require('body-parser') +const cors = require('cors') +const formData = require('express-form-data') +const session = require('express-session') +const lastOnline = require('./lastOnline') +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(lastOnline) +} diff --git a/controllers/application/academy/middlewares/isHalfRegister.js b/controllers/application/academy/middlewares/isHalfRegister.js index 72c3ec3..4285c46 100644 --- a/controllers/application/academy/middlewares/isHalfRegister.js +++ b/controllers/application/academy/middlewares/isHalfRegister.js @@ -1,53 +1,53 @@ -const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر -const jwt = require('jsonwebtoken') - -module.exports = async (req, res, next) => { - console.log(3); - try { - console.log(31); - if (!('authorization' in req.headers)) { - return res.status(401).send({ - status: 'error', - code: 401, - message: 'You are not authorized!' - }) - } - console.log(32); - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - - if (!decodedToken) { - return res.status(401).send({ - status: 'error', - code: 401, - message: 'Your token is not valid!' - }) - } - console.log(33); - const user = await UserModel.findById(decodedToken.id) - if (!user.user_name || - !user.first_name || - !user.last_name || - !user.user_type || - !user.password) { - return res.status(403).send({ - status: 'error', - code: 403, - message: 'لطفا از بخش تنظیمات، ثبت نام خود را کامل کنید', - type: 'not_register' - }) - } - console.log(34); - req.user = user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود. - next() - } catch (error) { - console.log(35); - console.error('not_register middleware:', error) - res.status(500).send({ - status: 'error', - code: 500, - message: 'Server error' - }) - } -} +const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر +const jwt = require('jsonwebtoken') + +module.exports = async (req, res, next) => { + console.log(3); + try { + console.log(31); + if (!('authorization' in req.headers)) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'You are not authorized!' + }) + } + console.log(32); + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + + if (!decodedToken) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'Your token is not valid!' + }) + } + console.log(33); + const user = await UserModel.findById(decodedToken.id) + if (!user.user_name || + !user.first_name || + !user.last_name || + !user.user_type || + !user.password) { + return res.status(403).send({ + status: 'error', + code: 403, + message: 'لطفا از بخش تنظیمات، ثبت نام خود را کامل کنید', + type: 'not_register' + }) + } + console.log(34); + req.user = user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود. + next() + } catch (error) { + console.log(35); + console.error('not_register middleware:', error) + res.status(500).send({ + status: 'error', + code: 500, + message: 'Server error' + }) + } +} diff --git a/controllers/application/academy/middlewares/isRegister.js b/controllers/application/academy/middlewares/isRegister.js index 262872b..1f97f96 100644 --- a/controllers/application/academy/middlewares/isRegister.js +++ b/controllers/application/academy/middlewares/isRegister.js @@ -1,50 +1,50 @@ -const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر -const jwt = require('jsonwebtoken') - -module.exports = async (req, res, next) => { - try { - if (!('authorization' in req.headers)) { - return res.status(401).send({ - status: 'error', - code: 401, - message: 'You are not authorized!' - }) - } - - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - - if (!decodedToken) { - return res.status(401).send({ - status: 'error', - code: 401, - message: 'Your token is not valid!' - }) - } - - const user = await UserModel.findById(decodedToken.id) - if (!user.user_name || - !user.first_name || - !user.last_name || - !user.password) { - return res.status(403).send({ - status: 'error', - code: 403, - message: 'لطفا از بخش تنظیمات، ثبت نام خود را کامل کنید', - type: 'not_register' - }) - } - - - req.user = user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود. - next() - } catch (error) { - console.error('not_register middleware:', error) - res.status(500).send({ - status: 'error', - code: 500, - message: 'Server error' - }) - } -} +const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر +const jwt = require('jsonwebtoken') + +module.exports = async (req, res, next) => { + try { + if (!('authorization' in req.headers)) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'You are not authorized!' + }) + } + + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + + if (!decodedToken) { + return res.status(401).send({ + status: 'error', + code: 401, + message: 'Your token is not valid!' + }) + } + + const user = await UserModel.findById(decodedToken.id) + if (!user.user_name || + !user.first_name || + !user.last_name || + !user.password) { + return res.status(403).send({ + status: 'error', + code: 403, + message: 'لطفا از بخش تنظیمات، ثبت نام خود را کامل کنید', + type: 'not_register' + }) + } + + + req.user = user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود. + next() + } catch (error) { + console.error('not_register middleware:', error) + res.status(500).send({ + status: 'error', + code: 500, + message: 'Server error' + }) + } +} diff --git a/controllers/application/academy/middlewares/lastOnline.js b/controllers/application/academy/middlewares/lastOnline.js index 02e6532..716a901 100644 --- a/controllers/application/academy/middlewares/lastOnline.js +++ b/controllers/application/academy/middlewares/lastOnline.js @@ -1,21 +1,21 @@ -const jwt = require('jsonwebtoken') -const UserModel = require('../models/UserModel') - -module.exports = async (req, res, next) => { - if (req.header && req.header('Authorization')) { - const token = await req.header('Authorization').split(' ')[1] - const decodedToken = await jwt.verify(token, process.env.APP_SECRET) - const user = await UserModel.findById(decodedToken.id) - if (user) { - try { - user.last_online = new Date() - await user.save() // ذخیره تغییرات در دیتابیس - } catch (error) { - // بررسی و پردازش خطاها - console.error('Error updating last online time:', error) - } - } - } - - next() -} +const jwt = require('jsonwebtoken') +const UserModel = require('../models/UserModel') + +module.exports = async (req, res, next) => { + if (req.header && req.header('Authorization')) { + const token = await req.header('Authorization').split(' ')[1] + const decodedToken = await jwt.verify(token, process.env.APP_SECRET) + const user = await UserModel.findById(decodedToken.id) + if (user) { + try { + user.last_online = new Date() + await user.save() // ذخیره تغییرات در دیتابیس + } catch (error) { + // بررسی و پردازش خطاها + console.error('Error updating last online time:', error) + } + } + } + + next() +} diff --git a/controllers/application/academy/middlewares/upload.js b/controllers/application/academy/middlewares/upload.js index f95eb44..2229afb 100644 --- a/controllers/application/academy/middlewares/upload.js +++ b/controllers/application/academy/middlewares/upload.js @@ -1,24 +1,24 @@ -const multer = require('multer'); -const path = require('path'); -const fs = require('fs-extra'); - -const uploadDir = path.join(__dirname, '../storage/posts'); -fs.ensureDirSync(uploadDir); // مطمئن میشه مسیر وجود داره - -const storage = multer.diskStorage({ - destination: (req, file, cb) => cb(null, uploadDir), - filename: (req, file, cb) => { - const ext = path.extname(file.originalname); - const name = file.originalname.replace(ext, '').replace(/\s/g, '_'); - cb(null, `${Date.now()}-${name}${ext}`); - } -}); - -const fileFilter = (req, file, cb) => { - if (!file.mimetype.startsWith('image/')) return cb(new Error('فقط تصاویر مجاز است')); - cb(null, true); -}; - -const upload = multer({ storage, fileFilter, limits: { files: 10 } }); // حداکثر ۱۰ فایل - -module.exports = upload; +const multer = require('multer'); +const path = require('path'); +const fs = require('fs-extra'); + +const uploadDir = path.join(__dirname, '../storage/posts'); +fs.ensureDirSync(uploadDir); // مطمئن میشه مسیر وجود داره + +const storage = multer.diskStorage({ + destination: (req, file, cb) => cb(null, uploadDir), + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + const name = file.originalname.replace(ext, '').replace(/\s/g, '_'); + cb(null, `${Date.now()}-${name}${ext}`); + } +}); + +const fileFilter = (req, file, cb) => { + if (!file.mimetype.startsWith('image/')) return cb(new Error('فقط تصاویر مجاز است')); + cb(null, true); +}; + +const upload = multer({ storage, fileFilter, limits: { files: 10 } }); // حداکثر ۱۰ فایل + +module.exports = upload; diff --git a/controllers/application/academy/models/AcademyCategoryModel.js b/controllers/application/academy/models/AcademyCategoryModel.js index 7f55200..be50bf0 100644 --- a/controllers/application/academy/models/AcademyCategoryModel.js +++ b/controllers/application/academy/models/AcademyCategoryModel.js @@ -1,190 +1,190 @@ -const mongoose = require('mongoose'); -const mongoosePaginate = require('mongoose-paginate-v2'); -const timestamp = require('mongoose-timestamp'); - -const AcademyCategorySchema = new mongoose.Schema({ - title: { - type: String, - required: [true, 'عنوان دسته‌بندی الزامی است'], - trim: true, - minLength: [2, 'عنوان باید حداقل ۲ کاراکتر باشد'], - maxLength: [100, 'عنوان باید حداکثر ۱۰۰ کاراکتر باشد'], - unique: true, - index: true, - }, - slug: { - type: String, - required: true, - unique: true, - lowercase: true, - trim: true, - index: true, - }, - description: { - type: String, - required: false, - trim: true, - maxLength: 500, - default: null, - }, - icon: { - type: String, - required: false, - default: null, - }, - color: { - type: String, - required: false, - default: '#3B82F6', - match: /^#[0-9A-F]{6}$/i, - }, - parent: { - type: mongoose.Schema.Types.ObjectId, - ref: 'AcademyCategory', - required: false, - default: null, - index: true, - }, - level: { - type: Number, - required: true, - default: 0, - min: 0, - max: 3, - }, - order: { - type: Number, - required: true, - default: 0, - index: true, - }, - image: { - type: String, - required: false, - default: null, - }, - course_count: { - type: Number, - required: true, - default: 0, - min: 0, - }, - status: { - type: String, - enum: ['active', 'inactive', 'deleted'], - required: true, - default: 'active', - index: true, - }, - is_featured: { - type: Boolean, - required: true, - default: false, - index: true, - }, - seo_title: { - type: String, - required: false, - trim: true, - maxLength: 70, - }, - seo_description: { - type: String, - required: false, - trim: true, - maxLength: 160, - }, - created_by: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true, - }, - updated_by: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: false, - }, -}, { - toJSON: { virtuals: true }, - toObject: { virtuals: true } -}); - -// Virtual برای زیردسته‌ها -AcademyCategorySchema.virtual('children', { - ref: 'AcademyCategory', - localField: '_id', - foreignField: 'parent', - justOne: false, - options: { sort: { order: 1, title: 1 } } -}); - -// Virtual برای دوره‌های این دسته -AcademyCategorySchema.virtual('courses', { - ref: 'Course', - localField: '_id', - foreignField: 'category_id', - justOne: false, - match: { status: 'accept' } -}); - -// Middleware: قبل از save -AcademyCategorySchema.pre('save', async function(next) { - if (this.isModified('title') || this.isNew) { - // تولید slug - this.slug = this.title - .replace(/[^\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C\uFB8E\u0698a-zA-Z0-9\s]/g, '') - .trim() - .replace(/\s+/g, '-') - .toLowerCase(); - - // اگه parent داره، سطح رو محاسبه کن - if (this.parent) { - const parent = await this.constructor.findById(this.parent); - if (parent) { - this.level = parent.level + 1; - } - } else { - this.level = 0; - } - } - next(); -}); - -// استاتیک متدها -AcademyCategorySchema.statics = { - // دریافت درخت دسته‌بندی‌ها - async getTree() { - const categories = await this.find({ status: 'active', parent: null }) - .sort({ order: 1, title: 1 }) - .populate({ - path: 'children', - match: { status: 'active' }, - options: { sort: { order: 1, title: 1 } }, - populate: { - path: 'children', - match: { status: 'active' }, - options: { sort: { order: 1, title: 1 } } - } - }); - return categories; - }, - - // افزایش تعداد دوره‌ها - async incrementCourseCount(categoryId, increment = 1) { - return this.findByIdAndUpdate(categoryId, { - $inc: { course_count: increment } - }); - }, - - // دریافت دسته‌بندی با اسلاگ - async getBySlug(slug) { - return this.findOne({ slug, status: 'active' }) - .populate('children') - .populate('parent'); - } -}; - -AcademyCategorySchema.plugin(timestamp); -AcademyCategorySchema.plugin(mongoosePaginate); - +const mongoose = require('mongoose'); +const mongoosePaginate = require('mongoose-paginate-v2'); +const timestamp = require('mongoose-timestamp'); + +const AcademyCategorySchema = new mongoose.Schema({ + title: { + type: String, + required: [true, 'عنوان دسته‌بندی الزامی است'], + trim: true, + minLength: [2, 'عنوان باید حداقل ۲ کاراکتر باشد'], + maxLength: [100, 'عنوان باید حداکثر ۱۰۰ کاراکتر باشد'], + unique: true, + index: true, + }, + slug: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + index: true, + }, + description: { + type: String, + required: false, + trim: true, + maxLength: 500, + default: null, + }, + icon: { + type: String, + required: false, + default: null, + }, + color: { + type: String, + required: false, + default: '#3B82F6', + match: /^#[0-9A-F]{6}$/i, + }, + parent: { + type: mongoose.Schema.Types.ObjectId, + ref: 'AcademyCategory', + required: false, + default: null, + index: true, + }, + level: { + type: Number, + required: true, + default: 0, + min: 0, + max: 3, + }, + order: { + type: Number, + required: true, + default: 0, + index: true, + }, + image: { + type: String, + required: false, + default: null, + }, + course_count: { + type: Number, + required: true, + default: 0, + min: 0, + }, + status: { + type: String, + enum: ['active', 'inactive', 'deleted'], + required: true, + default: 'active', + index: true, + }, + is_featured: { + type: Boolean, + required: true, + default: false, + index: true, + }, + seo_title: { + type: String, + required: false, + trim: true, + maxLength: 70, + }, + seo_description: { + type: String, + required: false, + trim: true, + maxLength: 160, + }, + created_by: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, + }, + updated_by: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: false, + }, +}, { + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}); + +// Virtual برای زیردسته‌ها +AcademyCategorySchema.virtual('children', { + ref: 'AcademyCategory', + localField: '_id', + foreignField: 'parent', + justOne: false, + options: { sort: { order: 1, title: 1 } } +}); + +// Virtual برای دوره‌های این دسته +AcademyCategorySchema.virtual('courses', { + ref: 'Course', + localField: '_id', + foreignField: 'category_id', + justOne: false, + match: { status: 'accept' } +}); + +// Middleware: قبل از save +AcademyCategorySchema.pre('save', async function(next) { + if (this.isModified('title') || this.isNew) { + // تولید slug + this.slug = this.title + .replace(/[^\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C\uFB8E\u0698a-zA-Z0-9\s]/g, '') + .trim() + .replace(/\s+/g, '-') + .toLowerCase(); + + // اگه parent داره، سطح رو محاسبه کن + if (this.parent) { + const parent = await this.constructor.findById(this.parent); + if (parent) { + this.level = parent.level + 1; + } + } else { + this.level = 0; + } + } + next(); +}); + +// استاتیک متدها +AcademyCategorySchema.statics = { + // دریافت درخت دسته‌بندی‌ها + async getTree() { + const categories = await this.find({ status: 'active', parent: null }) + .sort({ order: 1, title: 1 }) + .populate({ + path: 'children', + match: { status: 'active' }, + options: { sort: { order: 1, title: 1 } }, + populate: { + path: 'children', + match: { status: 'active' }, + options: { sort: { order: 1, title: 1 } } + } + }); + return categories; + }, + + // افزایش تعداد دوره‌ها + async incrementCourseCount(categoryId, increment = 1) { + return this.findByIdAndUpdate(categoryId, { + $inc: { course_count: increment } + }); + }, + + // دریافت دسته‌بندی با اسلاگ + async getBySlug(slug) { + return this.findOne({ slug, status: 'active' }) + .populate('children') + .populate('parent'); + } +}; + +AcademyCategorySchema.plugin(timestamp); +AcademyCategorySchema.plugin(mongoosePaginate); + module.exports = mongoose.model('AcademyCategory', AcademyCategorySchema); \ No newline at end of file diff --git a/controllers/application/academy/models/AcademyContentModel.js b/controllers/application/academy/models/AcademyContentModel.js index f96c2af..092990f 100644 --- a/controllers/application/academy/models/AcademyContentModel.js +++ b/controllers/application/academy/models/AcademyContentModel.js @@ -1,71 +1,71 @@ -const mongoose = require('mongoose'); -const timestamp = require('mongoose-timestamp'); -const mongoosePaginate = require('mongoose-paginate-v2'); - -const modelSchema = new mongoose.Schema({ - course_images: { - type: [String], // آرایه‌ای از مسیرهای تصاویر - required: false, - default: [], - }, - course_video: { - type: String, // مسیر ویدئو - required: false, - trim: true, - default: null, - }, - type: { - type: String, // نوع پست: 'image' یا 'video' - required: true, - enum: ['image', 'video'], - }, - files: [ - { - path: { type: String, required: false }, - type: { type: String, enum: ['image', 'video'], required: false }, - } - ], - caption: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 1000, - default: null, - }, - status: { - type: String, - required: false, - trim: true, - enum: ['pending', 'accept', 'reject'], - default: 'accept', - }, - user_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - }, - likes: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - }], - comments: { - type: Object, - default: null, - }, - courseId: { - type: String, - }, - - is_free: { - type: Boolean, - }, - file_name: { - type: String, - }, - -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model('AcademyContent', modelSchema); +const mongoose = require('mongoose'); +const timestamp = require('mongoose-timestamp'); +const mongoosePaginate = require('mongoose-paginate-v2'); + +const modelSchema = new mongoose.Schema({ + course_images: { + type: [String], // آرایه‌ای از مسیرهای تصاویر + required: false, + default: [], + }, + course_video: { + type: String, // مسیر ویدئو + required: false, + trim: true, + default: null, + }, + type: { + type: String, // نوع پست: 'image' یا 'video' + required: true, + enum: ['image', 'video'], + }, + files: [ + { + path: { type: String, required: false }, + type: { type: String, enum: ['image', 'video'], required: false }, + } + ], + caption: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 1000, + default: null, + }, + status: { + type: String, + required: false, + trim: true, + enum: ['pending', 'accept', 'reject'], + default: 'accept', + }, + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + }, + likes: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + }], + comments: { + type: Object, + default: null, + }, + courseId: { + type: String, + }, + + is_free: { + type: Boolean, + }, + file_name: { + type: String, + }, + +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model('AcademyContent', modelSchema); module.exports = PostModel; \ No newline at end of file diff --git a/controllers/application/academy/models/AcademyLikeModel.js b/controllers/application/academy/models/AcademyLikeModel.js index bfc2048..358f036 100644 --- a/controllers/application/academy/models/AcademyLikeModel.js +++ b/controllers/application/academy/models/AcademyLikeModel.js @@ -1,25 +1,25 @@ -const mongoose = require("mongoose"); -const timestamp = require("mongoose-timestamp"); -const mongoosePaginate = require("mongoose-paginate-v2"); - -const modelSchema = new mongoose.Schema({ - - user_id: { - type: String, - required: true, - trim: true, - default: 0, - }, - course_id: { - type: String, - required: true, - trim: true, - default: 0, - }, - -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model("Likes", modelSchema); -module.exports = PostModel; +const mongoose = require("mongoose"); +const timestamp = require("mongoose-timestamp"); +const mongoosePaginate = require("mongoose-paginate-v2"); + +const modelSchema = new mongoose.Schema({ + + user_id: { + type: String, + required: true, + trim: true, + default: 0, + }, + course_id: { + type: String, + required: true, + trim: true, + default: 0, + }, + +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model("Likes", modelSchema); +module.exports = PostModel; diff --git a/controllers/application/academy/models/AcademyModel.js b/controllers/application/academy/models/AcademyModel.js index 9a86d18..9f8e758 100644 --- a/controllers/application/academy/models/AcademyModel.js +++ b/controllers/application/academy/models/AcademyModel.js @@ -1,55 +1,55 @@ -const mongoose = require("mongoose"); -const timestamp = require("mongoose-timestamp"); -const mongoosePaginate = require("mongoose-paginate-v2"); - -const modelSchema = new mongoose.Schema({ - academy_image: { - type: String, - required: false, - trim: true, - default: null, - }, - academy_name: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null, - }, - userId: { - type: String, - required: true, - }, - rate: { - type: Number, - trim: true, - default: 0 - }, - number_of_rate: { - type: Number, - trim: true, - default: 0 - }, - sheba: { - type: String, - required: false, - default: null - }, - bio: { - type: String, - required: false, - default: null - }, - tag: { - type: Object, - required: false, - default: null - }, - -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model("Academy", modelSchema); -module.exports = PostModel; +const mongoose = require("mongoose"); +const timestamp = require("mongoose-timestamp"); +const mongoosePaginate = require("mongoose-paginate-v2"); + +const modelSchema = new mongoose.Schema({ + academy_image: { + type: String, + required: false, + trim: true, + default: null, + }, + academy_name: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null, + }, + userId: { + type: String, + required: true, + }, + rate: { + type: Number, + trim: true, + default: 0 + }, + number_of_rate: { + type: Number, + trim: true, + default: 0 + }, + sheba: { + type: String, + required: false, + default: null + }, + bio: { + type: String, + required: false, + default: null + }, + tag: { + type: Object, + required: false, + default: null + }, + +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model("Academy", modelSchema); +module.exports = PostModel; diff --git a/controllers/application/academy/models/AcademyPaymentModel.js b/controllers/application/academy/models/AcademyPaymentModel.js index 28e1923..b7c9fa7 100644 --- a/controllers/application/academy/models/AcademyPaymentModel.js +++ b/controllers/application/academy/models/AcademyPaymentModel.js @@ -1,24 +1,24 @@ -const mongoose = require("mongoose"); -const timestamp = require("mongoose-timestamp"); -const mongoosePaginate = require("mongoose-paginate-v2"); - -const modelSchema = new mongoose.Schema({ - price: { type: Number, required: true }, // قیمت نهایی شامل مالیات - discountAmount: { type: Number, default: 0 }, // مبلغ تخفیف - taxAmount: { type: Number, default: 0 }, // مبلغ مالیات - taxRate: { type: Number, default: 9 }, // درصد مالیات - course_name: { type: String, required: true }, - course_id: { type: mongoose.Schema.Types.ObjectId, ref: "Course", required: true }, - academy_id: { type: mongoose.Schema.Types.ObjectId, ref: "Academy", required: true }, - user_id: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - payment_authority: { type: String }, - payment_ref_id: { type: String }, - status: { type: String, enum: ["pending", "success", "failed", "settled"], default: "success" }, - - -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model("AcademyPayment", modelSchema); -module.exports = PostModel; +const mongoose = require("mongoose"); +const timestamp = require("mongoose-timestamp"); +const mongoosePaginate = require("mongoose-paginate-v2"); + +const modelSchema = new mongoose.Schema({ + price: { type: Number, required: true }, // قیمت نهایی شامل مالیات + discountAmount: { type: Number, default: 0 }, // مبلغ تخفیف + taxAmount: { type: Number, default: 0 }, // مبلغ مالیات + taxRate: { type: Number, default: 9 }, // درصد مالیات + course_name: { type: String, required: true }, + course_id: { type: mongoose.Schema.Types.ObjectId, ref: "Course", required: true }, + academy_id: { type: mongoose.Schema.Types.ObjectId, ref: "Academy", required: true }, + user_id: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + payment_authority: { type: String }, + payment_ref_id: { type: String }, + status: { type: String, enum: ["pending", "success", "failed", "settled"], default: "success" }, + + +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model("AcademyPayment", modelSchema); +module.exports = PostModel; diff --git a/controllers/application/academy/models/AdminModel.js b/controllers/application/academy/models/AdminModel.js index 83a358b..4bca3a2 100644 --- a/controllers/application/academy/models/AdminModel.js +++ b/controllers/application/academy/models/AdminModel.js @@ -1,80 +1,80 @@ -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate-v2') -const timestamp = require('mongoose-timestamp') -const adminSchema = new mongoose.Schema({ - mobile: { - type: String, - required: false, - trim: true, - unique: true - }, - otp: { - type: String, - required: false, - trim: true, - minLength: 6, - maxLength: 6 - }, - user_name: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - password: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - first_name: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - last_name: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - user_type: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - profile_image: { - type: String, - required: false, - trim: true - }, - gender: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - last_online: { - type: Date, // ذخیره تاریخ و زمان آخرین بازدید - default: Date.now // تنظیم تاریخ به تاریخ فعلی - } -}) - -adminSchema.plugin(timestamp) -adminSchema.plugin(mongoosePaginate) -const AdminModel = mongoose.model('Admin', adminSchema) -module.exports = AdminModel +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate-v2') +const timestamp = require('mongoose-timestamp') +const adminSchema = new mongoose.Schema({ + mobile: { + type: String, + required: false, + trim: true, + unique: true + }, + otp: { + type: String, + required: false, + trim: true, + minLength: 6, + maxLength: 6 + }, + user_name: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + password: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + first_name: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + last_name: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + user_type: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + profile_image: { + type: String, + required: false, + trim: true + }, + gender: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + last_online: { + type: Date, // ذخیره تاریخ و زمان آخرین بازدید + default: Date.now // تنظیم تاریخ به تاریخ فعلی + } +}) + +adminSchema.plugin(timestamp) +adminSchema.plugin(mongoosePaginate) +const AdminModel = mongoose.model('Admin', adminSchema) +module.exports = AdminModel diff --git a/controllers/application/academy/models/AdvertisingCategoryModel.js b/controllers/application/academy/models/AdvertisingCategoryModel.js index 582a963..855035a 100644 --- a/controllers/application/academy/models/AdvertisingCategoryModel.js +++ b/controllers/application/academy/models/AdvertisingCategoryModel.js @@ -1,23 +1,23 @@ -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate-v2') -const timestamp = require('mongoose-timestamp') -const advertisingCategorySchema = new mongoose.Schema({ - title: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - status: { - type: Boolean, - required: false, - default: true - } -}) - -advertisingCategorySchema.plugin(timestamp) -advertisingCategorySchema.plugin(mongoosePaginate) -const AdvertisingCategoryModel = mongoose.model('AdvertisingCategory', advertisingCategorySchema) -module.exports = AdvertisingCategoryModel +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate-v2') +const timestamp = require('mongoose-timestamp') +const advertisingCategorySchema = new mongoose.Schema({ + title: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + status: { + type: Boolean, + required: false, + default: true + } +}) + +advertisingCategorySchema.plugin(timestamp) +advertisingCategorySchema.plugin(mongoosePaginate) +const AdvertisingCategoryModel = mongoose.model('AdvertisingCategory', advertisingCategorySchema) +module.exports = AdvertisingCategoryModel diff --git a/controllers/application/academy/models/AdvertisingCommentModel.js b/controllers/application/academy/models/AdvertisingCommentModel.js index 6eb4640..f6ea2b4 100644 --- a/controllers/application/academy/models/AdvertisingCommentModel.js +++ b/controllers/application/academy/models/AdvertisingCommentModel.js @@ -1,32 +1,32 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') -const mongoosePaginate = require('mongoose-paginate-v2') - -const advertisingCommentSchema = new mongoose.Schema({ - advertisingId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Advertising', - required: true - }, - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - text: { - type: String, - required: true, - trim: true - }, - status: { - type: String, - enum: ['pending', 'accepted', 'rejected'], - default: 'pending' - } -}) - -advertisingCommentSchema.plugin(timestamp) -advertisingCommentSchema.plugin(mongoosePaginate) -const AdvertisingComment = mongoose.model('AdvertisingComment', advertisingCommentSchema) - -module.exports = AdvertisingComment +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') +const mongoosePaginate = require('mongoose-paginate-v2') + +const advertisingCommentSchema = new mongoose.Schema({ + advertisingId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Advertising', + required: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + text: { + type: String, + required: true, + trim: true + }, + status: { + type: String, + enum: ['pending', 'accepted', 'rejected'], + default: 'pending' + } +}) + +advertisingCommentSchema.plugin(timestamp) +advertisingCommentSchema.plugin(mongoosePaginate) +const AdvertisingComment = mongoose.model('AdvertisingComment', advertisingCommentSchema) + +module.exports = AdvertisingComment diff --git a/controllers/application/academy/models/AdvertisingFeaturesModel.js b/controllers/application/academy/models/AdvertisingFeaturesModel.js index 045b3b2..4081d59 100644 --- a/controllers/application/academy/models/AdvertisingFeaturesModel.js +++ b/controllers/application/academy/models/AdvertisingFeaturesModel.js @@ -1,27 +1,27 @@ -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate-v2') -const timestamp = require('mongoose-timestamp') -const advertisingFeaturesSchema = new mongoose.Schema({ - title: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - status: { - type: Boolean, - required: false, - default: true - }, - type: { - type: String, - default: 'boolean' - } -}) - -advertisingFeaturesSchema.plugin(timestamp) -advertisingFeaturesSchema.plugin(mongoosePaginate) -const AdvertisingFeaturesModel = mongoose.model('AdvertisingFeatures', advertisingFeaturesSchema) -module.exports = AdvertisingFeaturesModel +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate-v2') +const timestamp = require('mongoose-timestamp') +const advertisingFeaturesSchema = new mongoose.Schema({ + title: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + status: { + type: Boolean, + required: false, + default: true + }, + type: { + type: String, + default: 'boolean' + } +}) + +advertisingFeaturesSchema.plugin(timestamp) +advertisingFeaturesSchema.plugin(mongoosePaginate) +const AdvertisingFeaturesModel = mongoose.model('AdvertisingFeatures', advertisingFeaturesSchema) +module.exports = AdvertisingFeaturesModel diff --git a/controllers/application/academy/models/AdvertisingLikeModel.js b/controllers/application/academy/models/AdvertisingLikeModel.js index 752e97c..f8552dd 100644 --- a/controllers/application/academy/models/AdvertisingLikeModel.js +++ b/controllers/application/academy/models/AdvertisingLikeModel.js @@ -1,20 +1,20 @@ -const mongoose = require('mongoose') - -const advertisingLikeSchema = new mongoose.Schema({ - advertisingId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Advertising', - required: true - }, - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - } - -}) - -// تعریف مدل AdvertisingLikeModel -const AdvertisingLikeModel = mongoose.model('AdvertisingLike', advertisingLikeSchema) - -module.exports = AdvertisingLikeModel +const mongoose = require('mongoose') + +const advertisingLikeSchema = new mongoose.Schema({ + advertisingId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Advertising', + required: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + } + +}) + +// تعریف مدل AdvertisingLikeModel +const AdvertisingLikeModel = mongoose.model('AdvertisingLike', advertisingLikeSchema) + +module.exports = AdvertisingLikeModel diff --git a/controllers/application/academy/models/AdvertisingModel.js b/controllers/application/academy/models/AdvertisingModel.js index bac1130..3046a48 100644 --- a/controllers/application/academy/models/AdvertisingModel.js +++ b/controllers/application/academy/models/AdvertisingModel.js @@ -1,230 +1,230 @@ -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate-v2') -const timestamp = require('mongoose-timestamp') -const advertisingSchema = new mongoose.Schema({ - title: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - category: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - description: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 1024, - default: null - }, - province: { - type: Object, - default: null - }, - city: { - type: Object, - default: null - }, - neighbourhood: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - address: { - type: String, - trim: true, - minLength: 1, - maxLength: 512, - default: null - }, - lat: { - type: String, - trim: true, - minLength: 1, - maxLength: 256, - default: null - }, - lng: { - type: String, - trim: true, - minLength: 1, - maxLength: 256, - default: null - }, - images: [{ - type: String, - required: false, - trim: true, - default: null - }], - services: [{ - title: { - type: String, - required: true, - trim: true - }, - originalPrice: { - type: String, - required: true - }, - discountPrice: { - type: String, - required: false - }, - discountPercentage: { - type: Number, - required: false - }, - status: { - type: String, - default: "UnderReview", - required: false - }, - image: { - type: String, - required: false, - trim: true - } - }], - features: [{ - title: { - type: String, - required: false, - trim: true - }, - value: { - type: Boolean, - required: true - } - }], - contactInfo: { - phone: { - type: String, - required: false, - trim: true, - default: null - }, - mobile: { - type: String, - required: false, - trim: true, - default: null - }, - telegramLink: { - type: String, - required: false, - trim: true, - default: null - }, - whatsappNumber: { - type: String, - required: false, - trim: true, - default: null - }, - instagramLink: { - type: String, - required: false, - trim: true, - default: null - }, - saveInfoForNextAds: { - type: Boolean, - required: true, - default: false - } - }, - type: { - type: String, - enum: ['free', 'normal', 'special', 'highlight'], - default: 'normal' - }, - status: { - type: String, - enum: ['pre_payment', 'paid', 'accepted', 'rejected', 'expired'], - default: 'pre_payment' - }, - payment_status: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - creator_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User' - }, - reject_reason: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 1024, - default: null - }, - mostDiscountPercentage: { - type: Number, - required: false - }, - showDiscount: { - type: Boolean, - required: false, - default: false - }, - likesCount: { - type: Number, - default: 0 - }, - commentsCount: { - type: Number, - default: 0 - }, - ratings: [{ - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - rating: { - type: Number, - required: true, - min: 1, - max: 5 - } - }], - averageRating: { - type: Number, - default: 0 - }, - ratingsCount: { - type: Number, - default: 0 - }, - viewCount: { - type: Number, - default: 0 - }, - acceptedAt: { - type: Date, - default: null - } -}) - -advertisingSchema.plugin(timestamp) -advertisingSchema.plugin(mongoosePaginate) -const AdvertisingModel = mongoose.model('Advertising', advertisingSchema) -module.exports = AdvertisingModel +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate-v2') +const timestamp = require('mongoose-timestamp') +const advertisingSchema = new mongoose.Schema({ + title: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + category: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + description: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 1024, + default: null + }, + province: { + type: Object, + default: null + }, + city: { + type: Object, + default: null + }, + neighbourhood: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + address: { + type: String, + trim: true, + minLength: 1, + maxLength: 512, + default: null + }, + lat: { + type: String, + trim: true, + minLength: 1, + maxLength: 256, + default: null + }, + lng: { + type: String, + trim: true, + minLength: 1, + maxLength: 256, + default: null + }, + images: [{ + type: String, + required: false, + trim: true, + default: null + }], + services: [{ + title: { + type: String, + required: true, + trim: true + }, + originalPrice: { + type: String, + required: true + }, + discountPrice: { + type: String, + required: false + }, + discountPercentage: { + type: Number, + required: false + }, + status: { + type: String, + default: "UnderReview", + required: false + }, + image: { + type: String, + required: false, + trim: true + } + }], + features: [{ + title: { + type: String, + required: false, + trim: true + }, + value: { + type: Boolean, + required: true + } + }], + contactInfo: { + phone: { + type: String, + required: false, + trim: true, + default: null + }, + mobile: { + type: String, + required: false, + trim: true, + default: null + }, + telegramLink: { + type: String, + required: false, + trim: true, + default: null + }, + whatsappNumber: { + type: String, + required: false, + trim: true, + default: null + }, + instagramLink: { + type: String, + required: false, + trim: true, + default: null + }, + saveInfoForNextAds: { + type: Boolean, + required: true, + default: false + } + }, + type: { + type: String, + enum: ['free', 'normal', 'special', 'highlight'], + default: 'normal' + }, + status: { + type: String, + enum: ['pre_payment', 'paid', 'accepted', 'rejected', 'expired'], + default: 'pre_payment' + }, + payment_status: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + creator_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + reject_reason: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 1024, + default: null + }, + mostDiscountPercentage: { + type: Number, + required: false + }, + showDiscount: { + type: Boolean, + required: false, + default: false + }, + likesCount: { + type: Number, + default: 0 + }, + commentsCount: { + type: Number, + default: 0 + }, + ratings: [{ + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + rating: { + type: Number, + required: true, + min: 1, + max: 5 + } + }], + averageRating: { + type: Number, + default: 0 + }, + ratingsCount: { + type: Number, + default: 0 + }, + viewCount: { + type: Number, + default: 0 + }, + acceptedAt: { + type: Date, + default: null + } +}) + +advertisingSchema.plugin(timestamp) +advertisingSchema.plugin(mongoosePaginate) +const AdvertisingModel = mongoose.model('Advertising', advertisingSchema) +module.exports = AdvertisingModel diff --git a/controllers/application/academy/models/AdvertisingProfile.js b/controllers/application/academy/models/AdvertisingProfile.js index 435a2bf..70dfc5a 100644 --- a/controllers/application/academy/models/AdvertisingProfile.js +++ b/controllers/application/academy/models/AdvertisingProfile.js @@ -1,113 +1,113 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') -const mongoosePaginate = require('mongoose-paginate-v2') - -const advertisingProfileSchema = new mongoose.Schema({ - user: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - profile_image: { - type: String, - required: false, - trim: true, - default: null - }, - image_count: { - type: Number, - default: 0 - }, - like_count: { - type: Number, - default: 0 - }, - comment_count: { - type: Number, - default: 0 - }, - vitrine_name: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - category: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - about_us: { - type: String, - required: false, - minLength: 1, - maxLength: 1024, - default: null - }, - province: { - type: Object, - default: null - }, - city: { - type: Object, - default: null - }, - lat: { - type: String, - trim: true, - minLength: 1, - maxLength: 512, - default: null - }, - lng: { - type: String, - trim: true, - minLength: 1, - maxLength: 512, - default: null - }, - address: { - type: String, - required: false, - trim: true, - default: null - }, - neighbourhood: { - type: String, - required: false, - trim: true, - default: null - }, - adTotalRatings: { - type: Number, - default: 0 - }, - adRatingsCount: { - type: Number, - default: 0 - }, - adAverageRating: { - type: Number, - default: 0 - }, - contactInfo: { - phone: String, - mobile: String, - telegramLink: String, - whatsappNumber: String, - instagramLink: String, - saveInfoForNextAds: { type: Boolean, default: false } - } -}) - -advertisingProfileSchema.plugin(timestamp) -advertisingProfileSchema.plugin(mongoosePaginate) - -const AdvertisingProfileModel = mongoose.model('AdvertisingProfile', advertisingProfileSchema) - -module.exports = AdvertisingProfileModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') +const mongoosePaginate = require('mongoose-paginate-v2') + +const advertisingProfileSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + profile_image: { + type: String, + required: false, + trim: true, + default: null + }, + image_count: { + type: Number, + default: 0 + }, + like_count: { + type: Number, + default: 0 + }, + comment_count: { + type: Number, + default: 0 + }, + vitrine_name: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + category: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + about_us: { + type: String, + required: false, + minLength: 1, + maxLength: 1024, + default: null + }, + province: { + type: Object, + default: null + }, + city: { + type: Object, + default: null + }, + lat: { + type: String, + trim: true, + minLength: 1, + maxLength: 512, + default: null + }, + lng: { + type: String, + trim: true, + minLength: 1, + maxLength: 512, + default: null + }, + address: { + type: String, + required: false, + trim: true, + default: null + }, + neighbourhood: { + type: String, + required: false, + trim: true, + default: null + }, + adTotalRatings: { + type: Number, + default: 0 + }, + adRatingsCount: { + type: Number, + default: 0 + }, + adAverageRating: { + type: Number, + default: 0 + }, + contactInfo: { + phone: String, + mobile: String, + telegramLink: String, + whatsappNumber: String, + instagramLink: String, + saveInfoForNextAds: { type: Boolean, default: false } + } +}) + +advertisingProfileSchema.plugin(timestamp) +advertisingProfileSchema.plugin(mongoosePaginate) + +const AdvertisingProfileModel = mongoose.model('AdvertisingProfile', advertisingProfileSchema) + +module.exports = AdvertisingProfileModel diff --git a/controllers/application/academy/models/AdvertisingRatingModel.js b/controllers/application/academy/models/AdvertisingRatingModel.js index 150059e..ac8b79b 100644 --- a/controllers/application/academy/models/AdvertisingRatingModel.js +++ b/controllers/application/academy/models/AdvertisingRatingModel.js @@ -1,26 +1,26 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') - -const advertisingRatingSchema = new mongoose.Schema({ - advertisingId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Advertising', - required: true - }, - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - rating: { - type: Number, - required: true, - min: 1, - max: 5 - } -}) - -advertisingRatingSchema.plugin(timestamp) - -const AdvertisingRatingModel = mongoose.model('AdvertisingRating', advertisingRatingSchema) -module.exports = AdvertisingRatingModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') + +const advertisingRatingSchema = new mongoose.Schema({ + advertisingId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Advertising', + required: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + rating: { + type: Number, + required: true, + min: 1, + max: 5 + } +}) + +advertisingRatingSchema.plugin(timestamp) + +const AdvertisingRatingModel = mongoose.model('AdvertisingRating', advertisingRatingSchema) +module.exports = AdvertisingRatingModel diff --git a/controllers/application/academy/models/AdvertisingTypeModel.js b/controllers/application/academy/models/AdvertisingTypeModel.js index c860da5..5b49649 100644 --- a/controllers/application/academy/models/AdvertisingTypeModel.js +++ b/controllers/application/academy/models/AdvertisingTypeModel.js @@ -1,25 +1,25 @@ -const mongoose = require('mongoose') - -const advertisingTypeSchema = new mongoose.Schema({ - name: { - type: String, - required: true, - trim: true, - minLength: 1, - maxLength: 255 - }, - price: { - type: Number, - required: true, - trim: true, - minLength: 1, - maxLength: 255 - }, - status: { - type: String, - default: 'active' - } -}) - -const AdvertisingTypeModel = mongoose.model('AdvertisingType', advertisingTypeSchema) -module.exports = AdvertisingTypeModel +const mongoose = require('mongoose') + +const advertisingTypeSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true, + minLength: 1, + maxLength: 255 + }, + price: { + type: Number, + required: true, + trim: true, + minLength: 1, + maxLength: 255 + }, + status: { + type: String, + default: 'active' + } +}) + +const AdvertisingTypeModel = mongoose.model('AdvertisingType', advertisingTypeSchema) +module.exports = AdvertisingTypeModel diff --git a/controllers/application/academy/models/CommentModel.js b/controllers/application/academy/models/CommentModel.js index 07c4103..2c0e8dd 100644 --- a/controllers/application/academy/models/CommentModel.js +++ b/controllers/application/academy/models/CommentModel.js @@ -1,54 +1,55 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') -const mongoosePaginate = require('mongoose-paginate-v2') - -const commentSchema = new mongoose.Schema({ - user: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - project: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Project', - required: false - }, - offer: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Offer', - required: false - }, - creator: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - rating: { - type: Number, - required: true - }, - comment: { - type: String, - trim: true - }, - post: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Post', - required: false - }, - comment_for: { - type: String, - enum: ['user', 'project', 'post'], - required: true - }, - status: { - type: String, - enum: ['pending', 'accepted', 'rejected'], - default: 'pending' - } -}) - -commentSchema.plugin(timestamp) -commentSchema.plugin(mongoosePaginate) -const CommentModel = mongoose.model('Comment', commentSchema) -module.exports = CommentModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') +const mongoosePaginate = require('mongoose-paginate-v2') + +const commentSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + project: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Project', + required: false + }, + offer: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Offer', + required: false + }, + creator: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + rating: { + type: Number, + required: false, + default: null + }, + comment: { + type: String, + trim: true + }, + post: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Post', + required: false + }, + comment_for: { + type: String, + enum: ['user', 'project', 'post'], + required: true + }, + status: { + type: String, + enum: ['pending', 'accepted', 'rejected'], + default: 'pending' + } +}) + +commentSchema.plugin(timestamp) +commentSchema.plugin(mongoosePaginate) +const CommentModel = mongoose.model('Comment', commentSchema) +module.exports = CommentModel diff --git a/controllers/application/academy/models/CourseComentModel.js b/controllers/application/academy/models/CourseComentModel.js index 9f541ed..b5c7d5c 100644 --- a/controllers/application/academy/models/CourseComentModel.js +++ b/controllers/application/academy/models/CourseComentModel.js @@ -1,40 +1,40 @@ -const mongoose = require("mongoose"); -const timestamp = require("mongoose-timestamp"); -const mongoosePaginate = require("mongoose-paginate-v2"); - -const modelSchema = new mongoose.Schema({ - - user_id: { - type: String, - required: true, - trim: true, - }, - course_id: { - type: String, - required: true, - trim: true, - }, - comment: { - type: String, - required: true, - trim: true, - default: 0, - }, - rate: { - type: String, - required: true, - trim: true, - default: 0, - }, - status: { - type: String, - enum: ['pending', 'accepted', 'rejected'], - default: 'accepted' - } - -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model("CourseComment", modelSchema); -module.exports = PostModel; +const mongoose = require("mongoose"); +const timestamp = require("mongoose-timestamp"); +const mongoosePaginate = require("mongoose-paginate-v2"); + +const modelSchema = new mongoose.Schema({ + + user_id: { + type: String, + required: true, + trim: true, + }, + course_id: { + type: String, + required: true, + trim: true, + }, + comment: { + type: String, + required: true, + trim: true, + default: 0, + }, + rate: { + type: String, + required: true, + trim: true, + default: 0, + }, + status: { + type: String, + enum: ['pending', 'accepted', 'rejected'], + default: 'accepted' + } + +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model("CourseComment", modelSchema); +module.exports = PostModel; diff --git a/controllers/application/academy/models/CourseModel.js b/controllers/application/academy/models/CourseModel.js index 220acd6..f8a5e67 100644 --- a/controllers/application/academy/models/CourseModel.js +++ b/controllers/application/academy/models/CourseModel.js @@ -1,79 +1,79 @@ -const mongoose = require("mongoose"); -const timestamp = require("mongoose-timestamp"); -const mongoosePaginate = require("mongoose-paginate-v2"); - -const modelSchema = new mongoose.Schema({ - price: { - type: String, - required: true, - }, - cuorse_name: { - type: String, - required: true, - }, - category:{ - type: String - }, - offer: { - type: String - }, - academyId:{ - type: String - }, - caption: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 1000, - default: null, - }, - status: { - type: String, - required: false, - trim: true, - enum: ['pending', 'accept', 'reject'], - default: 'accept', - }, - user_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - }, - likes: { - type: Number, - required: false, - trim: true, - default: 0, - }, - comments: { - type: Object, - default: null, - }, - type: { - type: String, - required: false, - trim: true, - enum: ['normal', 'pro', 'legend'], - default: "normal", - }, - course_time: { - type: String - }, - number_of_course_content: { - type: String - }, - teacher_number: { - type: String - }, - course_image: { - type: String - }, - teacher_name: { - type: String - }, -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model("Course", modelSchema); -module.exports = PostModel; +const mongoose = require("mongoose"); +const timestamp = require("mongoose-timestamp"); +const mongoosePaginate = require("mongoose-paginate-v2"); + +const modelSchema = new mongoose.Schema({ + price: { + type: String, + required: true, + }, + cuorse_name: { + type: String, + required: true, + }, + category:{ + type: String + }, + offer: { + type: String + }, + academyId:{ + type: String + }, + caption: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 1000, + default: null, + }, + status: { + type: String, + required: false, + trim: true, + enum: ['pending', 'accept', 'reject'], + default: 'accept', + }, + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + }, + likes: { + type: Number, + required: false, + trim: true, + default: 0, + }, + comments: { + type: Object, + default: null, + }, + type: { + type: String, + required: false, + trim: true, + enum: ['normal', 'pro', 'legend'], + default: "normal", + }, + course_time: { + type: String + }, + number_of_course_content: { + type: String + }, + teacher_number: { + type: String + }, + course_image: { + type: String + }, + teacher_name: { + type: String + }, +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model("Course", modelSchema); +module.exports = PostModel; diff --git a/controllers/application/academy/models/CoursePaymentModel.js b/controllers/application/academy/models/CoursePaymentModel.js index 7ddc068..cb6de53 100644 --- a/controllers/application/academy/models/CoursePaymentModel.js +++ b/controllers/application/academy/models/CoursePaymentModel.js @@ -1,22 +1,22 @@ -const mongoose = require("mongoose"); -const timestamp = require("mongoose-timestamp"); -const mongoosePaginate = require("mongoose-paginate-v2"); - -const modelSchema = new mongoose.Schema({ - price: { - type: String, - required: true, - }, - cuorse_type: { - type: String, - required: false, - trim: true, - enum: ['normal', 'pro', 'legend'], - }, - -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model("CoursePayment", modelSchema); -module.exports = PostModel; +const mongoose = require("mongoose"); +const timestamp = require("mongoose-timestamp"); +const mongoosePaginate = require("mongoose-paginate-v2"); + +const modelSchema = new mongoose.Schema({ + price: { + type: String, + required: true, + }, + cuorse_type: { + type: String, + required: false, + trim: true, + enum: ['normal', 'pro', 'legend'], + }, + +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model("CoursePayment", modelSchema); +module.exports = PostModel; diff --git a/controllers/application/academy/models/ExpertiseModel.js b/controllers/application/academy/models/ExpertiseModel.js index fbf0076..29b490a 100644 --- a/controllers/application/academy/models/ExpertiseModel.js +++ b/controllers/application/academy/models/ExpertiseModel.js @@ -1,47 +1,47 @@ -// const mongoose = require('mongoose') -// const timestamp = require('mongoose-timestamp') - -// const subExpertiseSchema = new mongoose.Schema({ -// id: { -// type: mongoose.Schema.Types.ObjectId, -// ref: 'ExpertiseModel' -// }, -// name: { -// type: String -// } -// }) - -// const expertiseSchema = new mongoose.Schema({ -// expertise: { -// type: String, -// unique: true -// }, -// sub_expertise: [subExpertiseSchema] -// }) - -// expertiseSchema.plugin(timestamp) - -// const ExpertiseModel = mongoose.model('Expertise', expertiseSchema) -// module.exports = ExpertiseModel - -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') - -const subExpertiseSchema = new mongoose.Schema({ - name: { - type: String - } -}) - -const expertiseSchema = new mongoose.Schema({ - expertise: { - type: String, - unique: true - }, - sub_expertise: [subExpertiseSchema] -}) - -expertiseSchema.plugin(timestamp) - -const ExpertiseModel = mongoose.model('Expertise', expertiseSchema) -module.exports = ExpertiseModel +// const mongoose = require('mongoose') +// const timestamp = require('mongoose-timestamp') + +// const subExpertiseSchema = new mongoose.Schema({ +// id: { +// type: mongoose.Schema.Types.ObjectId, +// ref: 'ExpertiseModel' +// }, +// name: { +// type: String +// } +// }) + +// const expertiseSchema = new mongoose.Schema({ +// expertise: { +// type: String, +// unique: true +// }, +// sub_expertise: [subExpertiseSchema] +// }) + +// expertiseSchema.plugin(timestamp) + +// const ExpertiseModel = mongoose.model('Expertise', expertiseSchema) +// module.exports = ExpertiseModel + +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') + +const subExpertiseSchema = new mongoose.Schema({ + name: { + type: String + } +}) + +const expertiseSchema = new mongoose.Schema({ + expertise: { + type: String, + unique: true + }, + sub_expertise: [subExpertiseSchema] +}) + +expertiseSchema.plugin(timestamp) + +const ExpertiseModel = mongoose.model('Expertise', expertiseSchema) +module.exports = ExpertiseModel diff --git a/controllers/application/academy/models/ExploreInteractionModel.js b/controllers/application/academy/models/ExploreInteractionModel.js new file mode 100644 index 0000000..dfb59e7 --- /dev/null +++ b/controllers/application/academy/models/ExploreInteractionModel.js @@ -0,0 +1,54 @@ +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') + +const exploreInteractionSchema = new mongoose.Schema({ + viewerId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + targetType: { + type: String, + enum: ['post', 'profile', 'academy'], + required: true + }, + targetId: { + type: mongoose.Schema.Types.ObjectId, + required: true + }, + authorId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + default: null + }, + contentType: { + type: String, + enum: ['image', 'video', 'academy'], + default: 'image' + }, + action: { + type: String, + enum: [ + 'view', + 'profile_visit', + 'share', + 'like', + 'comment', + 'offer', + 'watch_complete', + 'skip', + 'dwell' + ], + default: 'view' + }, + dwellMs: { + type: Number, + default: null + } +}) + +exploreInteractionSchema.index({ viewerId: 1, createdAt: -1 }) +exploreInteractionSchema.plugin(timestamp) + +module.exports = mongoose.model('ExploreInteraction', exploreInteractionSchema) diff --git a/controllers/application/academy/models/IsPaymentCourseModel.js b/controllers/application/academy/models/IsPaymentCourseModel.js index b467f87..0524f3c 100644 --- a/controllers/application/academy/models/IsPaymentCourseModel.js +++ b/controllers/application/academy/models/IsPaymentCourseModel.js @@ -1,15 +1,15 @@ -const mongoose = require("mongoose"); -const timestamp = require("mongoose-timestamp"); -const mongoosePaginate = require("mongoose-paginate-v2"); - -const modelSchema = new mongoose.Schema({ - user_id: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - course_id: { type: mongoose.Schema.Types.ObjectId, ref: "Course", required: true }, - price_paid: { type: Number, required: true }, // قیمت پرداخت شده - purchased_at: { type: Date, default: Date.now }, -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model("IsPaymentCourse", modelSchema); -module.exports = PostModel; +const mongoose = require("mongoose"); +const timestamp = require("mongoose-timestamp"); +const mongoosePaginate = require("mongoose-paginate-v2"); + +const modelSchema = new mongoose.Schema({ + user_id: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + course_id: { type: mongoose.Schema.Types.ObjectId, ref: "Course", required: true }, + price_paid: { type: Number, required: true }, // قیمت پرداخت شده + purchased_at: { type: Date, default: Date.now }, +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model("IsPaymentCourse", modelSchema); +module.exports = PostModel; diff --git a/controllers/application/academy/models/LikeModel.js b/controllers/application/academy/models/LikeModel.js index 3751511..1957530 100644 --- a/controllers/application/academy/models/LikeModel.js +++ b/controllers/application/academy/models/LikeModel.js @@ -1,25 +1,25 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') - -const likeSchema = new mongoose.Schema({ - postId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Post', - required: true - }, - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - likes: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'User' - }] -}) - -// تعریف مدل LikeModel -likeSchema.plugin(timestamp) -const LikeModel = mongoose.model('Like', likeSchema) - -module.exports = LikeModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') + +const likeSchema = new mongoose.Schema({ + postId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Post', + required: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + likes: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }] +}) + +// تعریف مدل LikeModel +likeSchema.plugin(timestamp) +const LikeModel = mongoose.model('Like', likeSchema) + +module.exports = LikeModel diff --git a/controllers/application/academy/models/MessageModel.js b/controllers/application/academy/models/MessageModel.js index d694642..9701f15 100644 --- a/controllers/application/academy/models/MessageModel.js +++ b/controllers/application/academy/models/MessageModel.js @@ -1,51 +1,66 @@ -const mongoose = require('mongoose') -const { Schema } = mongoose - -const messageSchema = new Schema({ - senderId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - receiverId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - content: { - type: String, - required: false, - trim: true - }, - file: { - type: String, - required: false - }, - fileType: { - type: String, - enum: ['image', 'video', 'file', 'voice', 'location'], - required: false - }, - replyToId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Message', - required: false - }, - forwardedFrom: { - userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, - userName: String, - displayName: String - }, - createdAt: { - type: Date, - default: Date.now - }, - readStatus: { - type: Number, - default: 0 - } -}) - -const MessageModel = mongoose.model('Message', messageSchema) - -module.exports = MessageModel +const mongoose = require('mongoose') +const { Schema } = mongoose + +const messageSchema = new Schema({ + senderId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + receiverId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + content: { + type: String, + required: false, + trim: true + }, + file: { + type: String, + required: false + }, + fileType: { + type: String, + enum: ['image', 'video', 'file', 'voice', 'location'], + required: false + }, + replyToId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Message', + required: false + }, + forwardedFrom: { + userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + userName: String, + displayName: String + }, + createdAt: { + type: Date, + default: Date.now + }, + readStatus: { + type: Number, + default: 0 + }, + /** Auto-delete after this time (self-destruct messages) */ + expiresAt: { + type: Date, + required: false, + index: true + }, + /** View-once photo / video / voice */ + viewOnce: { + type: Boolean, + default: false + }, + viewOnceOpenedAt: { + type: Date, + required: false + } +}) + +const MessageModel = mongoose.model('Message', messageSchema) + +module.exports = MessageModel diff --git a/controllers/application/academy/models/NotificationModel.js b/controllers/application/academy/models/NotificationModel.js index 727b797..ff79e19 100644 --- a/controllers/application/academy/models/NotificationModel.js +++ b/controllers/application/academy/models/NotificationModel.js @@ -1,37 +1,37 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') -const mongoosePaginate = require('mongoose-paginate-v2') - -const notificationSchema = new mongoose.Schema({ - user_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', // مرجع به مدل کاربر - required: true - }, - project_post_id: { - type: mongoose.Schema.Types.ObjectId, - required: false - }, - type: { - type: String, - required: true - }, - title: { - type: String, - required: true - }, - description: { - type: String, - required: true - }, - read: { - type: Boolean, - default: false // وضعیت خوانده شده یا نشده - } -}) - -notificationSchema.plugin(timestamp) -notificationSchema.plugin(mongoosePaginate) - -const NotificationModel = mongoose.model('Notification', notificationSchema) -module.exports = NotificationModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') +const mongoosePaginate = require('mongoose-paginate-v2') + +const notificationSchema = new mongoose.Schema({ + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', // مرجع به مدل کاربر + required: true + }, + project_post_id: { + type: mongoose.Schema.Types.ObjectId, + required: false + }, + type: { + type: String, + required: true + }, + title: { + type: String, + required: true + }, + description: { + type: String, + required: true + }, + read: { + type: Boolean, + default: false // وضعیت خوانده شده یا نشده + } +}) + +notificationSchema.plugin(timestamp) +notificationSchema.plugin(mongoosePaginate) + +const NotificationModel = mongoose.model('Notification', notificationSchema) +module.exports = NotificationModel diff --git a/controllers/application/academy/models/OfferModel.js b/controllers/application/academy/models/OfferModel.js index f36d940..ec36550 100644 --- a/controllers/application/academy/models/OfferModel.js +++ b/controllers/application/academy/models/OfferModel.js @@ -1,32 +1,32 @@ -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate-v2') -const timestamp = require('mongoose-timestamp') - -const offerSchema = new mongoose.Schema({ - sender: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - receiver: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - status: { - type: String, - enum: ['pending', 'accepted', 'rejected'], // وضعیت درخواست - default: 'pending' - }, - transaction_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Payment', - default: null - } -}) - -offerSchema.plugin(timestamp) -offerSchema.plugin(mongoosePaginate) - -const OfferModel = mongoose.model('Offer', offerSchema) -module.exports = OfferModel +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate-v2') +const timestamp = require('mongoose-timestamp') + +const offerSchema = new mongoose.Schema({ + sender: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + receiver: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + status: { + type: String, + enum: ['pending', 'accepted', 'rejected'], // وضعیت درخواست + default: 'pending' + }, + transaction_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Payment', + default: null + } +}) + +offerSchema.plugin(timestamp) +offerSchema.plugin(mongoosePaginate) + +const OfferModel = mongoose.model('Offer', offerSchema) +module.exports = OfferModel diff --git a/controllers/application/academy/models/OfferTypeModel.js b/controllers/application/academy/models/OfferTypeModel.js index f765a8f..fa2b49c 100644 --- a/controllers/application/academy/models/OfferTypeModel.js +++ b/controllers/application/academy/models/OfferTypeModel.js @@ -1,21 +1,21 @@ -const mongoose = require('mongoose') - -const offerTypeModelSchema = new mongoose.Schema({ - name: { - type: String, - required: true, - trim: true, - minLength: 1, - maxLength: 255 - }, - price: { - type: Number, - required: true, - trim: true, - minLength: 1, - maxLength: 255 - } -}) - -const OfferTypeModel = mongoose.model('OfferTypeModel', offerTypeModelSchema) -module.exports = OfferTypeModel +const mongoose = require('mongoose') + +const offerTypeModelSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true, + minLength: 1, + maxLength: 255 + }, + price: { + type: Number, + required: true, + trim: true, + minLength: 1, + maxLength: 255 + } +}) + +const OfferTypeModel = mongoose.model('OfferTypeModel', offerTypeModelSchema) +module.exports = OfferTypeModel diff --git a/controllers/application/academy/models/PaymentModel.js b/controllers/application/academy/models/PaymentModel.js index 41e4e51..c78a252 100644 --- a/controllers/application/academy/models/PaymentModel.js +++ b/controllers/application/academy/models/PaymentModel.js @@ -1,46 +1,46 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') -const mongoosePaginate = require('mongoose-paginate-v2') - -const paymentSchema = new mongoose.Schema({ - amount: { - type: Number, - required: true - }, - status: { - type: String, - enum: ['pending', 'successful', 'failed'], - default: 'pending' - }, - authority: { - type: String, - required: false - }, - user_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User' - }, - project_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Project' - }, - advertising_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Advertising' - }, - type: { - type: String, - required: true - }, - installment_step: { - type: String, - required: false, - default: null - } -}) - -paymentSchema.plugin(timestamp) -paymentSchema.plugin(mongoosePaginate) - -const PaymentModel = mongoose.model('Payment', paymentSchema) -module.exports = PaymentModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') +const mongoosePaginate = require('mongoose-paginate-v2') + +const paymentSchema = new mongoose.Schema({ + amount: { + type: Number, + required: true + }, + status: { + type: String, + enum: ['pending', 'successful', 'failed'], + default: 'pending' + }, + authority: { + type: String, + required: false + }, + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + project_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Project' + }, + advertising_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Advertising' + }, + type: { + type: String, + required: true + }, + installment_step: { + type: String, + required: false, + default: null + } +}) + +paymentSchema.plugin(timestamp) +paymentSchema.plugin(mongoosePaginate) + +const PaymentModel = mongoose.model('Payment', paymentSchema) +module.exports = PaymentModel diff --git a/controllers/application/academy/models/PostModel.js b/controllers/application/academy/models/PostModel.js index 784547e..ff6d8cc 100644 --- a/controllers/application/academy/models/PostModel.js +++ b/controllers/application/academy/models/PostModel.js @@ -1,60 +1,60 @@ -const mongoose = require('mongoose'); -const timestamp = require('mongoose-timestamp'); -const mongoosePaginate = require('mongoose-paginate-v2'); - -const modelSchema = new mongoose.Schema({ - post_images: { - type: [String], // آرایه‌ای از مسیرهای تصاویر - required: false, - default: [], - }, - post_video: { - type: String, // مسیر ویدئو - required: false, - trim: true, - default: null, - }, - type: { - type: String, // نوع پست: 'image' یا 'video' - required: true, - enum: ['image', 'video'], - }, - files: [ - { - path: { type: String, required: true }, - type: { type: String, enum: ['image', 'video'], required: true }, - } - ], - caption: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 1000, - default: null, - }, - status: { - type: String, - required: false, - trim: true, - enum: ['pending', 'accept', 'reject'], - default: 'pending', - }, - user_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - }, - likes: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - }], - comments: { - type: Object, - default: null, - }, -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model('Post', modelSchema); +const mongoose = require('mongoose'); +const timestamp = require('mongoose-timestamp'); +const mongoosePaginate = require('mongoose-paginate-v2'); + +const modelSchema = new mongoose.Schema({ + post_images: { + type: [String], // آرایه‌ای از مسیرهای تصاویر + required: false, + default: [], + }, + post_video: { + type: String, // مسیر ویدئو + required: false, + trim: true, + default: null, + }, + type: { + type: String, // نوع پست: 'image' یا 'video' + required: true, + enum: ['image', 'video'], + }, + files: [ + { + path: { type: String, required: true }, + type: { type: String, enum: ['image', 'video'], required: true }, + } + ], + caption: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 1000, + default: null, + }, + status: { + type: String, + required: false, + trim: true, + enum: ['pending', 'accept', 'reject'], + default: 'pending', + }, + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + }, + likes: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + }], + comments: { + type: Object, + default: null, + }, +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model('Post', modelSchema); module.exports = PostModel; \ No newline at end of file diff --git a/controllers/application/academy/models/ProjectModel.js b/controllers/application/academy/models/ProjectModel.js index fc5b38b..4b4382e 100644 --- a/controllers/application/academy/models/ProjectModel.js +++ b/controllers/application/academy/models/ProjectModel.js @@ -1,186 +1,186 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') -const mongoosePaginate = require('mongoose-paginate-v2') -const projectSchema = new mongoose.Schema({ - title: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - expertise: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - sub_expertise: { - type: [String], // تغییر نوع به آرایه از استرینگ - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - gender: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - age: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - conversation_projects: { - type: Boolean, - required: false, - default: null - }, - province: { - type: Object, - default: null - }, - city: { - type: Object, - default: null - }, - offer_time: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - offer_price: { - type: Number, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - description: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 1024, - default: null - }, - project_type: { - type: String, - enum: ['free', 'normal', 'force', 'highlight'], - default: 'normal' - }, - payment_status: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - // فیلد برای وضعیت پروژه - status: { - type: String, - enum: ['pre_payment', 'paid', 'accepted', 'rejected', 'done', 'cancled', 'ongoing'], - default: 'pre_payment' - }, - public_status: { - type: String, - enum: ['public', 'private'], - default: 'public' - }, - created_for_user: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - default: null - }, - creator_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User' - }, - requested_users: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'User' - }], - selected_user: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - default: null - }, - final_price: { - type: Number, - required: false, - default: null - }, - final_time: { - type: Number, - required: false, - default: null - }, - installments: [{ - installment_number: { - type: Number, - required: true - }, - amount: { - type: Number, - required: true - }, - due_date: { - type: Date, - required: true - } - }], - reject_reason: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 1024, - default: null - }, - ratings: [{ - creator_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - rating: { - type: Number, - required: true - }, - comment: { - type: String, - trim: true - } - }], - acceptedAt: { - type: Date, - default: null - }, - isExpired: { - type: Boolean, - default: false - } - -}) - -projectSchema.plugin(timestamp) -// اضافه کردن پیجینیشن به مدل -projectSchema.plugin(mongoosePaginate) -const ProjectModel = mongoose.model('Project', projectSchema) -module.exports = ProjectModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') +const mongoosePaginate = require('mongoose-paginate-v2') +const projectSchema = new mongoose.Schema({ + title: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + expertise: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + sub_expertise: { + type: [String], // تغییر نوع به آرایه از استرینگ + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + gender: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + age: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + conversation_projects: { + type: Boolean, + required: false, + default: null + }, + province: { + type: Object, + default: null + }, + city: { + type: Object, + default: null + }, + offer_time: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + offer_price: { + type: Number, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + description: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 1024, + default: null + }, + project_type: { + type: String, + enum: ['free', 'normal', 'force', 'highlight'], + default: 'normal' + }, + payment_status: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + // فیلد برای وضعیت پروژه + status: { + type: String, + enum: ['pre_payment', 'paid', 'accepted', 'rejected', 'done', 'cancled', 'ongoing'], + default: 'pre_payment' + }, + public_status: { + type: String, + enum: ['public', 'private'], + default: 'public' + }, + created_for_user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + default: null + }, + creator_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + requested_users: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }], + selected_user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + default: null + }, + final_price: { + type: Number, + required: false, + default: null + }, + final_time: { + type: Number, + required: false, + default: null + }, + installments: [{ + installment_number: { + type: Number, + required: true + }, + amount: { + type: Number, + required: true + }, + due_date: { + type: Date, + required: true + } + }], + reject_reason: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 1024, + default: null + }, + ratings: [{ + creator_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + rating: { + type: Number, + required: true + }, + comment: { + type: String, + trim: true + } + }], + acceptedAt: { + type: Date, + default: null + }, + isExpired: { + type: Boolean, + default: false + } + +}) + +projectSchema.plugin(timestamp) +// اضافه کردن پیجینیشن به مدل +projectSchema.plugin(mongoosePaginate) +const ProjectModel = mongoose.model('Project', projectSchema) +module.exports = ProjectModel diff --git a/controllers/application/academy/models/RequestModel.js b/controllers/application/academy/models/RequestModel.js index cb3193a..3049ec1 100644 --- a/controllers/application/academy/models/RequestModel.js +++ b/controllers/application/academy/models/RequestModel.js @@ -1,33 +1,33 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') - -const requestSchema = new mongoose.Schema({ - project: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Project', - required: true - }, - user: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true - }, - time: { - type: Number, - required: true - }, - price: { - type: Number, - required: true - }, - status: { - type: String, - enum: ['pending', 'accepted', 'rejected'], - default: 'pending' - } -}) - -requestSchema.plugin(timestamp) - -const RequestModel = mongoose.model('Request', requestSchema) -module.exports = RequestModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') + +const requestSchema = new mongoose.Schema({ + project: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Project', + required: true + }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + time: { + type: Number, + required: true + }, + price: { + type: Number, + required: true + }, + status: { + type: String, + enum: ['pending', 'accepted', 'rejected'], + default: 'pending' + } +}) + +requestSchema.plugin(timestamp) + +const RequestModel = mongoose.model('Request', requestSchema) +module.exports = RequestModel diff --git a/controllers/application/academy/models/SettingsModel.js b/controllers/application/academy/models/SettingsModel.js index 3463eb0..8d41208 100644 --- a/controllers/application/academy/models/SettingsModel.js +++ b/controllers/application/academy/models/SettingsModel.js @@ -1,19 +1,19 @@ -// models/Setting.js -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') - -const settingSchema = new mongoose.Schema({ - key: { - type: String, - required: true, - unique: true - }, - value: { - type: String, - required: true - } -}) -settingSchema.plugin(timestamp) - -const SettingModel = mongoose.model('Setting', settingSchema) -module.exports = SettingModel +// models/Setting.js +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') + +const settingSchema = new mongoose.Schema({ + key: { + type: String, + required: true, + unique: true + }, + value: { + type: String, + required: true + } +}) +settingSchema.plugin(timestamp) + +const SettingModel = mongoose.model('Setting', settingSchema) +module.exports = SettingModel diff --git a/controllers/application/academy/models/StateCity.js b/controllers/application/academy/models/StateCity.js index cbf3ae2..5f24232 100644 --- a/controllers/application/academy/models/StateCity.js +++ b/controllers/application/academy/models/StateCity.js @@ -1,19 +1,19 @@ -const mongoose = require('mongoose') - -const provincesSchema = new mongoose.Schema({ - id: Number, - name: String, - slug: String -}) - -const citiesSchema = new mongoose.Schema({ - id: Number, - name: String, - slug: String, - province_id: Number -}) - -const ProvinceModel = mongoose.model('Province', provincesSchema) -const CityModel = mongoose.model('City', citiesSchema) - -module.exports = { ProvinceModel, CityModel } +const mongoose = require('mongoose') + +const provincesSchema = new mongoose.Schema({ + id: Number, + name: String, + slug: String +}) + +const citiesSchema = new mongoose.Schema({ + id: Number, + name: String, + slug: String, + province_id: Number +}) + +const ProvinceModel = mongoose.model('Province', provincesSchema) +const CityModel = mongoose.model('City', citiesSchema) + +module.exports = { ProvinceModel, CityModel } diff --git a/controllers/application/academy/models/StoryModel.js b/controllers/application/academy/models/StoryModel.js new file mode 100644 index 0000000..098f18d --- /dev/null +++ b/controllers/application/academy/models/StoryModel.js @@ -0,0 +1,41 @@ +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') + +const storySchema = new mongoose.Schema({ + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + media_path: { + type: String, + required: true + }, + media_type: { + type: String, + enum: ['image', 'video'], + required: true + }, + expires_at: { + type: Date, + required: true, + index: true + }, + viewers: [{ + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + viewed_at: { + type: Date, + default: Date.now + } + }] +}) + +storySchema.index({ expires_at: 1 }, { expireAfterSeconds: 0 }) + +storySchema.plugin(timestamp) + +module.exports = mongoose.model('Story', storySchema) diff --git a/controllers/application/academy/models/TaxModel.js b/controllers/application/academy/models/TaxModel.js index 86c0bf4..70b27e7 100644 --- a/controllers/application/academy/models/TaxModel.js +++ b/controllers/application/academy/models/TaxModel.js @@ -1,15 +1,15 @@ -const mongoose = require("mongoose"); -const timestamp = require("mongoose-timestamp"); -const mongoosePaginate = require("mongoose-paginate-v2"); - -const modelSchema = new mongoose.Schema({ - type: { type: String, enum: ["course", "product", "service"], required: true }, - tax: { type: Number, required: true, default: 9 }, // درصد مالیات - is_active: { type: Boolean, default: true }, - -}); - -modelSchema.plugin(timestamp); -modelSchema.plugin(mongoosePaginate); -const PostModel = mongoose.model("Tax", modelSchema); -module.exports = PostModel; +const mongoose = require("mongoose"); +const timestamp = require("mongoose-timestamp"); +const mongoosePaginate = require("mongoose-paginate-v2"); + +const modelSchema = new mongoose.Schema({ + type: { type: String, enum: ["course", "product", "service"], required: true }, + tax: { type: Number, required: true, default: 9 }, // درصد مالیات + is_active: { type: Boolean, default: true }, + +}); + +modelSchema.plugin(timestamp); +modelSchema.plugin(mongoosePaginate); +const PostModel = mongoose.model("Tax", modelSchema); +module.exports = PostModel; diff --git a/controllers/application/academy/models/TicketMessageModel.js b/controllers/application/academy/models/TicketMessageModel.js index 84331ee..b054d05 100644 --- a/controllers/application/academy/models/TicketMessageModel.js +++ b/controllers/application/academy/models/TicketMessageModel.js @@ -1,20 +1,20 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') -const mongoosePaginate = require('mongoose-paginate-v2') - -const ticketMessageSchema = new mongoose.Schema({ - text: { type: String, required: false }, - file: { - type: String, // مسیر فایل در ذخیره شود - required: false // فایل اختیاری است - }, - senderType: { type: String, enum: ['User', 'Admin'], required: true }, - senderId: { type: mongoose.Schema.Types.ObjectId, refPath: 'senderType', required: true }, - ticket: { type: mongoose.Schema.Types.ObjectId, ref: 'Ticket', required: true } -}) - -ticketMessageSchema.plugin(timestamp) -// اضافه کردن پیجینیشن به مدل -ticketMessageSchema.plugin(mongoosePaginate) -const TicketMessageModel = mongoose.model('TicketMessage', ticketMessageSchema) -module.exports = TicketMessageModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') +const mongoosePaginate = require('mongoose-paginate-v2') + +const ticketMessageSchema = new mongoose.Schema({ + text: { type: String, required: false }, + file: { + type: String, // مسیر فایل در ذخیره شود + required: false // فایل اختیاری است + }, + senderType: { type: String, enum: ['User', 'Admin'], required: true }, + senderId: { type: mongoose.Schema.Types.ObjectId, refPath: 'senderType', required: true }, + ticket: { type: mongoose.Schema.Types.ObjectId, ref: 'Ticket', required: true } +}) + +ticketMessageSchema.plugin(timestamp) +// اضافه کردن پیجینیشن به مدل +ticketMessageSchema.plugin(mongoosePaginate) +const TicketMessageModel = mongoose.model('TicketMessage', ticketMessageSchema) +module.exports = TicketMessageModel diff --git a/controllers/application/academy/models/TicketModel.js b/controllers/application/academy/models/TicketModel.js index ba81f97..4157fb3 100644 --- a/controllers/application/academy/models/TicketModel.js +++ b/controllers/application/academy/models/TicketModel.js @@ -1,24 +1,24 @@ -const mongoose = require('mongoose') -const timestamp = require('mongoose-timestamp') -const mongoosePaginate = require('mongoose-paginate-v2') - -const ticketSchema = new mongoose.Schema({ - title: { type: String, required: true }, - status: { type: String, enum: ['Pending', 'Answered', 'Customer Response', 'Closed'], default: 'Pending' }, - user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, - new_message: { - type: Boolean, - default: false // وضعیت خوانده شده یا نشده - } -}) - -ticketSchema.pre('save', function (next) { - this.updatedAt = Date.now() - next() -}) - -ticketSchema.plugin(timestamp) -// اضافه کردن پیجینیشن به مدل -ticketSchema.plugin(mongoosePaginate) -const TicketModel = mongoose.model('Ticket', ticketSchema) -module.exports = TicketModel +const mongoose = require('mongoose') +const timestamp = require('mongoose-timestamp') +const mongoosePaginate = require('mongoose-paginate-v2') + +const ticketSchema = new mongoose.Schema({ + title: { type: String, required: true }, + status: { type: String, enum: ['Pending', 'Answered', 'Customer Response', 'Closed'], default: 'Pending' }, + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + new_message: { + type: Boolean, + default: false // وضعیت خوانده شده یا نشده + } +}) + +ticketSchema.pre('save', function (next) { + this.updatedAt = Date.now() + next() +}) + +ticketSchema.plugin(timestamp) +// اضافه کردن پیجینیشن به مدل +ticketSchema.plugin(mongoosePaginate) +const TicketModel = mongoose.model('Ticket', ticketSchema) +module.exports = TicketModel diff --git a/controllers/application/academy/models/TypeAndPriceModel.js b/controllers/application/academy/models/TypeAndPriceModel.js index b922d1d..2b059d1 100644 --- a/controllers/application/academy/models/TypeAndPriceModel.js +++ b/controllers/application/academy/models/TypeAndPriceModel.js @@ -1,21 +1,21 @@ -const mongoose = require('mongoose') - -const typeAndPriceSchema = new mongoose.Schema({ - name: { - type: String, - required: true, - trim: true, - minLength: 1, - maxLength: 255 - }, - price: { - type: Number, - required: true, - trim: true, - minLength: 1, - maxLength: 255 - } -}) - -const TypeAndPriceModel = mongoose.model('TypeAndPrice', typeAndPriceSchema) -module.exports = TypeAndPriceModel +const mongoose = require('mongoose') + +const typeAndPriceSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true, + minLength: 1, + maxLength: 255 + }, + price: { + type: Number, + required: true, + trim: true, + minLength: 1, + maxLength: 255 + } +}) + +const TypeAndPriceModel = mongoose.model('TypeAndPrice', typeAndPriceSchema) +module.exports = TypeAndPriceModel diff --git a/controllers/application/academy/models/UserModel.js b/controllers/application/academy/models/UserModel.js index ddf6b87..ff3433e 100644 --- a/controllers/application/academy/models/UserModel.js +++ b/controllers/application/academy/models/UserModel.js @@ -1,381 +1,391 @@ -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate-v2') -const timestamp = require('mongoose-timestamp') -const userSchema = new mongoose.Schema({ - mobile: { - type: String, - required: false, - trim: true, - unique: true - }, - otp: { - type: String, - required: false, - trim: true, - minLength: 6, - maxLength: 6, - default: null - }, - user_name: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - password: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - first_name: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - last_name: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - user_type: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - is_verified: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: 'none' - }, - profile_image: { - type: String, - required: false, - trim: true, - default: null - }, - national_card_image: { - type: String, - required: false, - trim: true, - default: null - }, - expertise: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - sub_expertise: { - type: [String], // تغییر نوع به آرایه از استرینگ - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - gender: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - height: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - weight: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - size: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - eye_color: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - hair_color: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - cooperation_type: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - cooperation_abroad: { - type: Boolean, - required: false, - default: null - }, - conversation_projects: { - type: Boolean, - required: false, - default: null - }, - bio: { - type: String, - required: false, - minLength: 1, - maxLength: 1024, - default: null - }, - national_code: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - birthday: { - type: Date, - required: false, - default: null - }, - is_Register: { - type: String, - - default: false - }, - shaba: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 255, - default: null - }, - address: { - type: String, - trim: true, - minLength: 1, - maxLength: 512, - default: null - }, - province: { - type: Object, - default: null - }, - city: { - type: Object, - default: null - }, - lat: { - type: String, - trim: true, - minLength: 1, - maxLength: 512, - default: null - }, - lng: { - type: String, - trim: true, - minLength: 1, - maxLength: 512, - default: null - }, - show_location: { - type: Boolean, - required: false, - default: null - }, - rate: { - type: String, - trim: true, - maxLength: 512, - default: null - }, - referral_code: { - type: String, - trim: true, - minLength: 1, - maxLength: 512, - default: null - }, - shop_id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Shop', - default: null - }, - last_online: { - type: Date, // ذخیره تاریخ و زمان آخرین بازدید - default: Date.now // تنظیم تاریخ به تاریخ فعلی - }, - business_license: { - type: String, - required: false, - trim: true, - default: null - }, - user_level: { - type: String, - trim: true, - minLength: 1, - maxLength: 512, - default: null - }, - user_score: { - type: String, - trim: true, - minLength: 1, - maxLength: 512, - default: null - }, - last_post: { - type: Object, - default: null - }, - // user_ratings: [{ - // project_id: { - // type: mongoose.Schema.Types.ObjectId, - // ref: 'Project', - // required: true - // }, - // creator_id: { - // type: mongoose.Schema.Types.ObjectId, - // ref: 'User', - // required: true - // }, - // rating: { - // type: Number, - // required: true - // }, - // comment: { - // type: String, - // trim: true - // } - // }], - daily_free_request: { - type: Number, - default: 1 - }, - last_free_request_date: { - type: Date, - default: null - }, - blocked_by: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'User' - }], - blocked_users: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'User' - }], - block_status: { - type: Boolean, - required: false, - default: null - }, - advertising_profiles: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'AdvertisingProfile' - }], - services: [{ - title: { - type: String, - required: true, - trim: true - }, - originalPrice: { - type: String, - required: true - }, - discountPrice: { - type: String, - required: false - }, - discountPercentage: { - type: Number, - required: false - }, - status: { - type: String, - default: "UnderReview", - required: false - }, - image: { - type: String, - required: false, - trim: true - } - }], - monthly_free_offer: { - type: Number, - default: 1 - }, - last_free_offer_date: { - type: Date, - default: null - } - // contactInfo: { - // phone: String, - // mobile: String, - // telegramLink: String, - // whatsappNumber: String, - // instagramLink: String, - // saveInfoForNextAds: { type: Boolean, default: false } - // }, - // adTotalRatings: { - // type: Number, - // default: 0 - // }, - // adRatingsCount: { - // type: Number, - // default: 0 - // }, - // adAverageRating: { - // type: Number, - // default: 0 - // } -}) - -userSchema.plugin(timestamp) -userSchema.plugin(mongoosePaginate) -const UserModel = mongoose.model('User', userSchema) -module.exports = UserModel +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate-v2') +const timestamp = require('mongoose-timestamp') +const userSchema = new mongoose.Schema({ + mobile: { + type: String, + required: false, + trim: true, + unique: true + }, + otp: { + type: String, + required: false, + trim: true, + minLength: 6, + maxLength: 6, + default: null + }, + otpSentAt: { + type: Date, + required: false, + default: null + }, + user_name: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + password: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + first_name: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + last_name: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + user_type: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + is_verified: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: 'none' + }, + profile_image: { + type: String, + required: false, + trim: true, + default: null + }, + national_card_image: { + type: String, + required: false, + trim: true, + default: null + }, + expertise: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + sub_expertise: { + type: [String], // تغییر نوع به آرایه از استرینگ + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + gender: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + height: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + weight: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + size: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + eye_color: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + hair_color: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + cooperation_type: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + cooperation_abroad: { + type: Boolean, + required: false, + default: null + }, + conversation_projects: { + type: Boolean, + required: false, + default: null + }, + bio: { + type: String, + required: false, + minLength: 1, + maxLength: 1024, + default: null + }, + national_code: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + birthday: { + type: Date, + required: false, + default: null + }, + is_Register: { + type: String, + + default: false + }, + shaba: { + type: String, + required: false, + trim: true, + minLength: 1, + maxLength: 255, + default: null + }, + address: { + type: String, + trim: true, + minLength: 1, + maxLength: 512, + default: null + }, + province: { + type: Object, + default: null + }, + city: { + type: Object, + default: null + }, + lat: { + type: String, + trim: true, + minLength: 1, + maxLength: 512, + default: null + }, + lng: { + type: String, + trim: true, + minLength: 1, + maxLength: 512, + default: null + }, + show_location: { + type: Boolean, + required: false, + default: null + }, + rate: { + type: String, + trim: true, + maxLength: 512, + default: null + }, + referral_code: { + type: String, + trim: true, + minLength: 1, + maxLength: 512, + default: null + }, + shop_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Shop', + default: null + }, + last_online: { + type: Date, // ذخیره تاریخ و زمان آخرین بازدید + default: Date.now // تنظیم تاریخ به تاریخ فعلی + }, + business_license: { + type: String, + required: false, + trim: true, + default: null + }, + user_level: { + type: String, + trim: true, + minLength: 1, + maxLength: 512, + default: null + }, + user_score: { + type: String, + trim: true, + minLength: 1, + maxLength: 512, + default: null + }, + last_post: { + type: Object, + default: null + }, + // user_ratings: [{ + // project_id: { + // type: mongoose.Schema.Types.ObjectId, + // ref: 'Project', + // required: true + // }, + // creator_id: { + // type: mongoose.Schema.Types.ObjectId, + // ref: 'User', + // required: true + // }, + // rating: { + // type: Number, + // required: true + // }, + // comment: { + // type: String, + // trim: true + // } + // }], + daily_free_request: { + type: Number, + default: 1 + }, + last_free_request_date: { + type: Date, + default: null + }, + blocked_by: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }], + blocked_users: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }], + block_status: { + type: Boolean, + required: false, + default: null + }, + blocked_until: { + type: Date, + required: false, + default: null + }, + advertising_profiles: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'AdvertisingProfile' + }], + services: [{ + title: { + type: String, + required: true, + trim: true + }, + originalPrice: { + type: String, + required: true + }, + discountPrice: { + type: String, + required: false + }, + discountPercentage: { + type: Number, + required: false + }, + status: { + type: String, + default: "UnderReview", + required: false + }, + image: { + type: String, + required: false, + trim: true + } + }], + monthly_free_offer: { + type: Number, + default: 1 + }, + last_free_offer_date: { + type: Date, + default: null + } + // contactInfo: { + // phone: String, + // mobile: String, + // telegramLink: String, + // whatsappNumber: String, + // instagramLink: String, + // saveInfoForNextAds: { type: Boolean, default: false } + // }, + // adTotalRatings: { + // type: Number, + // default: 0 + // }, + // adRatingsCount: { + // type: Number, + // default: 0 + // }, + // adAverageRating: { + // type: Number, + // default: 0 + // } +}) + +userSchema.plugin(timestamp) +userSchema.plugin(mongoosePaginate) +const UserModel = mongoose.model('User', userSchema) +module.exports = UserModel diff --git a/controllers/application/academy/models/VersionModel.js b/controllers/application/academy/models/VersionModel.js index 4fc532f..51aaf0b 100644 --- a/controllers/application/academy/models/VersionModel.js +++ b/controllers/application/academy/models/VersionModel.js @@ -1,12 +1,12 @@ -// models/VersionModel.js -const mongoose = require('mongoose') - -const versionSchema = new mongoose.Schema({ - version: { type: String, required: true }, - mandatory: { type: Boolean, required: true }, - releaseNotes: { type: String }, - updateUrls: { type: [String], required: true } // لینک‌های آپدیت -}) - -const VersionModel = mongoose.model('Version', versionSchema) -module.exports = VersionModel +// models/VersionModel.js +const mongoose = require('mongoose') + +const versionSchema = new mongoose.Schema({ + version: { type: String, required: true }, + mandatory: { type: Boolean, required: true }, + releaseNotes: { type: String }, + updateUrls: { type: [String], required: true } // لینک‌های آپدیت +}) + +const VersionModel = mongoose.model('Version', versionSchema) +module.exports = VersionModel diff --git a/controllers/application/academy/models/license.js b/controllers/application/academy/models/license.js index 3d3733e..13c59a2 100644 --- a/controllers/application/academy/models/license.js +++ b/controllers/application/academy/models/license.js @@ -1,14 +1,14 @@ -const mongoose = require('mongoose') -const mongoosePaginate = require('mongoose-paginate-v2') -const timestamp = require('mongoose-timestamp') -const license = new mongoose.Schema({ - - userId : String, - Confirmation : { type : Boolean, default : false}, - licenseImg : { type : String }, -}) - -license.plugin(timestamp) -license.plugin(mongoosePaginate) -const LicenseMoldel = mongoose.model('license', license) -module.exports = LicenseMoldel +const mongoose = require('mongoose') +const mongoosePaginate = require('mongoose-paginate-v2') +const timestamp = require('mongoose-timestamp') +const license = new mongoose.Schema({ + + userId : String, + Confirmation : { type : Boolean, default : false}, + licenseImg : { type : String }, +}) + +license.plugin(timestamp) +license.plugin(mongoosePaginate) +const LicenseMoldel = mongoose.model('license', license) +module.exports = LicenseMoldel diff --git a/controllers/application/academy/offerController.js b/controllers/application/academy/offerController.js new file mode 100644 index 0000000..a6c0c4a --- /dev/null +++ b/controllers/application/academy/offerController.js @@ -0,0 +1,423 @@ +/* eslint-disable camelcase */ +const jwt = require('jsonwebtoken') +const jMoment = require('moment-jalaali') +const OfferModel = require('../../../models/OfferModel') +const UserModel = require('../../../models/UserModel') +const OfferTypeModel = require('../../../models/OfferTypeModel') +const PaymentModel = require('../../../models/PaymentModel') +const NotificationModel = require('../../../models/NotificationModel') +const { default: axios } = require('axios') +const CommentModel = require('../../../models/CommentModel') +const { + resolveRatingForComment, + recalculateUserRating +} = require('../../../utils/commentRating') + +const getOfferPriceFromDatabase = async (itemName) => { + const item = await OfferTypeModel.findOne({ name: itemName }) + if (!item) { + throw new Error('Item not found in database') + } + return item.price +} + +const getOfferTypes = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const user = await UserModel.findById(userId) + if (!user) { + return res.status(422).json({ + error: true, + message: 'کاربر یافت نشد' + }) + } + let offerTypes = await OfferTypeModel.find({}, 'name price') + + // بررسی برای مواردی مانند پیدا نشدن انواع پروژه + if (!offerTypes) { + return res.status(404).json({ message: 'انواع پروژه یافت نشد.' }) + } + // اگر کاربر درخواست ماهانه نداشته باشد، نوع پروژه ماهانه را حذف کنید + if (user.monthly_free_offer <= 0) { + offerTypes = offerTypes.filter(projectType => projectType.name !== 'free') + } + // ارسال انواع پروژه به کاربر + res.status(200).json({ offerTypes }) + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error('Error in getOfferTypes:', error) + res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) + } +} +const getUserOffers = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const { page = 1, limit = 10, status_filter, id } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست + let filter = {} + let filterbyid = {receiver: id} + let filterbyidsender = {sender: id} + if (status_filter === 'درخواست') { + filter = { sender: userId } + } else { + filter = { receiver: userId } + } + + // تنظیم گزینه‌های صفحه‌بندی + const options = { + page: parseInt(page), // تبدیل صفحه به عدد صحیح + limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح + sort: { createdAt: -1 }, // بر اساس زمان ایجاد (createdAt) مرتب کنید (نزولی) + populate: [ + { + path: 'sender', + select: 'profile_image user_level first_name last_name user_name is_verified user_score rate' + }, + { + path: 'receiver', + select: 'profile_image user_level first_name last_name user_name is_verified user_score rate' + } + ] + } + + // دریافت آفرهای صفحه‌بندی شده + let offer = await OfferModel.paginate(filter, options) + let offerbyid = await OfferModel.paginate(filterbyid, options) + let offerbyidsender = await OfferModel.paginate(filterbyidsender, options) + + const totalPages = offer.totalPages + const totalItems = offer.totalDocs + + // فرمت کردن تاریخ و ارسال پاسخ + offer = offer?.docs.map(offer => { + const jDate = jMoment(offer.createdAt).format('jYYYY-jMM-jDD HH:mm') + return { + ...offer._doc, + createdAt: jDate + } + }) + res.json({ + offerbyidsender, + offerbyid, + offer, + totalPages, // ارسال تعداد کل صفحات + totalItems // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + next(error) + } +} + +const updateOfferStatus = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id // ID کاربری که درخواست را به‌روزرسانی می‌کند + const reciverUser = await UserModel.findById(userId) + + const { offerId, action } = req.body // آیدی آفر و اکشن (accept یا reject) + + // بررسی اعتبار ورودی‌ها + if (!offerId || !['accept', 'reject'].includes(action)) { + return res.status(400).json({ error: 'Invalid input' }) + } + + // یافتن آفر موردنظر + const offer = await OfferModel.findById(offerId) + const senderUser = await UserModel.findById(offer?.sender) + + if (!offer) { + return res.status(404).json({ error: 'Offer not found' }) + } + + // بررسی اینکه آیا کاربر گیرنده این آفر است یا خیر + if (offer.receiver.toString() !== userId) { + return res.status(403).json({ error: 'You are not authorized to update this offer' }) + } + + // بررسی اینکه آیا آفر قبلاً به‌روزرسانی شده یا نه + if (offer.status !== 'pending') { + return res.status(400).json({ error: 'Offer has already been updated', message: 'وضعیت این درخواست قبلا تغییر کرده است.' }) + } + + // به‌روزرسانی وضعیت آفر بر اساس اکشن + offer.status = action === 'accept' ? 'accepted' : 'rejected' + await offer.save() + + if (action === 'accept') { + const data = JSON.stringify({ + mobile: senderUser?.mobile, + templateId: '633448', + parameters: [ + { name: 'USER', value: reciverUser?.first_name + ' ' + reciverUser?.last_name }, + { name: 'EMPLOYER', value: senderUser?.first_name + ' ' + senderUser?.last_name } + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + axios(config) + .then(function (response) { + }) + .catch(function (error) { + console.log(error) + }) + } else { + const data = JSON.stringify({ + mobile: senderUser?.mobile, + templateId: '682678', + parameters: [ + { name: 'USER', value: reciverUser?.first_name + ' ' + reciverUser?.last_name }, + { name: 'EMPLOYER', value: senderUser?.first_name + ' ' + senderUser?.last_name } + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + axios(config) + .then(function (response) { + }) + .catch(function (error) { + console.log(error) + }) + } + res.status(200).json({ + message: `Offer has been ${offer.status} successfully`, + offerId: offer._id, + status: offer.status + }) + } catch (error) { + next(error) + } +} + +// Controller function for handling successful advertising payment +const handleSuccessfulOfferPayment = async (req, res) => { + try { + const { reciverId, offerType } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + // Check user's free offer eligibility + const user = await UserModel.findById(userId) + if (!user) return res.status(404).json({ error: 'User not found' }) + // Check user's free offer eligibility + const reciverUser = await UserModel.findById(reciverId) + if (!user) return res.status(404).json({ error: 'User not found' }) + + let price + if (user.monthly_free_offer > 0 && offerType === 'free') { + // Use free offer and decrement counter + price = 0 + user.monthly_free_offer -= 1 + await user.save() + } else { + // Retrieve offer price based on type + price = await getOfferPriceFromDatabase(offerType) + + // Create a new payment entry + const payment = new PaymentModel({ + amount: Number(price), + status: 'successful', + authority: '', // Add authority if available + user_id: userId, + type: 'offer', + installment_step: null + }) + await payment.save() + } + // Create a new payment entry + const payment = new PaymentModel({ + amount: Number(price), + status: 'successful', + authority: '', // Add authority if available + user_id: userId, + type: 'offer', + installment_step: null + }) + await payment.save() + + // Create a new offer + const offer = new OfferModel({ + sender: userId, + receiver: reciverId, + transaction_id: payment._id // Link the payment to the offer + }) + await offer.save() + console.log(reciverUser?.first_name) + + // Send notification to selected user + const notification = new NotificationModel({ + user_id: reciverId, + project_post_id: offer?._id, + type: 'new_offer', + title: 'درخواست همکاری جدید', + description: `${reciverUser?.first_name} عزیز یک درخواست همکاری برای شما در مجموعه مدستاگرام ثبت شد` + }) + await notification.save() + + // Send Sms + const data = JSON.stringify({ + mobile: reciverUser?.mobile, + templateId: '569006', + parameters: [ + { name: 'FIRSTNAME', value: reciverUser?.first_name }, + { name: 'LASTNAME', value: reciverUser?.last_name } + + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + axios(config) + .then(function (response) { + }) + .catch(function (error) { + console.log(error) + }) + + // Send event based on payment status + + // Emit a success event + if ('eventEmitter' in req.app) { + req.app.get('eventEmitter').emit('paymentSuccess', { + message: 'Payment and offer created successfully', + offerId: offer._id + }) + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + res.status(200).json({ + message: 'Payment and offer recorded successfully', + offerId: offer._id, + price + }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +// Controller function for handling failed advertising payment +const handleFailedOfferPayment = async (req, res) => { + try { + const { offerType } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const price = await getOfferPriceFromDatabase(offerType) + + // Create a new payment entry + const payment = new PaymentModel({ + amount: Number(price), + status: 'failed', + authority: '', // Add authority if available + user_id: userId, + type: 'offer', + installment_step: null + }) + await payment.save() + + // Emit a failure event + if ('eventEmitter' in req.app) { + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment failed' }) + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + res.status(200).json({ message: 'Payment failed', price }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} + +const createOfferComment = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { offerId, comment, rate, user_id } = req.body + if (!offerId || !comment || !user_id) { + return res.status(400).send({ message: 'All fields are required' }) + } + + const { ratingValue, isNewRating, requiresRating } = await resolveRatingForComment({ + creatorId: userId, + targetUserId: user_id, + rate + }) + + if (requiresRating) { + return res.status(400).send({ message: 'Rating must be between 1 and 5' }) + } + + const newComment = new CommentModel({ + user: user_id, + offer: offerId, + creator: userId, + rating: ratingValue, + comment, + comment_for: 'user', + status: 'pending' + }) + await newComment.save() + + if (isNewRating) { + await recalculateUserRating(user_id) + } + + res.status(200).json({ message: 'نظر شما با موفقیت ثبت شد' }) + } catch (error) { + next(error) + } +} + +module.exports = { + getUserOffers, + getOfferTypes, + handleSuccessfulOfferPayment, + handleFailedOfferPayment, + updateOfferStatus, + createOfferComment +} diff --git a/controllers/application/academy/routes/application/academy/index.js b/controllers/application/academy/routes/application/academy/index.js new file mode 100644 index 0000000..6ef9da8 --- /dev/null +++ b/controllers/application/academy/routes/application/academy/index.js @@ -0,0 +1,389 @@ +const express = require("express"); +const router = express.Router(); +const academyController = require("../../../controllers/application/academy/academyControllers"); +const multer = require("multer"); +const path = require("path"); +const fs = require("fs"); +// const academyCategoryController = require("../../../controllers/application/academy/academyCategoryController"); + +// ========================================== +// ⚙️ تنظیمات اولیه و دایرکتوری‌ها +// ========================================== + +// اطمینان از وجود دایرکتوری‌ها +const ensureDir = (dir) => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + console.log("DEBUG: Created directory:", dir); + } +}; + +const BASE_DIR = process.cwd(); +const courseVideoDir = path.join(BASE_DIR, "storage", "courses", "videos"); +const courseImageDir = path.join(BASE_DIR, "storage", "courses", "images"); + +console.log("DEBUG: BASE_DIR:", BASE_DIR); +console.log("DEBUG: courseVideoDir:", courseVideoDir); +console.log("DEBUG: courseImageDir:", courseImageDir); + +ensureDir(courseVideoDir); +ensureDir(courseImageDir); + +// ========================================== +// 📁 تنظیمات آپلود فایل با Multer +// ========================================== + +// تنظیمات ذخیره‌سازی فایل‌ها +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + console.log("DEBUG: Multer destination - fieldname:", file.fieldname); + // استفاده از دایرکتوری موقت برای آپلود اولیه + const tempDir = "/tmp/modstagram-uploads"; + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + cb(null, tempDir); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); + const filename = uniqueSuffix + path.extname(file.originalname); + console.log("DEBUG: Generated filename:", filename); + cb(null, filename); + }, +}); + +// تنظیمات Multer با محدودیت حجم 500 مگابایت +const upload = multer({ + storage: storage, + limits: { + fileSize: 500 * 1024 * 1024, // 500 MB + }, +}); + +// ========================================== +// 🏢 مسیرهای مربوط به آکادمی +// ========================================== + +// دریافت اطلاعات آکادمی کاربر جاری +router.get("/", academyController.findAcademy); + +// یافتن آکادمی با شناسه +router.post("/findAcademyById", academyController.findAcademyById); + +// تکمیل پروفایل آکادمی +router.post("/academy/profile", academyController.academyProfile); + +// ========================================== +// 📚 مسیرهای مدیریت دوره‌ها (Courses) +// ========================================== + +// ایجاد دوره جدید +router.post("/academy/creat/course", academyController.creatCourse); + +// ویرایش دوره +router.patch("/academy/update/course", academyController.updateCourse); + +// حذف دوره (تغییر وضعیت به reject) +router.delete("/academy/delete/course", academyController.deleteCourse); + +// دریافت لیست دوره‌های آکادمی جاری +router.get("/academy/get/course", academyController.getCourse); + +// دریافت لیست همه دوره‌ها با فیلتر و صفحه‌بندی +router.get("/academy/get/curses", academyController.getCourses); + +// محتوای رایگان آموزشگاه برای اکسپلور +router.get( + "/academy/explore/free-content", + academyController.getFreeAcademyExploreContent +); + +// دریافت دوره‌های یک آکادمی خاص +router.get("/academy/get/getAcademyCourse/:Id", academyController.getAcademyCourse); + +// ========================================== +// 💳 مسیرهای پرداخت و مالی +// ========================================== + +// پرداخت برای اشتراک پرو +router.post("/academy/course/payment", academyController.proPayment); + +// کال‌بک درگاه پرداخت زرین‌پال (اشتراک پرو) +router.get("/academy/course/pro", academyController.handleCoursePaymentCallback); + +// پرداخت برای خرید دوره +router.post("/academy/course/payment-web", academyController.coursePayment); + +// کال‌بک درگاه پرداخت زرین‌پال (خرید دوره) +router.get("/academy/course/payment/verify", academyController.CoursePaymentCallback); + +// ========================================== +// 🎬 مسیرهای محتوای دوره (ویدیوها و فایل‌ها) +// ========================================== + +// دریافت محتوای دوره (ویدیوها) +router.get("/academy/course/content", academyController.getCourseContent); + +// حذف ویدیو از دوره +router.delete("/academy/course/content/video", academyController.deleteCourseVideo); + +// ========================================== +// 📹 مسیرهای آپلود ویدیو (۳ روش مختلف) +// ========================================== + +// روش 1: آپلود معمولی با Multipart (با استفاده از Multer) +router.post( + "/academy/course/creat/video", + (req, res, next) => { + console.log("===== STEP 1: Route hit ====="); + console.log("Content-Type:", req.headers["content-type"]); + next(); + }, + (req, res, next) => { + console.log("===== STEP 2: Before multer ====="); + next(); + }, + (req, res, next) => { + // اجرای manual multer با try-catch + upload.single("video")(req, res, (err) => { + if (err) { + console.log("===== MULTER ERROR ====="); + console.log("Error name:", err.name); + console.log("Error message:", err.message); + console.log("Full error:", err); + return res.status(400).json({ + success: false, + message: `خطا در آپلود: ${err.message}`, + error: err.toString(), + }); + } + console.log("===== STEP 3: Multer completed successfully ====="); + next(); + }); + }, + (req, res, next) => { + console.log("===== STEP 4: After multer ====="); + console.log("req.file:", req.file); + console.log("req.body:", req.body); + + if (!req.file) { + console.log("ERROR: No file in req.file!"); + return res.status(400).json({ + success: false, + message: "فایلی دریافت نشد", + }); + } + next(); + }, + academyController.creatCourseVideo +); + +// روش 2: آپلود با Base64 (برای فایل‌های کوچک) +router.post( + "/academy/course/creat/video-base64", + academyController.creatCourseVideoBase64 +); + +// روش 3: آپلود تکه‌تکه (Chunk Upload) - مناسب برای فایل‌های بزرگ +// دریافت هر تکه از ویدیو +router.post( + "/academy/course/creat/video-chunk", + academyController.creatCourseVideoChunk +); + +// ادغام تکه‌ها بعد از اتمام آپلود +router.post( + "/academy/course/creat/video-chunk/merge", + academyController.mergeVideoChunks +); + +// بررسی وضعیت آپلود (برای قابلیت Resume) +router.get("/academy/course/upload-status", academyController.getUploadStatus); + +// ========================================== +// ❤️ مسیرهای لایک و تعامل +// ========================================== + +// لایک کردن محتوای دوره +router.post("/academy/course/like", academyController.likeCourseContent); + +// حذف لایک (دیسلایک) +router.post("/academy/course/dislike", academyController.dontLikeCourseContent); + +// بررسی وضعیت لایک کاربر +router.post("/academy/course/is-like", academyController.isLikeCourseContent); + +// دریافت تعداد لایک‌های یک دوره +router.get( + "/academy/course/likes-count/:courseId", + academyController.getCourseLikesCount +); + +// دریافت لیست دوره‌های لایک شده توسط کاربر +router.get( + "/academy/user/liked-courses", + academyController.getUserLikedCourses +); + +// ========================================== +// 💬 مسیرهای کامنت و نظرات +// ========================================== + +// دریافت تعداد کل کامنت‌های یک دوره +router.get( + "/course/:courseId/comments/count", + academyController.getTotalCommentsCount +); + +// دریافت لیست کامنت‌های یک دوره +router.get("/course/:courseId/comments", academyController.getCourseComments); + +// ایجاد کامنت جدید +router.post("/comment/create", academyController.createComment); + +// تغییر وضعیت کامنت (تایید/رد) +router.patch( + "/comment/:commentId/status", + academyController.updateCommentStatus +); + +// حذف کامنت +router.delete("/comment/:commentId/delete", academyController.deleteComment); + +// ========================================== +// 🛒 مسیرهای دوره‌های خریداری شده +// ========================================== + +// دریافت دوره‌های خریداری شده توسط کاربر جاری +router.get( + "/course/getUserPurchasedCourses", + academyController.getUserPurchasedCourses +); + +// دریافت دوره‌های خریداری شده (با populate - روش بهینه) +router.get( + "/course/getUserPurchasedCoursesWithPopulate", + academyController.getUserPurchasedCoursesWithPopulate +); + +// دریافت همه پرداخت‌های آکادمی +router.get( + "/course/getAllAcademyPayments", + academyController.getAllAcademyPayments +); + +// بررسی خرید دوره توسط کاربر +router.get( + "/course/checkCoursePurchase/:courseId", + academyController.checkCoursePurchase +); + +// ========================================== +// 📊 مسیرهای آمار و گزارشات (STATIC) +// ========================================== + +// آمار پرداخت‌ها +router.get("/payments/stats", academyController.getPaymentStats); + +// آمار دوره‌ها +router.get("/courses/stats", academyController.getCourseStats); + +// آمار دوره‌های خریداری شده +router.get("/purchased/stats", academyController.getPurchasedStats); + +// آمار آکادمی‌ها +router.get("/academies/stats", academyController.getAcademyStats); + +// ========================================== +// ⚙️ مسیرهای تنظیمات (Settings) +// ========================================== + +// دریافت همه تنظیمات +router.get("/settings/all", academyController.getAllSettings); +router.get("/settings", academyController.getAllSettings); + +// به‌روزرسانی همه تنظیمات +router.put("/settings/all", academyController.updateAllSettings); +router.put("/settings", academyController.updateAllSettings); + +// دریافت و ویرایش قیمت دوره‌ها +router.get("/settings/course-prices", academyController.getCoursePrices); +router.put("/settings/course-price", academyController.updateCoursePrice); + +// دریافت و ویرایش مالیات +router.get("/settings/tax", academyController.getTax); +router.put("/settings/tax", academyController.updateTax); + +// ========================================== +// 🏷️ مسیرهای دسته‌بندی آکادمی (Categories) +// ========================================== + +// دریافت لیست دسته‌بندی‌ها (با فیلتر و صفحه‌بندی) +router.get('/categories', academyController.getAll); + +// دریافت درخت دسته‌بندی (ساختار سلسله‌مراتبی) +router.get('/categories/tree', academyController.getTree); + +// دریافت دسته‌بندی‌های ویژه (برای نمایش در هوم پیج) +router.get('/categories/featured', academyController.getFeatured); + +// دریافت دسته‌بندی با اسلاگ (برای سئو و لینک‌های زیبا) +router.get('/categories/slug/:slug', academyController.getBySlug); + +// دریافت یک دسته‌بندی با شناسه +router.get('/categories/:id', academyController.getOne); + +// ایجاد دسته‌بندی جدید +router.post('/categories', academyController.create); + +// ویرایش دسته‌بندی (هم PUT و هم PATCH پشتیبانی می‌شود) +router.put('/categories/:id', academyController.update); +router.patch('/categories/:id', academyController.update); + +// حذف دسته‌بندی (سافت دیلیت - فقط غیرفعال می‌شود) +router.delete('/categories/:id', academyController.remove); + +// حذف فیزیکی دسته‌بندی (به همراه query string permanent=true) +router.delete('/categories/:id/permanent', (req, res) => { + req.query.permanent = 'true'; + academyController.remove(req, res); +}); + +// تغییر وضعیت دسته‌بندی (فعال/غیرفعال) +router.patch('/categories/:id/status', academyController.toggleStatus); + +// افزایش تعداد دوره‌های دسته‌بندی (هر بار یک دوره جدید اضافه می‌شود) +router.post('/categories/:id/increment-count', academyController.incrementCourseCount); + +// ========================================== +// 👑 مسیرهای مدیریتی (پنل ادمین) +// ========================================== + +// ------ مدیریت پرداخت‌ها ------ +router.get("/payments", academyController.getAllPayments); +router.put("/payments/status", academyController.updatePaymentStatus); + +// ------ مدیریت دوره‌ها ------ +router.get("/courses", academyController.getAllCourses); +router.get("/courses/:id", academyController.getCourseById); +router.put("/courses/:id", academyController.updateCourse); +router.post("/courses", academyController.createCourse); +router.put("/academy/courses/status", academyController.updateCourseStatus); +router.delete("/courses/:courseId", academyController.admindeleteCourse); + +// ------ مدیریت دوره‌های خریداری شده ------ +router.get("/purchased", academyController.getAllPurchasedCourses); + +// ------ مدیریت آکادمی‌ها ------ +router.get("/academies", academyController.getAllAcademies); +router.get("/academies/:id", academyController.getAcademyById); +router.put("/academies/:id", academyController.updateAcademy); +router.post("/academies", academyController.createAcademy); +router.put("/academies/status", academyController.updateAcademyStatus); +router.delete("/academies/:id", academyController.deleteAcademy); + +// ========================================== +// 📤 خروجی روت‌ها +// ========================================== + +module.exports = router; \ No newline at end of file diff --git a/controllers/application/academy/routes/application/advertising/index.js b/controllers/application/academy/routes/application/advertising/index.js new file mode 100644 index 0000000..3001fa6 --- /dev/null +++ b/controllers/application/academy/routes/application/advertising/index.js @@ -0,0 +1,45 @@ +const express = require('express') +const blockCheck = require('../../../middlewares/blockCheck') +const { createAdvertisingValidationRules, createAdvertising, getAdvertisingTypes, getAdvertisingCategory, getSingleAdvertising, getAdvertisingFeatures, getAdvertisings, toggleLike, addComment, getComments, rateAdvertising, getUserAdvertisings, editAdvertising, getUserSingleAdvertising, getUserAdvertisingProfile, updateUserAdvertisingProfile, getAdvertisingsWeb, getSingleAdvertisingWeb, getUserAdvertisingProfileWeb } = require('../../../controllers/application/advertising/advertisingController') +const auth = require('../../../middlewares/auth') +const { initiateAdvertisingPayment, handleAdvertisingPaymentCallback, handleAdvertisingRepublishPaymentCallback, republishAdvertisingPayment, handleSuccessfulAdvertisingPayment, handleFailedAdvertisingPayment, handleFailedAdvertisingRepublishPayment, handleSuccessfulAdvertisingRepublishPayment } = require('../../../controllers/application/payment/paymentController') +const { initiateAdvertisingPaymentWeb, handleAdvertisingPaymentCallbackWeb, republishAdvertisingPaymentWeb, handleAdvertisingRepublishPaymentCallbackWeb } = require('../../../controllers/application/payment/paymentControllerWeb') +const router = express.Router() +router.get('/', [auth], getAdvertisings) +router.get('/web', getAdvertisingsWeb) +router.get('/profile', [auth], getUserAdvertisingProfile) +router.get('/profile-web', getUserAdvertisingProfileWeb) +router.patch('/profile', [auth], [blockCheck], updateUserAdvertisingProfile) +router.get('/user-advertising', [auth], getUserAdvertisings) +router.post('/create', [auth], [blockCheck], createAdvertisingValidationRules(), createAdvertising) +router.get('/types', [auth], getAdvertisingTypes) +router.get('/categories', getAdvertisingCategory) +router.get('/features', [auth], getAdvertisingFeatures) +router.get('/get/:advertisingId', [auth], getSingleAdvertising) +router.get('/get/web/:advertisingId', getSingleAdvertisingWeb) +router.get('/get-detail/:advertisingId', [auth], getUserSingleAdvertising) +router.post('/initiate-payment', [auth], [blockCheck], initiateAdvertisingPayment) +router.post('/initiate-payment-web', [auth], [blockCheck], initiateAdvertisingPaymentWeb) +router.post('/republish-payment', [auth], [blockCheck], republishAdvertisingPayment) +router.post('/republish-payment-web', [auth], [blockCheck], republishAdvertisingPaymentWeb) +router.get('/payment', handleAdvertisingPaymentCallback) +router.get('/payment-web', handleAdvertisingPaymentCallbackWeb) +router.get('/republish-verify-payment', handleAdvertisingRepublishPaymentCallback) +router.get('/republish-verify-payment-web', handleAdvertisingRepublishPaymentCallbackWeb) +router.post('/like', [blockCheck], [auth], toggleLike) +router.post('/comment', [blockCheck], [auth], addComment) +router.get('/comments', getComments) +router.post('/rate', [blockCheck], [auth], rateAdvertising) +router.post('/edit/:id', [blockCheck], [auth], editAdvertising) + +// Route for handling successful payment +router.post('/payment-success', handleSuccessfulAdvertisingPayment) +// Route for handling failed payment +router.post('/payment-failed', handleFailedAdvertisingPayment) +// Route for handling successful payment +router.post('/republish-payment-success', handleSuccessfulAdvertisingRepublishPayment) +// Route for handling failed payment +router.post('/republish-payment-failed', handleFailedAdvertisingRepublishPayment) + +module.exports = router +module.exports = router diff --git a/controllers/application/academy/routes/application/city/index.js b/controllers/application/academy/routes/application/city/index.js new file mode 100644 index 0000000..084c42d --- /dev/null +++ b/controllers/application/academy/routes/application/city/index.js @@ -0,0 +1,7 @@ +const express = require('express') +const router = express.Router() +const citysController = require('../../../controllers/application/citysController') + +router.get('/:id', citysController.getCities) + +module.exports = router diff --git a/controllers/application/academy/routes/application/expertise/index.js b/controllers/application/academy/routes/application/expertise/index.js new file mode 100644 index 0000000..fdf4f1d --- /dev/null +++ b/controllers/application/academy/routes/application/expertise/index.js @@ -0,0 +1,6 @@ +const express = require('express') +const { getExpertise } = require('../../../controllers/application/expertise/expertiseController') +const router = express.Router() +router.get('/', getExpertise) + +module.exports = router diff --git a/controllers/application/academy/routes/application/financial/index.js b/controllers/application/academy/routes/application/financial/index.js new file mode 100644 index 0000000..0123e94 --- /dev/null +++ b/controllers/application/academy/routes/application/financial/index.js @@ -0,0 +1,6 @@ +const express = require('express') +const { getFinancial } = require('../../../controllers/application/financial/financialController') +const router = express.Router() +router.get('/', getFinancial) + +module.exports = router diff --git a/controllers/application/academy/routes/application/login/index.js b/controllers/application/academy/routes/application/login/index.js new file mode 100644 index 0000000..6ee30c8 --- /dev/null +++ b/controllers/application/academy/routes/application/login/index.js @@ -0,0 +1,14 @@ +const express = require('express') +const router = express.Router() +const { loginValidationRules, loginUser } = require('../../../controllers/application/login/loginController') +const { verifyUser } = require('../../../controllers/application/login/verifyController') +const { changePasswordUser, changePasswordValidationRules, editChangePasswordUser } = require('../../../controllers/application/login/changePasswordController') +const { loginWithUserName } = require('../../../controllers/application/login/loginWithUserName') + +router.post('/', loginValidationRules(), loginUser) +router.post('/verify', verifyUser) +router.post('/change_password', changePasswordValidationRules(), changePasswordUser) +router.patch('/change_password', changePasswordValidationRules(), editChangePasswordUser) +router.post('/login_username', loginWithUserName) + +module.exports = router diff --git a/controllers/application/academy/routes/application/messages/index.js b/controllers/application/academy/routes/application/messages/index.js new file mode 100644 index 0000000..c74cc8b --- /dev/null +++ b/controllers/application/academy/routes/application/messages/index.js @@ -0,0 +1,6 @@ +const express = require('express') +const { getMessages, getUnreadMessages } = require('../../../controllers/application/messages/messageController') +const router = express.Router() +router.get('/', getMessages) +router.get('/unread', getUnreadMessages) +module.exports = router diff --git a/controllers/application/academy/routes/application/notification/index.js b/controllers/application/academy/routes/application/notification/index.js new file mode 100644 index 0000000..8ef0a3e --- /dev/null +++ b/controllers/application/academy/routes/application/notification/index.js @@ -0,0 +1,6 @@ +const express = require('express') +const { getNotifications, unreadNotifications } = require('../../../controllers/application/notification/notificationController') +const router = express.Router() +router.get('/', getNotifications) +router.get('/unread', unreadNotifications) +module.exports = router diff --git a/controllers/application/academy/routes/application/offers/index.js b/controllers/application/academy/routes/application/offers/index.js new file mode 100644 index 0000000..ebd57e3 --- /dev/null +++ b/controllers/application/academy/routes/application/offers/index.js @@ -0,0 +1,18 @@ +const express = require('express') +const { getUserOffers, getOfferTypes, handleSuccessfulOfferPayment, handleFailedOfferPayment, updateOfferStatus, createOfferComment } = require('../../../controllers/application/offer/offerController') +const { initiateOfferPaymentWeb, handleOfferPaymentCallback } = require('../../../controllers/application/payment/paymentControllerWeb') +const blockCheck = require('../../../middlewares/blockCheck') +const isRegister = require('../../../middlewares/isRegister') +const auth = require('./../../../middlewares/auth') + +const router = express.Router() +router.get('/', getUserOffers) +router.get('/types', [auth], getOfferTypes) +router.post('/update-status', [auth], updateOfferStatus) +router.post('/payment-success', [auth], handleSuccessfulOfferPayment) +router.post('/payment-failed', [auth], handleFailedOfferPayment) +router.post('/comment', [auth], createOfferComment) +router.post('/initiate-payment-web', [auth], [blockCheck], initiateOfferPaymentWeb) +router.get('/payment-web', handleOfferPaymentCallback) + +module.exports = router diff --git a/controllers/application/academy/routes/application/posts/index.js b/controllers/application/academy/routes/application/posts/index.js new file mode 100644 index 0000000..3ec627a --- /dev/null +++ b/controllers/application/academy/routes/application/posts/index.js @@ -0,0 +1,205 @@ +const express = require('express'); +const { createPostValidationRules, createPost, getUserPosts, getUserPostsWeb, getPostByIdWeb } = require('../../../controllers/application/posts/postController'); +const { toggleLike } = require('../../../controllers/application/posts/likeController'); +const blockCheck = require('../../../middlewares/blockCheck'); +const auth = require('../../../middlewares/auth'); +const isHalfRegister = require('../../../middlewares/isHalfRegister'); +const path = require('path'); +const fs = require('fs'); +const multer = require('multer'); + +console.log('DEBUG: Starting to load routes/posts.js'); + +const ensureDir = (dir) => { + console.log('DEBUG: Ensuring directory exists:', dir); + if (!fs.existsSync(dir)) { + console.log('DEBUG: Directory does not exist, creating:', dir); + fs.mkdirSync(dir, { recursive: true }); + } else { + console.log('DEBUG: Directory already exists:', dir); + } +}; + +const imageDir = path.join(__dirname, '../../../storage/posts/images'); +const videoDir = path.join(__dirname, '../../../storage/posts/videos'); +console.log('DEBUG: imageDir path:', imageDir); +console.log('DEBUG: videoDir path:', videoDir); +ensureDir(imageDir); +ensureDir(videoDir); + +console.log('DEBUG: Setting up multer storage'); + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const dir = file.mimetype.startsWith('video/') ? videoDir : imageDir; + console.log('DEBUG: multer destination for file', file.originalname, ':', dir); + cb(null, dir); + }, + filename: (req, file, cb) => { + const filename = `${Date.now()}-${file.originalname}`; + console.log('DEBUG: Generated filename for', file.originalname, ':', filename); + cb(null, filename); + }, +}); + +console.log('DEBUG: Setting up multer instance with limits and filter'); + +const upload = multer({ + storage, + limits: { + fileSize: 100 * 1024 * 1024, // حداکثر 100MB + files: 10, // حداکثر 10 فایل + fieldSize: 1024 * 1024, + fieldNameSize: 100, + }, + fileFilter: (req, file, cb) => { + console.log('DEBUG: Checking file filter for', file.originalname, 'mimetype:', file.mimetype); + if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) { + console.log('DEBUG: File accepted:', file.originalname); + cb(null, true); + } else { + console.log('DEBUG: File rejected:', file.originalname); + cb(new Error('فقط تصاویر یا ویدئوها مجاز هستند'), false); + } + }, +}).array('post_files', 10); + +const multerErrorHandler = (err, req, res, next) => { + console.log('DEBUG: Request headers:', JSON.stringify(req.headers, null, 2)); + console.log('DEBUG: Request body (raw):', req.body); + console.log('DEBUG: Request files:', req.files || 'No files received'); + if (err instanceof multer.MulterError) { + console.error('DEBUG: Multer error:', err.message, err.code, err.field); + return res.status(400).json({ + error: true, + message: `خطای آپلود: ${err.message} (${err.code})`, + field: err.field, + }); + } else if (err) { + console.error('DEBUG: File filter error:', err.message); + return res.status(400).json({ + error: true, + message: err.message, + }); + } + console.log('DEBUG: Multer processing completed, files:', req.files || 'No files'); + console.log('DEBUG: Request body after multer:', req.body); + next(); +}; + +console.log('DEBUG: Setting up express router'); + +const router = express.Router(); + +// روت اصلی با multipart/form-data +router.post( + '/create', + (req, res, next) => { + console.log('DEBUG: reached auth'); + console.log('DEBUG: Raw request headers:', JSON.stringify(req.headers, null, 2)); + next(); + }, + auth, + (req, res, next) => { + console.log('DEBUG: reached blockCheck'); + next(); + }, + blockCheck, + (req, res, next) => { + console.log('DEBUG: reached isHalfRegister'); + next(); + }, + isHalfRegister, + (req, res, next) => { + console.log('DEBUG: reached multer'); + console.log('DEBUG: Content-Length:', req.headers['content-length']); + console.log('DEBUG: Content-Type:', req.headers['content-type']); + next(); + }, + upload, + multerErrorHandler, + (req, res, next) => { + console.log('DEBUG: reached validation'); + console.log('DEBUG: Parsed files:', req.files || 'No files'); + console.log('DEBUG: Parsed body:', req.body); + next(); + }, + createPostValidationRules(), + (req, res, next) => { + console.log('DEBUG: reached controller'); + next(); + }, + createPost +); + +// روت جدید برای Base64 +router.post( + '/create-base64', + (req, res, next) => { + console.log('DEBUG: reached auth for base64'); + console.log('DEBUG: Raw request headers:', JSON.stringify(req.headers, null, 2)); + next(); + }, + auth, + (req, res, next) => { + console.log('DEBUG: reached blockCheck for base64'); + next(); + }, + blockCheck, + (req, res, next) => { + console.log('DEBUG: reached isHalfRegister for base64'); + next(); + }, + isHalfRegister, + (req, res, next) => { + console.log('DEBUG: Request body (base64):', JSON.stringify(req.body, null, 2)); + next(); + }, + createPostValidationRules(), + async (req, res) => { + try { + const { files, caption } = req.body; + console.log('DEBUG: Received files (base64):', files.length); + console.log('DEBUG: Received caption:', caption); + + // تبدیل Base64 به فایل + const savedFiles = await Promise.all( + files.map(async (file, index) => { + const buffer = Buffer.from(file.data.split(',')[1], 'base64'); + const dir = file.type.startsWith('video/') ? videoDir : imageDir; + const filename = `${Date.now()}-${index}-${file.name}`; + const filePath = path.join(dir, filename); + console.log('DEBUG: Saving file:', filePath); + await fs.promises.writeFile(filePath, buffer); + return { + path: filePath, + filename, + mimetype: file.type, + }; + }) + ); + + // فراخوانی createPost با فرمت مشابه + req.files = savedFiles; + req.body = { caption }; + console.log('DEBUG: Prepared files for createPost:', savedFiles); + console.log('DEBUG: Prepared body for createPost:', req.body); + await createPost(req, res); + } catch (err) { + console.error('DEBUG: Base64 processing error:', err.message); + res.status(400).json({ + error: true, + message: `خطای پردازش فایل‌ها: ${err.message}`, + }); + } + } +); + +router.post('/like', [auth, blockCheck], toggleLike); +router.get('/web/:postId', getPostByIdWeb); +router.get('/user-posts', [auth], getUserPosts); +router.get('/user-posts/web', getUserPostsWeb); + +console.log('DEBUG: Finished loading routes/posts.js'); + +module.exports = router; \ No newline at end of file diff --git a/controllers/application/academy/routes/application/profile/index.js b/controllers/application/academy/routes/application/profile/index.js new file mode 100644 index 0000000..2c7bd31 --- /dev/null +++ b/controllers/application/academy/routes/application/profile/index.js @@ -0,0 +1,8 @@ +const express = require('express') +const { getProfile , getUserById} = require('../../../controllers/application/profile/profileController') +const auth = require('../../../middlewares/auth') +const router = express.Router() +router.get('/',[auth], getProfile) +router.get('/:id', getUserById) + +module.exports = router diff --git a/controllers/application/academy/routes/application/projects/index.js b/controllers/application/academy/routes/application/projects/index.js new file mode 100644 index 0000000..67be3d8 --- /dev/null +++ b/controllers/application/academy/routes/application/projects/index.js @@ -0,0 +1,44 @@ +const express = require('express') +const { createProject, createProjectValidationRules } = require('../../../controllers/application/projects/createProjectController') +const { getProjects, getSingleProjects, getSingleProjectsWeb, getUserProjects, getUserProjectsWeb } = require('../../../controllers/application/projects/getProjectController') +const { getProjectTypes } = require('../../../controllers/application/projects/projectTypesController') +const { initiatePayment, handlePaymentCallback, paymentRequestStepOne, paymentRequestStepTwo, paymentRequestStepAll, paymentRequestStepOneCallback, paymentRequestStepTwoCallback, paymentRequestStepAllCallback, handleSuccessfulPayment, handleFailedPayment } = require('../../../controllers/application/payment/paymentController') +const auth = require('../../../middlewares/auth') +const { requestProjectValidationRules, requestProject, acceptProject, editRequestProject } = require('../../../controllers/application/projects/requestProjectController') +const { editProject, doneProject, cancleProject, editProjectValidationRules, setRateProject, doneProjectWeb } = require('../../../controllers/application/projects/updateProjectController') +const blockCheck = require('../../../middlewares/blockCheck') +const isRegister = require('../../../middlewares/isRegister') +const { initiatePaymentWeb, handlePaymentCallbackWeb, paymentRequestAcceptWeb } = require('../../../controllers/application/payment/paymentControllerWeb') +const router = express.Router() +router.get('/', getProjects) +router.get('/user-projects', [auth], getUserProjects) +router.get('/user-projects/web', getUserProjectsWeb) +router.get('/types', [auth], getProjectTypes) +router.get('/get/:projectId', [auth], getSingleProjects) +router.get('/get/web/:projectId', getSingleProjectsWeb) +router.get('/payment', handlePaymentCallback) +router.get('/payment-web', handlePaymentCallbackWeb) +router.post('/create', [blockCheck], [auth], createProjectValidationRules(), createProject) +router.post('/edit', [blockCheck], [auth], editProjectValidationRules(), editProject) +router.post('/rate', [blockCheck], [auth], [isRegister], setRateProject) +router.post('/initiate-payment', [blockCheck], [isRegister], [auth], initiatePayment) +router.post('/initiate-payment-web', [blockCheck], [isRegister], [auth], initiatePaymentWeb) +router.post('/request', [blockCheck], [isRegister], [auth], requestProjectValidationRules(), requestProject) +router.post('/edit-request', [isRegister], [blockCheck], [auth], requestProjectValidationRules(), editRequestProject) +router.post('/payment-request-step-one', [isRegister], [blockCheck], [auth], paymentRequestStepOne) +router.post('/payment-request-step-one/web', [isRegister], [blockCheck], [auth], paymentRequestAcceptWeb) +router.post('/payment-request-step-two', [isRegister], [blockCheck], [auth], paymentRequestStepTwo) +router.post('/payment-request-all', [isRegister], [blockCheck], [auth], paymentRequestStepAll) +router.get('/payment-request-step-one-callback', paymentRequestStepOneCallback) +router.get('/payment-request-step-two-callback', paymentRequestStepTwoCallback) +router.get('/payment-request-step-all-callback', paymentRequestStepAllCallback) +router.post('/accept', [isRegister], [blockCheck], [auth], acceptProject) +router.post('/done', [isRegister], [auth], doneProject) +router.post('/done/web', [isRegister], [auth], doneProjectWeb) +router.post('/cancle', [isRegister], [blockCheck], [auth], cancleProject) + +// Route for handling successful payment +router.post('/payment-success', handleSuccessfulPayment) +// Route for handling failed payment +router.post('/payment-failed', handleFailedPayment) +module.exports = router diff --git a/controllers/application/academy/routes/application/provinces/index.js b/controllers/application/academy/routes/application/provinces/index.js new file mode 100644 index 0000000..1b346c6 --- /dev/null +++ b/controllers/application/academy/routes/application/provinces/index.js @@ -0,0 +1,7 @@ +const express = require('express') +const router = express.Router() +const provincesController = require('../../../controllers/application/provincesController') + +router.get('/', provincesController.getProvinces) + +module.exports = router diff --git a/controllers/application/academy/routes/application/register/index.js b/controllers/application/academy/routes/application/register/index.js new file mode 100644 index 0000000..9978c9d --- /dev/null +++ b/controllers/application/academy/routes/application/register/index.js @@ -0,0 +1,16 @@ +const express = require('express') +const router = express.Router() +const { registerValidationRules, registerUser } = require('../../../controllers/application/register/registerController') +const { verifyUser } = require('../../../controllers/application/register/verifyController') +const { setUserName, updateUserName } = require('../../../controllers/application/register/usernameController') +const { setUserFullName, setUserFullNameValidationRules } = require('../../../controllers/application/register/userFullNameController') +const { setUserType } = require('../../../controllers/application/register/userTypeController') + +router.post('/', registerValidationRules(), registerUser) +router.post('/verify', verifyUser) +router.post('/username', setUserName) +router.patch('/username', updateUserName) +router.post('/fullname', setUserFullNameValidationRules(), setUserFullName) +router.post('/user_type', setUserType) + +module.exports = router diff --git a/controllers/application/academy/routes/application/search-web/index.js b/controllers/application/academy/routes/application/search-web/index.js new file mode 100644 index 0000000..54f78f6 --- /dev/null +++ b/controllers/application/academy/routes/application/search-web/index.js @@ -0,0 +1,6 @@ +const express = require('express') +const { getSearch } = require('../../../controllers/application/search-web/searchController') +const router = express.Router() +router.get('/', getSearch) + +module.exports = router diff --git a/controllers/application/academy/routes/application/search/index.js b/controllers/application/academy/routes/application/search/index.js new file mode 100644 index 0000000..5db29bc --- /dev/null +++ b/controllers/application/academy/routes/application/search/index.js @@ -0,0 +1,6 @@ +const express = require('express') +const { getSearch } = require('../../../controllers/application/search/searchController') +const router = express.Router() +router.get('/', getSearch) + +module.exports = router diff --git a/controllers/application/academy/routes/application/settings/index.js b/controllers/application/academy/routes/application/settings/index.js new file mode 100644 index 0000000..bff48f0 --- /dev/null +++ b/controllers/application/academy/routes/application/settings/index.js @@ -0,0 +1,6 @@ +const express = require('express') +const { getSetting } = require('../../../controllers/application/settings/settingsController') +const router = express.Router() +router.get('/:key', getSetting) + +module.exports = router diff --git a/controllers/application/academy/routes/application/stories/index.js b/controllers/application/academy/routes/application/stories/index.js new file mode 100644 index 0000000..0a5d4eb --- /dev/null +++ b/controllers/application/academy/routes/application/stories/index.js @@ -0,0 +1,19 @@ +const express = require('express') +const auth = require('../../../middlewares/auth') +const blockCheck = require('../../../middlewares/blockCheck') +const isHalfRegister = require('../../../middlewares/isHalfRegister') +const { + createStoryBase64, + getStoriesFeed, + markStoryViewed, + deleteStory +} = require('../../../controllers/application/stories/storyController') + +const router = express.Router() + +router.get('/feed', getStoriesFeed) +router.post('/create-base64', auth, blockCheck, isHalfRegister, createStoryBase64) +router.post('/view', auth, blockCheck, markStoryViewed) +router.delete('/:storyId', auth, blockCheck, deleteStory) + +module.exports = router diff --git a/controllers/application/academy/routes/application/tickets/index.js b/controllers/application/academy/routes/application/tickets/index.js new file mode 100644 index 0000000..5174c7e --- /dev/null +++ b/controllers/application/academy/routes/application/tickets/index.js @@ -0,0 +1,9 @@ +const express = require('express') +const { getUserTickets, createTicket, addUserMessageToTicket, getTicketMessages } = require('../../../controllers/application/tickets/ticketsController') +const router = express.Router() +router.get('/', getUserTickets) +router.get('/messages', getTicketMessages) +router.post('/create', createTicket) +router.post('/message', addUserMessageToTicket) + +module.exports = router diff --git a/controllers/application/academy/routes/application/users/index.js b/controllers/application/academy/routes/application/users/index.js new file mode 100644 index 0000000..065efed --- /dev/null +++ b/controllers/application/academy/routes/application/users/index.js @@ -0,0 +1,23 @@ +const express = require('express') +const { getUsers, getSingleUser, blockUser, unblockUser, getComments,createLicense, getAllLicenses,updateLicenseConfirmation, getLicenseByUserId, inviteEmployer, getUserContactInfo, getPostsWeb, getSingleUserWeb, createUserComment, recordExploreInteraction } = require('../../../controllers/application/users/getUserController') +const blockCheck = require('../../../middlewares/blockCheck') +const isRegister = require('../../../middlewares/isRegister') +const auth = require('../../../middlewares/auth') +const router = express.Router() +router.get('/', [auth], getUsers) +router.get('/web', getPostsWeb) +router.get('/get', [auth], getSingleUser) +router.get('/get/web', getSingleUserWeb) +router.post('/block', [auth], [isRegister], [blockCheck], blockUser) +router.post('/unblock', [auth], [isRegister], [blockCheck], unblockUser) +router.get('/comments', [auth], getComments) +router.post('/comments', [auth], [isRegister], [blockCheck], createUserComment) +router.post('/explore/interaction', [auth], recordExploreInteraction) +router.get('/invite-employer', [auth], [isRegister], [blockCheck], inviteEmployer) +router.get('/contact-info', [auth], getUserContactInfo) +router.get('/License', getAllLicenses) +router.get('/License/:userId', getLicenseByUserId) +router.post('/License', createLicense) +router.put('/License/:userId', updateLicenseConfirmation) + +module.exports = router diff --git a/controllers/application/academy/routes/application/verify/index.js b/controllers/application/academy/routes/application/verify/index.js new file mode 100644 index 0000000..206ba2f --- /dev/null +++ b/controllers/application/academy/routes/application/verify/index.js @@ -0,0 +1,40 @@ +const express = require('express') +const { setProfileImage, updateProfileImage } = require('../../../controllers/application/verify/setProfileImageController') +const { setExpertise, updateExpertise } = require('../../../controllers/application/verify/expertiseController') +const { setGender } = require('../../../controllers/application/verify/genderController') +const { setSizes, updateSizes } = require('../../../controllers/application/verify/sizesController') +const { setColors, updateColors } = require('../../../controllers/application/verify/colorsController') +const { setCooperationType, updateCooperationType } = require('../../../controllers/application/verify/cooperationTypeController') +const { setConversation, updateConversation } = require('../../../controllers/application/verify/conversationController') +const { saveAuth, saveAuthValidationRules, updateShaba } = require('../../../controllers/application/verify/authController') +const { setAddress, updateAddress } = require('../../../controllers/application/verify/addressController') +const { setNationalCardImage } = require('../../../controllers/application/verify/setNationalCardImageController') +const { completeRegistration } = require('../../../controllers/application/verify/completeRegistrationController') +const blockCheck = require('../../../middlewares/blockCheck') +const auth = require('../../../middlewares/auth') +const { setServices, updateServices } = require('../../../controllers/application/verify/servicesController') +const router = express.Router() + +router.post('/profile_image', setProfileImage) +router.patch('/profile_image', [blockCheck], [auth], updateProfileImage) +router.post('/expertise', setExpertise) +router.patch('/expertise', [blockCheck], [auth], updateExpertise) +router.post('/gender', setGender) +router.post('/sizes', setSizes) +router.patch('/sizes', [blockCheck], [auth], updateSizes) +router.post('/services', setServices) +router.patch('/services', [blockCheck], [auth], updateServices) +router.post('/colors', setColors) +router.patch('/colors', [blockCheck], [auth], updateColors) +router.post('/cooperation_type', setCooperationType) +router.patch('/cooperation_type', [blockCheck], [auth], updateCooperationType) +router.post('/conversation', setConversation) +router.patch('/conversation', [blockCheck], [auth], updateConversation) +router.post('/auth', saveAuthValidationRules(), saveAuth) +router.patch('/shaba', [blockCheck], [auth], updateShaba) +router.post('/address', setAddress) +router.patch('/address', [blockCheck], [auth], updateAddress) +router.post('/national_card_image', setNationalCardImage) +router.post('/complete', completeRegistration) + +module.exports = router diff --git a/controllers/application/academy/routes/application/version/index.js b/controllers/application/academy/routes/application/version/index.js new file mode 100644 index 0000000..ab8eaec --- /dev/null +++ b/controllers/application/academy/routes/application/version/index.js @@ -0,0 +1,6 @@ +const express = require('express') +const { getVersion } = require('../../../controllers/application/version/versionController') +const router = express.Router() +router.get('/', getVersion) + +module.exports = router diff --git a/controllers/application/academy/routes/application/workroom/index.js b/controllers/application/academy/routes/application/workroom/index.js new file mode 100644 index 0000000..f7d6cba --- /dev/null +++ b/controllers/application/academy/routes/application/workroom/index.js @@ -0,0 +1,8 @@ +const express = require('express') +const { getProjects, getSingleProject, getProjectRequests } = require('../../../controllers/application/workroom/workroomController') +const router = express.Router() +router.get('/', getProjects) +router.get('/get', getSingleProject) +router.get('/get-requests/:projectId', getProjectRequests) + +module.exports = router diff --git a/controllers/application/academy/routes/index.js b/controllers/application/academy/routes/index.js new file mode 100644 index 0000000..7367a17 --- /dev/null +++ b/controllers/application/academy/routes/index.js @@ -0,0 +1,79 @@ +const registerRouter = require('./application/register') +const loginRouter = require('./application/login') +const verifyRouter = require('./application/verify') +const provincesRouter = require('./application/provinces') +const citiesRouter = require('./application/city') +const projectsRouter = require('./application/projects') +const usersRouter = require('./application/users') +const postsRouter = require('./application/posts') +const expertiseRouter = require('./application/expertise') +const profileRouter = require('./application/profile') +const workroomRouter = require('./application/workroom') +const messagesRouter = require('./application/messages') +const searchRouter = require('./application/search') +const searchRouterWeb = require('./application/search-web') +const financialRouter = require('./application/financial') +const notificationRouter = require('./application/notification') +const ticketsRouter = require('./application/tickets') +const advertisingRouter = require('./application/advertising') +const settingsRouter = require('./application/settings') +const versionRouter = require('./application/version') +const offerRouter = require('./application/offers') +const academyRoute = require('./application/academy') +const storiesRouter = require('./application/stories') + +const projectsPanelRouter = require('./panel/projects') +const postsPanelRouter = require('./panel/posts') +const expertisePanelRouter = require('./panel/expertise') +const loginPanelRouter = require('./panel/login') +const usersPanelRouter = require('./panel/users') +const ticketsPanelRouter = require('./panel/tickets') +const financialPanelRouter = require('./panel/financial') +const commentsPanelRouter = require('./panel/comments') +const advertisingPanelRouter = require('./panel/advertising') +const shopsPanelRouter = require('./panel/shops') +const settingsPanelRouter = require('./panel/settings') +const versionPanelRouter = require('./panel/version') + +const auth = require('../middlewares/auth') +const adminAuth = require('../middlewares/adminAuth') +module.exports = (app) => { + // App + app.use('/api/v1/register', registerRouter) + app.use('/api/v1/login', loginRouter) + app.use('/api/v1/verify', [auth], verifyRouter) + app.use('/api/v1/provinces', provincesRouter) + app.use('/api/v1/cities', citiesRouter) + app.use('/api/v1/projects', projectsRouter) + app.use('/api/v1/users', usersRouter) + app.use('/api/v1/posts', postsRouter) + app.use('/api/v1/expertise', expertiseRouter) + app.use('/api/v1/profile', profileRouter) + app.use('/api/v1/workroom', [auth], workroomRouter) + app.use('/api/v1/messages', [auth], messagesRouter) + app.use('/api/v1/notifications', [auth], notificationRouter) + app.use('/api/v1/search', [auth], searchRouter) + app.use('/api/v1/search-web', searchRouterWeb) + app.use('/api/v1/financial', [auth], financialRouter) + app.use('/api/v1/tickets', [auth], ticketsRouter) + app.use('/api/v1/offers', offerRouter) + app.use('/api/v1/advertising', advertisingRouter) + app.use('/api/v1/settings', settingsRouter) + app.use('/api/v1/version', versionRouter) + app.use('/api/v1/academy', academyRoute) + app.use('/api/v1/stories', storiesRouter) + + // Panel + app.use('/api/v1/panel/login', loginPanelRouter) + app.use('/api/v1/panel/users', usersPanelRouter) + app.use('/api/v1/panel/projects', [adminAuth], projectsPanelRouter) + app.use('/api/v1/panel/posts', [adminAuth], postsPanelRouter) + app.use('/api/v1/panel/expertise', [adminAuth], expertisePanelRouter) + app.use('/api/v1/panel/tickets', [adminAuth], ticketsPanelRouter) + app.use('/api/v1/panel/financial', [adminAuth], financialPanelRouter) + app.use('/api/v1/panel/comments', [adminAuth], commentsPanelRouter) + app.use('/api/v1/panel/advertising', [adminAuth], advertisingPanelRouter) + app.use('/api/v1/panel/shops', [adminAuth], shopsPanelRouter) + app.use('/api/v1/panel/settings', [adminAuth], settingsPanelRouter) + app.use('/api/v1/panel/version', [adminAuth], versionPanelRouter) +} diff --git a/controllers/application/academy/routes/panel/advertising/index.js b/controllers/application/academy/routes/panel/advertising/index.js new file mode 100644 index 0000000..34a7a0b --- /dev/null +++ b/controllers/application/academy/routes/panel/advertising/index.js @@ -0,0 +1,22 @@ +const express = require('express') +const { getAdvertisings, getSingleAdvertising, acceptAdvertising, rejectAdvertising, getAdvertisingCategories, getAdvertisingCategoryById,createAdvertisingCategory , deleteAdvertisingCategory } = require('../../../controllers/panel/advertising/advertisingController') +const { getAdvertisingComments, getAdvertisingCommentDetail, acceptAdvertisingComment } = require('../../../controllers/panel/comments/advertisingCommentController') +const router = express.Router() +router.get('/', getAdvertisings) +router.get('/detail', getSingleAdvertising) +router.post('/accept', acceptAdvertising) +router.post('/reject', rejectAdvertising) + +router.get('/comments', getAdvertisingComments) +router.get('/comments/detail', getAdvertisingCommentDetail) +router.post('/comments/accept', acceptAdvertisingComment) + +router.post('/advertising-categories', createAdvertisingCategory) +router.delete('/advertising-categories/:id', deleteAdvertisingCategory) +// لیست دسته‌بندی‌ها (پشتیبانی از query params) +router.get('/advertising-categories', getAdvertisingCategories); + +// گرفتن یک دسته‌بندی با id +router.get('/advertising-categories/:id', getAdvertisingCategoryById); + +module.exports = router diff --git a/controllers/application/academy/routes/panel/comments/index.js b/controllers/application/academy/routes/panel/comments/index.js new file mode 100644 index 0000000..e64028d --- /dev/null +++ b/controllers/application/academy/routes/panel/comments/index.js @@ -0,0 +1,8 @@ +const express = require('express') +const { acceptComment, getComments, getCommentDetail } = require('../../../controllers/panel/comments/commentController') +const router = express.Router() +router.get('/', getComments) +router.get('/detail', getCommentDetail) +router.post('/accept', acceptComment) + +module.exports = router diff --git a/controllers/application/academy/routes/panel/expertise/index.js b/controllers/application/academy/routes/panel/expertise/index.js new file mode 100644 index 0000000..56e8dfe --- /dev/null +++ b/controllers/application/academy/routes/panel/expertise/index.js @@ -0,0 +1,9 @@ +const express = require('express') +const { createExpertise, createSubExpertise, getExpertise, editExpertise } = require('../../../controllers/panel/expertise/expertiseController') +const router = express.Router() +router.get('/', getExpertise) +router.post('/create', createExpertise) +router.post('/create/sub', createSubExpertise) +router.put('/update/:id', editExpertise) + +module.exports = router diff --git a/controllers/application/academy/routes/panel/financial/index.js b/controllers/application/academy/routes/panel/financial/index.js new file mode 100644 index 0000000..65f8f60 --- /dev/null +++ b/controllers/application/academy/routes/panel/financial/index.js @@ -0,0 +1,7 @@ +const express = require('express') +const { getFinancial, getFinancialDetail } = require('../../../controllers/panel/financial/financialController') +const router = express.Router() +router.get('/', getFinancial) +router.get('/detail', getFinancialDetail) + +module.exports = router diff --git a/controllers/application/academy/routes/panel/login/index.js b/controllers/application/academy/routes/panel/login/index.js new file mode 100644 index 0000000..58adb2e --- /dev/null +++ b/controllers/application/academy/routes/panel/login/index.js @@ -0,0 +1,8 @@ +const express = require('express') +const router = express.Router() +const { adminLogin, changePassword } = require('../../../controllers/panel/login/loginController') +const adminAuth = require('../../../middlewares/adminAuth') +router.post('/', adminLogin) +router.post('/change-password', [adminAuth], changePassword) + +module.exports = router diff --git a/controllers/application/academy/routes/panel/posts/index.js b/controllers/application/academy/routes/panel/posts/index.js new file mode 100644 index 0000000..5b8d4f7 --- /dev/null +++ b/controllers/application/academy/routes/panel/posts/index.js @@ -0,0 +1,8 @@ +const express = require('express') +const { acceptPost, getPosts, getPostDetail } = require('../../../controllers/panel/posts/postController') +const router = express.Router() +router.get('/', getPosts) +router.get('/detail', getPostDetail) +router.post('/accept', acceptPost) + +module.exports = router diff --git a/controllers/application/academy/routes/panel/projects/index.js b/controllers/application/academy/routes/panel/projects/index.js new file mode 100644 index 0000000..61c9f14 --- /dev/null +++ b/controllers/application/academy/routes/panel/projects/index.js @@ -0,0 +1,11 @@ +const express = require('express') +const { createProjectTypes, getProjectTypes } = require('../../../controllers/panel/projects/projectTypesController') +const { getProjects, getProjectDetails, acceptProject, rejectProject } = require('../../../controllers/panel/projects/projectController') +const router = express.Router() +router.get('/', getProjects) +router.get('/detail', getProjectDetails) +router.post('/accept', acceptProject) +router.post('/reject', rejectProject) +router.get('/types', getProjectTypes) +router.post('/types/create', createProjectTypes) +module.exports = router diff --git a/controllers/application/academy/routes/panel/settings/index.js b/controllers/application/academy/routes/panel/settings/index.js new file mode 100644 index 0000000..aebc745 --- /dev/null +++ b/controllers/application/academy/routes/panel/settings/index.js @@ -0,0 +1,7 @@ +const express = require('express') +const { getSetting, updateSetting } = require('../../../controllers/panel/settings/settingsController') +const router = express.Router() +router.get('/:key', getSetting) +router.put('/:key', updateSetting) + +module.exports = router diff --git a/controllers/application/academy/routes/panel/shops/index.js b/controllers/application/academy/routes/panel/shops/index.js new file mode 100644 index 0000000..bbeb7d6 --- /dev/null +++ b/controllers/application/academy/routes/panel/shops/index.js @@ -0,0 +1,8 @@ +const express = require('express') +const { getShops, getShopDetail, updateShop } = require('../../../controllers/panel/shops/shopsController') +const router = express.Router() +router.get('/', getShops) +router.get('/detail', getShopDetail) +router.put('/update', updateShop) + +module.exports = router diff --git a/controllers/application/academy/routes/panel/tickets/index.js b/controllers/application/academy/routes/panel/tickets/index.js new file mode 100644 index 0000000..08c12f0 --- /dev/null +++ b/controllers/application/academy/routes/panel/tickets/index.js @@ -0,0 +1,9 @@ +const express = require('express') +const { addAdminMessageToTicket, getAllTickets, getTicketMessages, closeTicket } = require('../../../controllers/panel/tickets/ticketsController') +const router = express.Router() +router.get('/', getAllTickets) +router.get('/messages', getTicketMessages) +router.post('/message', addAdminMessageToTicket) +router.post('/close', closeTicket) + +module.exports = router diff --git a/controllers/application/academy/routes/panel/users/index.js b/controllers/application/academy/routes/panel/users/index.js new file mode 100644 index 0000000..d353f39 --- /dev/null +++ b/controllers/application/academy/routes/panel/users/index.js @@ -0,0 +1,12 @@ +const express = require('express') +const { getUsers, getUserDetail,changePass, updateUser, verifyUser, rejectUser, updateUserStatus } = require('../../../controllers/panel/users/usersController') +const router = express.Router() +router.get('/', getUsers) +router.post('/change-password', changePass) +router.get('/detail', getUserDetail) +router.put('/update', updateUser) +router.post('/verify', verifyUser) +router.post('/reject', rejectUser) +router.post('/update-status', updateUserStatus); + +module.exports = router diff --git a/controllers/application/academy/routes/panel/version/index.js b/controllers/application/academy/routes/panel/version/index.js new file mode 100644 index 0000000..c726f6a --- /dev/null +++ b/controllers/application/academy/routes/panel/version/index.js @@ -0,0 +1,7 @@ +const express = require('express') +const { getVersion, upsertVersion } = require('../../../controllers/panel/version/versionController') +const router = express.Router() +router.get('/', getVersion) +router.post('/', upsertVersion) + +module.exports = router diff --git a/controllers/application/advertising/advertisingController.js b/controllers/application/advertising/advertisingController.js index 772da30..5f7a693 100644 --- a/controllers/application/advertising/advertisingController.js +++ b/controllers/application/advertising/advertisingController.js @@ -1,1442 +1,1442 @@ -/* eslint-disable eqeqeq */ -/* eslint-disable camelcase */ -const jwt = require('jsonwebtoken') -const { check, validationResult } = require('express-validator') -const { ProvinceModel, CityModel } = require('../../../models/StateCity') -const UserModel = require('../../../models/UserModel') -const AdvertisingModel = require('../../../models/AdvertisingModel') -const fs = require('fs-extra') -const path = require('path') -const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel') -const AdvertisingCategoryModel = require('../../../models/AdvertisingCategoryModel') -const AdvertisingFeaturesModel = require('../../../models/AdvertisingFeaturesModel') -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 [ - check('title').notEmpty().withMessage('عنوان نمی‌تواند خالی باشد'), - check('category').notEmpty().withMessage('دسته‌بندی نمی‌تواند خالی باشد'), - check('province').notEmpty().withMessage('استان نمی‌تواند خالی باشد'), - check('city').notEmpty().withMessage('شهر نمی‌تواند خالی باشد') - ] -} -// تابع کمکی برای ذخیره تصاویر -const saveFile = async (file, folder, userName) => { - const uploadDir = path.join(__dirname, '../../../../storage', folder) - if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }) - - const uniqueFileName = `${userName}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(file.name)}` - const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(file.path, filePath) - return `/${folder}/${uniqueFileName}` -} - -const createAdvertising = async (req, res, next) => { - try { - // ✅ بررسی Authorization header - const authHeader = req.header('Authorization') - if (!authHeader) return res.status(401).json({ error: true, message: 'Access Denied' }) - - const token = authHeader.split(' ')[1] - if (!token) return res.status(401).json({ error: true, message: 'Access Denied' }) - - let decodedToken - try { - decodedToken = jwt.verify(token, process.env.APP_SECRET) - } catch (err) { - return res.status(401).json({ error: true, message: 'توکن نامعتبر است' }) - } - - const user = await UserModel.findById(decodedToken.id) - if (!user) return res.status(422).json({ error: true, message: 'شما دسترسی به این بخش ندارید' }) - - // ✅ اعتبارسنجی فرم - const errors = validationResult(req) - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }) - - const { - category, - title, - description, - province, - city, - neighbourhood, - address, - lat, - lng, - services, - features, - contactInfo, - mostDiscountPercentage, - showDiscount, - type - } = req.body - - if (!title || !category || !province || !city) { - return res.status(422).json({ error: true, message: 'اطلاعات ارسالی اشتباه است' }) - } - - const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) - const cityFind = await CityModel.findOne({ id: city }) - - // ✅ مدیریت خدمات و تصاویرشان - let parsedServices = [] - try { - parsedServices = services ? JSON.parse(services) : [] - } catch (err) { - return res.status(422).json({ error: true, message: 'فرمت خدمات اشتباه است' }) - } - - const servicesWithImages = [] - for (const service of parsedServices) { - const serviceImages = [] - const serviceId = service.id - const serviceFiles = req.files?.serviceImages?.[serviceId] - const imageFiles = Array.isArray(serviceFiles) ? serviceFiles : serviceFiles ? [serviceFiles] : [] - - for (const file of imageFiles) { - const imageUrl = await saveFile(file, 'services', user.user_name) - serviceImages.push(imageUrl) - service.image = imageUrl - } - servicesWithImages.push({ ...service, images: serviceImages }) - } - - // ✅ مدیریت تصاویر تبلیغ - const imageFiles = Array.isArray(req.files?.images) ? req.files.images : req.files?.images ? [req.files.images] : [] - const images = [] - for (const file of imageFiles) { - const imageUrl = await saveFile(file, 'advertising', user.user_name) - images.push(imageUrl) - } - - // ✅ مدیریت ویژگی‌ها - let parsedFeatures = [] - try { - parsedFeatures = features ? JSON.parse(features) : [] - } catch (err) { - return res.status(422).json({ error: true, message: 'فرمت ویژگی‌ها اشتباه است' }) - } - - // ✅ ایجاد رکورد جدید - const newAd = new AdvertisingModel({ - title, - category, - description, - province: provinceFind, - city: cityFind, - neighbourhood, - address, - lat, - lng, - images, - services: servicesWithImages, - features: parsedFeatures, - contactInfo, - creator_id: user._id, - mostDiscountPercentage, - showDiscount, - type - }) - - await newAd.save() - - // ✅ ذخیره اطلاعات تماس برای استفاده بعدی - if (contactInfo?.saveInfoForNextAds) { - let advertisingProfile = await AdvertisingProfileModel.findOne({ user: user._id }) - if (advertisingProfile) { - advertisingProfile.contactInfo = contactInfo - await advertisingProfile.save() - } else { - advertisingProfile = new AdvertisingProfileModel({ user: user._id, contactInfo }) - await advertisingProfile.save() - } - } - - return res.status(201).json({ - success: true, - message: 'ویترین با موفقیت ایجاد شد', - data: { id: newAd._id } - }) - } catch (error) { - next(error) - } -} -// const getAdvertisings = async (req, res, next) => { -// let previousPage = 0 // تعریف متغیر سراسری برای نگهداری شماره صفحه قبلی - -// 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 { page = 1, limit = 10, province, city, category, search } = req.query -// const currentPage = parseInt(page) // شماره صفحه جاری - -// const filter = { -// payment_status: 'done', -// status: 'accepted' -// } -// if (province) { -// const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) -// filter.province = provinceFind -// } -// if (city) { -// const cityFind = await CityModel.findOne({ id: city }) -// filter.city = cityFind -// } -// if (category) { -// filter.category = category -// } -// if (search) { -// filter.$or = [ -// { title: { $regex: search, $options: 'i' } }, -// { category: { $regex: search, $options: 'i' } } -// ] -// } -// const options = { -// page: parseInt(page), -// limit: parseInt(limit), -// sort: { acceptedAt: -1, createdAt: -1 } // مرتب‌سازی بر اساس acceptedAt و سپس createdAt -// } -// const advertisings = await AdvertisingModel.paginate(filter, options) - -// // اگر شماره صفحه تغییر کرده بود، تعداد بازدیدها را به روز رسانی کنید -// if (currentPage !== previousPage) { -// await AdvertisingModel.updateMany( -// { _id: { $in: advertisings.docs.map(ad => ad._id) } }, -// { $inc: { viewCount: 1 } } -// ) -// previousPage = currentPage // به‌روزرسانی شماره صفحه قبلی -// } -// // دریافت لیست لایک هر ویترین و اضافه کردن آن به اطلاعات تبلیغات -// const advertisingIds = advertisings.docs.map(ad => ad._id) -// const advertisingLikes = await AdvertisingLikeModel.find({ advertisingId: { $in: advertisingIds } }) - -// const creatorIds = advertisings.docs.map(ad => ad.creator_id) -// const creators = await AdvertisingProfileModel.find({ user: { $in: creatorIds } }) -// const newAds = advertisings.docs.map(ad => { -// const creator = creators.find(user => user.user.toString() === ad.creator_id.toString()) -// const likedByUser = advertisingLikes.some(like => like.advertisingId.toString() === ad._id.toString() && like.userId.toString() === decodedToken.id) -// return { -// // فیلدهای مشخص‌شده از ad._doc -// _id: ad._doc._id, -// category: ad._doc.category, -// title: ad._doc.title, -// province: ad._doc.province, -// city: ad._doc.city, -// neighbourhood: ad._doc.neighbourhood, -// type: ad._doc.type, -// mostDiscountPercentage: ad._doc.mostDiscountPercentage, -// images: ad._doc.images, -// likedByUser, -// likesCount: ad._doc.likesCount, -// commentsCount: ad._doc.commentsCount, -// showDiscount: ad._doc.showDiscount, -// viewCount: ad._doc.viewCount, -// rate: { -// adTotalRatings: creator?.adTotalRatings, -// adAverageRating: creator?.adAverageRating -// } -// } -// }) -// res.status(200).json({ -// advertisings: newAds, -// totalPages: advertisings.totalPages, -// totalItems: advertisings.totalDocs -// }) -// } catch (error) { -// next(error) -// } -// } -const getAdvertisings = async (req, res, next) => { - let previousPage = 0 // تعریف متغیر سراسری برای نگهداری شماره صفحه قبلی - - 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 { page = 1, limit = 10, province, city, category, search, sort } = req.query - const currentPage = parseInt(page) // شماره صفحه جاری - - const filter = { - payment_status: 'done', - status: 'accepted' - } - if (province) { - const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) - filter.province = provinceFind - } - if (city) { - const cityFind = await CityModel.findOne({ id: city }) - filter.city = cityFind - } - if (category) { - filter.category = category - } - if (search) { - filter.$or = [ - { title: { $regex: search, $options: 'i' } }, - { category: { $regex: search, $options: 'i' } } - ] - } - - // مرتب‌سازی بر اساس پارامتر sort - let sortOptions = { acceptedAt: -1, createdAt: -1 } // مرتب‌سازی پیش‌فرض - if (sort) { - const sortFields = sort.split(',') - sortFields.forEach(field => { - if (field === 'mostDiscount') { - sortOptions = { mostDiscountPercentage: -1 } - } else if (field === 'highestRating') { - sortOptions = { 'rate.adAverageRating': -1 } - } - }) - } - - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: sortOptions // استفاده از مرتب‌سازی تنظیم‌شده - } - const advertisings = await AdvertisingModel.paginate(filter, options) - - // اگر شماره صفحه تغییر کرده بود، تعداد بازدیدها را به روز رسانی کنید - if (currentPage !== previousPage) { - await AdvertisingModel.updateMany( - { _id: { $in: advertisings.docs.map(ad => ad._id) } }, - { $inc: { viewCount: 1 } } - ) - previousPage = currentPage // به‌روزرسانی شماره صفحه قبلی - } - // دریافت لیست لایک هر ویترین و اضافه کردن آن به اطلاعات تبلیغات - const advertisingIds = advertisings.docs.map(ad => ad._id) - const advertisingLikes = await AdvertisingLikeModel.find({ advertisingId: { $in: advertisingIds } }) - - const creatorIds = advertisings.docs.map(ad => ad.creator_id) - const creators = await AdvertisingProfileModel.find({ user: { $in: creatorIds } }) - const newAds = advertisings.docs.map(ad => { - const creator = creators.find(user => user.user.toString() === ad.creator_id.toString()) - const likedByUser = advertisingLikes.some(like => like.advertisingId.toString() === ad._id.toString() && like.userId.toString() === decodedToken.id) - return { - // فیلدهای مشخص‌شده از ad._doc - _id: ad._doc._id, - category: ad._doc.category, - title: ad._doc.title, - province: ad._doc.province, - city: ad._doc.city, - neighbourhood: ad._doc.neighbourhood, - type: ad._doc.type, - mostDiscountPercentage: ad._doc.mostDiscountPercentage, - images: ad._doc.images, - likedByUser, - likesCount: ad._doc.likesCount, - commentsCount: ad._doc.commentsCount, - showDiscount: ad._doc.showDiscount, - viewCount: ad._doc.viewCount, - rate: { - adTotalRatings: creator?.adTotalRatings, - adAverageRating: creator?.adAverageRating - } - } - }) - res.status(200).json({ - advertisings: newAds, - totalPages: advertisings.totalPages, - totalItems: advertisings.totalDocs - }) - } catch (error) { - next(error) - } -} - -const getAdvertisingsWeb = async (req, res, next) => { - let previousPage = 0 - let userId = null - - try { - const token = req.header('Authorization') - if (token) { - try { - const decodedToken = jwt.verify(token.split(' ')[1], process.env.APP_SECRET) - userId = decodedToken.id - } catch (err) { - console.warn('Invalid token:', err.message) - } - } - - const { page = 1, limit = 10, province, city, category, search, sort } = req.query - const currentPage = parseInt(page) - - const filter = { - // payment_status: 'done', - status: 'accepted' - } - if (province) { - const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) - filter.province = provinceFind - } - if (city) { - const cityFind = await CityModel.findOne({ id: city }) - filter.city = cityFind - } - if (category) { - filter.category = category - } - if (search) { - filter.$or = [ - { title: { $regex: search, $options: 'i' } }, - { category: { $regex: search, $options: 'i' } } - ] - } - - let sortOptions = { acceptedAt: -1, createdAt: -1 } - if (sort) { - const sortFields = sort.split(',') - sortFields.forEach(field => { - if (field === 'mostDiscount') { - sortOptions = { mostDiscountPercentage: -1 } - } else if (field === 'highestRating') { - sortOptions = { 'rate.adAverageRating': -1 } - } - }) - } - - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: sortOptions - } - const advertisings = await AdvertisingModel.paginate(filter, options) - - if (currentPage !== previousPage) { - await AdvertisingModel.updateMany( - { _id: { $in: advertisings.docs.map(ad => ad._id) } }, - { $inc: { viewCount: 1 } } - ) - previousPage = currentPage - } - - const advertisingIds = advertisings.docs.map(ad => ad._id) - const creatorIds = advertisings.docs.map(ad => ad.creator_id) - const creators = await AdvertisingProfileModel.find({ user: { $in: creatorIds } }) - - let advertisingLikes = [] - if (userId) { - advertisingLikes = await AdvertisingLikeModel.find({ advertisingId: { $in: advertisingIds }, userId }) - } - - const newAds = advertisings.docs.map(ad => { - const creator = creators.find(user => user.user.toString() === ad.creator_id.toString()) - const likedByUser = userId ? advertisingLikes.some(like => like.advertisingId.toString() === ad._id.toString()) : false - - return { - _id: ad._doc._id, - category: ad._doc.category, - title: ad._doc.title, - province: ad._doc.province, - city: ad._doc.city, - neighbourhood: ad._doc.neighbourhood, - type: ad._doc.type, - mostDiscountPercentage: ad._doc.mostDiscountPercentage, - images: ad._doc.images, - likedByUser, - likesCount: ad._doc.likesCount, - commentsCount: ad._doc.commentsCount, - showDiscount: ad._doc.showDiscount, - viewCount: ad._doc.viewCount, - rate: { - adTotalRatings: creator?.adTotalRatings, - adAverageRating: creator?.adAverageRating - } - } - }) - - res.status(200).json({ - advertisings: newAds, - totalPages: advertisings.totalPages, - totalItems: advertisings.totalDocs - }) - } catch (error) { - next(error) - } -} - -const getSingleAdvertising = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const advertisingId = req.params.advertisingId - const advertising = await AdvertisingModel.findById(advertisingId) - .populate('creator_id', '_id user_name first_name last_name') - - if (!advertising) { - return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) - } - - // دریافت تعداد لایک‌ها و بررسی اینکه آیا کاربر ویترین را لایک کرده است یا خیر - const likesCount = await AdvertisingLikeModel.countDocuments({ advertisingId }) - const likedByUser = await AdvertisingLikeModel.findOne({ advertisingId, userId }) - - // دریافت تعداد کامنت‌ها - const commentsCount = await AdvertisingComment.countDocuments({ advertisingId, status: 'accepted' }) - const creator = await AdvertisingProfileModel.findOne({ user: advertising.creator_id._id }) - - // اضافه کردن تعداد لایک‌ها، تعداد کامنت‌ها و وضعیت لایک به تبلیغ - const advertisingData = { - ...advertising._doc, - likesCount, - likedByUser: !!likedByUser, - commentsCount, - creatorId: creator?._id - } - - res.status(200).json({ - advertising: advertisingData, - rate: { - adTotalRatings: creator?.adTotalRatings, - adAverageRating: creator?.adAverageRating - } - }) - } catch (error) { - next(error) - } -} -const getSingleAdvertisingWeb = async (req, res, next) => { - try { - let userId = null - let likedByUser = false - - // بررسی وجود توکن - const authHeader = req.header('Authorization') - if (authHeader) { - const tokenParts = authHeader.split(' ') - if (tokenParts.length === 2) { - try { - const decodedToken = jwt.verify(tokenParts[1], process.env.APP_SECRET) - userId = decodedToken.id - } catch (err) { - console.error('Invalid Token:', err.message) - } - } - } - - const advertisingId = req.params.advertisingId - const advertising = await AdvertisingModel.findById(advertisingId) - .populate('creator_id', '_id user_name first_name last_name') - - if (!advertising) { - return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) - } - - // دریافت تعداد لایک‌ها - const likesCount = await AdvertisingLikeModel.countDocuments({ advertisingId }) - - // دریافت تعداد کامنت‌ها - const commentsCount = await AdvertisingComment.countDocuments({ advertisingId, status: 'accepted' }) - - // دریافت اطلاعات سازنده تبلیغ - const creator = await AdvertisingProfileModel.findOne({ user: advertising.creator_id._id }) - - // اگر کاربر احراز هویت شده باشد، وضعیت لایک‌شدن توسط او بررسی می‌شود - if (userId) { - likedByUser = !!(await AdvertisingLikeModel.findOne({ advertisingId, userId })) - } - - // آماده‌سازی داده‌های تبلیغ - const advertisingData = { - ...advertising._doc, - likesCount, - likedByUser, - commentsCount, - creatorId: creator?._id - } - - res.status(200).json({ - advertising: advertisingData, - rate: { - adTotalRatings: creator?.adTotalRatings, - adAverageRating: creator?.adAverageRating - } - }) - } catch (error) { - next(error) - } -} - -const getAdvertisingTypes = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - // const decodedToken = jwt.verify(token, process.env.APP_SECRET) - // const userId = decodedToken.id - - // const user = await UserModel.findById(userId) - // if (!user) { - // return res.status(422).json({ - // error: true, - // message: 'کاربر یافت نشد' - // }) - // } - // درخواست انواع تبلیغات از دیتابیس - // let projectTypes = await AdvertisingTypeModel.find({}, 'name price') - const advertisingTypes = await AdvertisingTypeModel.find({}, 'name price') - - // بررسی برای مواردی مانند پیدا نشدن انواع تبلیغات - if (!advertisingTypes) { - return res.status(404).json({ message: 'انواع تبلیغات یافت نشد.' }) - } - // اگر کاربر درخواست رایگان روزانه نداشته باشد، نوع تبلیغات رایگان را حذف کنید - // if (user.daily_free_request <= 0) { - // advertisingTypes = advertisingTypes.filter(projectType => projectType.name !== 'free') - // } - // ارسال انواع تبلیغات به کاربر - res.status(200).json({ advertisingTypes }) - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error('Error in getAdvertisingTypes:', error) - res.status(500).json({ message: 'خطا در دریافت انواع تبلیغات.' }) - } -} -const getAdvertisingCategory = async (req, res, next) => { - try { - // const token = req.header('Authorization').split(' ')[1] - // if (!token) return res.status(401).send('Access Denied') - // const decodedToken = jwt.verify(token, process.env.APP_SECRET) - // const userId = decodedToken.id - - // const user = await UserModel.findById(userId) - // if (!user) { - // return res.status(422).json({ - // error: true, - // message: 'کاربر یافت نشد' - // }) - // } - // let projectTypes = await AdvertisingTypeModel.find({}, 'name price') - const categories = await AdvertisingCategoryModel.find({}) - - // بررسی برای مواردی مانند پیدا نشدن انواع پروژه - if (!categories) { - return res.status(404).json({ message: 'دسته بندی یافت نشد.' }) - } - // if (user.daily_free_request <= 0) { - // categories = categories.filter(projectType => projectType.name !== 'free') - // } - // ارسال انواع پروژه به کاربر - res.status(200).json({ categories }) - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error('Error in getAdvertisingTypes:', error) - res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) - } -} - -const getAdvertisingFeatures = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - // const decodedToken = jwt.verify(token, process.env.APP_SECRET) - // const userId = decodedToken.id - - // const user = await UserModel.findById(userId) - // if (!user) { - // return res.status(422).json({ - // error: true, - // message: 'کاربر یافت نشد' - // }) - // } - // let projectTypes = await AdvertisingTypeModel.find({}, 'name price') - const features = await AdvertisingFeaturesModel.find({}) - - // بررسی برای مواردی مانند پیدا نشدن امکانات - if (!features) { - return res.status(404).json({ message: 'امکانات یافت نشد.' }) - } - // if (user.daily_free_request <= 0) { - // features = features.filter(projectType => projectType.name !== 'free') - // } - // ارسال امکانات به کاربر - res.status(200).json({ features }) - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error('Error in getAdvertisingTypes:', error) - res.status(500).json({ message: 'خطا در دریافت امکانات.' }) - } -} -const toggleLike = async (req, res, next) => { - try { - const { advertisingId } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - // بررسی وجود تبلیغات با استفاده از advertisingId - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) - } - // بررسی وضعیت لایک کردن توسط کاربر - const isLiked = await AdvertisingLikeModel.findOne({ advertisingId, userId }) - if (isLiked) { - // حذف لایک - await AdvertisingLikeModel.findOneAndDelete({ advertisingId, userId }) - // کاهش تعداد لایک‌ها در تبلیغات - advertising.likesCount -= 1 - await advertising.save() - return res.status(200).json({ message: 'لایک با موفقیت پاک شد' }) - } else { - // اضافه کردن لایک - await AdvertisingLikeModel.create({ advertisingId, userId }) - // افزایش تعداد لایک‌ها در تبلیغات - 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) { - next(error) - } -} -const addComment = async (req, res, next) => { - try { - const { advertisingId, text } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - // eslint-disable-next-line no-unused-vars - const user = await UserModel.findById(decodedToken.id) - // if (user.is_verified !== 'verified') { - // return res.status(422).json({ - // error: true, - // message: 'مدارک شما تایید نشده است' - // }) - // } - const newComment = new AdvertisingComment({ - advertisingId, - userId, - text - }) - - await newComment.save() - - 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) { - next(error) - } -} -const getComments = async (req, res, next) => { - try { - const { advertisingId, page = 1, limit = 10 } = req.query - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: { createdAt: -1 }, - populate: [{ path: 'userId', select: '_id profile_image user_type user_name first_name last_name is_verified' }] - } - - const comments = await AdvertisingComment.paginate({ advertisingId, status: 'accepted' }, options) - res.status(200).json({ - comments: comments.docs, - totalPages: comments.totalPages, - totalItems: comments.totalDocs - }) - } catch (error) { - next(error) - } -} -const rateAdvertising = async (req, res, next) => { - try { - const { advertisingId, rating } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - if (rating < 1 || rating > 5) { - return res.status(400).json({ message: 'امتیاز باید بین 1 تا 5 باشد' }) - } - - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) - } - - const existingRating = await AdvertisingRatingModel.findOne({ advertisingId, userId }) - if (existingRating) { - return res.status(400).json({ message: 'شما قبلا برای این ویترین امتیاز ثبت کرده‌اید' }) - } else { - const newRating = new AdvertisingRatingModel({ - advertisingId, - userId, - rating - }) - await newRating.save() - - await createRatingNotification({ - ownerId: advertising.creator_id, - raterId: userId, - entityId: advertising._id, - type: 'billboard_rating' - }) - } - - // به‌روزرسانی امتیازات کلی کاربر - const advertisingProfile = await AdvertisingProfileModel.findOne({ user: advertising.creator_id }) - if (advertisingProfile) { - const totalUserRatings = await AdvertisingRatingModel.aggregate([ - { - $lookup: { - from: 'advertisings', - localField: 'advertisingId', - foreignField: '_id', - as: 'advertising' - } - }, - { $unwind: '$advertising' }, - { $match: { 'advertising.creator_id': advertising.creator_id } }, - { $group: { _id: null, total: { $sum: '$rating' }, count: { $sum: 1 } } } - ]) - - if (totalUserRatings.length > 0) { - const { total, count } = totalUserRatings[0] - advertisingProfile.adTotalRatings = total - advertisingProfile.adRatingsCount = count - advertisingProfile.adAverageRating = (total / count).toFixed(1) - } else { - advertisingProfile.adTotalRatings = 0 - advertisingProfile.adRatingsCount = 0 - advertisingProfile.adAverageRating = 0 - } - - await advertisingProfile.save() - } else { - // ایجاد پروفایل تبلیغاتی جدید در صورت عدم وجود - const newAdvertisingProfile = new AdvertisingProfileModel({ - user: advertising.creator_id, - adTotalRatings: rating, - adRatingsCount: 1, - adAverageRating: rating.toFixed(1) - }) - await newAdvertisingProfile.save() - } - - res.status(200).json({ message: 'امتیاز با موفقیت ثبت شد' }) - } catch (error) { - next(error) - } -} -const getUserAdvertisings = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const { page = 1, limit = 10, status, search } = req.query - - const filter = { - creator_id: userId - } - - if (status) { - filter.status = status - } - if (search) { - filter.$or = [ - { title: { $regex: search, $options: 'i' } }, - { category: { $regex: search, $options: 'i' } } - ] - } - - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: { updatedAt: -1 } - } - - const advertisings = await AdvertisingModel.paginate(filter, options) - - // دریافت لیست لایک هر ویترین و اضافه کردن آن به اطلاعات تبلیغات - const advertisingIds = advertisings.docs.map(ad => ad._id) - const advertisingLikes = await AdvertisingLikeModel.find({ advertisingId: { $in: advertisingIds } }) - const creator = await AdvertisingProfileModel.findOne({ user: userId }) - - const newAds = advertisings.docs.map(ad => { - const likedByUser = advertisingLikes.some(like => like.advertisingId.toString() === ad._id.toString() && like.userId.toString() === decodedToken.id) - let remainingTime = null - if (ad.status === 'accepted' && ad.acceptedAt) { - const expirationDate = new Date(ad.acceptedAt) - expirationDate.setDate(expirationDate.getDate() + 15) - const currentDate = new Date() - const diffMilliseconds = expirationDate - currentDate - const diffDays = Math.floor(diffMilliseconds / (1000 * 60 * 60 * 24)) - const diffHours = Math.floor((diffMilliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) - remainingTime = `${diffDays} روز و ${diffHours} ساعت` - } - return { - _id: ad._doc._id, - category: ad._doc.category, - title: ad._doc.title, - province: ad._doc.province, - city: ad._doc.city, - neighbourhood: ad._doc.neighbourhood, - type: ad._doc.type, - mostDiscountPercentage: ad._doc.mostDiscountPercentage, - images: ad._doc.images, - likedByUser, - likesCount: ad._doc.likesCount, - commentsCount: ad._doc.commentsCount, - status: ad._doc.status, - viewCount: ad._doc.viewCount, - remainingTime, - rate: { - adTotalRatings: creator?.adTotalRatings, - adAverageRating: creator?.adAverageRating - } - } - }) - - res.status(200).json({ - advertisings: newAds, - totalPages: advertisings.totalPages, - totalItems: advertisings.totalDocs - }) - } catch (error) { - next(error) - } -} -const getUserSingleAdvertising = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const user = await UserModel.findById(userId) - if (!user) { - return res.status(422).json({ - error: true, - message: 'شما دسترسی به این بخش ندارید' - }) - } - const advertisingId = req.params.advertisingId - const advertising = await AdvertisingModel.findById(advertisingId) - .populate('creator_id', '_id user_name first_name last_name') - - if (!advertising) { - return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) - } - - // دریافت تعداد لایک‌ها و بررسی اینکه آیا کاربر ویترین را لایک کرده است یا خیر - const likesCount = await AdvertisingLikeModel.countDocuments({ advertisingId }) - const likedByUser = await AdvertisingLikeModel.findOne({ advertisingId, userId }) - - // دریافت تعداد کامنت‌ها - const commentsCount = await AdvertisingComment.countDocuments({ advertisingId, status: 'accepted' }) - const creator = await AdvertisingProfileModel.findOne({ user: advertising.creator_id._id }) - - // اضافه کردن تعداد لایک‌ها، تعداد کامنت‌ها و وضعیت لایک به تبلیغ - const advertisingData = { - ...advertising._doc, - likesCount, - likedByUser: !!likedByUser, - commentsCount, - creatorId: creator?._id - - } - - res.status(200).json({ - advertising: advertisingData, - rate: { - adTotalRatings: creator?.adTotalRatings, - adAverageRating: creator?.adAverageRating - } - }) - } catch (error) { - next(error) - } -} -const editAdvertising = 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(422).json({ - error: true, - message: 'شما دسترسی به این بخش ندارید' - }) - } - // if (user.is_verified !== 'verified') { - // return res.status(422).json({ - // error: true, - // message: 'مدارک شما تایید نشده است' - // }) - // } - - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - - const { id } = req.params - const { - category, - title, - description, - province, - city, - neighbourhood, - address, - lat, - lng, - services, - features, - contactInfo, - mostDiscountPercentage, - showDiscount, - type - } = req.body - - if (!title || !category || !province || !city) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const advertising = await AdvertisingModel.findOne({ - _id: id, - creator_id: user._id, - status: { $in: ['pre_payment', 'paid', 'rejected', 'expired'] } - }) - - if (!advertising) { - return res.status(404).json({ - error: true, - message: 'تبلیغ مورد نظر یافت نشد یا شما اجازه ویرایش آن را ندارید' - }) - } - - const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) - const cityFind = await CityModel.findOne({ id: city }) - - // مدیریت تصاویر برای خدمات - const servicesWithImages = [] - const parsedServices = JSON.parse(services) - if (parsedServices && parsedServices.length > 0) { - for (const service of parsedServices) { - if (typeof service.image === 'string' && service.image.startsWith('/services/')) { - servicesWithImages.push(service) - } else { - const serviceImages = [] - const serviceId = service.id - if (req.files && req.files.serviceImages && req.files.serviceImages[serviceId]) { - const imageFiles = Array.isArray(req.files.serviceImages[serviceId]) ? req.files.serviceImages[serviceId] : [req.files.serviceImages[serviceId]] - const uploadDir = path.join(__dirname, '../../../../storage/services') - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - for (const imageFile of imageFiles) { - const uniqueFileName = `${user.user_name}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(imageFile.name)}` - const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(imageFile.path, filePath) - const imageUrl = `/services/${uniqueFileName}` - serviceImages.push(imageUrl) - service.image = imageUrl - } - } - servicesWithImages.push({ ...service, image: serviceImages[0] }) - } - } - } - // مدیریت تصاویر - const images = [] - - // اضافه کردن تصاویر موجود - if (Array.isArray(req.body.existingImages)) { - req.body.existingImages.forEach(image => { - if (typeof image === 'string' && image.startsWith('/advertising/')) { - images.push(image) - } - }) - } - - // اضافه کردن تصاویر جدید - if (req.files && req.files.images) { - const imageFiles = Array.isArray(req.files.images) ? req.files.images : [req.files.images] - const uploadDir = path.join(__dirname, '../../../../storage/advertising') - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - for (const imageFile of imageFiles) { - // بررسی فرمت تصویر - if (imageFile && typeof imageFile === 'object' && imageFile.path) { - const uniqueFileName = `${user.user_name}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(imageFile.name)}` - const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(imageFile.path, filePath) - const imageUrl = `/advertising/${uniqueFileName}` - images.push(imageUrl) - } - } - } - const parsedFeatures = JSON.parse(features) - - advertising.title = title - advertising.category = category - advertising.description = description - advertising.province = provinceFind - advertising.city = cityFind - advertising.neighbourhood = neighbourhood - advertising.address = address - advertising.lat = lat - advertising.lng = lng - advertising.images = images - advertising.services = servicesWithImages - advertising.features = parsedFeatures - advertising.contactInfo = contactInfo - advertising.mostDiscountPercentage = mostDiscountPercentage - advertising.showDiscount = showDiscount - advertising.type = type - - await advertising.save() - - if (contactInfo && contactInfo.saveInfoForNextAds) { - user.contactInfo = contactInfo - await user.save() - } - res.status(200).json({ - message: 'ویترین با موفقیت ویرایش شد', - id: advertising._id - }) - } catch (error) { - next(error) - } -} -const getUserAdvertisingProfile = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const vitrineId = req.query.vitrineId - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const user = await UserModel.findById(decodedToken.id) - if (!user) { - return res.status(422).json({ - error: true, - message: 'شما دسترسی به این بخش ندارید' - }) - } - // if (user.is_verified !== 'verified') { - // return res.status(422).json({ - // error: true, - // message: 'مدارک شما تایید نشده است' - // }) - // } - - // دریافت اطلاعات پروفایل تبلیغاتی کاربر - const advertisingProfile = await AdvertisingProfileModel.findById(vitrineId) - if (!advertisingProfile) { - return res.status(404).json({ - error: true, - message: 'پروفایل تبلیغاتی یافت نشد' - }) - } - - // دریافت تبلیغات‌های کاربر - const userAds = await AdvertisingModel.find({ creator_id: advertisingProfile?.user }) - - // جمع‌آوری اطلاعات از تبلیغات‌ها - let allImages = [] - let allServices = [] - let allFeatures = [] - let totalCommentsCount = 0 - let totalLikesCount = 0 - - for (const ad of userAds) { - allImages = allImages.concat(ad.images) - allServices = allServices.concat(ad.services) - allFeatures = allFeatures.concat(ad.features) - totalCommentsCount += ad.commentsCount - totalLikesCount += ad.likesCount - } - - // حذف آیتم‌های null و تکراری از allFeatures - const uniqueFeatures = [] - const featureTitles = new Set() - for (const feature of allFeatures) { - if (feature && !featureTitles.has(feature.title)) { - uniqueFeatures.push(feature) - featureTitles.add(feature.title) - } - } - - // آماده‌سازی اطلاعات نهایی - const profileDetails = { - _id: advertisingProfile._id, - profile_image: advertisingProfile.profile_image, - vitrine_name: advertisingProfile.vitrine_name, - category: advertisingProfile.category, - about_us: advertisingProfile.about_us, - province: advertisingProfile.province, - city: advertisingProfile.city, - lat: advertisingProfile.lat, - lng: advertisingProfile.lng, - neighbourhood: advertisingProfile.neighbourhood, - adTotalRatings: advertisingProfile.adTotalRatings, - adRatingsCount: advertisingProfile.adRatingsCount, - adAverageRating: advertisingProfile.adAverageRating, - contactInfo: advertisingProfile.contactInfo, - address: advertisingProfile.address, - allImages: allImages?.reverse(), - allServices, - allFeatures: uniqueFeatures, - totalCommentsCount, - totalLikesCount, - is_self: !!(advertisingProfile && advertisingProfile?.user == decodedToken.id) - } - - // ارسال پاسخ به کاربر - res.status(200).json({ profileDetails }) - } catch (error) { - next(error) - } -} -const getUserAdvertisingProfileWeb = async (req, res, next) => { - try { - let userId = null - let isSelf = false - - // بررسی توکن (در صورت وجود) - const authHeader = req.header('Authorization') - if (authHeader) { - const tokenParts = authHeader.split(' ') - if (tokenParts.length === 2) { - try { - const decodedToken = jwt.verify(tokenParts[1], process.env.APP_SECRET) - userId = decodedToken.id - } catch (err) { - console.error('Invalid Token:', err.message) - } - } - } - - const vitrineId = req.query.vitrineId - console.log(vitrineId) - - // دریافت اطلاعات پروفایل تبلیغاتی کاربر - const advertisingProfile = await AdvertisingProfileModel.findById(vitrineId) - if (!advertisingProfile) { - return res.status(404).json({ - error: true, - message: 'پروفایل تبلیغاتی یافت نشد' - }) - } - - // دریافت تبلیغات‌های کاربر - const userAds = await AdvertisingModel.find({ creator_id: advertisingProfile?.user }) - - // جمع‌آوری اطلاعات از تبلیغات‌ها - let allImages = [] - let allServices = [] - let allFeatures = [] - let totalCommentsCount = 0 - let totalLikesCount = 0 - - for (const ad of userAds) { - allImages = allImages.concat(ad.images) - allServices = allServices.concat(ad.services) - allFeatures = allFeatures.concat(ad.features) - totalCommentsCount += ad.commentsCount - totalLikesCount += ad.likesCount - } - - // حذف آیتم‌های null و تکراری از allFeatures - const uniqueFeatures = [] - const featureTitles = new Set() - for (const feature of allFeatures) { - if (feature && !featureTitles.has(feature.title)) { - uniqueFeatures.push(feature) - featureTitles.add(feature.title) - } - } - - // بررسی اینکه کاربر خودش صاحب این ویترین است یا نه - if (userId) { - isSelf = advertisingProfile?.user == userId - } - - // آماده‌سازی اطلاعات نهایی - const profileDetails = { - _id: advertisingProfile._id, - profile_image: advertisingProfile.profile_image, - vitrine_name: advertisingProfile.vitrine_name, - category: advertisingProfile.category, - about_us: advertisingProfile.about_us, - province: advertisingProfile.province, - city: advertisingProfile.city, - lat: advertisingProfile.lat, - lng: advertisingProfile.lng, - neighbourhood: advertisingProfile.neighbourhood, - adTotalRatings: advertisingProfile.adTotalRatings, - adRatingsCount: advertisingProfile.adRatingsCount, - adAverageRating: advertisingProfile.adAverageRating, - contactInfo: advertisingProfile.contactInfo, - address: advertisingProfile.address, - allImages: allImages?.reverse(), - allServices, - allFeatures: uniqueFeatures, - totalCommentsCount, - totalLikesCount, - is_self: isSelf - } - - // ارسال پاسخ به کاربر - res.status(200).json({ profileDetails }) - } catch (error) { - next(error) - } -} - -const updateUserAdvertisingProfile = 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(422).json({ - error: true, - message: 'شما دسترسی به این بخش ندارید' - }) - } - - const advertisingProfile = await AdvertisingProfileModel.findOne({ user: decodedToken.id }) - if (!advertisingProfile) { - return res.status(404).json({ - error: true, - message: 'پروفایل تبلیغاتی یافت نشد' - }) - } - - if (advertisingProfile.user.toString() !== decodedToken.id) { - return res.status(403).json({ - error: true, - message: 'شما اجازه ویرایش این پروفایل را ندارید' - }) - } - - const { profile_image } = req.files - const { - // profile_image, - vitrine_name, - category, - about_us, - province, - city, - lat, - lng, - address, - neighbourhood, - contactInfo - } = req.body - - const updateFields = {} - const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) - const cityFind = await CityModel.findOne({ id: city }) - // if (profile_image !== undefined && profile_image !== null && !profile_image.startsWith('/profiles/')) { - if (profile_image !== undefined && profile_image !== null) { - const uploadDir = path.join(__dirname, '../../../../storage/profiles') - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - const uniqueFileName = `${advertisingProfile?.vitrine_name}-${Date.now()}${path.extname(profile_image.name)}` - const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(profile_image.path, filePath) - updateFields.profile_image = `/profiles/${uniqueFileName}` - } - if (vitrine_name !== undefined) updateFields.vitrine_name = vitrine_name - if (category !== undefined) updateFields.category = category - if (about_us !== undefined) updateFields.about_us = about_us - if (province !== undefined) updateFields.province = provinceFind - if (city !== undefined) updateFields.city = cityFind - if (lat !== undefined) updateFields.lat = lat - if (lng !== undefined) updateFields.lng = lng - if (address !== undefined) updateFields.address = address - if (neighbourhood !== undefined) updateFields.neighbourhood = neighbourhood - if (contactInfo !== undefined) updateFields.contactInfo = contactInfo - - const updatedProfile = await AdvertisingProfileModel.findByIdAndUpdate( - advertisingProfile._id, - { $set: updateFields }, - { new: true } - ) - - res.status(200).json({ - success: true, - message: 'پروفایل با موفقیت به‌روزرسانی شد', - updatedProfile - }) - } catch (error) { - next(error) - } -} - -module.exports = { - createAdvertisingValidationRules, - createAdvertising, - getAdvertisingTypes, - getAdvertisingCategory, - getSingleAdvertising, - getAdvertisingFeatures, - getAdvertisings, - toggleLike, - addComment, - getComments, - rateAdvertising, - getUserAdvertisings, - editAdvertising, - getUserSingleAdvertising, - getUserAdvertisingProfile, - updateUserAdvertisingProfile, - getAdvertisingsWeb, - getSingleAdvertisingWeb, - getUserAdvertisingProfileWeb -} +/* eslint-disable eqeqeq */ +/* eslint-disable camelcase */ +const jwt = require('jsonwebtoken') +const { check, validationResult } = require('express-validator') +const { ProvinceModel, CityModel } = require('../../../models/StateCity') +const UserModel = require('../../../models/UserModel') +const AdvertisingModel = require('../../../models/AdvertisingModel') +const fs = require('fs-extra') +const path = require('path') +const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel') +const AdvertisingCategoryModel = require('../../../models/AdvertisingCategoryModel') +const AdvertisingFeaturesModel = require('../../../models/AdvertisingFeaturesModel') +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 [ + check('title').notEmpty().withMessage('عنوان نمی‌تواند خالی باشد'), + check('category').notEmpty().withMessage('دسته‌بندی نمی‌تواند خالی باشد'), + check('province').notEmpty().withMessage('استان نمی‌تواند خالی باشد'), + check('city').notEmpty().withMessage('شهر نمی‌تواند خالی باشد') + ] +} +// تابع کمکی برای ذخیره تصاویر +const saveFile = async (file, folder, userName) => { + const uploadDir = path.join(__dirname, '../../../../storage', folder) + if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }) + + const uniqueFileName = `${userName}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(file.name)}` + const filePath = path.join(uploadDir, uniqueFileName) + await fs.move(file.path, filePath) + return `/${folder}/${uniqueFileName}` +} + +const createAdvertising = async (req, res, next) => { + try { + // ✅ بررسی Authorization header + const authHeader = req.header('Authorization') + if (!authHeader) return res.status(401).json({ error: true, message: 'Access Denied' }) + + const token = authHeader.split(' ')[1] + if (!token) return res.status(401).json({ error: true, message: 'Access Denied' }) + + let decodedToken + try { + decodedToken = jwt.verify(token, process.env.APP_SECRET) + } catch (err) { + return res.status(401).json({ error: true, message: 'توکن نامعتبر است' }) + } + + const user = await UserModel.findById(decodedToken.id) + if (!user) return res.status(422).json({ error: true, message: 'شما دسترسی به این بخش ندارید' }) + + // ✅ اعتبارسنجی فرم + const errors = validationResult(req) + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }) + + const { + category, + title, + description, + province, + city, + neighbourhood, + address, + lat, + lng, + services, + features, + contactInfo, + mostDiscountPercentage, + showDiscount, + type + } = req.body + + if (!title || !category || !province || !city) { + return res.status(422).json({ error: true, message: 'اطلاعات ارسالی اشتباه است' }) + } + + const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) + const cityFind = await CityModel.findOne({ id: city }) + + // ✅ مدیریت خدمات و تصاویرشان + let parsedServices = [] + try { + parsedServices = services ? JSON.parse(services) : [] + } catch (err) { + return res.status(422).json({ error: true, message: 'فرمت خدمات اشتباه است' }) + } + + const servicesWithImages = [] + for (const service of parsedServices) { + const serviceImages = [] + const serviceId = service.id + const serviceFiles = req.files?.serviceImages?.[serviceId] + const imageFiles = Array.isArray(serviceFiles) ? serviceFiles : serviceFiles ? [serviceFiles] : [] + + for (const file of imageFiles) { + const imageUrl = await saveFile(file, 'services', user.user_name) + serviceImages.push(imageUrl) + service.image = imageUrl + } + servicesWithImages.push({ ...service, images: serviceImages }) + } + + // ✅ مدیریت تصاویر تبلیغ + const imageFiles = Array.isArray(req.files?.images) ? req.files.images : req.files?.images ? [req.files.images] : [] + const images = [] + for (const file of imageFiles) { + const imageUrl = await saveFile(file, 'advertising', user.user_name) + images.push(imageUrl) + } + + // ✅ مدیریت ویژگی‌ها + let parsedFeatures = [] + try { + parsedFeatures = features ? JSON.parse(features) : [] + } catch (err) { + return res.status(422).json({ error: true, message: 'فرمت ویژگی‌ها اشتباه است' }) + } + + // ✅ ایجاد رکورد جدید + const newAd = new AdvertisingModel({ + title, + category, + description, + province: provinceFind, + city: cityFind, + neighbourhood, + address, + lat, + lng, + images, + services: servicesWithImages, + features: parsedFeatures, + contactInfo, + creator_id: user._id, + mostDiscountPercentage, + showDiscount, + type + }) + + await newAd.save() + + // ✅ ذخیره اطلاعات تماس برای استفاده بعدی + if (contactInfo?.saveInfoForNextAds) { + let advertisingProfile = await AdvertisingProfileModel.findOne({ user: user._id }) + if (advertisingProfile) { + advertisingProfile.contactInfo = contactInfo + await advertisingProfile.save() + } else { + advertisingProfile = new AdvertisingProfileModel({ user: user._id, contactInfo }) + await advertisingProfile.save() + } + } + + return res.status(201).json({ + success: true, + message: 'ویترین با موفقیت ایجاد شد', + data: { id: newAd._id } + }) + } catch (error) { + next(error) + } +} +// const getAdvertisings = async (req, res, next) => { +// let previousPage = 0 // تعریف متغیر سراسری برای نگهداری شماره صفحه قبلی + +// 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 { page = 1, limit = 10, province, city, category, search } = req.query +// const currentPage = parseInt(page) // شماره صفحه جاری + +// const filter = { +// payment_status: 'done', +// status: 'accepted' +// } +// if (province) { +// const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) +// filter.province = provinceFind +// } +// if (city) { +// const cityFind = await CityModel.findOne({ id: city }) +// filter.city = cityFind +// } +// if (category) { +// filter.category = category +// } +// if (search) { +// filter.$or = [ +// { title: { $regex: search, $options: 'i' } }, +// { category: { $regex: search, $options: 'i' } } +// ] +// } +// const options = { +// page: parseInt(page), +// limit: parseInt(limit), +// sort: { acceptedAt: -1, createdAt: -1 } // مرتب‌سازی بر اساس acceptedAt و سپس createdAt +// } +// const advertisings = await AdvertisingModel.paginate(filter, options) + +// // اگر شماره صفحه تغییر کرده بود، تعداد بازدیدها را به روز رسانی کنید +// if (currentPage !== previousPage) { +// await AdvertisingModel.updateMany( +// { _id: { $in: advertisings.docs.map(ad => ad._id) } }, +// { $inc: { viewCount: 1 } } +// ) +// previousPage = currentPage // به‌روزرسانی شماره صفحه قبلی +// } +// // دریافت لیست لایک هر ویترین و اضافه کردن آن به اطلاعات تبلیغات +// const advertisingIds = advertisings.docs.map(ad => ad._id) +// const advertisingLikes = await AdvertisingLikeModel.find({ advertisingId: { $in: advertisingIds } }) + +// const creatorIds = advertisings.docs.map(ad => ad.creator_id) +// const creators = await AdvertisingProfileModel.find({ user: { $in: creatorIds } }) +// const newAds = advertisings.docs.map(ad => { +// const creator = creators.find(user => user.user.toString() === ad.creator_id.toString()) +// const likedByUser = advertisingLikes.some(like => like.advertisingId.toString() === ad._id.toString() && like.userId.toString() === decodedToken.id) +// return { +// // فیلدهای مشخص‌شده از ad._doc +// _id: ad._doc._id, +// category: ad._doc.category, +// title: ad._doc.title, +// province: ad._doc.province, +// city: ad._doc.city, +// neighbourhood: ad._doc.neighbourhood, +// type: ad._doc.type, +// mostDiscountPercentage: ad._doc.mostDiscountPercentage, +// images: ad._doc.images, +// likedByUser, +// likesCount: ad._doc.likesCount, +// commentsCount: ad._doc.commentsCount, +// showDiscount: ad._doc.showDiscount, +// viewCount: ad._doc.viewCount, +// rate: { +// adTotalRatings: creator?.adTotalRatings, +// adAverageRating: creator?.adAverageRating +// } +// } +// }) +// res.status(200).json({ +// advertisings: newAds, +// totalPages: advertisings.totalPages, +// totalItems: advertisings.totalDocs +// }) +// } catch (error) { +// next(error) +// } +// } +const getAdvertisings = async (req, res, next) => { + let previousPage = 0 // تعریف متغیر سراسری برای نگهداری شماره صفحه قبلی + + 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 { page = 1, limit = 10, province, city, category, search, sort } = req.query + const currentPage = parseInt(page) // شماره صفحه جاری + + const filter = { + payment_status: 'done', + status: 'accepted' + } + if (province) { + const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) + filter.province = provinceFind + } + if (city) { + const cityFind = await CityModel.findOne({ id: city }) + filter.city = cityFind + } + if (category) { + filter.category = category + } + if (search) { + filter.$or = [ + { title: { $regex: search, $options: 'i' } }, + { category: { $regex: search, $options: 'i' } } + ] + } + + // مرتب‌سازی بر اساس پارامتر sort + let sortOptions = { acceptedAt: -1, createdAt: -1 } // مرتب‌سازی پیش‌فرض + if (sort) { + const sortFields = sort.split(',') + sortFields.forEach(field => { + if (field === 'mostDiscount') { + sortOptions = { mostDiscountPercentage: -1 } + } else if (field === 'highestRating') { + sortOptions = { 'rate.adAverageRating': -1 } + } + }) + } + + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: sortOptions // استفاده از مرتب‌سازی تنظیم‌شده + } + const advertisings = await AdvertisingModel.paginate(filter, options) + + // اگر شماره صفحه تغییر کرده بود، تعداد بازدیدها را به روز رسانی کنید + if (currentPage !== previousPage) { + await AdvertisingModel.updateMany( + { _id: { $in: advertisings.docs.map(ad => ad._id) } }, + { $inc: { viewCount: 1 } } + ) + previousPage = currentPage // به‌روزرسانی شماره صفحه قبلی + } + // دریافت لیست لایک هر ویترین و اضافه کردن آن به اطلاعات تبلیغات + const advertisingIds = advertisings.docs.map(ad => ad._id) + const advertisingLikes = await AdvertisingLikeModel.find({ advertisingId: { $in: advertisingIds } }) + + const creatorIds = advertisings.docs.map(ad => ad.creator_id) + const creators = await AdvertisingProfileModel.find({ user: { $in: creatorIds } }) + const newAds = advertisings.docs.map(ad => { + const creator = creators.find(user => user.user.toString() === ad.creator_id.toString()) + const likedByUser = advertisingLikes.some(like => like.advertisingId.toString() === ad._id.toString() && like.userId.toString() === decodedToken.id) + return { + // فیلدهای مشخص‌شده از ad._doc + _id: ad._doc._id, + category: ad._doc.category, + title: ad._doc.title, + province: ad._doc.province, + city: ad._doc.city, + neighbourhood: ad._doc.neighbourhood, + type: ad._doc.type, + mostDiscountPercentage: ad._doc.mostDiscountPercentage, + images: ad._doc.images, + likedByUser, + likesCount: ad._doc.likesCount, + commentsCount: ad._doc.commentsCount, + showDiscount: ad._doc.showDiscount, + viewCount: ad._doc.viewCount, + rate: { + adTotalRatings: creator?.adTotalRatings, + adAverageRating: creator?.adAverageRating + } + } + }) + res.status(200).json({ + advertisings: newAds, + totalPages: advertisings.totalPages, + totalItems: advertisings.totalDocs + }) + } catch (error) { + next(error) + } +} + +const getAdvertisingsWeb = async (req, res, next) => { + let previousPage = 0 + let userId = null + + try { + const token = req.header('Authorization') + if (token) { + try { + const decodedToken = jwt.verify(token.split(' ')[1], process.env.APP_SECRET) + userId = decodedToken.id + } catch (err) { + console.warn('Invalid token:', err.message) + } + } + + const { page = 1, limit = 10, province, city, category, search, sort } = req.query + const currentPage = parseInt(page) + + const filter = { + // payment_status: 'done', + status: 'accepted' + } + if (province) { + const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) + filter.province = provinceFind + } + if (city) { + const cityFind = await CityModel.findOne({ id: city }) + filter.city = cityFind + } + if (category) { + filter.category = category + } + if (search) { + filter.$or = [ + { title: { $regex: search, $options: 'i' } }, + { category: { $regex: search, $options: 'i' } } + ] + } + + let sortOptions = { acceptedAt: -1, createdAt: -1 } + if (sort) { + const sortFields = sort.split(',') + sortFields.forEach(field => { + if (field === 'mostDiscount') { + sortOptions = { mostDiscountPercentage: -1 } + } else if (field === 'highestRating') { + sortOptions = { 'rate.adAverageRating': -1 } + } + }) + } + + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: sortOptions + } + const advertisings = await AdvertisingModel.paginate(filter, options) + + if (currentPage !== previousPage) { + await AdvertisingModel.updateMany( + { _id: { $in: advertisings.docs.map(ad => ad._id) } }, + { $inc: { viewCount: 1 } } + ) + previousPage = currentPage + } + + const advertisingIds = advertisings.docs.map(ad => ad._id) + const creatorIds = advertisings.docs.map(ad => ad.creator_id) + const creators = await AdvertisingProfileModel.find({ user: { $in: creatorIds } }) + + let advertisingLikes = [] + if (userId) { + advertisingLikes = await AdvertisingLikeModel.find({ advertisingId: { $in: advertisingIds }, userId }) + } + + const newAds = advertisings.docs.map(ad => { + const creator = creators.find(user => user.user.toString() === ad.creator_id.toString()) + const likedByUser = userId ? advertisingLikes.some(like => like.advertisingId.toString() === ad._id.toString()) : false + + return { + _id: ad._doc._id, + category: ad._doc.category, + title: ad._doc.title, + province: ad._doc.province, + city: ad._doc.city, + neighbourhood: ad._doc.neighbourhood, + type: ad._doc.type, + mostDiscountPercentage: ad._doc.mostDiscountPercentage, + images: ad._doc.images, + likedByUser, + likesCount: ad._doc.likesCount, + commentsCount: ad._doc.commentsCount, + showDiscount: ad._doc.showDiscount, + viewCount: ad._doc.viewCount, + rate: { + adTotalRatings: creator?.adTotalRatings, + adAverageRating: creator?.adAverageRating + } + } + }) + + res.status(200).json({ + advertisings: newAds, + totalPages: advertisings.totalPages, + totalItems: advertisings.totalDocs + }) + } catch (error) { + next(error) + } +} + +const getSingleAdvertising = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const advertisingId = req.params.advertisingId + const advertising = await AdvertisingModel.findById(advertisingId) + .populate('creator_id', '_id user_name first_name last_name') + + if (!advertising) { + return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) + } + + // دریافت تعداد لایک‌ها و بررسی اینکه آیا کاربر ویترین را لایک کرده است یا خیر + const likesCount = await AdvertisingLikeModel.countDocuments({ advertisingId }) + const likedByUser = await AdvertisingLikeModel.findOne({ advertisingId, userId }) + + // دریافت تعداد کامنت‌ها + const commentsCount = await AdvertisingComment.countDocuments({ advertisingId, status: 'accepted' }) + const creator = await AdvertisingProfileModel.findOne({ user: advertising.creator_id._id }) + + // اضافه کردن تعداد لایک‌ها، تعداد کامنت‌ها و وضعیت لایک به تبلیغ + const advertisingData = { + ...advertising._doc, + likesCount, + likedByUser: !!likedByUser, + commentsCount, + creatorId: creator?._id + } + + res.status(200).json({ + advertising: advertisingData, + rate: { + adTotalRatings: creator?.adTotalRatings, + adAverageRating: creator?.adAverageRating + } + }) + } catch (error) { + next(error) + } +} +const getSingleAdvertisingWeb = async (req, res, next) => { + try { + let userId = null + let likedByUser = false + + // بررسی وجود توکن + const authHeader = req.header('Authorization') + if (authHeader) { + const tokenParts = authHeader.split(' ') + if (tokenParts.length === 2) { + try { + const decodedToken = jwt.verify(tokenParts[1], process.env.APP_SECRET) + userId = decodedToken.id + } catch (err) { + console.error('Invalid Token:', err.message) + } + } + } + + const advertisingId = req.params.advertisingId + const advertising = await AdvertisingModel.findById(advertisingId) + .populate('creator_id', '_id user_name first_name last_name') + + if (!advertising) { + return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) + } + + // دریافت تعداد لایک‌ها + const likesCount = await AdvertisingLikeModel.countDocuments({ advertisingId }) + + // دریافت تعداد کامنت‌ها + const commentsCount = await AdvertisingComment.countDocuments({ advertisingId, status: 'accepted' }) + + // دریافت اطلاعات سازنده تبلیغ + const creator = await AdvertisingProfileModel.findOne({ user: advertising.creator_id._id }) + + // اگر کاربر احراز هویت شده باشد، وضعیت لایک‌شدن توسط او بررسی می‌شود + if (userId) { + likedByUser = !!(await AdvertisingLikeModel.findOne({ advertisingId, userId })) + } + + // آماده‌سازی داده‌های تبلیغ + const advertisingData = { + ...advertising._doc, + likesCount, + likedByUser, + commentsCount, + creatorId: creator?._id + } + + res.status(200).json({ + advertising: advertisingData, + rate: { + adTotalRatings: creator?.adTotalRatings, + adAverageRating: creator?.adAverageRating + } + }) + } catch (error) { + next(error) + } +} + +const getAdvertisingTypes = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + // const decodedToken = jwt.verify(token, process.env.APP_SECRET) + // const userId = decodedToken.id + + // const user = await UserModel.findById(userId) + // if (!user) { + // return res.status(422).json({ + // error: true, + // message: 'کاربر یافت نشد' + // }) + // } + // درخواست انواع تبلیغات از دیتابیس + // let projectTypes = await AdvertisingTypeModel.find({}, 'name price') + const advertisingTypes = await AdvertisingTypeModel.find({}, 'name price') + + // بررسی برای مواردی مانند پیدا نشدن انواع تبلیغات + if (!advertisingTypes) { + return res.status(404).json({ message: 'انواع تبلیغات یافت نشد.' }) + } + // اگر کاربر درخواست رایگان روزانه نداشته باشد، نوع تبلیغات رایگان را حذف کنید + // if (user.daily_free_request <= 0) { + // advertisingTypes = advertisingTypes.filter(projectType => projectType.name !== 'free') + // } + // ارسال انواع تبلیغات به کاربر + res.status(200).json({ advertisingTypes }) + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error('Error in getAdvertisingTypes:', error) + res.status(500).json({ message: 'خطا در دریافت انواع تبلیغات.' }) + } +} +const getAdvertisingCategory = async (req, res, next) => { + try { + // const token = req.header('Authorization').split(' ')[1] + // if (!token) return res.status(401).send('Access Denied') + // const decodedToken = jwt.verify(token, process.env.APP_SECRET) + // const userId = decodedToken.id + + // const user = await UserModel.findById(userId) + // if (!user) { + // return res.status(422).json({ + // error: true, + // message: 'کاربر یافت نشد' + // }) + // } + // let projectTypes = await AdvertisingTypeModel.find({}, 'name price') + const categories = await AdvertisingCategoryModel.find({}) + + // بررسی برای مواردی مانند پیدا نشدن انواع پروژه + if (!categories) { + return res.status(404).json({ message: 'دسته بندی یافت نشد.' }) + } + // if (user.daily_free_request <= 0) { + // categories = categories.filter(projectType => projectType.name !== 'free') + // } + // ارسال انواع پروژه به کاربر + res.status(200).json({ categories }) + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error('Error in getAdvertisingTypes:', error) + res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) + } +} + +const getAdvertisingFeatures = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + // const decodedToken = jwt.verify(token, process.env.APP_SECRET) + // const userId = decodedToken.id + + // const user = await UserModel.findById(userId) + // if (!user) { + // return res.status(422).json({ + // error: true, + // message: 'کاربر یافت نشد' + // }) + // } + // let projectTypes = await AdvertisingTypeModel.find({}, 'name price') + const features = await AdvertisingFeaturesModel.find({}) + + // بررسی برای مواردی مانند پیدا نشدن امکانات + if (!features) { + return res.status(404).json({ message: 'امکانات یافت نشد.' }) + } + // if (user.daily_free_request <= 0) { + // features = features.filter(projectType => projectType.name !== 'free') + // } + // ارسال امکانات به کاربر + res.status(200).json({ features }) + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error('Error in getAdvertisingTypes:', error) + res.status(500).json({ message: 'خطا در دریافت امکانات.' }) + } +} +const toggleLike = async (req, res, next) => { + try { + const { advertisingId } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + // بررسی وجود تبلیغات با استفاده از advertisingId + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) + } + // بررسی وضعیت لایک کردن توسط کاربر + const isLiked = await AdvertisingLikeModel.findOne({ advertisingId, userId }) + if (isLiked) { + // حذف لایک + await AdvertisingLikeModel.findOneAndDelete({ advertisingId, userId }) + // کاهش تعداد لایک‌ها در تبلیغات + advertising.likesCount -= 1 + await advertising.save() + return res.status(200).json({ message: 'لایک با موفقیت پاک شد' }) + } else { + // اضافه کردن لایک + await AdvertisingLikeModel.create({ advertisingId, userId }) + // افزایش تعداد لایک‌ها در تبلیغات + 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) { + next(error) + } +} +const addComment = async (req, res, next) => { + try { + const { advertisingId, text } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + // eslint-disable-next-line no-unused-vars + const user = await UserModel.findById(decodedToken.id) + // if (user.is_verified !== 'verified') { + // return res.status(422).json({ + // error: true, + // message: 'مدارک شما تایید نشده است' + // }) + // } + const newComment = new AdvertisingComment({ + advertisingId, + userId, + text + }) + + await newComment.save() + + 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) { + next(error) + } +} +const getComments = async (req, res, next) => { + try { + const { advertisingId, page = 1, limit = 10 } = req.query + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: { createdAt: -1 }, + populate: [{ path: 'userId', select: '_id profile_image user_type user_name first_name last_name is_verified' }] + } + + const comments = await AdvertisingComment.paginate({ advertisingId, status: 'accepted' }, options) + res.status(200).json({ + comments: comments.docs, + totalPages: comments.totalPages, + totalItems: comments.totalDocs + }) + } catch (error) { + next(error) + } +} +const rateAdvertising = async (req, res, next) => { + try { + const { advertisingId, rating } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + if (rating < 1 || rating > 5) { + return res.status(400).json({ message: 'امتیاز باید بین 1 تا 5 باشد' }) + } + + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) + } + + const existingRating = await AdvertisingRatingModel.findOne({ advertisingId, userId }) + if (existingRating) { + return res.status(400).json({ message: 'شما قبلا برای این ویترین امتیاز ثبت کرده‌اید' }) + } else { + const newRating = new AdvertisingRatingModel({ + advertisingId, + userId, + rating + }) + await newRating.save() + + await createRatingNotification({ + ownerId: advertising.creator_id, + raterId: userId, + entityId: advertising._id, + type: 'billboard_rating' + }) + } + + // به‌روزرسانی امتیازات کلی کاربر + const advertisingProfile = await AdvertisingProfileModel.findOne({ user: advertising.creator_id }) + if (advertisingProfile) { + const totalUserRatings = await AdvertisingRatingModel.aggregate([ + { + $lookup: { + from: 'advertisings', + localField: 'advertisingId', + foreignField: '_id', + as: 'advertising' + } + }, + { $unwind: '$advertising' }, + { $match: { 'advertising.creator_id': advertising.creator_id } }, + { $group: { _id: null, total: { $sum: '$rating' }, count: { $sum: 1 } } } + ]) + + if (totalUserRatings.length > 0) { + const { total, count } = totalUserRatings[0] + advertisingProfile.adTotalRatings = total + advertisingProfile.adRatingsCount = count + advertisingProfile.adAverageRating = (total / count).toFixed(1) + } else { + advertisingProfile.adTotalRatings = 0 + advertisingProfile.adRatingsCount = 0 + advertisingProfile.adAverageRating = 0 + } + + await advertisingProfile.save() + } else { + // ایجاد پروفایل تبلیغاتی جدید در صورت عدم وجود + const newAdvertisingProfile = new AdvertisingProfileModel({ + user: advertising.creator_id, + adTotalRatings: rating, + adRatingsCount: 1, + adAverageRating: rating.toFixed(1) + }) + await newAdvertisingProfile.save() + } + + res.status(200).json({ message: 'امتیاز با موفقیت ثبت شد' }) + } catch (error) { + next(error) + } +} +const getUserAdvertisings = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { page = 1, limit = 10, status, search } = req.query + + const filter = { + creator_id: userId + } + + if (status) { + filter.status = status + } + if (search) { + filter.$or = [ + { title: { $regex: search, $options: 'i' } }, + { category: { $regex: search, $options: 'i' } } + ] + } + + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: { updatedAt: -1 } + } + + const advertisings = await AdvertisingModel.paginate(filter, options) + + // دریافت لیست لایک هر ویترین و اضافه کردن آن به اطلاعات تبلیغات + const advertisingIds = advertisings.docs.map(ad => ad._id) + const advertisingLikes = await AdvertisingLikeModel.find({ advertisingId: { $in: advertisingIds } }) + const creator = await AdvertisingProfileModel.findOne({ user: userId }) + + const newAds = advertisings.docs.map(ad => { + const likedByUser = advertisingLikes.some(like => like.advertisingId.toString() === ad._id.toString() && like.userId.toString() === decodedToken.id) + let remainingTime = null + if (ad.status === 'accepted' && ad.acceptedAt) { + const expirationDate = new Date(ad.acceptedAt) + expirationDate.setDate(expirationDate.getDate() + 15) + const currentDate = new Date() + const diffMilliseconds = expirationDate - currentDate + const diffDays = Math.floor(diffMilliseconds / (1000 * 60 * 60 * 24)) + const diffHours = Math.floor((diffMilliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + remainingTime = `${diffDays} روز و ${diffHours} ساعت` + } + return { + _id: ad._doc._id, + category: ad._doc.category, + title: ad._doc.title, + province: ad._doc.province, + city: ad._doc.city, + neighbourhood: ad._doc.neighbourhood, + type: ad._doc.type, + mostDiscountPercentage: ad._doc.mostDiscountPercentage, + images: ad._doc.images, + likedByUser, + likesCount: ad._doc.likesCount, + commentsCount: ad._doc.commentsCount, + status: ad._doc.status, + viewCount: ad._doc.viewCount, + remainingTime, + rate: { + adTotalRatings: creator?.adTotalRatings, + adAverageRating: creator?.adAverageRating + } + } + }) + + res.status(200).json({ + advertisings: newAds, + totalPages: advertisings.totalPages, + totalItems: advertisings.totalDocs + }) + } catch (error) { + next(error) + } +} +const getUserSingleAdvertising = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const user = await UserModel.findById(userId) + if (!user) { + return res.status(422).json({ + error: true, + message: 'شما دسترسی به این بخش ندارید' + }) + } + const advertisingId = req.params.advertisingId + const advertising = await AdvertisingModel.findById(advertisingId) + .populate('creator_id', '_id user_name first_name last_name') + + if (!advertising) { + return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) + } + + // دریافت تعداد لایک‌ها و بررسی اینکه آیا کاربر ویترین را لایک کرده است یا خیر + const likesCount = await AdvertisingLikeModel.countDocuments({ advertisingId }) + const likedByUser = await AdvertisingLikeModel.findOne({ advertisingId, userId }) + + // دریافت تعداد کامنت‌ها + const commentsCount = await AdvertisingComment.countDocuments({ advertisingId, status: 'accepted' }) + const creator = await AdvertisingProfileModel.findOne({ user: advertising.creator_id._id }) + + // اضافه کردن تعداد لایک‌ها، تعداد کامنت‌ها و وضعیت لایک به تبلیغ + const advertisingData = { + ...advertising._doc, + likesCount, + likedByUser: !!likedByUser, + commentsCount, + creatorId: creator?._id + + } + + res.status(200).json({ + advertising: advertisingData, + rate: { + adTotalRatings: creator?.adTotalRatings, + adAverageRating: creator?.adAverageRating + } + }) + } catch (error) { + next(error) + } +} +const editAdvertising = 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(422).json({ + error: true, + message: 'شما دسترسی به این بخش ندارید' + }) + } + // if (user.is_verified !== 'verified') { + // return res.status(422).json({ + // error: true, + // message: 'مدارک شما تایید نشده است' + // }) + // } + + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + + const { id } = req.params + const { + category, + title, + description, + province, + city, + neighbourhood, + address, + lat, + lng, + services, + features, + contactInfo, + mostDiscountPercentage, + showDiscount, + type + } = req.body + + if (!title || !category || !province || !city) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const advertising = await AdvertisingModel.findOne({ + _id: id, + creator_id: user._id, + status: { $in: ['pre_payment', 'paid', 'rejected', 'expired'] } + }) + + if (!advertising) { + return res.status(404).json({ + error: true, + message: 'تبلیغ مورد نظر یافت نشد یا شما اجازه ویرایش آن را ندارید' + }) + } + + const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) + const cityFind = await CityModel.findOne({ id: city }) + + // مدیریت تصاویر برای خدمات + const servicesWithImages = [] + const parsedServices = JSON.parse(services) + if (parsedServices && parsedServices.length > 0) { + for (const service of parsedServices) { + if (typeof service.image === 'string' && service.image.startsWith('/services/')) { + servicesWithImages.push(service) + } else { + const serviceImages = [] + const serviceId = service.id + if (req.files && req.files.serviceImages && req.files.serviceImages[serviceId]) { + const imageFiles = Array.isArray(req.files.serviceImages[serviceId]) ? req.files.serviceImages[serviceId] : [req.files.serviceImages[serviceId]] + const uploadDir = path.join(__dirname, '../../../../storage/services') + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + for (const imageFile of imageFiles) { + const uniqueFileName = `${user.user_name}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(imageFile.name)}` + const filePath = path.join(uploadDir, uniqueFileName) + await fs.move(imageFile.path, filePath) + const imageUrl = `/services/${uniqueFileName}` + serviceImages.push(imageUrl) + service.image = imageUrl + } + } + servicesWithImages.push({ ...service, image: serviceImages[0] }) + } + } + } + // مدیریت تصاویر + const images = [] + + // اضافه کردن تصاویر موجود + if (Array.isArray(req.body.existingImages)) { + req.body.existingImages.forEach(image => { + if (typeof image === 'string' && image.startsWith('/advertising/')) { + images.push(image) + } + }) + } + + // اضافه کردن تصاویر جدید + if (req.files && req.files.images) { + const imageFiles = Array.isArray(req.files.images) ? req.files.images : [req.files.images] + const uploadDir = path.join(__dirname, '../../../../storage/advertising') + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + for (const imageFile of imageFiles) { + // بررسی فرمت تصویر + if (imageFile && typeof imageFile === 'object' && imageFile.path) { + const uniqueFileName = `${user.user_name}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(imageFile.name)}` + const filePath = path.join(uploadDir, uniqueFileName) + await fs.move(imageFile.path, filePath) + const imageUrl = `/advertising/${uniqueFileName}` + images.push(imageUrl) + } + } + } + const parsedFeatures = JSON.parse(features) + + advertising.title = title + advertising.category = category + advertising.description = description + advertising.province = provinceFind + advertising.city = cityFind + advertising.neighbourhood = neighbourhood + advertising.address = address + advertising.lat = lat + advertising.lng = lng + advertising.images = images + advertising.services = servicesWithImages + advertising.features = parsedFeatures + advertising.contactInfo = contactInfo + advertising.mostDiscountPercentage = mostDiscountPercentage + advertising.showDiscount = showDiscount + advertising.type = type + + await advertising.save() + + if (contactInfo && contactInfo.saveInfoForNextAds) { + user.contactInfo = contactInfo + await user.save() + } + res.status(200).json({ + message: 'ویترین با موفقیت ویرایش شد', + id: advertising._id + }) + } catch (error) { + next(error) + } +} +const getUserAdvertisingProfile = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const vitrineId = req.query.vitrineId + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const user = await UserModel.findById(decodedToken.id) + if (!user) { + return res.status(422).json({ + error: true, + message: 'شما دسترسی به این بخش ندارید' + }) + } + // if (user.is_verified !== 'verified') { + // return res.status(422).json({ + // error: true, + // message: 'مدارک شما تایید نشده است' + // }) + // } + + // دریافت اطلاعات پروفایل تبلیغاتی کاربر + const advertisingProfile = await AdvertisingProfileModel.findById(vitrineId) + if (!advertisingProfile) { + return res.status(404).json({ + error: true, + message: 'پروفایل تبلیغاتی یافت نشد' + }) + } + + // دریافت تبلیغات‌های کاربر + const userAds = await AdvertisingModel.find({ creator_id: advertisingProfile?.user }) + + // جمع‌آوری اطلاعات از تبلیغات‌ها + let allImages = [] + let allServices = [] + let allFeatures = [] + let totalCommentsCount = 0 + let totalLikesCount = 0 + + for (const ad of userAds) { + allImages = allImages.concat(ad.images) + allServices = allServices.concat(ad.services) + allFeatures = allFeatures.concat(ad.features) + totalCommentsCount += ad.commentsCount + totalLikesCount += ad.likesCount + } + + // حذف آیتم‌های null و تکراری از allFeatures + const uniqueFeatures = [] + const featureTitles = new Set() + for (const feature of allFeatures) { + if (feature && !featureTitles.has(feature.title)) { + uniqueFeatures.push(feature) + featureTitles.add(feature.title) + } + } + + // آماده‌سازی اطلاعات نهایی + const profileDetails = { + _id: advertisingProfile._id, + profile_image: advertisingProfile.profile_image, + vitrine_name: advertisingProfile.vitrine_name, + category: advertisingProfile.category, + about_us: advertisingProfile.about_us, + province: advertisingProfile.province, + city: advertisingProfile.city, + lat: advertisingProfile.lat, + lng: advertisingProfile.lng, + neighbourhood: advertisingProfile.neighbourhood, + adTotalRatings: advertisingProfile.adTotalRatings, + adRatingsCount: advertisingProfile.adRatingsCount, + adAverageRating: advertisingProfile.adAverageRating, + contactInfo: advertisingProfile.contactInfo, + address: advertisingProfile.address, + allImages: allImages?.reverse(), + allServices, + allFeatures: uniqueFeatures, + totalCommentsCount, + totalLikesCount, + is_self: !!(advertisingProfile && advertisingProfile?.user == decodedToken.id) + } + + // ارسال پاسخ به کاربر + res.status(200).json({ profileDetails }) + } catch (error) { + next(error) + } +} +const getUserAdvertisingProfileWeb = async (req, res, next) => { + try { + let userId = null + let isSelf = false + + // بررسی توکن (در صورت وجود) + const authHeader = req.header('Authorization') + if (authHeader) { + const tokenParts = authHeader.split(' ') + if (tokenParts.length === 2) { + try { + const decodedToken = jwt.verify(tokenParts[1], process.env.APP_SECRET) + userId = decodedToken.id + } catch (err) { + console.error('Invalid Token:', err.message) + } + } + } + + const vitrineId = req.query.vitrineId + console.log(vitrineId) + + // دریافت اطلاعات پروفایل تبلیغاتی کاربر + const advertisingProfile = await AdvertisingProfileModel.findById(vitrineId) + if (!advertisingProfile) { + return res.status(404).json({ + error: true, + message: 'پروفایل تبلیغاتی یافت نشد' + }) + } + + // دریافت تبلیغات‌های کاربر + const userAds = await AdvertisingModel.find({ creator_id: advertisingProfile?.user }) + + // جمع‌آوری اطلاعات از تبلیغات‌ها + let allImages = [] + let allServices = [] + let allFeatures = [] + let totalCommentsCount = 0 + let totalLikesCount = 0 + + for (const ad of userAds) { + allImages = allImages.concat(ad.images) + allServices = allServices.concat(ad.services) + allFeatures = allFeatures.concat(ad.features) + totalCommentsCount += ad.commentsCount + totalLikesCount += ad.likesCount + } + + // حذف آیتم‌های null و تکراری از allFeatures + const uniqueFeatures = [] + const featureTitles = new Set() + for (const feature of allFeatures) { + if (feature && !featureTitles.has(feature.title)) { + uniqueFeatures.push(feature) + featureTitles.add(feature.title) + } + } + + // بررسی اینکه کاربر خودش صاحب این ویترین است یا نه + if (userId) { + isSelf = advertisingProfile?.user == userId + } + + // آماده‌سازی اطلاعات نهایی + const profileDetails = { + _id: advertisingProfile._id, + profile_image: advertisingProfile.profile_image, + vitrine_name: advertisingProfile.vitrine_name, + category: advertisingProfile.category, + about_us: advertisingProfile.about_us, + province: advertisingProfile.province, + city: advertisingProfile.city, + lat: advertisingProfile.lat, + lng: advertisingProfile.lng, + neighbourhood: advertisingProfile.neighbourhood, + adTotalRatings: advertisingProfile.adTotalRatings, + adRatingsCount: advertisingProfile.adRatingsCount, + adAverageRating: advertisingProfile.adAverageRating, + contactInfo: advertisingProfile.contactInfo, + address: advertisingProfile.address, + allImages: allImages?.reverse(), + allServices, + allFeatures: uniqueFeatures, + totalCommentsCount, + totalLikesCount, + is_self: isSelf + } + + // ارسال پاسخ به کاربر + res.status(200).json({ profileDetails }) + } catch (error) { + next(error) + } +} + +const updateUserAdvertisingProfile = 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(422).json({ + error: true, + message: 'شما دسترسی به این بخش ندارید' + }) + } + + const advertisingProfile = await AdvertisingProfileModel.findOne({ user: decodedToken.id }) + if (!advertisingProfile) { + return res.status(404).json({ + error: true, + message: 'پروفایل تبلیغاتی یافت نشد' + }) + } + + if (advertisingProfile.user.toString() !== decodedToken.id) { + return res.status(403).json({ + error: true, + message: 'شما اجازه ویرایش این پروفایل را ندارید' + }) + } + + const { profile_image } = req.files + const { + // profile_image, + vitrine_name, + category, + about_us, + province, + city, + lat, + lng, + address, + neighbourhood, + contactInfo + } = req.body + + const updateFields = {} + const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) + const cityFind = await CityModel.findOne({ id: city }) + // if (profile_image !== undefined && profile_image !== null && !profile_image.startsWith('/profiles/')) { + if (profile_image !== undefined && profile_image !== null) { + const uploadDir = path.join(__dirname, '../../../../storage/profiles') + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + const uniqueFileName = `${advertisingProfile?.vitrine_name}-${Date.now()}${path.extname(profile_image.name)}` + const filePath = path.join(uploadDir, uniqueFileName) + await fs.move(profile_image.path, filePath) + updateFields.profile_image = `/profiles/${uniqueFileName}` + } + if (vitrine_name !== undefined) updateFields.vitrine_name = vitrine_name + if (category !== undefined) updateFields.category = category + if (about_us !== undefined) updateFields.about_us = about_us + if (province !== undefined) updateFields.province = provinceFind + if (city !== undefined) updateFields.city = cityFind + if (lat !== undefined) updateFields.lat = lat + if (lng !== undefined) updateFields.lng = lng + if (address !== undefined) updateFields.address = address + if (neighbourhood !== undefined) updateFields.neighbourhood = neighbourhood + if (contactInfo !== undefined) updateFields.contactInfo = contactInfo + + const updatedProfile = await AdvertisingProfileModel.findByIdAndUpdate( + advertisingProfile._id, + { $set: updateFields }, + { new: true } + ) + + res.status(200).json({ + success: true, + message: 'پروفایل با موفقیت به‌روزرسانی شد', + updatedProfile + }) + } catch (error) { + next(error) + } +} + +module.exports = { + createAdvertisingValidationRules, + createAdvertising, + getAdvertisingTypes, + getAdvertisingCategory, + getSingleAdvertising, + getAdvertisingFeatures, + getAdvertisings, + toggleLike, + addComment, + getComments, + rateAdvertising, + getUserAdvertisings, + editAdvertising, + getUserSingleAdvertising, + getUserAdvertisingProfile, + updateUserAdvertisingProfile, + getAdvertisingsWeb, + getSingleAdvertisingWeb, + getUserAdvertisingProfileWeb +} diff --git a/controllers/application/citysController.js b/controllers/application/citysController.js index e411bc4..0e9e98d 100644 --- a/controllers/application/citysController.js +++ b/controllers/application/citysController.js @@ -1,16 +1,16 @@ -const { CityModel } = require('../../models/StateCity') -const getCities = async (req, res, next) => { - try { - const { id } = req.params - const cities = await CityModel.find({ province_id: id }) - - res.status(200).json({ - cities - }) - } catch (err) { - res.status(500).send('Internal Server Error') - } -} -module.exports = { - getCities -} +const { CityModel } = require('../../models/StateCity') +const getCities = async (req, res, next) => { + try { + const { id } = req.params + const cities = await CityModel.find({ province_id: id }) + + res.status(200).json({ + cities + }) + } catch (err) { + res.status(500).send('Internal Server Error') + } +} +module.exports = { + getCities +} diff --git a/controllers/application/expertise/expertiseController.js b/controllers/application/expertise/expertiseController.js index eb6e133..b4cf48d 100644 --- a/controllers/application/expertise/expertiseController.js +++ b/controllers/application/expertise/expertiseController.js @@ -1,13 +1,13 @@ -/* eslint-disable camelcase */ -const ExpertiseModel = require('../../../models/ExpertiseModel') -const getExpertise = async (req, res, next) => { - try { - const expertises = await ExpertiseModel.find().select('expertise sub_expertise') - res.json({ expertises }) - } catch (error) { - next(error) - } -} -module.exports = { - getExpertise -} +/* eslint-disable camelcase */ +const ExpertiseModel = require('../../../models/ExpertiseModel') +const getExpertise = async (req, res, next) => { + try { + const expertises = await ExpertiseModel.find().select('expertise sub_expertise') + res.json({ expertises }) + } catch (error) { + next(error) + } +} +module.exports = { + getExpertise +} diff --git a/controllers/application/financial/financialController.js b/controllers/application/financial/financialController.js index 9595a7c..4e60769 100644 --- a/controllers/application/financial/financialController.js +++ b/controllers/application/financial/financialController.js @@ -1,65 +1,65 @@ -const jwt = require('jsonwebtoken') -const PaymentModel = require('../../../models/PaymentModel') -const ProjectModel = require('../../../models/ProjectModel') -const moment = require('moment-jalaali') - -const getFinancial = async (req, res) => { - try { - // Extract user ID from JWT token - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const options = { - page: req.query.page || 1, // صفحه پیش‌فرض ۱ - limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه - sort: { createdAt: -1 } - } - // Find all payments associated with the user - const financial = await PaymentModel.paginate( - { user_id: userId }, // فیلتر - options // گزینه‌های پیجینیشن - ) - // Prepare the data to be sent back - const userPayments = await Promise.all(financial.docs.map(async (payment) => { - // Find the project associated with the payment - const project = await ProjectModel.findById(payment.project_id) - - if (!project) { - return null - } - // استفاده از کتابخانه moment-jalaali برای تبدیل تاریخ و زمان به شمسی - const shamsiDate = moment(payment.createdAt).format('jYYYY-jMM-jDD') - const shamsiTime = moment(payment.createdAt).format('HH:mm') - - // Extract the required information - const userPayment = { - _id: payment._id, - project_title: project.title, - project_id: project._id, - payment_date: shamsiDate, // Format date as YYYY-MM-DD - payment_time: shamsiTime, - iban: project.creator_id ? project.creator_id.iban : null, // Check if creator_id exists before accessing iban - amount: payment.amount, - payment_status: payment.status - } - - return userPayment - })) - - // Filter out null values - const validUserPayments = userPayments.filter(payment => payment !== null) - - // Send the payments back to the client - res.status(200).json({ - financial: financial, - totalPages: financial.totalPages, // ارسال تعداد کل صفحات - totalItems: financial.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} - -module.exports = { getFinancial } +const jwt = require('jsonwebtoken') +const PaymentModel = require('../../../models/PaymentModel') +const ProjectModel = require('../../../models/ProjectModel') +const moment = require('moment-jalaali') + +const getFinancial = async (req, res) => { + try { + // Extract user ID from JWT token + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const options = { + page: req.query.page || 1, // صفحه پیش‌فرض ۱ + limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه + sort: { createdAt: -1 } + } + // Find all payments associated with the user + const financial = await PaymentModel.paginate( + { user_id: userId }, // فیلتر + options // گزینه‌های پیجینیشن + ) + // Prepare the data to be sent back + const userPayments = await Promise.all(financial.docs.map(async (payment) => { + // Find the project associated with the payment + const project = await ProjectModel.findById(payment.project_id) + + if (!project) { + return null + } + // استفاده از کتابخانه moment-jalaali برای تبدیل تاریخ و زمان به شمسی + const shamsiDate = moment(payment.createdAt).format('jYYYY-jMM-jDD') + const shamsiTime = moment(payment.createdAt).format('HH:mm') + + // Extract the required information + const userPayment = { + _id: payment._id, + project_title: project.title, + project_id: project._id, + payment_date: shamsiDate, // Format date as YYYY-MM-DD + payment_time: shamsiTime, + iban: project.creator_id ? project.creator_id.iban : null, // Check if creator_id exists before accessing iban + amount: payment.amount, + payment_status: payment.status + } + + return userPayment + })) + + // Filter out null values + const validUserPayments = userPayments.filter(payment => payment !== null) + + // Send the payments back to the client + res.status(200).json({ + financial: financial, + totalPages: financial.totalPages, // ارسال تعداد کل صفحات + totalItems: financial.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} + +module.exports = { getFinancial } diff --git a/controllers/application/login/changePasswordController.js b/controllers/application/login/changePasswordController.js index a8b457b..39faaaf 100644 --- a/controllers/application/login/changePasswordController.js +++ b/controllers/application/login/changePasswordController.js @@ -1,77 +1,77 @@ -/* eslint-disable camelcase */ -const bcrypt = require('bcryptjs') -const { check, validationResult } = require('express-validator') - -const jwt = require('jsonwebtoken') -const UserModel = require('../../../models/UserModel') - -const changePasswordValidationRules = () => { - return [ - check('new_password') - .notEmpty().withMessage('رمز عبور نمی‌تواند خالی باشد') - .isLength({ min: 8, max: 25 }).withMessage('رمز عبور نباید کوتاه تر از 8 کاراکتر باشد') - .matches(/^(?=.*[a-zA-Zآ-ی])(?=.*\d).{8,}$/u).withMessage('رمز عبور باید شامل حروف و اعداد باشد') - ] -} -const changePasswordUser = async (req, res, next) => { - try { - const { new_password } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - // اعتبارسنجی ورودی ها - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - - // یافتن کاربر با شماره موبایل - const user = await UserModel.findById(userId) - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شماره موبایل یافت نشد' - }) - } - // هش کردن پسورد جدید و ذخیره در دیتابیس - const hashedPassword = await bcrypt.hash(new_password, 10) - user.password = hashedPassword - await user.save() - - return res.json({ - message: 'کلمه عبور با موفقیت ذخیره شد' - }) - } catch (error) { - next(error) - } -} - -const editChangePasswordUser = async (req, res, next) => { - try { - const { mobile, new_password } = req.body - const user = await UserModel.findOne({ mobile }) - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شماره موبایل یافت نشد' - }) - } - - // هش کردن پسورد جدید و ذخیره در دیتابیس - const hashedPassword = await bcrypt.hash(new_password, 10) - user.password = hashedPassword - await user.save() - - return res.json({ - message: 'کلمه عبور با موفقیت تغییر یافت' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - changePasswordUser, changePasswordValidationRules, editChangePasswordUser -} +/* eslint-disable camelcase */ +const bcrypt = require('bcryptjs') +const { check, validationResult } = require('express-validator') + +const jwt = require('jsonwebtoken') +const UserModel = require('../../../models/UserModel') + +const changePasswordValidationRules = () => { + return [ + check('new_password') + .notEmpty().withMessage('رمز عبور نمی‌تواند خالی باشد') + .isLength({ min: 8, max: 25 }).withMessage('رمز عبور نباید کوتاه تر از 8 کاراکتر باشد') + .matches(/^(?=.*[a-zA-Zآ-ی])(?=.*\d).{8,}$/u).withMessage('رمز عبور باید شامل حروف و اعداد باشد') + ] +} +const changePasswordUser = async (req, res, next) => { + try { + const { new_password } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + // اعتبارسنجی ورودی ها + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + + // یافتن کاربر با شماره موبایل + const user = await UserModel.findById(userId) + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شماره موبایل یافت نشد' + }) + } + // هش کردن پسورد جدید و ذخیره در دیتابیس + const hashedPassword = await bcrypt.hash(new_password, 10) + user.password = hashedPassword + await user.save() + + return res.json({ + message: 'کلمه عبور با موفقیت ذخیره شد' + }) + } catch (error) { + next(error) + } +} + +const editChangePasswordUser = async (req, res, next) => { + try { + const { mobile, new_password } = req.body + const user = await UserModel.findOne({ mobile }) + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شماره موبایل یافت نشد' + }) + } + + // هش کردن پسورد جدید و ذخیره در دیتابیس + const hashedPassword = await bcrypt.hash(new_password, 10) + user.password = hashedPassword + await user.save() + + return res.json({ + message: 'کلمه عبور با موفقیت تغییر یافت' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + changePasswordUser, changePasswordValidationRules, editChangePasswordUser +} diff --git a/controllers/application/login/loginController.js b/controllers/application/login/loginController.js index a14f46e..00abcc9 100644 --- a/controllers/application/login/loginController.js +++ b/controllers/application/login/loginController.js @@ -1,92 +1,92 @@ -const UserModel = require('../../../models/UserModel') -const { check, validationResult } = require('express-validator') -const { default: axios } = require('axios') -const { OTP_VALID_MS } = require('../../../utils/otpExpiry') -const loginValidationRules = () => { - return [ - check('mobile') - .notEmpty().withMessage('شماره موبایل نمی‌تواند خالی باشد') - .isLength({ min: 11, max: 11 }).withMessage('شماره موبایل باید دقیقاً 11 رقم باشد') - .matches(/^09[0-9]{9}$/).withMessage('فرمت شماره موبایل صحیح نیست') - ] -} - -const loginUser = async (req, res, next) => { - try { - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - - const { mobile } = req.body - if (!mobile) { - return res.status(422).json({ - error: true, - message: 'شماره موبایل نمی‌تواند خالی باشد' - }) - } - - // const existingUser = await UserModel.findOne({ mobile }) - // if (!existingUser) { - // return res.status(422).json({ error: true, message: 'کاربری با این شماره موبایل یافت نشد' }) - // } - function generateOTP () { - return Math.floor(100000 + Math.random() * 900000) - } - - const otp = generateOTP() - const otpSentAt = new Date() - // eslint-disable-next-line no-unused-vars - const user = await UserModel.findOneAndUpdate( - { mobile }, - { $set: { mobile, otp: String(otp), otpSentAt } }, - { upsert: true, new: true, lean: true } - ) - setTimeout(() => { - UserModel.findOneAndUpdate( - { mobile }, - { $set: { otp: null, otpSentAt: null } }, - { new: true } - ) - .then(() => {}) - .catch(error => console.error('Error setting OTP to null:', error)) - }, OTP_VALID_MS) - const data = JSON.stringify({ - mobile, - templateId: '930719', - parameters: [ - { name: 'CODE', value: otp.toString() } - ] - }) - - const config = { - method: 'post', - url: 'https://api.sms.ir/v1/send/verify', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/plain', - 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' - }, - data - } - - axios(config) - .then(function (response) { - }) - .catch(function (error) { - console.log(error) - }) - - res.status(200).json({ - success: true, - message: 'کد تایید ارسال شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - loginValidationRules, - loginUser -} +const UserModel = require('../../../models/UserModel') +const { check, validationResult } = require('express-validator') +const { default: axios } = require('axios') +const { OTP_VALID_MS } = require('../../../utils/otpExpiry') +const loginValidationRules = () => { + return [ + check('mobile') + .notEmpty().withMessage('شماره موبایل نمی‌تواند خالی باشد') + .isLength({ min: 11, max: 11 }).withMessage('شماره موبایل باید دقیقاً 11 رقم باشد') + .matches(/^09[0-9]{9}$/).withMessage('فرمت شماره موبایل صحیح نیست') + ] +} + +const loginUser = async (req, res, next) => { + try { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + + const { mobile } = req.body + if (!mobile) { + return res.status(422).json({ + error: true, + message: 'شماره موبایل نمی‌تواند خالی باشد' + }) + } + + // const existingUser = await UserModel.findOne({ mobile }) + // if (!existingUser) { + // return res.status(422).json({ error: true, message: 'کاربری با این شماره موبایل یافت نشد' }) + // } + function generateOTP () { + return Math.floor(100000 + Math.random() * 900000) + } + + const otp = generateOTP() + const otpSentAt = new Date() + // eslint-disable-next-line no-unused-vars + const user = await UserModel.findOneAndUpdate( + { mobile }, + { $set: { mobile, otp: String(otp), otpSentAt } }, + { upsert: true, new: true, lean: true } + ) + setTimeout(() => { + UserModel.findOneAndUpdate( + { mobile }, + { $set: { otp: null, otpSentAt: null } }, + { new: true } + ) + .then(() => {}) + .catch(error => console.error('Error setting OTP to null:', error)) + }, OTP_VALID_MS) + const data = JSON.stringify({ + mobile, + templateId: '930719', + parameters: [ + { name: 'CODE', value: otp.toString() } + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + + axios(config) + .then(function (response) { + }) + .catch(function (error) { + console.log(error) + }) + + res.status(200).json({ + success: true, + message: 'کد تایید ارسال شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + loginValidationRules, + loginUser +} diff --git a/controllers/application/login/loginWithUserName.js b/controllers/application/login/loginWithUserName.js index e0ffa74..cfaf3ad 100644 --- a/controllers/application/login/loginWithUserName.js +++ b/controllers/application/login/loginWithUserName.js @@ -1,104 +1,104 @@ -/* eslint-disable camelcase */ -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 { - // اعتبارسنجی ورودی ها - const { user_name, password } = req.body - if (!user_name || !password) { - return res.status(422).json({ - error: true, - message: INVALID_CREDENTIALS - }) - } - // یافتن کاربر با نام کاربری - const userLow = user_name.toLowerCase() - let user = await UserModel.findOne({ user_name: userLow }) - - if (!user) { - return res.status(422).json({ - error: true, - 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: INVALID_CREDENTIALS - }) - } - const isPasswordValid = await bcrypt.compare(password, user.password) - if (!isPasswordValid) { - return res.status(422).json({ - error: true, - message: INVALID_CREDENTIALS - }) - } - // اگر هم نام کاربری و هم رمز عبور درست بود، ارسال پیام موفقیت آمیز - const token = TokenService.sign({ id: user._id }) - // if (user.user_name === null) { - // return res.json({ - // message: 'ورود با موفقیت', - // page: 'username' - // }) - // } - - // if (!user.first_name && !user.last_name) { - // return res.json({ - // message: 'ورود با موفقیت', - // page: 'name' - // }) - // } - - // if (!user.user_type) { - // return res.json({ - // message: 'کد تایید صحیح بود', - // page: 'usertype' - // }) - // } - let step = '' - if (user.user_name === null) { step = 'user_name' } else if - (!user.password) { step = 'password' } else if - (!user.first_name) { step = 'first_name' } else if - (!user.user_type) { step = 'user_type' } - if (!user.user_type || user.user_name === null || !user.first_name || !user.last_name) { - return res.json({ - message: 'کد تایید صحیح بود', - token, - step, - user_type: null, - page: 'auth-page', - id: user._id - }) - } - return res.json({ - message: 'کد تایید صحیح بود', - token, - page: 'home', - step, - user_type: user.user_type, - id: user._id - }) - } catch (error) { - next(error) - } -} - -module.exports = { - loginWithUserName -} +/* eslint-disable camelcase */ +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 { + // اعتبارسنجی ورودی ها + const { user_name, password } = req.body + if (!user_name || !password) { + return res.status(422).json({ + error: true, + message: INVALID_CREDENTIALS + }) + } + // یافتن کاربر با نام کاربری + const userLow = user_name.toLowerCase() + let user = await UserModel.findOne({ user_name: userLow }) + + if (!user) { + return res.status(422).json({ + error: true, + 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: INVALID_CREDENTIALS + }) + } + const isPasswordValid = await bcrypt.compare(password, user.password) + if (!isPasswordValid) { + return res.status(422).json({ + error: true, + message: INVALID_CREDENTIALS + }) + } + // اگر هم نام کاربری و هم رمز عبور درست بود، ارسال پیام موفقیت آمیز + const token = TokenService.sign({ id: user._id }) + // if (user.user_name === null) { + // return res.json({ + // message: 'ورود با موفقیت', + // page: 'username' + // }) + // } + + // if (!user.first_name && !user.last_name) { + // return res.json({ + // message: 'ورود با موفقیت', + // page: 'name' + // }) + // } + + // if (!user.user_type) { + // return res.json({ + // message: 'کد تایید صحیح بود', + // page: 'usertype' + // }) + // } + let step = '' + if (user.user_name === null) { step = 'user_name' } else if + (!user.password) { step = 'password' } else if + (!user.first_name) { step = 'first_name' } else if + (!user.user_type) { step = 'user_type' } + if (!user.user_type || user.user_name === null || !user.first_name || !user.last_name) { + return res.json({ + message: 'کد تایید صحیح بود', + token, + step, + user_type: null, + page: 'auth-page', + id: user._id + }) + } + return res.json({ + message: 'کد تایید صحیح بود', + token, + page: 'home', + step, + user_type: user.user_type, + id: user._id + }) + } catch (error) { + next(error) + } +} + +module.exports = { + loginWithUserName +} diff --git a/controllers/application/login/verifyController.js b/controllers/application/login/verifyController.js index 2c57e90..1abb57c 100644 --- a/controllers/application/login/verifyController.js +++ b/controllers/application/login/verifyController.js @@ -1,109 +1,109 @@ -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) => { - try { - const { mobile, otp } = req.body - if (!mobile || !otp) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - 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)) { - return res.status(422).json({ - error: true, - message: 'کد تایید منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید' - }) - } - - if (String(user.otp) === String(otp)) { - // if (user.user_name === null) { - // return res.json({ - // message: 'کد تایید صحیح بود', - // page: 'username' - // }) - // } - - // if (!user.first_name && !user.last_name) { - // return res.json({ - // message: 'کد تایید صحیح بود', - // page: 'name' - // }) - // } - - // if (!user.user_type) { - // return res.json({ - // message: 'کد تایید صحیح بود', - // page: 'usertype' - // }) - // } - - // اگر همه فیلدها پر بودند - // return res.json({ - // message: 'کد تایید صحیح بود', - // token, - // user_type: user.user_type, - // page: 'home', - // id: user._id - // }) - let step = '' - if (user.user_name === null) { step = 'user_name' } else if - (!user.password) { step = 'password' } else if - (!user.first_name) { step = 'first_name' } else if - (!user.user_type) { step = 'user_type' } else { step = 'profile_image' } - if (!user.user_type || user.user_name === null || !user.first_name || !user.last_name) { - return res.json({ - message: 'کد تایید صحیح بود', - token, - step, - user_type: null, - page: 'auth-page', - id: user._id - }) - } - return res.json({ - message: 'کد تایید صحیح بود', - token, - user_type: user.user_type, - step, - page: 'home', - id: user._id - }) - } - - return res.status(422).json({ - error: true, - message: 'کد را اشتباه وارد کردید' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - verifyUser -} +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) => { + try { + const { mobile, otp } = req.body + if (!mobile || !otp) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + 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)) { + return res.status(422).json({ + error: true, + message: 'کد تایید منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید' + }) + } + + if (String(user.otp) === String(otp)) { + // if (user.user_name === null) { + // return res.json({ + // message: 'کد تایید صحیح بود', + // page: 'username' + // }) + // } + + // if (!user.first_name && !user.last_name) { + // return res.json({ + // message: 'کد تایید صحیح بود', + // page: 'name' + // }) + // } + + // if (!user.user_type) { + // return res.json({ + // message: 'کد تایید صحیح بود', + // page: 'usertype' + // }) + // } + + // اگر همه فیلدها پر بودند + // return res.json({ + // message: 'کد تایید صحیح بود', + // token, + // user_type: user.user_type, + // page: 'home', + // id: user._id + // }) + let step = '' + if (user.user_name === null) { step = 'user_name' } else if + (!user.password) { step = 'password' } else if + (!user.first_name) { step = 'first_name' } else if + (!user.user_type) { step = 'user_type' } else { step = 'profile_image' } + if (!user.user_type || user.user_name === null || !user.first_name || !user.last_name) { + return res.json({ + message: 'کد تایید صحیح بود', + token, + step, + user_type: null, + page: 'auth-page', + id: user._id + }) + } + return res.json({ + message: 'کد تایید صحیح بود', + token, + user_type: user.user_type, + step, + page: 'home', + id: user._id + }) + } + + return res.status(422).json({ + error: true, + message: 'کد را اشتباه وارد کردید' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + verifyUser +} diff --git a/controllers/application/messages/messageController.js b/controllers/application/messages/messageController.js index 4ce0412..c8e5d98 100644 --- a/controllers/application/messages/messageController.js +++ b/controllers/application/messages/messageController.js @@ -1,131 +1,131 @@ -const MessageModel = require('../../../models/MessageModel') -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') -const moment = require('moment-jalaali') -const { - viewerBlockedUser, - userBlockedViewer, -} = require('../../../utils/blockVisibility') -const getMessages = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const { search } = req.query - - // پیدا کردن تمام پیام‌هایی که کاربر مورد نظر آنها فرستنده یا گیرنده بوده است - const messageFilter = { - $or: [ - { senderId: userId }, - { receiverId: userId } - ] - } - - // پیدا کردن تمام پیام‌ها با فیلتر و سورت براساس جدیدترین پیام - const messages = await MessageModel.find(messageFilter).sort({ createdAt: -1 }) - - // استخراج شناسه‌های تمام کاربران مرتبط با این پیام‌ها (بدون تکرار) - const userIdToLastMessageDate = {} - messages.forEach(message => { - if (!userIdToLastMessageDate[message.senderId]) { - userIdToLastMessageDate[message.senderId] = message.createdAt - } - if (!userIdToLastMessageDate[message.receiverId]) { - userIdToLastMessageDate[message.receiverId] = message.createdAt - } - }) - - // تبدیل مجموعه شناسه‌های کاربر به آرایه - const userIdsArray = Object.keys(userIdToLastMessageDate) - - // ساخت فیلتر جستجو برای کاربران مرتبط - const userFilter = { _id: { $in: userIdsArray } } - - if (search) { - userFilter.$and = [ - { _id: { $in: userIdsArray } }, // فقط کاربران مرتبط - { - $or: [ - { first_name: { $regex: search, $options: 'i' } }, - { last_name: { $regex: search, $options: 'i' } }, - { user_name: { $regex: search, $options: 'i' } } - ] - } - ] - } - - // پیدا کردن اطلاعات کاربران مرتبط بر اساس فیلتر جستجو و شمارش تعداد پیام‌های خوانده نشده برای هر کاربر - const usersData = await UserModel.find(userFilter) - const usersWithUnreadCount = await Promise.all(usersData.map(async (user) => { - const unreadMessagesCount = await MessageModel.countDocuments({ receiverId: userId, senderId: user._id, readStatus: 0 }) - - let lastOnlineStatus - const now = moment() - const lastOnline = moment(user.last_online) - const diffMinutes = now.diff(lastOnline, 'minutes') - - if (diffMinutes < 1) { - lastOnlineStatus = 'آنلاین' - } else if (diffMinutes < 60) { - lastOnlineStatus = `${diffMinutes} دقیقه پیش` - } else if (diffMinutes < 1440) { - lastOnlineStatus = `${Math.floor(diffMinutes / 60)} ساعت پیش` - } else { - lastOnlineStatus = `${Math.floor(diffMinutes / 1440)} روز پیش` - } - - // بررسی آیا کاربر فعلی در لیست بلاک‌شده‌های این کاربر است یا خیر - const blockedByCurrentUser = viewerBlockedUser(user, userId) - const blockedYou = userBlockedViewer(user, userId) - - return { - first_name: user.first_name, - last_name: user.last_name, - user_name: user.user_name, - is_verified: user.is_verified, - profile_image: user.profile_image, - last_online: lastOnlineStatus, - _id: user._id, - unread_messages_count: unreadMessagesCount, - is_blocked: blockedByCurrentUser, - blocked_you: blockedYou, - last_message_date: userIdToLastMessageDate[user._id] - } - })) - - // حذف کاربر فعلی از آرایه و سورت کردن کاربران براساس آخرین پیام - const filteredUsersData = usersWithUnreadCount - .filter(user => user._id.toString() !== userId) - .sort((a, b) => new Date(b.last_message_date) - new Date(a.last_message_date)) - - res.status(200).json({ - filteredUsersData, - totalMessages: messages.length - }) - } catch (error) { - next(error) - } -} - -const getUnreadMessages = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - // پیدا کردن تمام پیام‌های کاربر که هنوز خوانده نشده‌اند - const unreadMessagesCount = await MessageModel.countDocuments({ receiverId: userId, readStatus: 0 }) - - res.status(200).json({ unread_messages_count: unreadMessagesCount }) - } catch (error) { - next(error) - } -} -module.exports = { - getMessages, getUnreadMessages -} +const MessageModel = require('../../../models/MessageModel') +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') +const moment = require('moment-jalaali') +const { + viewerBlockedUser, + userBlockedViewer, +} = require('../../../utils/blockVisibility') +const getMessages = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { search } = req.query + + // پیدا کردن تمام پیام‌هایی که کاربر مورد نظر آنها فرستنده یا گیرنده بوده است + const messageFilter = { + $or: [ + { senderId: userId }, + { receiverId: userId } + ] + } + + // پیدا کردن تمام پیام‌ها با فیلتر و سورت براساس جدیدترین پیام + const messages = await MessageModel.find(messageFilter).sort({ createdAt: -1 }) + + // استخراج شناسه‌های تمام کاربران مرتبط با این پیام‌ها (بدون تکرار) + const userIdToLastMessageDate = {} + messages.forEach(message => { + if (!userIdToLastMessageDate[message.senderId]) { + userIdToLastMessageDate[message.senderId] = message.createdAt + } + if (!userIdToLastMessageDate[message.receiverId]) { + userIdToLastMessageDate[message.receiverId] = message.createdAt + } + }) + + // تبدیل مجموعه شناسه‌های کاربر به آرایه + const userIdsArray = Object.keys(userIdToLastMessageDate) + + // ساخت فیلتر جستجو برای کاربران مرتبط + const userFilter = { _id: { $in: userIdsArray } } + + if (search) { + userFilter.$and = [ + { _id: { $in: userIdsArray } }, // فقط کاربران مرتبط + { + $or: [ + { first_name: { $regex: search, $options: 'i' } }, + { last_name: { $regex: search, $options: 'i' } }, + { user_name: { $regex: search, $options: 'i' } } + ] + } + ] + } + + // پیدا کردن اطلاعات کاربران مرتبط بر اساس فیلتر جستجو و شمارش تعداد پیام‌های خوانده نشده برای هر کاربر + const usersData = await UserModel.find(userFilter) + const usersWithUnreadCount = await Promise.all(usersData.map(async (user) => { + const unreadMessagesCount = await MessageModel.countDocuments({ receiverId: userId, senderId: user._id, readStatus: 0 }) + + let lastOnlineStatus + const now = moment() + const lastOnline = moment(user.last_online) + const diffMinutes = now.diff(lastOnline, 'minutes') + + if (diffMinutes < 1) { + lastOnlineStatus = 'آنلاین' + } else if (diffMinutes < 60) { + lastOnlineStatus = `${diffMinutes} دقیقه پیش` + } else if (diffMinutes < 1440) { + lastOnlineStatus = `${Math.floor(diffMinutes / 60)} ساعت پیش` + } else { + lastOnlineStatus = `${Math.floor(diffMinutes / 1440)} روز پیش` + } + + // بررسی آیا کاربر فعلی در لیست بلاک‌شده‌های این کاربر است یا خیر + const blockedByCurrentUser = viewerBlockedUser(user, userId) + const blockedYou = userBlockedViewer(user, userId) + + return { + first_name: user.first_name, + last_name: user.last_name, + user_name: user.user_name, + is_verified: user.is_verified, + profile_image: user.profile_image, + last_online: lastOnlineStatus, + _id: user._id, + unread_messages_count: unreadMessagesCount, + is_blocked: blockedByCurrentUser, + blocked_you: blockedYou, + last_message_date: userIdToLastMessageDate[user._id] + } + })) + + // حذف کاربر فعلی از آرایه و سورت کردن کاربران براساس آخرین پیام + const filteredUsersData = usersWithUnreadCount + .filter(user => user._id.toString() !== userId) + .sort((a, b) => new Date(b.last_message_date) - new Date(a.last_message_date)) + + res.status(200).json({ + filteredUsersData, + totalMessages: messages.length + }) + } catch (error) { + next(error) + } +} + +const getUnreadMessages = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + // پیدا کردن تمام پیام‌های کاربر که هنوز خوانده نشده‌اند + const unreadMessagesCount = await MessageModel.countDocuments({ receiverId: userId, readStatus: 0 }) + + res.status(200).json({ unread_messages_count: unreadMessagesCount }) + } catch (error) { + next(error) + } +} +module.exports = { + getMessages, getUnreadMessages +} diff --git a/controllers/application/notification/notificationController.js b/controllers/application/notification/notificationController.js index 373243e..3b44cad 100644 --- a/controllers/application/notification/notificationController.js +++ b/controllers/application/notification/notificationController.js @@ -1,68 +1,68 @@ -const jwt = require('jsonwebtoken') -const NotificationModel = require('../../../models/NotificationModel') -const jMoment = require('moment-jalaali') - -const getNotifications = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const { page = 1, limit = 10, search } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست - const filter = { user_id: userId } - if (search) { - filter.$or = [ - { title: { $regex: search, $options: 'i' } }, - { description: { $regex: search, $options: 'i' } } - ] - } - // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده - const options = { - page: parseInt(page), // تبدیل صفحه به عدد صحیح - limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح - sort: { createdAt: -1 } // بر اساس زمان ایجاد (createdAt) مرتب کنید (نزولی) - } - - // دریافت اعلان‌های مربوط به کاربر با استفاده از userId - let notifications = await NotificationModel.paginate(filter, options) - const totalPages = notifications.totalPages - const totalItems = notifications.totalDocs - notifications = notifications?.docs.map(notification => { - const jDate = jMoment(notification.createdAt).format('jYYYY-jMM-jDD HH:mm') - return { - ...notification._doc, - createdAt: jDate - } - }) - // تغییر وضعیت "read" اعلان‌ها به "true" - await NotificationModel.updateMany({ user_id: userId }, { read: true }) - res.json({ - notifications, - totalPages, // ارسال تعداد کل صفحات - totalItems // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - next(error) - } -} -const unreadNotifications = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - // دریافت تعداد اعلان‌های جدید برای کاربر - const newNotificationsCount = await NotificationModel.countDocuments({ user_id: userId, read: false }).exec() - - res.json({ newNotificationsCount }) - } catch (error) { - next(error) - } -} - -module.exports = { - getNotifications, unreadNotifications -} +const jwt = require('jsonwebtoken') +const NotificationModel = require('../../../models/NotificationModel') +const jMoment = require('moment-jalaali') + +const getNotifications = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const { page = 1, limit = 10, search } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست + const filter = { user_id: userId } + if (search) { + filter.$or = [ + { title: { $regex: search, $options: 'i' } }, + { description: { $regex: search, $options: 'i' } } + ] + } + // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده + const options = { + page: parseInt(page), // تبدیل صفحه به عدد صحیح + limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح + sort: { createdAt: -1 } // بر اساس زمان ایجاد (createdAt) مرتب کنید (نزولی) + } + + // دریافت اعلان‌های مربوط به کاربر با استفاده از userId + let notifications = await NotificationModel.paginate(filter, options) + const totalPages = notifications.totalPages + const totalItems = notifications.totalDocs + notifications = notifications?.docs.map(notification => { + const jDate = jMoment(notification.createdAt).format('jYYYY-jMM-jDD HH:mm') + return { + ...notification._doc, + createdAt: jDate + } + }) + // تغییر وضعیت "read" اعلان‌ها به "true" + await NotificationModel.updateMany({ user_id: userId }, { read: true }) + res.json({ + notifications, + totalPages, // ارسال تعداد کل صفحات + totalItems // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + next(error) + } +} +const unreadNotifications = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + // دریافت تعداد اعلان‌های جدید برای کاربر + const newNotificationsCount = await NotificationModel.countDocuments({ user_id: userId, read: false }).exec() + + res.json({ newNotificationsCount }) + } catch (error) { + next(error) + } +} + +module.exports = { + getNotifications, unreadNotifications +} diff --git a/controllers/application/payment/paymentController.js b/controllers/application/payment/paymentController.js index 1c387b4..eaca055 100644 --- a/controllers/application/payment/paymentController.js +++ b/controllers/application/payment/paymentController.js @@ -1,1185 +1,1185 @@ -/* eslint-disable eqeqeq */ -const axios = require('axios') -const TypeAndPriceModel = require('../../../models/TypeAndPriceModel') -const ProjectModel = require('../../../models/ProjectModel') -const PaymentModel = require('../../../models/PaymentModel') -const jwt = require('jsonwebtoken') -const RequestModel = require('../../../models/RequestModel') -const NotificationModel = require('../../../models/NotificationModel') -const UserModel = require('../../../models/UserModel') -const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel') -const AdvertisingModel = require('../../../models/AdvertisingModel') - -const apiKey = process.env.MERCHENT_CODE // Replace with your ZarinPal API Key - -// Function to get price based on item name from database -const getPriceFromDatabase = async (itemName) => { - const item = await TypeAndPriceModel.findOne({ name: itemName }) - if (!item) { - throw new Error('Item not found in database') - } - return item.price -} -const getAdvertisingPriceFromDatabase = async (itemName) => { - const item = await AdvertisingTypeModel.findOne({ name: itemName }) - if (!item) { - throw new Error('Item not found in database') - } - return item.price -} - -// Controller function for initiating payment -const initiatePayment = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - 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 - // Save project ID for later use in callback URL - req.session.projectId = projectId - const item = await TypeAndPriceModel.findOne({ name: itemName }) - const user = await UserModel.findById(userId) - - if (item.name === 'free' && item.price === 0) { - if (user.daily_free_request > 0) { - user.daily_free_request -= 1 - await user.save() - const project = await ProjectModel.findById(projectId) - if (!project) { - res.status(404).send('Project not found') - return - } - project.payment_status = 'done' // Set project status to 'done' - project.status = 'paid' // Set project status to 'done' - await project.save() - res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', projectId, type: 'free' }) - } else { - res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) - } - } else { - const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { - MerchantID: apiKey, - Amount: Number(price), - Description: `پرداخت برای سفارش شماره ${projectId}`, - CallbackURL: `${process.env.APP_URL}/projects/payment?projectId=${projectId}&itemName=${itemName}&userId=${userId}` // Assuming your backend URL for handling payment callback - }) - - const data = response.data - if (data.Status === 100) { - // Payment request was successful - const authority = data.Authority // The payment authority (شناسه پرداخت) - res.status(200).json({ authority }) - } else { - // Payment request failed - console.error('Error occurred while initiating payment:', data) - res.status(500).json({ error: 'Error occurred while initiating payment' }) - } - } - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} - -// Controller function for handling payment callback from ZarinPal -const handlePaymentCallback = async (req, res) => { - try { - // console.log(req.query) - // Extract payment status and authority from ZarinPal callback - const status = req.query.Status - const authority = req.query.Authority - const projectId = req.query.projectId // Get project ID from session - const itemName = req.query.itemName // Get project ID from session - const userId = req.query.userId // Get project ID from session - // Clear project ID from session - // Query ZarinPal API to verify payment status - const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' - const price = await getPriceFromDatabase(itemName) // Get price from database - const verificationResponse = await axios.post(verificationUrl, { - MerchantID: apiKey, - Authority: authority, - Amount: Number(price) - }) - - const verificationData = verificationResponse.data - if (verificationData.Status === 100) { - const payment = new PaymentModel({ - amount: Number(price), - status: 'successful', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - project_id: projectId, - type: 'create', - installment_step: null - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - - // Payment verification is successful - // Now, update project status to 'done' or desired status - const project = await ProjectModel.findById(projectId) - if (!project) { - res.status(404).send('Project not found') - return - } - project.payment_status = 'done' // Set project status to 'done' - project.status = 'paid' // Set project status to 'done' - await project.save() - - // Send event based on payment status - if ('eventEmitter' in req.app) { - if (status === 'OK') { - // Payment is successful - // Emit an event to be listened to in the frontend - // Example event name: 'paymentSuccess' - // Example data: { message: 'Payment successful' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - // Payment is canceled or failed - // Emit an event to be listened to in the frontend - // Example event name: 'paymentFailed' - // Example data: { message: 'Payment canceled or failed' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) - } - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - // Redirect to the callback URL with the project ID - res.redirect(`modstagram://SuccessPay?projectId=${projectId}&price=${price}&type=create`) - } else { - // Payment verification failed - const payment = new PaymentModel({ - amount: Number(price), - status: 'failed', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - project_id: projectId, - type: 'create' - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - res.redirect(`modstagram://FailedPay?projectId=${projectId}&price=${price}&type=create`) - } - } catch (error) { - // Error occurred while verifying payment - console.error('Error occurred:', error) - res.status(500).send('Internal server error') - } -} - -// Bazaar -const handleSuccessfulPayment = async (req, res) => { - try { - const { projectId } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const project = await ProjectModel.findById(projectId) - if (!project) { - res.status(404).send('Project not found') - return - } - - const price = await getPriceFromDatabase(project.project_type) - - // Create a new payment entry - const payment = new PaymentModel({ - amount: Number(price), - status: 'successful', - authority: '', // Add authority if available - user_id: userId, - project_id: projectId, - type: 'create', - installment_step: null - }) - await payment.save() - - // Update the project status to 'done' or 'paid' - project.payment_status = 'done' - project.status = 'paid' - await project.save() - - // Emit a success event - if ('eventEmitter' in req.app) { - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - res.status(200).json({ message: 'Payment recorded successfully', projectId, price }) - } catch (error) { - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -// Controller function for handling failed payment -const handleFailedPayment = async (req, res) => { - try { - const { projectId } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const project = await ProjectModel.findById(projectId) - if (!project) { - res.status(404).send('Project not found') - return - } - - const price = await getPriceFromDatabase(project.project_type) - - // Create a new payment entry - const payment = new PaymentModel({ - amount: Number(price), - status: 'failed', - authority: '', // Add authority if available - user_id: userId, - project_id: projectId, - type: 'create' - }) - await payment.save() - - // Emit a failure event - if ('eventEmitter' in req.app) { - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment failed' }) - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - res.status(200).json({ message: 'Payment failed', projectId, price }) - } catch (error) { - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} - -const paymentRequestStepOne = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const projectId = req.body.projectId - // Find the project - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ error: 'Project not found' }) - } - - // Get the final price from the request model - const requestId = req.body.requestId - const request = await RequestModel.findById(requestId) - if (!request) { - return res.status(404).json({ error: 'Request not found' }) - } - const halfAmount = request.price / 2 - - // Proceed with ZarinPal payment request - const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { - MerchantID: apiKey, - Amount: parseInt(halfAmount + halfAmount / 100 * 9), - Description: `پرداخت برای سفارش شماره ${projectId}`, - CallbackURL: `${process.env.APP_URL}/projects/payment-request-step-one-callback?projectId=${projectId}&requestId=${requestId}&userId=${userId}` // Callback URL including requestId - }) - - const data = response.data - if (data.Status === 100) { - // Payment request was successful - const authority = data.Authority // The payment authority - res.status(200).json({ authority }) - } else { - // Payment request failed - console.error('Error occurred while initiating payment:', data) - res.status(500).json({ error: 'Error occurred while initiating payment' }) - } - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -const paymentRequestStepTwo = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const projectId = req.body.projectId - const installmentNumber = req.body.installmentNumber - // Find the project - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ error: 'Project not found' }) - } - - // Calculate the amount for the second installment (1/4 of the total price) - const totalAmount = project.final_price - const secondInstallmentAmount = totalAmount / 4 - // Proceed with ZarinPal payment request for the second installment - const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { - MerchantID: apiKey, - Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9), - Description: `پرداخت برای سفارش شماره ${projectId}`, - CallbackURL: `${process.env.APP_URL}/projects/payment-request-step-two-callback?projectId=${projectId}&userId=${userId}&installmentNumber=${installmentNumber}` // Callback URL - }) - - const data = response.data - if (data.Status === 100) { - // Payment request was successful - const authority = data.Authority // The payment authority - res.status(200).json({ authority }) - } else { - // Payment request failed - console.error('Error occurred while initiating payment:', data) - res.status(500).json({ error: 'Error occurred while initiating payment' }) - } - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -const paymentRequestStepAll = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const projectId = req.body.projectId - - // Find the project - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ error: 'Project not found' }) - } - - // Calculate the amount for the second installment (1/4 of the total price) - const totalAmount = project.final_price - const secondInstallmentAmount = totalAmount / 2 - - // Proceed with ZarinPal payment request for the second installment - const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { - MerchantID: apiKey, - Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9), - Description: `پرداخت برای سفارش شماره ${projectId}`, - CallbackURL: `${process.env.APP_URL}/projects/payment-request-step-all-callback?projectId=${projectId}&userId=${userId}` // Callback URL - }) - - const data = response.data - if (data.Status === 100) { - // Payment request was successful - const authority = data.Authority // The payment authority - res.status(200).json({ authority }) - } else { - // Payment request failed - console.error('Error occurred while initiating payment:', data) - res.status(500).json({ error: 'Error occurred while initiating payment' }) - } - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -const paymentRequestStepOneCallback = async (req, res) => { - try { - const status = req.query.Status - const authority = req.query.Authority - const projectId = req.query.projectId - const requestId = req.query.requestId - const userId = req.query.userId - - // Verify payment status with ZarinPal - const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' - const request = await RequestModel.findById(requestId) - if (!request) { - return res.status(404).json({ error: 'Request not found' }) - } - const price = request.price / 2 - const verificationResponse = await axios.post(verificationUrl, { - MerchantID: apiKey, - Authority: authority, - Amount: parseInt(price + price / 100 * 9) - }) - - const verificationData = verificationResponse.data - if (verificationData.Status === 100) { - // Payment verification is successful - const payment = new PaymentModel({ - amount: price, - status: 'successful', - authority, - user_id: userId, - project_id: projectId, - type: 'installment', - installment_step: 1 - }) - await payment.save() - // Update project's installment details - - // Update project status - request.status = 'accepted' - await request.save() - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ error: 'Project not found' }) - } - project.selected_user = request.user - project.status = 'ongoing' - project.installments.push({ installment_number: 1, amount: price, due_date: new Date() }) - project.final_price = request.price - project.final_time = request.time - await project.save() - // Send notification to selected user - const notification = new NotificationModel({ - user_id: request.user, - project_post_id: projectId, - type: 'request_accepted', - title: 'انتخاب پیشنهاد', - description: `کارفرما پیشنهاد کاری شما برای پروژه ${project.title} را پذیرفت.` - }) - await notification.save() - - // Sms - - const userReciver = await UserModel.findById(request.user) - - const data = JSON.stringify({ - mobile: userReciver?.mobile, - templateId: '302876', - parameters: [ - { name: 'USER', value: userReciver?.first_name + ' ' + userReciver?.last_name }, - { name: 'PROJECT', value: project?.title } - ] - }) - - const config = { - method: 'post', - url: 'https://api.sms.ir/v1/send/verify', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/plain', - 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' - }, - data - } - axios(config) - .then(function (response) { - }) - .catch(function (error) { - console.log(error) - }) - - // End Sms - - // Send event based on payment status - if ('eventEmitter' in req.app) { - if (status === 'OK') { - // Payment is successful - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - // Payment is canceled or failed - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) - } - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - // Redirect to the callback URL with the project ID - res.redirect(`modstagram://SuccessPay?projectId=${projectId}&price=${price}&type=steps&requestId=${requestId}`) - } else { - // Payment verification failed - const payment = new PaymentModel({ - amount: price, - status: 'failed', - authority, - user_id: userId, - project_id: projectId, - type: 'installment' - }) - await payment.save() - res.redirect(`modstagram://FailedPay?projectId=${projectId}&price=${price}&type=steps&requestId=${requestId}`) - } - } catch (error) { - console.error('Error occurred:', error) - res.status(500).send('Internal server error') - } -} - -const paymentRequestStepTwoCallback = async (req, res) => { - try { - const status = req.query.Status - const authority = req.query.Authority - const projectId = req.query.projectId - const userId = req.query.userId - const installmentNumber = req.query.installmentNumber - - // Verify payment status with ZarinPal - const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ error: 'Project not found' }) - } - const totalAmount = project.final_price - const secondInstallmentAmount = totalAmount / 4 - const verificationResponse = await axios.post(verificationUrl, { - MerchantID: apiKey, - Authority: authority, - Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9) - }) - - const verificationData = verificationResponse.data - if (verificationData.Status === 100) { - // Payment verification is successful - const payment = new PaymentModel({ - amount: secondInstallmentAmount, - status: 'successful', - authority, - user_id: userId, - project_id: projectId, - type: 'installment', - installment_step: installmentNumber == 2 ? 2 : 3 - - }) - await payment.save() - - // Update project's installment details - project.installments.push({ installment_number: 2, amount: secondInstallmentAmount, due_date: new Date() }) - await project.save() - - // Send event based on payment status - if ('eventEmitter' in req.app) { - if (status === 'OK') { - // Payment is successful - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - // Payment is canceled or failed - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) - } - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - let description - // eslint-disable-next-line eqeqeq - if (installmentNumber == 2) { - description = 'پرداخت' - } else { - description = 'کارفرما کل مبلغ پروژه را پرداخت کرد.' - } - const notification = new NotificationModel({ - user_id: project.selected_user, - project_post_id: projectId, - type: 'payment_progress', - title: description, - description - }) - await notification.save() - // Redirect to the callback URL with the project ID - res.redirect(`modstagram://SuccessPay?projectId=${projectId}&price=${secondInstallmentAmount}&type=steps`) - } else { - // Payment verification failed - const payment = new PaymentModel({ - amount: secondInstallmentAmount, - status: 'failed', - authority, - user_id: userId, - project_id: projectId, - type: 'installment' - }) - await payment.save() - res.redirect(`modstagram://FailedPay?projectId=${projectId}&price=${secondInstallmentAmount}&type=steps`) - } - } catch (error) { - console.error('Error occurred:', error) - res.status(500).send('Internal server error') - } -} -const paymentRequestStepAllCallback = async (req, res) => { - try { - const status = req.query.Status - const authority = req.query.Authority - const projectId = req.query.projectId - const userId = req.query.userId - - // Verify payment status with ZarinPal - const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ error: 'Project not found' }) - } - const totalAmount = project.final_price - const secondInstallmentAmount = totalAmount / 2 - const verificationResponse = await axios.post(verificationUrl, { - MerchantID: apiKey, - Authority: authority, - Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9) - }) - - const verificationData = verificationResponse.data - if (verificationData.Status === 100) { - // Payment verification is successful - const payment = new PaymentModel({ - amount: secondInstallmentAmount, - status: 'successful', - authority, - user_id: userId, - project_id: projectId, - type: 'installment', - installment_step: 'all' - - }) - await payment.save() - - // Update project's installment details - project.installments.push({ installment_number: 2, amount: secondInstallmentAmount, due_date: new Date() }) - await project.save() - const notification = new NotificationModel({ - user_id: project.selected_user, - project_post_id: projectId, - type: 'payment_progress', - title: 'پرداخت', - description: 'کارفرما کل مبلغ پروژه را پرداخت کرد.' - }) - await notification.save() - - // Send event based on payment status - if ('eventEmitter' in req.app) { - if (status === 'OK') { - // Payment is successful - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - // Payment is canceled or failed - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) - } - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - // Redirect to the callback URL with the project ID - res.redirect(`modstagram://SuccessPay?projectId=${projectId}&price=${secondInstallmentAmount}&type=steps`) - } else { - // Payment verification failed - const payment = new PaymentModel({ - amount: secondInstallmentAmount, - status: 'failed', - authority, - user_id: userId, - project_id: projectId, - type: 'installment' - }) - await payment.save() - res.redirect(`modstagram://FailedPay?projectId=${projectId}&price=${secondInstallmentAmount}&type=steps`) - } - } catch (error) { - console.error('Error occurred:', error) - res.status(500).send('Internal server error') - } -} - -const initiateAdvertisingPayment = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const itemName = req.body.item_name // Get item name from request body - 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 - - // 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 (user.daily_free_request > 0) { - user.daily_free_request -= 1 - await user.save() - const project = await ProjectModel.findById(advertisingId) - if (!project) { - res.status(404).send('Project not found') - return - } - project.payment_status = 'done' // Set project status to 'done' - project.status = 'paid' // Set project status to 'done' - await project.save() - res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' }) - } else { - res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) - } - } else { - const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { - MerchantID: apiKey, - Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), - Description: `پرداخت برای سفارش شماره ${advertisingId}`, - CallbackURL: `${process.env.APP_URL}/advertising/payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback - }) - const data = response.data - if (data.Status === 100) { - // Payment request was successful - const authority = data.Authority // The payment authority (شناسه پرداخت) - res.status(200).json({ authority }) - } else { - // Payment request failed - console.error('Error occurred while initiating payment:', data) - res.status(500).json({ error: 'Error occurred while initiating payment' }) - } - } - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -const handleAdvertisingPaymentCallback = async (req, res) => { - try { - // console.log(req.query) - // Extract payment status and authority from ZarinPal callback - const status = req.query.Status - const authority = req.query.Authority - const advertisingId = req.query.advertisingId // Get project ID from session - const itemName = req.query.itemName // Get project ID from session - const userId = req.query.userId // Get project ID from session - const showDiscount = req.query.showDiscount // Get project ID from session - // Clear project ID from session - // Query ZarinPal API to verify payment status - - const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' - const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database - const verificationResponse = await axios.post(verificationUrl, { - MerchantID: apiKey, - Authority: authority, - Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price) - }) - - const verificationData = verificationResponse.data - if (verificationData.Status === 100) { - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), - status: 'successful', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - advertising_id: advertisingId, - type: 'advertising', - installment_step: null - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - - // Payment verification is successful - // Now, update project status to 'done' or desired status - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - res.status(404).send('Advertising not found') - return - } - advertising.payment_status = 'done' // Set advertising status to 'done' - advertising.status = 'paid' // Set advertising status to 'done' - await advertising.save() - - // Send event based on payment status - if ('eventEmitter' in req.app) { - if (status === 'OK') { - // Payment is successful - // Emit an event to be listened to in the frontend - // Example event name: 'paymentSuccess' - // Example data: { message: 'Payment successful' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - // Payment is canceled or failed - // Emit an event to be listened to in the frontend - // Example event name: 'paymentFailed' - // Example data: { message: 'Payment canceled or failed' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) - } - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - // Redirect to the callback URL with the advertising ID - res.redirect(`modstagram://SuccessPay?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)}&type=advertising&showDiscount=${showDiscount}`) - } else { - // Payment verification failed - const payment = new PaymentModel({ - amount: showDiscount ? Number(price + 20000) : Number(price), - status: 'failed', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - advertising_id: advertisingId, - type: 'advertising' - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - res.redirect(`modstagram://FailedPay?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)}&type=advertising&showDiscount=${showDiscount}`) - } - } catch (error) { - // Error occurred while verifying payment - console.error('Error occurred:', error) - res.status(500).send('Internal server error') - } -} - -// Controller function for handling successful advertising payment -const handleSuccessfulAdvertisingPayment = async (req, res) => { - try { - const { advertisingId } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const showDiscount = req.query.showDiscount // Get project ID from session - - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - res.status(404).send('Advertising not found') - return - } - - const price = await getAdvertisingPriceFromDatabase(advertising.type) - - // Create a new payment entry - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), - status: 'successful', - authority: '', // Add authority if available - user_id: userId, - advertising_id: advertisingId, - type: 'advertising', - installment_step: null - }) - await payment.save() - - // Update the advertising status to 'done' or 'paid' - advertising.payment_status = 'done' - advertising.status = 'paid' - await advertising.save() - - // Emit a success event - if ('eventEmitter' in req.app) { - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - res.status(200).json({ message: 'Payment recorded successfully', advertisingId, price }) - } catch (error) { - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -// Controller function for handling failed advertising payment -const handleFailedAdvertisingPayment = async (req, res) => { - try { - const { advertisingId } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const showDiscount = req.query.showDiscount // Get project ID from session - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - res.status(404).send('Advertising not found') - return - } - - const price = await getAdvertisingPriceFromDatabase(advertising.type) - - // Create a new payment entry - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), - status: 'failed', - authority: '', // Add authority if available - user_id: userId, - advertising_id: advertisingId, - type: 'advertising' - }) - await payment.save() - - // Emit a failure event - if ('eventEmitter' in req.app) { - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment failed' }) - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - res.status(200).json({ message: 'Payment failed', advertisingId, price }) - } catch (error) { - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} - -const republishAdvertisingPayment = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const itemName = req.body.item_name // Get item name from request body - 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 - // 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 (user.daily_free_request > 0) { - user.daily_free_request -= 1 - await user.save() - const project = await ProjectModel.findById(advertisingId) - if (!project) { - res.status(404).send('Project not found') - return - } - project.payment_status = 'done' // Set project status to 'done' - project.status = 'paid' // Set project status to 'done' - await project.save() - res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' }) - } else { - res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) - } - } else { - const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { - MerchantID: apiKey, - Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), - Description: `پرداخت برای سفارش شماره ${advertisingId}`, - CallbackURL: `${process.env.APP_URL}/advertising/republish-verify-payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback - }) - const data = response.data - if (data.Status === 100) { - // Payment request was successful - const authority = data.Authority // The payment authority (شناسه پرداخت) - res.status(200).json({ authority }) - } else { - // Payment request failed - console.error('Error occurred while initiating payment:', data) - res.status(500).json({ error: 'Error occurred while initiating payment' }) - } - } - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -const handleAdvertisingRepublishPaymentCallback = async (req, res) => { - try { - // console.log(req.query) - // Extract payment status and authority from ZarinPal callback - const status = req.query.Status - const authority = req.query.Authority - const advertisingId = req.query.advertisingId // Get project ID from session - const itemName = req.query.itemName // Get project ID from session - const userId = req.query.userId // Get project ID from session - const showDiscount = req.query.showDiscount // Get project ID from session - // Clear project ID from session - // Query ZarinPal API to verify payment status - const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' - const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database - const verificationResponse = await axios.post(verificationUrl, { - MerchantID: apiKey, - Authority: authority, - Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100) - }) - - const verificationData = verificationResponse.data - if (verificationData.Status === 100) { - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), - status: 'successful', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - advertising_id: advertisingId, - type: 'advertising', - installment_step: null - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - - // Payment verification is successful - // Now, update project status to 'done' or desired status - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - res.status(404).send('Advertising not found') - return - } - advertising.payment_status = 'done' // Set advertising status to 'done' - advertising.status = 'accepted' // Set advertising status to 'done' - advertising.acceptedAt = new Date() - await advertising.save() - - // Send event based on payment status - if ('eventEmitter' in req.app) { - if (status === 'OK') { - // Payment is successful - // Emit an event to be listened to in the frontend - // Example event name: 'paymentSuccess' - // Example data: { message: 'Payment successful' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - // Payment is canceled or failed - // Emit an event to be listened to in the frontend - // Example event name: 'paymentFailed' - // Example data: { message: 'Payment canceled or failed' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) - } - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - // Redirect to the callback URL with the advertising ID - res.redirect(`modstagram://SuccessPay?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)}&type=advertising-republish&showDiscount=${showDiscount}`) - } else { - // Payment verification failed - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), - status: 'failed', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - advertising_id: advertisingId, - type: 'advertising' - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - res.redirect(`modstagram://FailedPay?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)}&type=advertising-republish&showDiscount=${showDiscount}`) - } - } catch (error) { - // Error occurred while verifying payment - console.error('Error occurred:', error) - res.status(500).send('Internal server error') - } -} -// Controller function for handling successful republish advertising payment -const handleSuccessfulAdvertisingRepublishPayment = async (req, res) => { - try { - const { advertisingId } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const showDiscount = req.query.showDiscount - - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - res.status(404).send('Advertising not found') - return - } - - const price = await getAdvertisingPriceFromDatabase(advertising.type) - - // Create a new payment entry - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number((price + 20000) * 0.7) : Number(price * 0.7), - status: 'successful', - authority: '', // Add authority if available - user_id: userId, - advertising_id: advertisingId, - type: 'advertising-republish', - installment_step: null - }) - await payment.save() - - // Update the advertising status to 'done' or 'accepted' - advertising.payment_status = 'done' - advertising.status = 'accepted' - advertising.acceptedAt = new Date() - await advertising.save() - - // Emit a success event - if ('eventEmitter' in req.app) { - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Republish payment successful' }) - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - res.status(200).json({ message: 'Republish payment recorded successfully', advertisingId, price }) - } catch (error) { - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -// Controller function for handling failed republish advertising payment -const handleFailedAdvertisingRepublishPayment = async (req, res) => { - try { - const { advertisingId } = req.body - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const showDiscount = req.query.showDiscount - - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - res.status(404).send('Advertising not found') - return - } - - const price = await getAdvertisingPriceFromDatabase(advertising.type) - - // Create a new payment entry - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number((price + 20000) * 0.7) : Number(price * 0.7), - status: 'failed', - authority: '', // Add authority if available - user_id: userId, - advertising_id: advertisingId, - type: 'advertising-republish' - }) - await payment.save() - - // Emit a failure event - if ('eventEmitter' in req.app) { - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Republish payment failed' }) - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - res.status(200).json({ message: 'Republish payment failed', advertisingId, price }) - } catch (error) { - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} - -module.exports = { - initiatePayment, - handlePaymentCallback, - paymentRequestStepOne, - paymentRequestStepTwo, - paymentRequestStepAll, - paymentRequestStepOneCallback, - paymentRequestStepTwoCallback, - paymentRequestStepAllCallback, - initiateAdvertisingPayment, - handleAdvertisingPaymentCallback, - republishAdvertisingPayment, - handleAdvertisingRepublishPaymentCallback, - handleSuccessfulPayment, - handleFailedPayment, - handleSuccessfulAdvertisingPayment, - handleFailedAdvertisingPayment, - handleSuccessfulAdvertisingRepublishPayment, - handleFailedAdvertisingRepublishPayment -} +/* eslint-disable eqeqeq */ +const axios = require('axios') +const TypeAndPriceModel = require('../../../models/TypeAndPriceModel') +const ProjectModel = require('../../../models/ProjectModel') +const PaymentModel = require('../../../models/PaymentModel') +const jwt = require('jsonwebtoken') +const RequestModel = require('../../../models/RequestModel') +const NotificationModel = require('../../../models/NotificationModel') +const UserModel = require('../../../models/UserModel') +const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel') +const AdvertisingModel = require('../../../models/AdvertisingModel') + +const apiKey = process.env.MERCHENT_CODE // Replace with your ZarinPal API Key + +// Function to get price based on item name from database +const getPriceFromDatabase = async (itemName) => { + const item = await TypeAndPriceModel.findOne({ name: itemName }) + if (!item) { + throw new Error('Item not found in database') + } + return item.price +} +const getAdvertisingPriceFromDatabase = async (itemName) => { + const item = await AdvertisingTypeModel.findOne({ name: itemName }) + if (!item) { + throw new Error('Item not found in database') + } + return item.price +} + +// Controller function for initiating payment +const initiatePayment = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + 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 + // Save project ID for later use in callback URL + req.session.projectId = projectId + const item = await TypeAndPriceModel.findOne({ name: itemName }) + const user = await UserModel.findById(userId) + + if (item.name === 'free' && item.price === 0) { + if (user.daily_free_request > 0) { + user.daily_free_request -= 1 + await user.save() + const project = await ProjectModel.findById(projectId) + if (!project) { + res.status(404).send('Project not found') + return + } + project.payment_status = 'done' // Set project status to 'done' + project.status = 'paid' // Set project status to 'done' + await project.save() + res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', projectId, type: 'free' }) + } else { + res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) + } + } else { + const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { + MerchantID: apiKey, + Amount: Number(price), + Description: `پرداخت برای سفارش شماره ${projectId}`, + CallbackURL: `${process.env.APP_URL}/projects/payment?projectId=${projectId}&itemName=${itemName}&userId=${userId}` // Assuming your backend URL for handling payment callback + }) + + const data = response.data + if (data.Status === 100) { + // Payment request was successful + const authority = data.Authority // The payment authority (شناسه پرداخت) + res.status(200).json({ authority }) + } else { + // Payment request failed + console.error('Error occurred while initiating payment:', data) + res.status(500).json({ error: 'Error occurred while initiating payment' }) + } + } + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} + +// Controller function for handling payment callback from ZarinPal +const handlePaymentCallback = async (req, res) => { + try { + // console.log(req.query) + // Extract payment status and authority from ZarinPal callback + const status = req.query.Status + const authority = req.query.Authority + const projectId = req.query.projectId // Get project ID from session + const itemName = req.query.itemName // Get project ID from session + const userId = req.query.userId // Get project ID from session + // Clear project ID from session + // Query ZarinPal API to verify payment status + const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' + const price = await getPriceFromDatabase(itemName) // Get price from database + const verificationResponse = await axios.post(verificationUrl, { + MerchantID: apiKey, + Authority: authority, + Amount: Number(price) + }) + + const verificationData = verificationResponse.data + if (verificationData.Status === 100) { + const payment = new PaymentModel({ + amount: Number(price), + status: 'successful', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + project_id: projectId, + type: 'create', + installment_step: null + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + + // Payment verification is successful + // Now, update project status to 'done' or desired status + const project = await ProjectModel.findById(projectId) + if (!project) { + res.status(404).send('Project not found') + return + } + project.payment_status = 'done' // Set project status to 'done' + project.status = 'paid' // Set project status to 'done' + await project.save() + + // Send event based on payment status + if ('eventEmitter' in req.app) { + if (status === 'OK') { + // Payment is successful + // Emit an event to be listened to in the frontend + // Example event name: 'paymentSuccess' + // Example data: { message: 'Payment successful' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + // Payment is canceled or failed + // Emit an event to be listened to in the frontend + // Example event name: 'paymentFailed' + // Example data: { message: 'Payment canceled or failed' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) + } + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + // Redirect to the callback URL with the project ID + res.redirect(`modstagram://SuccessPay?projectId=${projectId}&price=${price}&type=create`) + } else { + // Payment verification failed + const payment = new PaymentModel({ + amount: Number(price), + status: 'failed', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + project_id: projectId, + type: 'create' + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + res.redirect(`modstagram://FailedPay?projectId=${projectId}&price=${price}&type=create`) + } + } catch (error) { + // Error occurred while verifying payment + console.error('Error occurred:', error) + res.status(500).send('Internal server error') + } +} + +// Bazaar +const handleSuccessfulPayment = async (req, res) => { + try { + const { projectId } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const project = await ProjectModel.findById(projectId) + if (!project) { + res.status(404).send('Project not found') + return + } + + const price = await getPriceFromDatabase(project.project_type) + + // Create a new payment entry + const payment = new PaymentModel({ + amount: Number(price), + status: 'successful', + authority: '', // Add authority if available + user_id: userId, + project_id: projectId, + type: 'create', + installment_step: null + }) + await payment.save() + + // Update the project status to 'done' or 'paid' + project.payment_status = 'done' + project.status = 'paid' + await project.save() + + // Emit a success event + if ('eventEmitter' in req.app) { + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + res.status(200).json({ message: 'Payment recorded successfully', projectId, price }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +// Controller function for handling failed payment +const handleFailedPayment = async (req, res) => { + try { + const { projectId } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const project = await ProjectModel.findById(projectId) + if (!project) { + res.status(404).send('Project not found') + return + } + + const price = await getPriceFromDatabase(project.project_type) + + // Create a new payment entry + const payment = new PaymentModel({ + amount: Number(price), + status: 'failed', + authority: '', // Add authority if available + user_id: userId, + project_id: projectId, + type: 'create' + }) + await payment.save() + + // Emit a failure event + if ('eventEmitter' in req.app) { + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment failed' }) + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + res.status(200).json({ message: 'Payment failed', projectId, price }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} + +const paymentRequestStepOne = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const projectId = req.body.projectId + // Find the project + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ error: 'Project not found' }) + } + + // Get the final price from the request model + const requestId = req.body.requestId + const request = await RequestModel.findById(requestId) + if (!request) { + return res.status(404).json({ error: 'Request not found' }) + } + const halfAmount = request.price / 2 + + // Proceed with ZarinPal payment request + const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { + MerchantID: apiKey, + Amount: parseInt(halfAmount + halfAmount / 100 * 9), + Description: `پرداخت برای سفارش شماره ${projectId}`, + CallbackURL: `${process.env.APP_URL}/projects/payment-request-step-one-callback?projectId=${projectId}&requestId=${requestId}&userId=${userId}` // Callback URL including requestId + }) + + const data = response.data + if (data.Status === 100) { + // Payment request was successful + const authority = data.Authority // The payment authority + res.status(200).json({ authority }) + } else { + // Payment request failed + console.error('Error occurred while initiating payment:', data) + res.status(500).json({ error: 'Error occurred while initiating payment' }) + } + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +const paymentRequestStepTwo = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const projectId = req.body.projectId + const installmentNumber = req.body.installmentNumber + // Find the project + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ error: 'Project not found' }) + } + + // Calculate the amount for the second installment (1/4 of the total price) + const totalAmount = project.final_price + const secondInstallmentAmount = totalAmount / 4 + // Proceed with ZarinPal payment request for the second installment + const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { + MerchantID: apiKey, + Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9), + Description: `پرداخت برای سفارش شماره ${projectId}`, + CallbackURL: `${process.env.APP_URL}/projects/payment-request-step-two-callback?projectId=${projectId}&userId=${userId}&installmentNumber=${installmentNumber}` // Callback URL + }) + + const data = response.data + if (data.Status === 100) { + // Payment request was successful + const authority = data.Authority // The payment authority + res.status(200).json({ authority }) + } else { + // Payment request failed + console.error('Error occurred while initiating payment:', data) + res.status(500).json({ error: 'Error occurred while initiating payment' }) + } + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +const paymentRequestStepAll = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const projectId = req.body.projectId + + // Find the project + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ error: 'Project not found' }) + } + + // Calculate the amount for the second installment (1/4 of the total price) + const totalAmount = project.final_price + const secondInstallmentAmount = totalAmount / 2 + + // Proceed with ZarinPal payment request for the second installment + const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { + MerchantID: apiKey, + Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9), + Description: `پرداخت برای سفارش شماره ${projectId}`, + CallbackURL: `${process.env.APP_URL}/projects/payment-request-step-all-callback?projectId=${projectId}&userId=${userId}` // Callback URL + }) + + const data = response.data + if (data.Status === 100) { + // Payment request was successful + const authority = data.Authority // The payment authority + res.status(200).json({ authority }) + } else { + // Payment request failed + console.error('Error occurred while initiating payment:', data) + res.status(500).json({ error: 'Error occurred while initiating payment' }) + } + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +const paymentRequestStepOneCallback = async (req, res) => { + try { + const status = req.query.Status + const authority = req.query.Authority + const projectId = req.query.projectId + const requestId = req.query.requestId + const userId = req.query.userId + + // Verify payment status with ZarinPal + const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' + const request = await RequestModel.findById(requestId) + if (!request) { + return res.status(404).json({ error: 'Request not found' }) + } + const price = request.price / 2 + const verificationResponse = await axios.post(verificationUrl, { + MerchantID: apiKey, + Authority: authority, + Amount: parseInt(price + price / 100 * 9) + }) + + const verificationData = verificationResponse.data + if (verificationData.Status === 100) { + // Payment verification is successful + const payment = new PaymentModel({ + amount: price, + status: 'successful', + authority, + user_id: userId, + project_id: projectId, + type: 'installment', + installment_step: 1 + }) + await payment.save() + // Update project's installment details + + // Update project status + request.status = 'accepted' + await request.save() + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ error: 'Project not found' }) + } + project.selected_user = request.user + project.status = 'ongoing' + project.installments.push({ installment_number: 1, amount: price, due_date: new Date() }) + project.final_price = request.price + project.final_time = request.time + await project.save() + // Send notification to selected user + const notification = new NotificationModel({ + user_id: request.user, + project_post_id: projectId, + type: 'request_accepted', + title: 'انتخاب پیشنهاد', + description: `کارفرما پیشنهاد کاری شما برای پروژه ${project.title} را پذیرفت.` + }) + await notification.save() + + // Sms + + const userReciver = await UserModel.findById(request.user) + + const data = JSON.stringify({ + mobile: userReciver?.mobile, + templateId: '302876', + parameters: [ + { name: 'USER', value: userReciver?.first_name + ' ' + userReciver?.last_name }, + { name: 'PROJECT', value: project?.title } + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + axios(config) + .then(function (response) { + }) + .catch(function (error) { + console.log(error) + }) + + // End Sms + + // Send event based on payment status + if ('eventEmitter' in req.app) { + if (status === 'OK') { + // Payment is successful + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + // Payment is canceled or failed + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) + } + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + // Redirect to the callback URL with the project ID + res.redirect(`modstagram://SuccessPay?projectId=${projectId}&price=${price}&type=steps&requestId=${requestId}`) + } else { + // Payment verification failed + const payment = new PaymentModel({ + amount: price, + status: 'failed', + authority, + user_id: userId, + project_id: projectId, + type: 'installment' + }) + await payment.save() + res.redirect(`modstagram://FailedPay?projectId=${projectId}&price=${price}&type=steps&requestId=${requestId}`) + } + } catch (error) { + console.error('Error occurred:', error) + res.status(500).send('Internal server error') + } +} + +const paymentRequestStepTwoCallback = async (req, res) => { + try { + const status = req.query.Status + const authority = req.query.Authority + const projectId = req.query.projectId + const userId = req.query.userId + const installmentNumber = req.query.installmentNumber + + // Verify payment status with ZarinPal + const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ error: 'Project not found' }) + } + const totalAmount = project.final_price + const secondInstallmentAmount = totalAmount / 4 + const verificationResponse = await axios.post(verificationUrl, { + MerchantID: apiKey, + Authority: authority, + Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9) + }) + + const verificationData = verificationResponse.data + if (verificationData.Status === 100) { + // Payment verification is successful + const payment = new PaymentModel({ + amount: secondInstallmentAmount, + status: 'successful', + authority, + user_id: userId, + project_id: projectId, + type: 'installment', + installment_step: installmentNumber == 2 ? 2 : 3 + + }) + await payment.save() + + // Update project's installment details + project.installments.push({ installment_number: 2, amount: secondInstallmentAmount, due_date: new Date() }) + await project.save() + + // Send event based on payment status + if ('eventEmitter' in req.app) { + if (status === 'OK') { + // Payment is successful + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + // Payment is canceled or failed + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) + } + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + let description + // eslint-disable-next-line eqeqeq + if (installmentNumber == 2) { + description = 'پرداخت' + } else { + description = 'کارفرما کل مبلغ پروژه را پرداخت کرد.' + } + const notification = new NotificationModel({ + user_id: project.selected_user, + project_post_id: projectId, + type: 'payment_progress', + title: description, + description + }) + await notification.save() + // Redirect to the callback URL with the project ID + res.redirect(`modstagram://SuccessPay?projectId=${projectId}&price=${secondInstallmentAmount}&type=steps`) + } else { + // Payment verification failed + const payment = new PaymentModel({ + amount: secondInstallmentAmount, + status: 'failed', + authority, + user_id: userId, + project_id: projectId, + type: 'installment' + }) + await payment.save() + res.redirect(`modstagram://FailedPay?projectId=${projectId}&price=${secondInstallmentAmount}&type=steps`) + } + } catch (error) { + console.error('Error occurred:', error) + res.status(500).send('Internal server error') + } +} +const paymentRequestStepAllCallback = async (req, res) => { + try { + const status = req.query.Status + const authority = req.query.Authority + const projectId = req.query.projectId + const userId = req.query.userId + + // Verify payment status with ZarinPal + const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ error: 'Project not found' }) + } + const totalAmount = project.final_price + const secondInstallmentAmount = totalAmount / 2 + const verificationResponse = await axios.post(verificationUrl, { + MerchantID: apiKey, + Authority: authority, + Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9) + }) + + const verificationData = verificationResponse.data + if (verificationData.Status === 100) { + // Payment verification is successful + const payment = new PaymentModel({ + amount: secondInstallmentAmount, + status: 'successful', + authority, + user_id: userId, + project_id: projectId, + type: 'installment', + installment_step: 'all' + + }) + await payment.save() + + // Update project's installment details + project.installments.push({ installment_number: 2, amount: secondInstallmentAmount, due_date: new Date() }) + await project.save() + const notification = new NotificationModel({ + user_id: project.selected_user, + project_post_id: projectId, + type: 'payment_progress', + title: 'پرداخت', + description: 'کارفرما کل مبلغ پروژه را پرداخت کرد.' + }) + await notification.save() + + // Send event based on payment status + if ('eventEmitter' in req.app) { + if (status === 'OK') { + // Payment is successful + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + // Payment is canceled or failed + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) + } + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + // Redirect to the callback URL with the project ID + res.redirect(`modstagram://SuccessPay?projectId=${projectId}&price=${secondInstallmentAmount}&type=steps`) + } else { + // Payment verification failed + const payment = new PaymentModel({ + amount: secondInstallmentAmount, + status: 'failed', + authority, + user_id: userId, + project_id: projectId, + type: 'installment' + }) + await payment.save() + res.redirect(`modstagram://FailedPay?projectId=${projectId}&price=${secondInstallmentAmount}&type=steps`) + } + } catch (error) { + console.error('Error occurred:', error) + res.status(500).send('Internal server error') + } +} + +const initiateAdvertisingPayment = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const itemName = req.body.item_name // Get item name from request body + 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 + + // 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 (user.daily_free_request > 0) { + user.daily_free_request -= 1 + await user.save() + const project = await ProjectModel.findById(advertisingId) + if (!project) { + res.status(404).send('Project not found') + return + } + project.payment_status = 'done' // Set project status to 'done' + project.status = 'paid' // Set project status to 'done' + await project.save() + res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' }) + } else { + res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) + } + } else { + const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { + MerchantID: apiKey, + Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), + Description: `پرداخت برای سفارش شماره ${advertisingId}`, + CallbackURL: `${process.env.APP_URL}/advertising/payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback + }) + const data = response.data + if (data.Status === 100) { + // Payment request was successful + const authority = data.Authority // The payment authority (شناسه پرداخت) + res.status(200).json({ authority }) + } else { + // Payment request failed + console.error('Error occurred while initiating payment:', data) + res.status(500).json({ error: 'Error occurred while initiating payment' }) + } + } + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +const handleAdvertisingPaymentCallback = async (req, res) => { + try { + // console.log(req.query) + // Extract payment status and authority from ZarinPal callback + const status = req.query.Status + const authority = req.query.Authority + const advertisingId = req.query.advertisingId // Get project ID from session + const itemName = req.query.itemName // Get project ID from session + const userId = req.query.userId // Get project ID from session + const showDiscount = req.query.showDiscount // Get project ID from session + // Clear project ID from session + // Query ZarinPal API to verify payment status + + const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' + const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database + const verificationResponse = await axios.post(verificationUrl, { + MerchantID: apiKey, + Authority: authority, + Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price) + }) + + const verificationData = verificationResponse.data + if (verificationData.Status === 100) { + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), + status: 'successful', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + advertising_id: advertisingId, + type: 'advertising', + installment_step: null + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + + // Payment verification is successful + // Now, update project status to 'done' or desired status + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + res.status(404).send('Advertising not found') + return + } + advertising.payment_status = 'done' // Set advertising status to 'done' + advertising.status = 'paid' // Set advertising status to 'done' + await advertising.save() + + // Send event based on payment status + if ('eventEmitter' in req.app) { + if (status === 'OK') { + // Payment is successful + // Emit an event to be listened to in the frontend + // Example event name: 'paymentSuccess' + // Example data: { message: 'Payment successful' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + // Payment is canceled or failed + // Emit an event to be listened to in the frontend + // Example event name: 'paymentFailed' + // Example data: { message: 'Payment canceled or failed' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) + } + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + // Redirect to the callback URL with the advertising ID + res.redirect(`modstagram://SuccessPay?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)}&type=advertising&showDiscount=${showDiscount}`) + } else { + // Payment verification failed + const payment = new PaymentModel({ + amount: showDiscount ? Number(price + 20000) : Number(price), + status: 'failed', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + advertising_id: advertisingId, + type: 'advertising' + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + res.redirect(`modstagram://FailedPay?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)}&type=advertising&showDiscount=${showDiscount}`) + } + } catch (error) { + // Error occurred while verifying payment + console.error('Error occurred:', error) + res.status(500).send('Internal server error') + } +} + +// Controller function for handling successful advertising payment +const handleSuccessfulAdvertisingPayment = async (req, res) => { + try { + const { advertisingId } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const showDiscount = req.query.showDiscount // Get project ID from session + + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + res.status(404).send('Advertising not found') + return + } + + const price = await getAdvertisingPriceFromDatabase(advertising.type) + + // Create a new payment entry + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), + status: 'successful', + authority: '', // Add authority if available + user_id: userId, + advertising_id: advertisingId, + type: 'advertising', + installment_step: null + }) + await payment.save() + + // Update the advertising status to 'done' or 'paid' + advertising.payment_status = 'done' + advertising.status = 'paid' + await advertising.save() + + // Emit a success event + if ('eventEmitter' in req.app) { + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + res.status(200).json({ message: 'Payment recorded successfully', advertisingId, price }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +// Controller function for handling failed advertising payment +const handleFailedAdvertisingPayment = async (req, res) => { + try { + const { advertisingId } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const showDiscount = req.query.showDiscount // Get project ID from session + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + res.status(404).send('Advertising not found') + return + } + + const price = await getAdvertisingPriceFromDatabase(advertising.type) + + // Create a new payment entry + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), + status: 'failed', + authority: '', // Add authority if available + user_id: userId, + advertising_id: advertisingId, + type: 'advertising' + }) + await payment.save() + + // Emit a failure event + if ('eventEmitter' in req.app) { + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment failed' }) + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + res.status(200).json({ message: 'Payment failed', advertisingId, price }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} + +const republishAdvertisingPayment = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const itemName = req.body.item_name // Get item name from request body + 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 + // 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 (user.daily_free_request > 0) { + user.daily_free_request -= 1 + await user.save() + const project = await ProjectModel.findById(advertisingId) + if (!project) { + res.status(404).send('Project not found') + return + } + project.payment_status = 'done' // Set project status to 'done' + project.status = 'paid' // Set project status to 'done' + await project.save() + res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' }) + } else { + res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) + } + } else { + const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { + MerchantID: apiKey, + Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), + Description: `پرداخت برای سفارش شماره ${advertisingId}`, + CallbackURL: `${process.env.APP_URL}/advertising/republish-verify-payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback + }) + const data = response.data + if (data.Status === 100) { + // Payment request was successful + const authority = data.Authority // The payment authority (شناسه پرداخت) + res.status(200).json({ authority }) + } else { + // Payment request failed + console.error('Error occurred while initiating payment:', data) + res.status(500).json({ error: 'Error occurred while initiating payment' }) + } + } + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +const handleAdvertisingRepublishPaymentCallback = async (req, res) => { + try { + // console.log(req.query) + // Extract payment status and authority from ZarinPal callback + const status = req.query.Status + const authority = req.query.Authority + const advertisingId = req.query.advertisingId // Get project ID from session + const itemName = req.query.itemName // Get project ID from session + const userId = req.query.userId // Get project ID from session + const showDiscount = req.query.showDiscount // Get project ID from session + // Clear project ID from session + // Query ZarinPal API to verify payment status + const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' + const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database + const verificationResponse = await axios.post(verificationUrl, { + MerchantID: apiKey, + Authority: authority, + Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100) + }) + + const verificationData = verificationResponse.data + if (verificationData.Status === 100) { + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), + status: 'successful', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + advertising_id: advertisingId, + type: 'advertising', + installment_step: null + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + + // Payment verification is successful + // Now, update project status to 'done' or desired status + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + res.status(404).send('Advertising not found') + return + } + advertising.payment_status = 'done' // Set advertising status to 'done' + advertising.status = 'accepted' // Set advertising status to 'done' + advertising.acceptedAt = new Date() + await advertising.save() + + // Send event based on payment status + if ('eventEmitter' in req.app) { + if (status === 'OK') { + // Payment is successful + // Emit an event to be listened to in the frontend + // Example event name: 'paymentSuccess' + // Example data: { message: 'Payment successful' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + // Payment is canceled or failed + // Emit an event to be listened to in the frontend + // Example event name: 'paymentFailed' + // Example data: { message: 'Payment canceled or failed' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) + } + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + // Redirect to the callback URL with the advertising ID + res.redirect(`modstagram://SuccessPay?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)}&type=advertising-republish&showDiscount=${showDiscount}`) + } else { + // Payment verification failed + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), + status: 'failed', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + advertising_id: advertisingId, + type: 'advertising' + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + res.redirect(`modstagram://FailedPay?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)}&type=advertising-republish&showDiscount=${showDiscount}`) + } + } catch (error) { + // Error occurred while verifying payment + console.error('Error occurred:', error) + res.status(500).send('Internal server error') + } +} +// Controller function for handling successful republish advertising payment +const handleSuccessfulAdvertisingRepublishPayment = async (req, res) => { + try { + const { advertisingId } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const showDiscount = req.query.showDiscount + + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + res.status(404).send('Advertising not found') + return + } + + const price = await getAdvertisingPriceFromDatabase(advertising.type) + + // Create a new payment entry + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number((price + 20000) * 0.7) : Number(price * 0.7), + status: 'successful', + authority: '', // Add authority if available + user_id: userId, + advertising_id: advertisingId, + type: 'advertising-republish', + installment_step: null + }) + await payment.save() + + // Update the advertising status to 'done' or 'accepted' + advertising.payment_status = 'done' + advertising.status = 'accepted' + advertising.acceptedAt = new Date() + await advertising.save() + + // Emit a success event + if ('eventEmitter' in req.app) { + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Republish payment successful' }) + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + res.status(200).json({ message: 'Republish payment recorded successfully', advertisingId, price }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +// Controller function for handling failed republish advertising payment +const handleFailedAdvertisingRepublishPayment = async (req, res) => { + try { + const { advertisingId } = req.body + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const showDiscount = req.query.showDiscount + + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + res.status(404).send('Advertising not found') + return + } + + const price = await getAdvertisingPriceFromDatabase(advertising.type) + + // Create a new payment entry + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number((price + 20000) * 0.7) : Number(price * 0.7), + status: 'failed', + authority: '', // Add authority if available + user_id: userId, + advertising_id: advertisingId, + type: 'advertising-republish' + }) + await payment.save() + + // Emit a failure event + if ('eventEmitter' in req.app) { + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Republish payment failed' }) + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + res.status(200).json({ message: 'Republish payment failed', advertisingId, price }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} + +module.exports = { + initiatePayment, + handlePaymentCallback, + paymentRequestStepOne, + paymentRequestStepTwo, + paymentRequestStepAll, + paymentRequestStepOneCallback, + paymentRequestStepTwoCallback, + paymentRequestStepAllCallback, + initiateAdvertisingPayment, + handleAdvertisingPaymentCallback, + republishAdvertisingPayment, + handleAdvertisingRepublishPaymentCallback, + handleSuccessfulPayment, + handleFailedPayment, + handleSuccessfulAdvertisingPayment, + handleFailedAdvertisingPayment, + handleSuccessfulAdvertisingRepublishPayment, + handleFailedAdvertisingRepublishPayment +} diff --git a/controllers/application/payment/paymentControllerWeb.js b/controllers/application/payment/paymentControllerWeb.js index 0746d65..d409670 100644 --- a/controllers/application/payment/paymentControllerWeb.js +++ b/controllers/application/payment/paymentControllerWeb.js @@ -1,935 +1,935 @@ -/* eslint-disable eqeqeq */ - -const TypeAndPriceModel = require('../../../models/TypeAndPriceModel'); -const ProjectModel = require('../../../models/ProjectModel'); -const PaymentModel = require('../../../models/PaymentModel'); -const jwt = require('jsonwebtoken'); -const RequestModel = require('../../../models/RequestModel'); -const NotificationModel = require('../../../models/NotificationModel'); -const UserModel = require('../../../models/UserModel'); -const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel'); -const AdvertisingModel = require('../../../models/AdvertisingModel'); -const OfferTypeModel = require('../../../models/OfferTypeModel'); -const OfferModel = require('../../../models/OfferModel'); -const { default: axios } = require('axios'); - -const apiKey = "c7c41e8a-918f-4741-bcd5-58f3bc51db73"; // اصلاح نام متغیر محیطی - -// Function to get price based on item name from database -const getPriceFromDatabase = async (itemName) => { - const item = await TypeAndPriceModel.findOne({ name: itemName }); - if (!item) { - throw new Error(`Item "${itemName}" not found in TypeAndPriceModel`); - } - return item.price; -}; - -const getAdvertisingPriceFromDatabase = async (itemName) => { - const item = await AdvertisingTypeModel.findOne({ name: itemName }); - if (!item) { - throw new Error(`Item "${itemName}" not found in AdvertisingTypeModel`); - } - return item.price; -}; - -const getOfferPriceFromDatabase = async (offerType) => { - const item = await OfferTypeModel.findOne({ name: offerType }); - if (!item) { - throw new Error(`Offer type "${offerType}" not found in OfferTypeModel`); - } - return item.price; -}; -// Controller function for initiating payment -const initiatePaymentWeb = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - 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 - // Save project ID for later use in callback URL - req.session.projectId = projectId - const item = await TypeAndPriceModel.findOne({ name: itemName }) - const user = await UserModel.findById(userId) - - if (item.name === 'free' && item.price === 0) { - if (user.daily_free_request > 0) { - user.daily_free_request -= 1 - await user.save() - const project = await ProjectModel.findById(projectId) - if (!project) { - res.status(404).send('Project not found') - return - } - project.payment_status = 'done' // Set project status to 'done' - project.status = 'paid' // Set project status to 'done' - await project.save() - res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', projectId, type: 'free' }) - } else { - res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) - } - } else { - const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { - MerchantID: apiKey, - Amount: Number(price), - Description: `پرداخت برای سفارش شماره ${projectId}`, - CallbackURL: `${process.env.APP_URL}/projects/payment-web?projectId=${projectId}&itemName=${itemName}&userId=${userId}` // Assuming your backend URL for handling payment callback - }) - - const data = response.data - if (data.Status === 100) { - // Payment request was successful - const authority = data.Authority // The payment authority (شناسه پرداخت) - res.status(200).json({ authority }) - } else { - // Payment request failed - console.error('Error occurred while initiating payment:', data) - res.status(500).json({ error: 'Error occurred while initiating payment' }) - } - } - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -// Controller function for handling payment callback from ZarinPal -const handlePaymentCallbackWeb = async (req, res) => { - try { - // console.log(req.query) - // Extract payment status and authority from ZarinPal callback - const status = req.query.Status - const authority = req.query.Authority - const projectId = req.query.projectId // Get project ID from session - const itemName = req.query.itemName // Get project ID from session - const userId = req.query.userId // Get project ID from session - // Clear project ID from session - // Query ZarinPal API to verify payment status - const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' - const price = await getPriceFromDatabase(itemName) // Get price from database - const verificationResponse = await axios.post(verificationUrl, { - MerchantID: apiKey, - Authority: authority, - Amount: Number(price) - }) - - const verificationData = verificationResponse.data - if (verificationData.Status === 100) { - const payment = new PaymentModel({ - amount: Number(price), - status: 'successful', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - project_id: projectId, - type: 'create', - installment_step: null - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - - // Payment verification is successful - // Now, update project status to 'done' or desired status - const project = await ProjectModel.findById(projectId) - if (!project) { - res.status(404).send('Project not found') - return - } - project.payment_status = 'done' // Set project status to 'done' - project.status = 'paid' // Set project status to 'done' - await project.save() - - // Send event based on payment status - if ('eventEmitter' in req.app) { - if (status === 'OK') { - // Payment is successful - // Emit an event to be listened to in the frontend - // Example event name: 'paymentSuccess' - // Example data: { message: 'Payment successful' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - // Payment is canceled or failed - // Emit an event to be listened to in the frontend - // Example event name: 'paymentFailed' - // Example data: { message: 'Payment canceled or failed' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) - } - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - // Redirect to the callback URL with the project ID - res.redirect(`https://modstagram.com/projects/payment/success?projectId=${projectId}&price=${price}&type=create`) - } else { - // Payment verification failed - const payment = new PaymentModel({ - amount: Number(price), - status: 'failed', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - project_id: projectId, - type: 'create' - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - res.redirect(`https://modstagram.com/projects/payment/failed?projectId=${projectId}&price=${price}&type=create`) - } - } catch (error) { - // Error occurred while verifying payment - console.error('Error occurred:', error) - res.status(500).send('Internal server error') - } -} - -const paymentRequestAcceptWeb = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const projectId = req.body.projectId - // Find the project - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ error: 'Project not found' }) - } - if (project.status == 'ongoing') { - return res.status(400).json({ message: 'برای این پروژه از قبل کاربر انتخاب شده' }) - } - // eslint-disable-next-line eqeqeq - if (userId != project.creator_id.toString()) { - return res.status(422).json({ - error: true, - message: 'شما نمیتوانید این پروژه را ویرایش کنید' - }) - } - - // Get the final price from the request model - const requestId = req.body.requestId - const request = await RequestModel.findById(requestId) - if (!request) { - return res.status(404).json({ error: 'Request not found' }) - } - request.status = 'accepted' - await request.save() - - project.selected_user = request.user - project.status = 'ongoing' - project.final_price = request.price - project.final_time = request.time - await project.save() - // Send notification to selected user - const notification = new NotificationModel({ - user_id: request.user, - project_post_id: projectId, - type: 'request_accepted', - title: 'انتخاب پیشنهاد', - description: `کارفرما پیشنهاد کاری شما برای پروژه ${project.title} را پذیرفت.` - }) - await notification.save() - - // Sms - const userReciver = await UserModel.findById(request.user) - const data = JSON.stringify({ - mobile: userReciver?.mobile, - templateId: '302876', - parameters: [ - { name: 'USER', value: userReciver?.first_name + ' ' + userReciver?.last_name }, - { name: 'PROJECT', value: project?.title } - ] - }) - const config = { - method: 'post', - url: 'https://api.sms.ir/v1/send/verify', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/plain', - 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' - }, - data - } - axios(config) - .then(function (response) { - }) - .catch(function (error) { - console.log(error) - }) - res.status(200).json({ - message: 'درخواست با موفقیت تایید شد' - }) - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -const initiateAdvertisingPaymentWeb = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const itemName = req.body.item_name // Get item name from request body - 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 - - // 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 (user.daily_free_request > 0) { - user.daily_free_request -= 1 - await user.save() - const project = await AdvertisingModel.findById(advertisingId) - if (!project) { - res.status(404).send('Advertising not found') - return - } - project.payment_status = 'done' // Set project status to 'done' - project.status = 'paid' // Set project status to 'done' - await project.save() - res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' }) - } else { - res.status(403).json({ message: 'شما قبلاً از بیلبورد رایگان این ماه استفاده کرده‌اید.' }) - } - } else { - const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { - MerchantID: process.env.ZARINPAL_MERCHANT_ID, - Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), - Description: `پرداخت برای سفارش شماره ${advertisingId}`, - CallbackURL: `${process.env.APP_SITE_CallBack}/advertising/payment-web?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback - }) - const data = response.data - if (data.Status === 100) { - // Payment request was successful - const authority = data.Authority // The payment authority (شناسه پرداخت) - res.status(200).json({ authority }) - } else { - // Payment request failed - console.error('Error occurred while initiating payment:', data) - res.status(500).json({ error: 'Error occurred while initiating payment' }) - } - } - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -const handleAdvertisingPaymentCallbackWeb = async (req, res) => { - try { - // console.log(req.query) - // Extract payment status and authority from ZarinPal callback - const status = req.query.Status - const authority = req.query.Authority - const advertisingId = req.query.advertisingId // Get project ID from session - const itemName = req.query.itemName // Get project ID from session - const userId = req.query.userId // Get project ID from session - const showDiscount = req.query.showDiscount // Get project ID from session - // Clear project ID from session - // Query ZarinPal API to verify payment status - - const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' - const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database - const verificationResponse = await axios.post(verificationUrl, { - MerchantID: apiKey, - Authority: authority, - Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price) - }) - - const verificationData = verificationResponse.data - if (verificationData.Status === 100) { - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), - status: 'successful', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - advertising_id: advertisingId, - type: 'advertising', - installment_step: null - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - - // Payment verification is successful - // Now, update project status to 'done' or desired status - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - res.status(404).send('Advertising not found') - return - } - advertising.payment_status = 'done' // Set advertising status to 'done' - advertising.status = 'paid' // Set advertising status to 'done' - await advertising.save() - - // Send event based on payment status - if ('eventEmitter' in req.app) { - if (status === 'OK') { - // Payment is successful - // Emit an event to be listened to in the frontend - // Example event name: 'paymentSuccess' - // Example data: { message: 'Payment successful' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - // Payment is canceled or failed - // Emit an event to be listened to in the frontend - // Example event name: 'paymentFailed' - // Example data: { message: 'Payment canceled or failed' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) - } - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - // Redirect to the callback URL with the advertising ID - res.redirect(`https://modstagram.com/billboards/payment/success?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)}&type=advertising&showDiscount=${showDiscount}`) - } else { - // Payment verification failed - const payment = new PaymentModel({ - amount: showDiscount ? Number(price + 20000) : Number(price), - status: 'failed', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - advertising_id: advertisingId, - type: 'advertising' - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - console.log('ss') - - await payment.save() - res.redirect(`https://modstagram.com/billboards/payment/failed?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)}&type=advertising&showDiscount=${showDiscount}`) - } - } catch (error) { - // Error occurred while verifying payment - console.error('Error occurred:', error) - res.status(500).send('Internal server error') - } -} - -const republishAdvertisingPaymentWeb = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const itemName = req.body.item_name // Get item name from request body - 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 - // 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 (user.daily_free_request > 0) { - user.daily_free_request -= 1 - await user.save() - const project = await AdvertisingModel.findById(advertisingId) - if (!project) { - res.status(404).send('Project not found') - return - } - project.payment_status = 'done' // Set project status to 'done' - project.status = 'paid' // Set project status to 'done' - await project.save() - res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' }) - } else { - res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) - } - } else { - const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { - MerchantID: apiKey, - Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), - Description: `پرداخت برای سفارش شماره ${advertisingId}`, - CallbackURL: `${process.env.APP_URL}/advertising/republish-verify-payment-web?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback - }) - const data = response.data - if (data.Status === 100) { - // Payment request was successful - const authority = data.Authority // The payment authority (شناسه پرداخت) - res.status(200).json({ authority }) - } else { - // Payment request failed - console.error('Error occurred while initiating payment:', data) - res.status(500).json({ error: 'Error occurred while initiating payment' }) - } - } - } catch (error) { - // Error occurred while getting price from database or sending the request - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -const handleAdvertisingRepublishPaymentCallbackWeb = async (req, res) => { - try { - // console.log(req.query) - // Extract payment status and authority from ZarinPal callback - const status = req.query.Status - const authority = req.query.Authority - const advertisingId = req.query.advertisingId // Get project ID from session - const itemName = req.query.itemName // Get project ID from session - const userId = req.query.userId // Get project ID from session - const showDiscount = req.query.showDiscount // Get project ID from session - // Clear project ID from session - // Query ZarinPal API to verify payment status - const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' - const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database - const verificationResponse = await axios.post(verificationUrl, { - MerchantID: apiKey, - Authority: authority, - Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100) - }) - - const verificationData = verificationResponse.data - if (verificationData.Status === 100) { - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), - status: 'successful', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - advertising_id: advertisingId, - type: 'advertising', - installment_step: null - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - - // Payment verification is successful - // Now, update project status to 'done' or desired status - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - res.status(404).send('Advertising not found') - return - } - advertising.payment_status = 'done' // Set advertising status to 'done' - advertising.status = 'accepted' // Set advertising status to 'done' - advertising.acceptedAt = new Date() - await advertising.save() - - // Send event based on payment status - if ('eventEmitter' in req.app) { - if (status === 'OK') { - // Payment is successful - // Emit an event to be listened to in the frontend - // Example event name: 'paymentSuccess' - // Example data: { message: 'Payment successful' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) - } else { - // Payment is canceled or failed - // Emit an event to be listened to in the frontend - // Example event name: 'paymentFailed' - // Example data: { message: 'Payment canceled or failed' } - // You can customize the event name and data as needed - req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) - } - } else { - console.error('Error: eventEmitter is not defined or not properly configured.') - } - - // Redirect to the callback URL with the advertising ID - res.redirect(`https://modstagram.com/billboards/payment/success?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)}&type=advertising-republish&showDiscount=${showDiscount}`) - } else { - // Payment verification failed - const payment = new PaymentModel({ - amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), - status: 'failed', - authority, - user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید - advertising_id: advertisingId, - type: 'advertising' - // دیگر اطلاعات مورد نیاز برای ثبت پرداخت - }) - await payment.save() - res.redirect(`https://modstagram.com/billboards/payment/failed?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)}&type=advertising-republish&showDiscount=${showDiscount}`) - } - } catch (error) { - // Error occurred while verifying payment - console.error('Error occurred:', error) - res.status(500).send('Internal server error') - } -} -// const initiateOfferPaymentWeb = async (req, res) => { -// try { -// const token = req.header('Authorization')?.split(' ')[1]; -// if (!token) { -// return res.status(401).json({ error: 'Access Denied: No token provided' }); -// } - -// let decodedToken; -// try { -// decodedToken = jwt.verify(token, process.env.APP_SECRET); -// } catch (err) { -// return res.status(401).json({ error: 'Invalid token' }); -// } - -// const userId = decodedToken.id; -// const { receiverId, offerType } = req.body; - -// // اعتبارسنجی ورودی‌ها -// if (!receiverId || !offerType) { -// return res.status(400).json({ error: 'receiverId and offerType are required' }); -// } - -// const user = await UserModel.findById(userId); -// if (!user) { -// return res.status(404).json({ error: 'User not found' }); -// } - -// const receiverUser = await UserModel.findById(receiverId); -// if (!receiverUser) { -// return res.status(404).json({ error: 'Receiver not found' }); -// } - -// let price; -// if (user.monthly_free_offer > 0 && offerType === 'free') { -// price = 0; -// user.monthly_free_offer -= 1; -// await user.save(); -// } else { -// price = await getOfferPriceFromDatabase(offerType); -// } - -// if (price === 0) { -// const offer = new OfferModel({ -// sender: userId, -// receiver: receiverId, -// transaction_id: null, -// }); -// await offer.save(); -// return res.status(200).json({ message: 'آفر رایگان با موفقیت ثبت شد', offerId: offer._id, type: 'free' }); -// } -// console.log('🔹 Initiating ZarinPal Payment'); -// console.log('MerchantID:', apiKey); -// console.log('Amount:', Number(price * 10)); -// console.log('Description:', `پرداخت برای ارسال آفر به ${receiverUser.first_name}`); -// console.log('CallbackURL:', `${process.env.APP_URL}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`); - -// const ZARINPAL_URL = 'https://api.zarinpal.com/pg/v4/payment/request.json'; -// // -// const response = await axios.post(ZARINPAL_URL, { -// merchant_id: process.env.ZARINPAL_MERCHANT_ID, // ❌ MerchantID نیست! -// amount: Number(price), // ✅ به ریال -// description: `پرداخت برای ارسال آفر به ${receiverUser.first_name}`, -// callback_url: `${process.env.APP_SITE_CallBack}/offer/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`, -// }, { headers: { 'Content-Type': 'application/json' } }); - -// const result = response.data.data; // ✅ data.data - -// if (result.code === 100) { // ✅ code === 100 -// return res.status(200).json({ -// authority: result.authority, -// paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}` -// }); -// } else { -// console.error('ZarinPal error:', data); -// return res.status(500).json({ error: `Failed to initiate payment: ZarinPal Status ${data.Status}` }); -// } -// } catch (error) { -// console.error('❌ Error in initiateOfferPaymentWeb:', error.message, error.stack); -// return res.status(500).json({ error: error.message || 'Internal server error' }); -// } -// } -const initiateOfferPaymentWeb = async (req, res) => { - try { - const token = req.header('Authorization')?.split(' ')[1]; - if (!token) return res.status(401).json({ error: 'No token' }); - - const decoded = jwt.verify(token, process.env.APP_SECRET); - const userId = decoded.id; - - const { receiverId, offerType } = req.body; - if (!receiverId || !offerType) - return res.status(400).json({ error: 'Invalid data' }); - - const user = await UserModel.findById(userId); - const receiver = await UserModel.findById(receiverId); - if (!user || !receiver) - return res.status(404).json({ error: 'User not found' }); - - let price = await getOfferPriceFromDatabase(offerType); // ✅ ریال - - // 🎁 free offer - if (offerType === 'free' && user.monthly_free_offer > 0) { - const offer = await OfferModel.create({ - sender: userId, - receiver: receiverId, - transaction_id: null, - }); - - user.monthly_free_offer -= 1; - await user.save(); - - return res.json({ - type: 'free', - offerId: offer._id, - }); - } - - const amount = Number(price) * 10; // ✅ ریال – بدون ×10 - const callbackUrl = `https://api.modstagram.com/api/v1/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`; - - const zarinpalRes = await axios.post( - 'https://api.zarinpal.com/pg/v4/payment/request.json', - { - merchant_id:"c7c41e8a-918f-4741-bcd5-58f3bc51db73", - amount, - description: `ارسال آفر به ${receiver.first_name}`, - callback_url: callbackUrl - - // callback_url: `${process.env.APP_SITE_CallBack}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`, - }, - { headers: { 'Content-Type': 'application/json' } } - ); - - // console.log(`${process.env.APP_SITE_CallBack}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`); - - - const result = zarinpalRes.data.data; - console.log(result); - console.log("ZarinPal Callback URL:", callbackUrl); - console.log("Initiate payment request:", { userId, receiverId, offerType, price }); - - - - - - if (result.code === 100) { - return res.json({ - authority: result.authority, - paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`, - }); - } - - return res.status(500).json({ error: 'ZarinPal error' }); - } catch (err) { - console.error(err); - return res.status(500).json({ error: 'Server error' }); - } -}; - - - -// try { -// const { receiverId, offerType, userId, Authority, Status } = req.query; - -// // ❗ چک اولیه -// if (Status !== 'OK' || !Authority) { -// return res.status(400).json({ -// success: false, -// message: 'پرداخت ناموفق یا لغو شد' -// }); -// } - -// if (!receiverId || !offerType || !userId) { -// return res.status(400).json({ -// success: false, -// message: 'پارامترهای ناقص' -// }); -// } - -// // پیدا کردن کاربرها -// const user = await UserModel.findById(userId); -// const receiverUser = await UserModel.findById(receiverId); - -// if (!user || !receiverUser) { -// return res.status(404).json({ -// success: false, -// message: 'کاربر یافت نشد' -// }); -// } - -// const price = await getOfferPriceFromDatabase(offerType); - -// const verifyResponse = await axios.post( -// 'https://api.zarinpal.com/pg/v4/payment/verify.json', -// { -// merchant_id: process.env.ZARINPAL_MERCHANT_ID, // یا MERCHENT_CODE -// authority: Authority, -// amount: price, // به ریال -// }, -// { headers: { 'Content-Type': 'application/json' } } -// ); - -// const verifyData = verifyResponse.data.data; - -// if (verifyData.code === 100) { -// // ✅ پرداخت موفق -// const payment = new PaymentModel({ -// amount: Number(price), -// status: 'successful', -// authority: Authority, -// ref_id: verifyData.ref_id, -// user_id: userId, -// type: 'offer', -// }); -// await payment.save(); - -// const offer = new OfferModel({ -// sender: userId, -// receiver: receiverId, -// transaction_id: payment._id, -// }); -// await offer.save(); - -// // نوتیفیکیشن و SMS (همون کد قبلی) -// const notification = new NotificationModel({ -// user_id: receiverId, -// project_post_id: offer._id, -// type: 'new_offer', -// title: 'درخواست همکاری جدید', -// description: `${user.first_name} یک درخواست همکاری برای شما ارسال کرده است`, -// }); -// await notification.save(); - -// const data = JSON.stringify({ -// mobile: receiverUser?.mobile, -// templateId: '569006', -// parameters: [ -// { name: 'FIRSTNAME', value: receiverUser?.first_name }, -// { name: 'LASTNAME', value: receiverUser?.last_name }, -// ], -// }); - -// const config = { -// method: 'post', -// url: 'https://api.sms.ir/v1/send/verify', -// headers: { -// 'Content-Type': 'application/json', -// Accept: 'text/plain', -// 'x-api-key': process.env.SMS_VERIFY_KEY, -// }, -// data, -// }; - -// await axios(config).catch((error) => { -// console.error('SMS error:', error.message); -// }); - -// if ('eventEmitter' in req.app && status === 'OK') { -// req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }); -// } else { -// req.app.get('eventEmitter')?.emit('paymentFailed', { message: 'Payment canceled or failed' }); -// } - -// res.redirect(`https://modstagram.com/offer/payment/success?userId=${userId}`); - -// return res.status(200).json({ -// success: true, -// message: 'پرداخت و آفر با موفقیت ثبت شد', -// ref_id: verifyData.ref_id, -// offerId: offer._id -// }); -// } else { -// // پرداخت ناموفق -// const payment = new PaymentModel({ -// amount: Number(price), -// status: 'failed', -// authority: Authority, -// user_id: userId, -// type: 'offer', -// }); -// await payment.save(); -// res.redirect(`https://modstagram.com/offer/payment/failed?userId=${userId}`); -// return res.status(400).json({ -// success: false, -// message: `خطا در تأیید پرداخت: ${verifyData.code}` -// }); - -// } -// } catch (error) { -// console.error('❌ Callback Error:', error.message); -// return res.status(500).json({ -// success: false, -// message: 'خطای سرور' -// }); -// } -// }; - -const handleOfferPaymentCallback = async (req, res) => { - - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - console.log('🔔 CALLBACK QUERY:', req.query); - - try { - const { Authority, Status, receiverId, offerType, userId } = req.query; - - if (Status !== 'OK') - return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`); - - const price = await getOfferPriceFromDatabase(offerType); // ریال - const amount = Number(price) * 10; - - const verify = await axios.post( - 'https://api.zarinpal.com/pg/v4/payment/verify.json', - { - merchant_id: "c7c41e8a-918f-4741-bcd5-58f3bc51db73", - authority: Authority, - amount, - }, - { headers: { 'Content-Type': 'application/json' } } - ); - - const data = verify.data.data; - console.log('🔔payment:', data.code); - if (data.code === 100) { - const payment = await PaymentModel.create({ - amount : (amount / 10), - status: 'successful', - authority: Authority, - ref_id: data.ref_id, - user_id: userId, - type: 'offer', - }); - - console.log('🔔payment:', payment); - - const offer = await OfferModel.create({ - sender: userId, - receiver: receiverId, - transaction_id: payment._id, - }); - - return res.redirect( - `${process.env.APP_SITE}/offer/payment/success?offerId=${offer._id}` - ); - } - - return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`); - } catch (err) { - console.error(err); - return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`); - } -}; - - - - - - -module.exports = { - initiatePaymentWeb, - handlePaymentCallbackWeb, - paymentRequestAcceptWeb, - initiateAdvertisingPaymentWeb, - handleAdvertisingPaymentCallbackWeb, - republishAdvertisingPaymentWeb, - handleAdvertisingRepublishPaymentCallbackWeb, - initiateOfferPaymentWeb, - handleOfferPaymentCallback, -}; +/* eslint-disable eqeqeq */ + +const TypeAndPriceModel = require('../../../models/TypeAndPriceModel'); +const ProjectModel = require('../../../models/ProjectModel'); +const PaymentModel = require('../../../models/PaymentModel'); +const jwt = require('jsonwebtoken'); +const RequestModel = require('../../../models/RequestModel'); +const NotificationModel = require('../../../models/NotificationModel'); +const UserModel = require('../../../models/UserModel'); +const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel'); +const AdvertisingModel = require('../../../models/AdvertisingModel'); +const OfferTypeModel = require('../../../models/OfferTypeModel'); +const OfferModel = require('../../../models/OfferModel'); +const { default: axios } = require('axios'); + +const apiKey = "c7c41e8a-918f-4741-bcd5-58f3bc51db73"; // اصلاح نام متغیر محیطی + +// Function to get price based on item name from database +const getPriceFromDatabase = async (itemName) => { + const item = await TypeAndPriceModel.findOne({ name: itemName }); + if (!item) { + throw new Error(`Item "${itemName}" not found in TypeAndPriceModel`); + } + return item.price; +}; + +const getAdvertisingPriceFromDatabase = async (itemName) => { + const item = await AdvertisingTypeModel.findOne({ name: itemName }); + if (!item) { + throw new Error(`Item "${itemName}" not found in AdvertisingTypeModel`); + } + return item.price; +}; + +const getOfferPriceFromDatabase = async (offerType) => { + const item = await OfferTypeModel.findOne({ name: offerType }); + if (!item) { + throw new Error(`Offer type "${offerType}" not found in OfferTypeModel`); + } + return item.price; +}; +// Controller function for initiating payment +const initiatePaymentWeb = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + 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 + // Save project ID for later use in callback URL + req.session.projectId = projectId + const item = await TypeAndPriceModel.findOne({ name: itemName }) + const user = await UserModel.findById(userId) + + if (item.name === 'free' && item.price === 0) { + if (user.daily_free_request > 0) { + user.daily_free_request -= 1 + await user.save() + const project = await ProjectModel.findById(projectId) + if (!project) { + res.status(404).send('Project not found') + return + } + project.payment_status = 'done' // Set project status to 'done' + project.status = 'paid' // Set project status to 'done' + await project.save() + res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', projectId, type: 'free' }) + } else { + res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) + } + } else { + const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { + MerchantID: apiKey, + Amount: Number(price), + Description: `پرداخت برای سفارش شماره ${projectId}`, + CallbackURL: `${process.env.APP_URL}/projects/payment-web?projectId=${projectId}&itemName=${itemName}&userId=${userId}` // Assuming your backend URL for handling payment callback + }) + + const data = response.data + if (data.Status === 100) { + // Payment request was successful + const authority = data.Authority // The payment authority (شناسه پرداخت) + res.status(200).json({ authority }) + } else { + // Payment request failed + console.error('Error occurred while initiating payment:', data) + res.status(500).json({ error: 'Error occurred while initiating payment' }) + } + } + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +// Controller function for handling payment callback from ZarinPal +const handlePaymentCallbackWeb = async (req, res) => { + try { + // console.log(req.query) + // Extract payment status and authority from ZarinPal callback + const status = req.query.Status + const authority = req.query.Authority + const projectId = req.query.projectId // Get project ID from session + const itemName = req.query.itemName // Get project ID from session + const userId = req.query.userId // Get project ID from session + // Clear project ID from session + // Query ZarinPal API to verify payment status + const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' + const price = await getPriceFromDatabase(itemName) // Get price from database + const verificationResponse = await axios.post(verificationUrl, { + MerchantID: apiKey, + Authority: authority, + Amount: Number(price) + }) + + const verificationData = verificationResponse.data + if (verificationData.Status === 100) { + const payment = new PaymentModel({ + amount: Number(price), + status: 'successful', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + project_id: projectId, + type: 'create', + installment_step: null + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + + // Payment verification is successful + // Now, update project status to 'done' or desired status + const project = await ProjectModel.findById(projectId) + if (!project) { + res.status(404).send('Project not found') + return + } + project.payment_status = 'done' // Set project status to 'done' + project.status = 'paid' // Set project status to 'done' + await project.save() + + // Send event based on payment status + if ('eventEmitter' in req.app) { + if (status === 'OK') { + // Payment is successful + // Emit an event to be listened to in the frontend + // Example event name: 'paymentSuccess' + // Example data: { message: 'Payment successful' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + // Payment is canceled or failed + // Emit an event to be listened to in the frontend + // Example event name: 'paymentFailed' + // Example data: { message: 'Payment canceled or failed' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) + } + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + // Redirect to the callback URL with the project ID + res.redirect(`https://modstagram.com/projects/payment/success?projectId=${projectId}&price=${price}&type=create`) + } else { + // Payment verification failed + const payment = new PaymentModel({ + amount: Number(price), + status: 'failed', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + project_id: projectId, + type: 'create' + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + res.redirect(`https://modstagram.com/projects/payment/failed?projectId=${projectId}&price=${price}&type=create`) + } + } catch (error) { + // Error occurred while verifying payment + console.error('Error occurred:', error) + res.status(500).send('Internal server error') + } +} + +const paymentRequestAcceptWeb = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const projectId = req.body.projectId + // Find the project + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ error: 'Project not found' }) + } + if (project.status == 'ongoing') { + return res.status(400).json({ message: 'برای این پروژه از قبل کاربر انتخاب شده' }) + } + // eslint-disable-next-line eqeqeq + if (userId != project.creator_id.toString()) { + return res.status(422).json({ + error: true, + message: 'شما نمیتوانید این پروژه را ویرایش کنید' + }) + } + + // Get the final price from the request model + const requestId = req.body.requestId + const request = await RequestModel.findById(requestId) + if (!request) { + return res.status(404).json({ error: 'Request not found' }) + } + request.status = 'accepted' + await request.save() + + project.selected_user = request.user + project.status = 'ongoing' + project.final_price = request.price + project.final_time = request.time + await project.save() + // Send notification to selected user + const notification = new NotificationModel({ + user_id: request.user, + project_post_id: projectId, + type: 'request_accepted', + title: 'انتخاب پیشنهاد', + description: `کارفرما پیشنهاد کاری شما برای پروژه ${project.title} را پذیرفت.` + }) + await notification.save() + + // Sms + const userReciver = await UserModel.findById(request.user) + const data = JSON.stringify({ + mobile: userReciver?.mobile, + templateId: '302876', + parameters: [ + { name: 'USER', value: userReciver?.first_name + ' ' + userReciver?.last_name }, + { name: 'PROJECT', value: project?.title } + ] + }) + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + axios(config) + .then(function (response) { + }) + .catch(function (error) { + console.log(error) + }) + res.status(200).json({ + message: 'درخواست با موفقیت تایید شد' + }) + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +const initiateAdvertisingPaymentWeb = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const itemName = req.body.item_name // Get item name from request body + 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 + + // 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 (user.daily_free_request > 0) { + user.daily_free_request -= 1 + await user.save() + const project = await AdvertisingModel.findById(advertisingId) + if (!project) { + res.status(404).send('Advertising not found') + return + } + project.payment_status = 'done' // Set project status to 'done' + project.status = 'paid' // Set project status to 'done' + await project.save() + res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' }) + } else { + res.status(403).json({ message: 'شما قبلاً از بیلبورد رایگان این ماه استفاده کرده‌اید.' }) + } + } else { + const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { + MerchantID: process.env.ZARINPAL_MERCHANT_ID, + Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), + Description: `پرداخت برای سفارش شماره ${advertisingId}`, + CallbackURL: `${process.env.APP_SITE_CallBack}/advertising/payment-web?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback + }) + const data = response.data + if (data.Status === 100) { + // Payment request was successful + const authority = data.Authority // The payment authority (شناسه پرداخت) + res.status(200).json({ authority }) + } else { + // Payment request failed + console.error('Error occurred while initiating payment:', data) + res.status(500).json({ error: 'Error occurred while initiating payment' }) + } + } + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +const handleAdvertisingPaymentCallbackWeb = async (req, res) => { + try { + // console.log(req.query) + // Extract payment status and authority from ZarinPal callback + const status = req.query.Status + const authority = req.query.Authority + const advertisingId = req.query.advertisingId // Get project ID from session + const itemName = req.query.itemName // Get project ID from session + const userId = req.query.userId // Get project ID from session + const showDiscount = req.query.showDiscount // Get project ID from session + // Clear project ID from session + // Query ZarinPal API to verify payment status + + const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' + const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database + const verificationResponse = await axios.post(verificationUrl, { + MerchantID: apiKey, + Authority: authority, + Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price) + }) + + const verificationData = verificationResponse.data + if (verificationData.Status === 100) { + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price), + status: 'successful', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + advertising_id: advertisingId, + type: 'advertising', + installment_step: null + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + + // Payment verification is successful + // Now, update project status to 'done' or desired status + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + res.status(404).send('Advertising not found') + return + } + advertising.payment_status = 'done' // Set advertising status to 'done' + advertising.status = 'paid' // Set advertising status to 'done' + await advertising.save() + + // Send event based on payment status + if ('eventEmitter' in req.app) { + if (status === 'OK') { + // Payment is successful + // Emit an event to be listened to in the frontend + // Example event name: 'paymentSuccess' + // Example data: { message: 'Payment successful' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + // Payment is canceled or failed + // Emit an event to be listened to in the frontend + // Example event name: 'paymentFailed' + // Example data: { message: 'Payment canceled or failed' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) + } + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + // Redirect to the callback URL with the advertising ID + res.redirect(`https://modstagram.com/billboards/payment/success?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)}&type=advertising&showDiscount=${showDiscount}`) + } else { + // Payment verification failed + const payment = new PaymentModel({ + amount: showDiscount ? Number(price + 20000) : Number(price), + status: 'failed', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + advertising_id: advertisingId, + type: 'advertising' + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + console.log('ss') + + await payment.save() + res.redirect(`https://modstagram.com/billboards/payment/failed?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)}&type=advertising&showDiscount=${showDiscount}`) + } + } catch (error) { + // Error occurred while verifying payment + console.error('Error occurred:', error) + res.status(500).send('Internal server error') + } +} + +const republishAdvertisingPaymentWeb = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const itemName = req.body.item_name // Get item name from request body + 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 + // 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 (user.daily_free_request > 0) { + user.daily_free_request -= 1 + await user.save() + const project = await AdvertisingModel.findById(advertisingId) + if (!project) { + res.status(404).send('Project not found') + return + } + project.payment_status = 'done' // Set project status to 'done' + project.status = 'paid' // Set project status to 'done' + await project.save() + res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' }) + } else { + res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' }) + } + } else { + const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', { + MerchantID: apiKey, + Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), + Description: `پرداخت برای سفارش شماره ${advertisingId}`, + CallbackURL: `${process.env.APP_URL}/advertising/republish-verify-payment-web?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback + }) + const data = response.data + if (data.Status === 100) { + // Payment request was successful + const authority = data.Authority // The payment authority (شناسه پرداخت) + res.status(200).json({ authority }) + } else { + // Payment request failed + console.error('Error occurred while initiating payment:', data) + res.status(500).json({ error: 'Error occurred while initiating payment' }) + } + } + } catch (error) { + // Error occurred while getting price from database or sending the request + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +const handleAdvertisingRepublishPaymentCallbackWeb = async (req, res) => { + try { + // console.log(req.query) + // Extract payment status and authority from ZarinPal callback + const status = req.query.Status + const authority = req.query.Authority + const advertisingId = req.query.advertisingId // Get project ID from session + const itemName = req.query.itemName // Get project ID from session + const userId = req.query.userId // Get project ID from session + const showDiscount = req.query.showDiscount // Get project ID from session + // Clear project ID from session + // Query ZarinPal API to verify payment status + const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' + const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database + const verificationResponse = await axios.post(verificationUrl, { + MerchantID: apiKey, + Authority: authority, + Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100) + }) + + const verificationData = verificationResponse.data + if (verificationData.Status === 100) { + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), + status: 'successful', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + advertising_id: advertisingId, + type: 'advertising', + installment_step: null + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + + // Payment verification is successful + // Now, update project status to 'done' or desired status + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + res.status(404).send('Advertising not found') + return + } + advertising.payment_status = 'done' // Set advertising status to 'done' + advertising.status = 'accepted' // Set advertising status to 'done' + advertising.acceptedAt = new Date() + await advertising.save() + + // Send event based on payment status + if ('eventEmitter' in req.app) { + if (status === 'OK') { + // Payment is successful + // Emit an event to be listened to in the frontend + // Example event name: 'paymentSuccess' + // Example data: { message: 'Payment successful' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }) + } else { + // Payment is canceled or failed + // Emit an event to be listened to in the frontend + // Example event name: 'paymentFailed' + // Example data: { message: 'Payment canceled or failed' } + // You can customize the event name and data as needed + req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment canceled or failed' }) + } + } else { + console.error('Error: eventEmitter is not defined or not properly configured.') + } + + // Redirect to the callback URL with the advertising ID + res.redirect(`https://modstagram.com/billboards/payment/success?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)}&type=advertising-republish&showDiscount=${showDiscount}`) + } else { + // Payment verification failed + const payment = new PaymentModel({ + amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100), + status: 'failed', + authority, + user_id: userId, // فرضاً شما از نوع میان‌افزارهای مدیریت کاربر استفاده می‌کنید و اطلاعات کاربر را از طریق `req.user` در دسترس دارید + advertising_id: advertisingId, + type: 'advertising' + // دیگر اطلاعات مورد نیاز برای ثبت پرداخت + }) + await payment.save() + res.redirect(`https://modstagram.com/billboards/payment/failed?projectId=${advertisingId}&price=${showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)}&type=advertising-republish&showDiscount=${showDiscount}`) + } + } catch (error) { + // Error occurred while verifying payment + console.error('Error occurred:', error) + res.status(500).send('Internal server error') + } +} +// const initiateOfferPaymentWeb = async (req, res) => { +// try { +// const token = req.header('Authorization')?.split(' ')[1]; +// if (!token) { +// return res.status(401).json({ error: 'Access Denied: No token provided' }); +// } + +// let decodedToken; +// try { +// decodedToken = jwt.verify(token, process.env.APP_SECRET); +// } catch (err) { +// return res.status(401).json({ error: 'Invalid token' }); +// } + +// const userId = decodedToken.id; +// const { receiverId, offerType } = req.body; + +// // اعتبارسنجی ورودی‌ها +// if (!receiverId || !offerType) { +// return res.status(400).json({ error: 'receiverId and offerType are required' }); +// } + +// const user = await UserModel.findById(userId); +// if (!user) { +// return res.status(404).json({ error: 'User not found' }); +// } + +// const receiverUser = await UserModel.findById(receiverId); +// if (!receiverUser) { +// return res.status(404).json({ error: 'Receiver not found' }); +// } + +// let price; +// if (user.monthly_free_offer > 0 && offerType === 'free') { +// price = 0; +// user.monthly_free_offer -= 1; +// await user.save(); +// } else { +// price = await getOfferPriceFromDatabase(offerType); +// } + +// if (price === 0) { +// const offer = new OfferModel({ +// sender: userId, +// receiver: receiverId, +// transaction_id: null, +// }); +// await offer.save(); +// return res.status(200).json({ message: 'آفر رایگان با موفقیت ثبت شد', offerId: offer._id, type: 'free' }); +// } +// console.log('🔹 Initiating ZarinPal Payment'); +// console.log('MerchantID:', apiKey); +// console.log('Amount:', Number(price * 10)); +// console.log('Description:', `پرداخت برای ارسال آفر به ${receiverUser.first_name}`); +// console.log('CallbackURL:', `${process.env.APP_URL}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`); + +// const ZARINPAL_URL = 'https://api.zarinpal.com/pg/v4/payment/request.json'; +// // +// const response = await axios.post(ZARINPAL_URL, { +// merchant_id: process.env.ZARINPAL_MERCHANT_ID, // ❌ MerchantID نیست! +// amount: Number(price), // ✅ به ریال +// description: `پرداخت برای ارسال آفر به ${receiverUser.first_name}`, +// callback_url: `${process.env.APP_SITE_CallBack}/offer/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`, +// }, { headers: { 'Content-Type': 'application/json' } }); + +// const result = response.data.data; // ✅ data.data + +// if (result.code === 100) { // ✅ code === 100 +// return res.status(200).json({ +// authority: result.authority, +// paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}` +// }); +// } else { +// console.error('ZarinPal error:', data); +// return res.status(500).json({ error: `Failed to initiate payment: ZarinPal Status ${data.Status}` }); +// } +// } catch (error) { +// console.error('❌ Error in initiateOfferPaymentWeb:', error.message, error.stack); +// return res.status(500).json({ error: error.message || 'Internal server error' }); +// } +// } +const initiateOfferPaymentWeb = async (req, res) => { + try { + const token = req.header('Authorization')?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'No token' }); + + const decoded = jwt.verify(token, process.env.APP_SECRET); + const userId = decoded.id; + + const { receiverId, offerType } = req.body; + if (!receiverId || !offerType) + return res.status(400).json({ error: 'Invalid data' }); + + const user = await UserModel.findById(userId); + const receiver = await UserModel.findById(receiverId); + if (!user || !receiver) + return res.status(404).json({ error: 'User not found' }); + + let price = await getOfferPriceFromDatabase(offerType); // ✅ ریال + + // 🎁 free offer + if (offerType === 'free' && user.monthly_free_offer > 0) { + const offer = await OfferModel.create({ + sender: userId, + receiver: receiverId, + transaction_id: null, + }); + + user.monthly_free_offer -= 1; + await user.save(); + + return res.json({ + type: 'free', + offerId: offer._id, + }); + } + + const amount = Number(price) * 10; // ✅ ریال – بدون ×10 + const callbackUrl = `https://api.modstagram.com/api/v1/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`; + + const zarinpalRes = await axios.post( + 'https://api.zarinpal.com/pg/v4/payment/request.json', + { + merchant_id:"c7c41e8a-918f-4741-bcd5-58f3bc51db73", + amount, + description: `ارسال آفر به ${receiver.first_name}`, + callback_url: callbackUrl + + // callback_url: `${process.env.APP_SITE_CallBack}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`, + }, + { headers: { 'Content-Type': 'application/json' } } + ); + + // console.log(`${process.env.APP_SITE_CallBack}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`); + + + const result = zarinpalRes.data.data; + console.log(result); + console.log("ZarinPal Callback URL:", callbackUrl); + console.log("Initiate payment request:", { userId, receiverId, offerType, price }); + + + + + + if (result.code === 100) { + return res.json({ + authority: result.authority, + paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`, + }); + } + + return res.status(500).json({ error: 'ZarinPal error' }); + } catch (err) { + console.error(err); + return res.status(500).json({ error: 'Server error' }); + } +}; + + + +// try { +// const { receiverId, offerType, userId, Authority, Status } = req.query; + +// // ❗ چک اولیه +// if (Status !== 'OK' || !Authority) { +// return res.status(400).json({ +// success: false, +// message: 'پرداخت ناموفق یا لغو شد' +// }); +// } + +// if (!receiverId || !offerType || !userId) { +// return res.status(400).json({ +// success: false, +// message: 'پارامترهای ناقص' +// }); +// } + +// // پیدا کردن کاربرها +// const user = await UserModel.findById(userId); +// const receiverUser = await UserModel.findById(receiverId); + +// if (!user || !receiverUser) { +// return res.status(404).json({ +// success: false, +// message: 'کاربر یافت نشد' +// }); +// } + +// const price = await getOfferPriceFromDatabase(offerType); + +// const verifyResponse = await axios.post( +// 'https://api.zarinpal.com/pg/v4/payment/verify.json', +// { +// merchant_id: process.env.ZARINPAL_MERCHANT_ID, // یا MERCHENT_CODE +// authority: Authority, +// amount: price, // به ریال +// }, +// { headers: { 'Content-Type': 'application/json' } } +// ); + +// const verifyData = verifyResponse.data.data; + +// if (verifyData.code === 100) { +// // ✅ پرداخت موفق +// const payment = new PaymentModel({ +// amount: Number(price), +// status: 'successful', +// authority: Authority, +// ref_id: verifyData.ref_id, +// user_id: userId, +// type: 'offer', +// }); +// await payment.save(); + +// const offer = new OfferModel({ +// sender: userId, +// receiver: receiverId, +// transaction_id: payment._id, +// }); +// await offer.save(); + +// // نوتیفیکیشن و SMS (همون کد قبلی) +// const notification = new NotificationModel({ +// user_id: receiverId, +// project_post_id: offer._id, +// type: 'new_offer', +// title: 'درخواست همکاری جدید', +// description: `${user.first_name} یک درخواست همکاری برای شما ارسال کرده است`, +// }); +// await notification.save(); + +// const data = JSON.stringify({ +// mobile: receiverUser?.mobile, +// templateId: '569006', +// parameters: [ +// { name: 'FIRSTNAME', value: receiverUser?.first_name }, +// { name: 'LASTNAME', value: receiverUser?.last_name }, +// ], +// }); + +// const config = { +// method: 'post', +// url: 'https://api.sms.ir/v1/send/verify', +// headers: { +// 'Content-Type': 'application/json', +// Accept: 'text/plain', +// 'x-api-key': process.env.SMS_VERIFY_KEY, +// }, +// data, +// }; + +// await axios(config).catch((error) => { +// console.error('SMS error:', error.message); +// }); + +// if ('eventEmitter' in req.app && status === 'OK') { +// req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' }); +// } else { +// req.app.get('eventEmitter')?.emit('paymentFailed', { message: 'Payment canceled or failed' }); +// } + +// res.redirect(`https://modstagram.com/offer/payment/success?userId=${userId}`); + +// return res.status(200).json({ +// success: true, +// message: 'پرداخت و آفر با موفقیت ثبت شد', +// ref_id: verifyData.ref_id, +// offerId: offer._id +// }); +// } else { +// // پرداخت ناموفق +// const payment = new PaymentModel({ +// amount: Number(price), +// status: 'failed', +// authority: Authority, +// user_id: userId, +// type: 'offer', +// }); +// await payment.save(); +// res.redirect(`https://modstagram.com/offer/payment/failed?userId=${userId}`); +// return res.status(400).json({ +// success: false, +// message: `خطا در تأیید پرداخت: ${verifyData.code}` +// }); + +// } +// } catch (error) { +// console.error('❌ Callback Error:', error.message); +// return res.status(500).json({ +// success: false, +// message: 'خطای سرور' +// }); +// } +// }; + +const handleOfferPaymentCallback = async (req, res) => { + + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + console.log('🔔 CALLBACK QUERY:', req.query); + + try { + const { Authority, Status, receiverId, offerType, userId } = req.query; + + if (Status !== 'OK') + return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`); + + const price = await getOfferPriceFromDatabase(offerType); // ریال + const amount = Number(price) * 10; + + const verify = await axios.post( + 'https://api.zarinpal.com/pg/v4/payment/verify.json', + { + merchant_id: "c7c41e8a-918f-4741-bcd5-58f3bc51db73", + authority: Authority, + amount, + }, + { headers: { 'Content-Type': 'application/json' } } + ); + + const data = verify.data.data; + console.log('🔔payment:', data.code); + if (data.code === 100) { + const payment = await PaymentModel.create({ + amount : (amount / 10), + status: 'successful', + authority: Authority, + ref_id: data.ref_id, + user_id: userId, + type: 'offer', + }); + + console.log('🔔payment:', payment); + + const offer = await OfferModel.create({ + sender: userId, + receiver: receiverId, + transaction_id: payment._id, + }); + + return res.redirect( + `${process.env.APP_SITE}/offer/payment/success?offerId=${offer._id}` + ); + } + + return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`); + } catch (err) { + console.error(err); + return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`); + } +}; + + + + + + +module.exports = { + initiatePaymentWeb, + handlePaymentCallbackWeb, + paymentRequestAcceptWeb, + initiateAdvertisingPaymentWeb, + handleAdvertisingPaymentCallbackWeb, + republishAdvertisingPaymentWeb, + handleAdvertisingRepublishPaymentCallbackWeb, + initiateOfferPaymentWeb, + handleOfferPaymentCallback, +}; diff --git a/controllers/application/posts/likeController.js b/controllers/application/posts/likeController.js index 9c2b078..7b8fe4c 100644 --- a/controllers/application/posts/likeController.js +++ b/controllers/application/posts/likeController.js @@ -1,72 +1,72 @@ -/* eslint-disable camelcase */ -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 { - const { postId } = req.body; - const token = req.header('Authorization')?.split(' ')[1]; - - if (!token) { - return res.status(401).json({ message: 'توکن ارائه نشده است' }); - } - - let userId; - try { - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - userId = decodedToken.id; - } catch (err) { - return res.status(401).json({ message: 'توکن نامعتبر است' }); - } - - if (!postId) { - return res.status(400).json({ message: 'شناسه پست الزامی است' }); - } - - const post = await PostModel.findById(postId); - if (!post) { - return res.status(404).json({ message: 'پست مورد نظر یافت نشد' }); - } - - const isLiked = post.likes.includes(userId); - if (isLiked) { - // unlike - post.likes.pull(userId); - await post.save(); - await LikeModel.findOneAndDelete({ postId, userId }); - - return res.status(200).json({ - message: 'لایک با موفقیت حذف شد', - is_liked: false, - likesCount: post.likes.length, - }); - } else { - // like - post.likes.push(userId); - 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, - likesCount: post.likes.length, - }); - } - } catch (error) { - console.error('خطا در toggleLike:', error); - res.status(500).json({ message: 'خطا در سرور' }); - next(error); - } -}; - +/* eslint-disable camelcase */ +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 { + const { postId } = req.body; + const token = req.header('Authorization')?.split(' ')[1]; + + if (!token) { + return res.status(401).json({ message: 'توکن ارائه نشده است' }); + } + + let userId; + try { + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + userId = decodedToken.id; + } catch (err) { + return res.status(401).json({ message: 'توکن نامعتبر است' }); + } + + if (!postId) { + return res.status(400).json({ message: 'شناسه پست الزامی است' }); + } + + const post = await PostModel.findById(postId); + if (!post) { + return res.status(404).json({ message: 'پست مورد نظر یافت نشد' }); + } + + const isLiked = post.likes.includes(userId); + if (isLiked) { + // unlike + post.likes.pull(userId); + await post.save(); + await LikeModel.findOneAndDelete({ postId, userId }); + + return res.status(200).json({ + message: 'لایک با موفقیت حذف شد', + is_liked: false, + likesCount: post.likes.length, + }); + } else { + // like + post.likes.push(userId); + 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, + likesCount: post.likes.length, + }); + } + } catch (error) { + console.error('خطا در toggleLike:', error); + res.status(500).json({ message: 'خطا در سرور' }); + next(error); + } +}; + module.exports = { toggleLike }; \ No newline at end of file diff --git a/controllers/application/posts/postController.js b/controllers/application/posts/postController.js index c0ac0c4..b6ff0e3 100644 --- a/controllers/application/posts/postController.js +++ b/controllers/application/posts/postController.js @@ -1,272 +1,272 @@ -/* eslint-disable camelcase */ -const PostModel = require('../../../models/PostModel'); -const UserModel = require('../../../models/UserModel'); -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); - return [ - check('caption') - .notEmpty().withMessage('توضیحات نمی‌تواند خالی باشد'), - ]; -}; - -const createPost = async (req, res) => { - try { - console.log('DEBUG: createPost called'); - console.log('DEBUG: req.files:', req.files); - console.log('DEBUG: req.body:', req.body); - - const { caption } = req.body; - const files = req.files || []; - const userId = req.user.id; // از میدل‌ویر auth - - if (!files || files.length === 0) { - console.log('DEBUG: No files provided'); - return res.status(400).json({ error: true, message: 'هیچ فایلی آپلود نشده است' }); - } - - if (!caption) { - console.log('DEBUG: No caption provided'); - return res.status(400).json({ error: true, message: 'کپشن الزامی است' }); - } - - const filePaths = files.map((file) => ({ - path: file.path, - type: file.mimetype.startsWith('video/') ? 'video' : 'image', - })); - - console.log('DEBUG: Saving post to MongoDB, filePaths:', filePaths); - const post = new PostModel({ - user_id: userId, - caption, - files: filePaths, - type: files[0].mimetype.startsWith('video/') ? 'video' : 'image', // اضافه کردن فیلد type - createdAt: new Date(), - }); - - await post.save(); - console.log('DEBUG: Post saved:', post._id); - res.status(201).json({ message: 'پست با موفقیت ایجاد شد', postId: post._id }); - } catch (err) { - console.error('DEBUG: createPost error:', err.message); - res.status(500).json({ error: true, message: `خطا در سرور: ${err.message}` }); - } -}; - - - - - - - -const getUserPosts = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const user_id = req.query.user_id - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userReqId = decodedToken.id - - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) - } - const user = await UserModel.findById(user_id) - - if (!user) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - const response = {} - const options = { - page: req.query.page || 1, // صفحه پیش‌فرض ۱ - limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه - sort: { createdAt: -1 } - } - if (user_id.toString() === userReqId.toString()) { - // کاربر خودش است، همه پست‌هایش را بررسی می‌کند - const posts = await PostModel.paginate( - { user_id: user._id, status: { $in: ['pending', 'accept'] } }, // فیلتر - options // گزینه‌های پیجینیشن - ) - response.posts = posts.docs - response.postsCount = posts.totalDocs - response.totalPages = posts.totalPages // ارسال تعداد کل صفحات - response.totalItems = posts.totalDocs - } else { - // دیگران، فقط پست‌هایی که وضعیتشان "accept" است را ببینند - const posts = await PostModel.paginate( - { user_id: user._id, status: 'accept' }, // فیلتر - options // گزینه‌های پیجینیشن - ) - response.posts = posts.docs - response.postsCount = posts.totalDocs - response.totalPages = posts.totalPages // ارسال تعداد کل صفحات - response.totalItems = posts.totalDocs - } - return res.status(200).json({ data: response }) - } catch (error) { - next(error) - } -} -const getUserPostsWeb = async (req, res, next) => { - try { - let userReqId = null - - // دریافت توکن و بررسی معتبر بودنش (اگر وجود داشت) - const authHeader = req.header('Authorization') - if (authHeader) { - const token = authHeader.split(' ')[1] - try { - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - userReqId = decodedToken.id - } catch (err) { - console.warn('توکن نامعتبر است، ادامه بدون احراز هویت') // فقط هشدار، نه ارور - } - } - - const user_id = req.query.user_id - - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) - } - - const user = await UserModel.findById(user_id) - if (!user) { - 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, - limit: req.query.limit || 10, - sort: { createdAt: -1 } - } - - if (userReqId && user_id.toString() === userReqId.toString()) { - // اگر کاربر لاگین کرده و درخواست برای خودش است، همه پست‌هایش را ببیند - const posts = await PostModel.paginate( - { user_id: user._id, status: { $in: ['pending', 'accept'] } }, - options - ) - data.posts = posts.docs - data.postsCount = posts.totalDocs - data.totalPages = posts.totalPages - data.totalItems = posts.totalDocs - } else { - // برای بقیه، فقط پست‌هایی که وضعیتشان "accept" است نمایش داده شود - const posts = await PostModel.paginate( - { user_id: user._id, status: 'accept' }, - options - ) - data.posts = posts.docs - data.postsCount = posts.totalDocs - data.totalPages = posts.totalPages - data.totalItems = posts.totalDocs - } - - return res.status(200).json(data) - } catch (error) { - next(error) - } -} - -const getPostByIdWeb = async (req, res, next) => { - try { - const { postId } = req.params - - if (!mongoose.Types.ObjectId.isValid(postId)) { - return res.status(400).json({ message: 'شناسه پست معتبر نیست' }) - } - - let userReqId = null - const authHeader = req.header('Authorization') - if (authHeader) { - const token = authHeader.split(' ')[1] - try { - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - userReqId = decodedToken.id - } catch (err) { - console.warn('توکن نامعتبر است، ادامه بدون احراز هویت') - } - } - - const post = await PostModel.findById(postId).lean() - if (!post || post.status !== 'accept') { - return res.status(404).json({ message: 'پست یافت نشد' }) - } - - const user = await UserModel.findById(post.user_id) - .select( - '_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image blocked_users' - ) - .lean() - - if (!user) { - return res.status(404).json({ message: 'پست یافت نشد' }) - } - - if (userReqId && viewerIsBlockedBy(user, userReqId)) { - return res.status(404).json({ message: 'پست یافت نشد' }) - } - - const LikeModel = require('../../../models/LikeModel') - const CommentModel = require('../../../models/CommentModel') - - let is_liked = false - if (userReqId) { - const like = await LikeModel.findOne({ - userId: userReqId, - postId: post._id, - }).lean() - is_liked = !!like - } - - const commentsCount = await CommentModel.countDocuments({ - post: post._id, - status: 'accepted', - }) - - return res.status(200).json({ - post: { - ...post, - user_name: user.user_name || '', - first_name: user.first_name || '', - last_name: user.last_name || '', - expertise: user.expertise || '', - sub_expertise: user.sub_expertise || [], - province: user.show_location ? user.province || {} : {}, - city: user.show_location ? user.city || {} : {}, - show_location: user.show_location, - user_level: user.user_level || '', - profile_image: user.profile_image || '', - likesCount: post.likes ? post.likes.length : 0, - is_liked, - commentsCount, - }, - }) - } catch (error) { - next(error) - } -} - -module.exports = { - createPostValidationRules, - createPost, - getUserPosts, - getUserPostsWeb, - getPostByIdWeb -} +/* eslint-disable camelcase */ +const PostModel = require('../../../models/PostModel'); +const UserModel = require('../../../models/UserModel'); +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); + return [ + check('caption') + .notEmpty().withMessage('توضیحات نمی‌تواند خالی باشد'), + ]; +}; + +const createPost = async (req, res) => { + try { + console.log('DEBUG: createPost called'); + console.log('DEBUG: req.files:', req.files); + console.log('DEBUG: req.body:', req.body); + + const { caption } = req.body; + const files = req.files || []; + const userId = req.user.id; // از میدل‌ویر auth + + if (!files || files.length === 0) { + console.log('DEBUG: No files provided'); + return res.status(400).json({ error: true, message: 'هیچ فایلی آپلود نشده است' }); + } + + if (!caption) { + console.log('DEBUG: No caption provided'); + return res.status(400).json({ error: true, message: 'کپشن الزامی است' }); + } + + const filePaths = files.map((file) => ({ + path: file.path, + type: file.mimetype.startsWith('video/') ? 'video' : 'image', + })); + + console.log('DEBUG: Saving post to MongoDB, filePaths:', filePaths); + const post = new PostModel({ + user_id: userId, + caption, + files: filePaths, + type: files[0].mimetype.startsWith('video/') ? 'video' : 'image', // اضافه کردن فیلد type + createdAt: new Date(), + }); + + await post.save(); + console.log('DEBUG: Post saved:', post._id); + res.status(201).json({ message: 'پست با موفقیت ایجاد شد', postId: post._id }); + } catch (err) { + console.error('DEBUG: createPost error:', err.message); + res.status(500).json({ error: true, message: `خطا در سرور: ${err.message}` }); + } +}; + + + + + + + +const getUserPosts = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const user_id = req.query.user_id + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userReqId = decodedToken.id + + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) + } + const user = await UserModel.findById(user_id) + + if (!user) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + const response = {} + const options = { + page: req.query.page || 1, // صفحه پیش‌فرض ۱ + limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه + sort: { createdAt: -1 } + } + if (user_id.toString() === userReqId.toString()) { + // کاربر خودش است، همه پست‌هایش را بررسی می‌کند + const posts = await PostModel.paginate( + { user_id: user._id, status: { $in: ['pending', 'accept'] } }, // فیلتر + options // گزینه‌های پیجینیشن + ) + response.posts = posts.docs + response.postsCount = posts.totalDocs + response.totalPages = posts.totalPages // ارسال تعداد کل صفحات + response.totalItems = posts.totalDocs + } else { + // دیگران، فقط پست‌هایی که وضعیتشان "accept" است را ببینند + const posts = await PostModel.paginate( + { user_id: user._id, status: 'accept' }, // فیلتر + options // گزینه‌های پیجینیشن + ) + response.posts = posts.docs + response.postsCount = posts.totalDocs + response.totalPages = posts.totalPages // ارسال تعداد کل صفحات + response.totalItems = posts.totalDocs + } + return res.status(200).json({ data: response }) + } catch (error) { + next(error) + } +} +const getUserPostsWeb = async (req, res, next) => { + try { + let userReqId = null + + // دریافت توکن و بررسی معتبر بودنش (اگر وجود داشت) + const authHeader = req.header('Authorization') + if (authHeader) { + const token = authHeader.split(' ')[1] + try { + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + userReqId = decodedToken.id + } catch (err) { + console.warn('توکن نامعتبر است، ادامه بدون احراز هویت') // فقط هشدار، نه ارور + } + } + + const user_id = req.query.user_id + + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) + } + + const user = await UserModel.findById(user_id) + if (!user) { + 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, + limit: req.query.limit || 10, + sort: { createdAt: -1 } + } + + if (userReqId && user_id.toString() === userReqId.toString()) { + // اگر کاربر لاگین کرده و درخواست برای خودش است، همه پست‌هایش را ببیند + const posts = await PostModel.paginate( + { user_id: user._id, status: { $in: ['pending', 'accept'] } }, + options + ) + data.posts = posts.docs + data.postsCount = posts.totalDocs + data.totalPages = posts.totalPages + data.totalItems = posts.totalDocs + } else { + // برای بقیه، فقط پست‌هایی که وضعیتشان "accept" است نمایش داده شود + const posts = await PostModel.paginate( + { user_id: user._id, status: 'accept' }, + options + ) + data.posts = posts.docs + data.postsCount = posts.totalDocs + data.totalPages = posts.totalPages + data.totalItems = posts.totalDocs + } + + return res.status(200).json(data) + } catch (error) { + next(error) + } +} + +const getPostByIdWeb = async (req, res, next) => { + try { + const { postId } = req.params + + if (!mongoose.Types.ObjectId.isValid(postId)) { + return res.status(400).json({ message: 'شناسه پست معتبر نیست' }) + } + + let userReqId = null + const authHeader = req.header('Authorization') + if (authHeader) { + const token = authHeader.split(' ')[1] + try { + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + userReqId = decodedToken.id + } catch (err) { + console.warn('توکن نامعتبر است، ادامه بدون احراز هویت') + } + } + + const post = await PostModel.findById(postId).lean() + if (!post || post.status !== 'accept') { + return res.status(404).json({ message: 'پست یافت نشد' }) + } + + const user = await UserModel.findById(post.user_id) + .select( + '_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image blocked_users' + ) + .lean() + + if (!user) { + return res.status(404).json({ message: 'پست یافت نشد' }) + } + + if (userReqId && viewerIsBlockedBy(user, userReqId)) { + return res.status(404).json({ message: 'پست یافت نشد' }) + } + + const LikeModel = require('../../../models/LikeModel') + const CommentModel = require('../../../models/CommentModel') + + let is_liked = false + if (userReqId) { + const like = await LikeModel.findOne({ + userId: userReqId, + postId: post._id, + }).lean() + is_liked = !!like + } + + const commentsCount = await CommentModel.countDocuments({ + post: post._id, + status: 'accepted', + }) + + return res.status(200).json({ + post: { + ...post, + user_name: user.user_name || '', + first_name: user.first_name || '', + last_name: user.last_name || '', + expertise: user.expertise || '', + sub_expertise: user.sub_expertise || [], + province: user.show_location ? user.province || {} : {}, + city: user.show_location ? user.city || {} : {}, + show_location: user.show_location, + user_level: user.user_level || '', + profile_image: user.profile_image || '', + likesCount: post.likes ? post.likes.length : 0, + is_liked, + commentsCount, + }, + }) + } catch (error) { + next(error) + } +} + +module.exports = { + createPostValidationRules, + createPost, + getUserPosts, + getUserPostsWeb, + getPostByIdWeb +} diff --git a/controllers/application/profile/profileController.js b/controllers/application/profile/profileController.js index 2fc0f74..cce7027 100644 --- a/controllers/application/profile/profileController.js +++ b/controllers/application/profile/profileController.js @@ -1,140 +1,140 @@ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - - -// GET user by ID from URL -const getUserById = async (req, res, next) => { - try { - const userId = req.params.id; // گرفتن آیدی از پارامتر URL - const service = req.query.services; - - const user = await UserModel.findById(userId); - if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); - } - - let isRegister = true; - // if ( - // !user.user_name || - // !user.first_name || - // !user.last_name || - // !user.user_type || - // !user.national_card_image || - // !user.password || - // !user.national_code - // ) { - // isRegister = false; - // } - - const response = { - _id: user._id, - user_type: user.user_type, - user_level: user.user_level, - first_name: user.first_name, - last_name: user.last_name, - 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, - height: user.height, - weight: user.weight, - size: user.size, - eye_color: user.eye_color, - hair_color: user.hair_color, - profile_image: user.profile_image, - lat: user.lat, - lng: user.lng, - address: user.address, - province: user.province, - city: user.city, - shaba: user.shaba, - national_code: user.national_code, - mobile: user.mobile, - cooperation_abroad: user.cooperation_abroad, - cooperation_type: user.cooperation_type, - conversation_projects: user.conversation_projects, - expertise: user.expertise, - sub_expertise: user.sub_expertise, - isRegister, - user, - services: service === "1" ? user.services : null - }; - - return res.status(200).json({ user: response }); - } catch (error) { - next(error); - } -}; - -const getProfile = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const service = req.query.services - - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const user = await UserModel.findById(userId) - let isRegister = true - if (!user.user_name || - !user.first_name || - !user.last_name || - !user.user_type || - !user.national_card_image || - !user.password || - !user.national_code - ) { - isRegister = false - } - if (!user) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - const response = { - _id: user._id, - user_type: user.user_type, - user_level: user.user_level, - first_name: user.first_name, - last_name: user.last_name, - 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, - height: user.height, - weight: user.weight, - size: user.size, - eye_color: user.eye_color, - hair_color: user.hair_color, - profile_image: user.profile_image, - lat: user.lat, - lng: user.lng, - address: user.address, - province: user.province, - city: user.city, - shaba: user.shaba, - national_code: user.national_code, - mobile: user.mobile, - cooperation_abroad: user.cooperation_abroad, - cooperation_type: user.cooperation_type, - conversation_projects: user.conversation_projects, - expertise: user.expertise, - sub_expertise: user.sub_expertise, - isRegister, - services: service === '1' ? user.services : null - } - return res.status(200).json({ user: response }) - } catch (error) { - next(error) - } -} - -module.exports = { - getProfile, - getUserById -}; +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + + +// GET user by ID from URL +const getUserById = async (req, res, next) => { + try { + const userId = req.params.id; // گرفتن آیدی از پارامتر URL + const service = req.query.services; + + const user = await UserModel.findById(userId); + if (!user) { + return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + } + + let isRegister = true; + // if ( + // !user.user_name || + // !user.first_name || + // !user.last_name || + // !user.user_type || + // !user.national_card_image || + // !user.password || + // !user.national_code + // ) { + // isRegister = false; + // } + + const response = { + _id: user._id, + user_type: user.user_type, + user_level: user.user_level, + first_name: user.first_name, + last_name: user.last_name, + 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, + height: user.height, + weight: user.weight, + size: user.size, + eye_color: user.eye_color, + hair_color: user.hair_color, + profile_image: user.profile_image, + lat: user.lat, + lng: user.lng, + address: user.address, + province: user.province, + city: user.city, + shaba: user.shaba, + national_code: user.national_code, + mobile: user.mobile, + cooperation_abroad: user.cooperation_abroad, + cooperation_type: user.cooperation_type, + conversation_projects: user.conversation_projects, + expertise: user.expertise, + sub_expertise: user.sub_expertise, + isRegister, + user, + services: service === "1" ? user.services : null + }; + + return res.status(200).json({ user: response }); + } catch (error) { + next(error); + } +}; + +const getProfile = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const service = req.query.services + + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const user = await UserModel.findById(userId) + let isRegister = true + if (!user.user_name || + !user.first_name || + !user.last_name || + !user.user_type || + !user.national_card_image || + !user.password || + !user.national_code + ) { + isRegister = false + } + if (!user) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + const response = { + _id: user._id, + user_type: user.user_type, + user_level: user.user_level, + first_name: user.first_name, + last_name: user.last_name, + 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, + height: user.height, + weight: user.weight, + size: user.size, + eye_color: user.eye_color, + hair_color: user.hair_color, + profile_image: user.profile_image, + lat: user.lat, + lng: user.lng, + address: user.address, + province: user.province, + city: user.city, + shaba: user.shaba, + national_code: user.national_code, + mobile: user.mobile, + cooperation_abroad: user.cooperation_abroad, + cooperation_type: user.cooperation_type, + conversation_projects: user.conversation_projects, + expertise: user.expertise, + sub_expertise: user.sub_expertise, + isRegister, + services: service === '1' ? user.services : null + } + return res.status(200).json({ user: response }) + } catch (error) { + next(error) + } +} + +module.exports = { + getProfile, + getUserById +}; diff --git a/controllers/application/projects/createProjectController.js b/controllers/application/projects/createProjectController.js index 8ff3ce1..a6c817c 100644 --- a/controllers/application/projects/createProjectController.js +++ b/controllers/application/projects/createProjectController.js @@ -1,221 +1,221 @@ -/* eslint-disable camelcase */ -const { ProvinceModel, CityModel } = require('../../../models/StateCity') -const ProjectModel = require('../../../models/ProjectModel') -const UserModel = require('../../../models/UserModel') -const { check, validationResult } = require('express-validator') -const jwt = require('jsonwebtoken') -const { - canCreateProject, - respondProjectProfileIncomplete -} = require('../../../utils/projectProfile') -const createProjectValidationRules = () => { - return [ - check('title').notEmpty().withMessage('عنوان نمی‌تواند خالی باشد'), - - check('expertise').notEmpty().withMessage('تخصص نمی‌تواند خالی باشد'), - - check('gender') - .isIn(['male', 'female']) - .withMessage('جنسیت باید male یا female باشد'), - - check('age').notEmpty().withMessage('سن نمی‌تواند خالی باشد'), - - check('conversation_projects') - .notEmpty() - .withMessage('پروژه‌های مکالمه نمی‌تواند خالی باشد') - .isBoolean() - .withMessage('مقدار پروژه‌های مکالمه باید یک مقدار boolean باشد'), - - check('province').notEmpty().withMessage('استان نمی‌تواند خالی باشد'), - - check('city').notEmpty().withMessage('شهر نمی‌تواند خالی باشد'), - - check('offer_time') - .notEmpty() - .withMessage('زمان پروژه نمی‌تواند خالی باشد'), - - check('offer_price') - .notEmpty() - .withMessage('قیمت پیشنهادی نمی‌تواند خالی باشد'), - - check('description') - .notEmpty() - .withMessage('توضیحات نمی‌تواند خالی باشد'), - - check('project_type') - .notEmpty() - .withMessage('نوع پروژه نمی‌تواند خالی باشد') - .isIn(['free', 'normal', 'force', 'highlight']) - .withMessage('نوع پروژه باید normal یا force یا highlight باشد') - ] -} - -const createProject = 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(422).json({ - error: true, - message: 'شما دسترسی به این بخش ندارید' - }) - } - if (!canCreateProject(user)) { - return respondProjectProfileIncomplete(res) - } - // اعتبارسنجی درخواست - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - const { - title, - expertise, - sub_expertise, - gender, - age, - conversation_projects, - province, - city, - offer_time, - offer_price, - description, - project_type, - public_status, - created_for_user, - number_of_person - } = req.body - if ( - !title || - !expertise || - !age || - conversation_projects === null || - !province || - !city || - !offer_time || - !offer_price || - !description || - !project_type - ) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const provinceFind = await ProvinceModel.findOne( - { id: province }, - { _id: 0 } - ) - const cityFind = await CityModel.findOne({ id: city }) - // اگر پروژه برای یک کاربر خاص باید ساخته شود - if (public_status && public_status === 'private' && created_for_user) { - const createdUser = await UserModel.findById(created_for_user) - if (!createdUser) { - return res.status(422).json({ - error: true, message: 'کاربر مورد نظر برای ساخت پروژه یافت نشد' - }) - } - // ایجاد شیء پروژه - const projectData = { - title, - expertise, - sub_expertise, - gender, - age, - conversation_projects, - province, - city, - offer_time, - offer_price, - description, - project_type, - creator_id: user._id, // ارتباط با کاربر - public_status, - created_for_user - } - - // ساخت شیء پروژه - const project = new ProjectModel(projectData) - - // ذخیره کردن رکورد در پایگاه داده - await project.save() - - res.status(201).json({ message: 'پروژه با موفقیت ساخته شد', id: project._id } - ) - } else { - if (number_of_person > 1) { - for (let i = 1; i <= number_of_person; i++) { - let currentProjectType - - if (project_type === 'free' && i === 1) { - currentProjectType = 'free' - } else if (project_type === 'highlight' || project_type === 'force') { - currentProjectType = project_type - } else { - currentProjectType = 'normal' - } - - const newProject = new ProjectModel({ - title: `${title}_${i}`, - expertise, - sub_expertise, - gender, - age, - conversation_projects, - province: provinceFind, - city: cityFind, - offer_time, - offer_price, - description, - creator_id: user._id, // ارتباط با کاربر - project_type: currentProjectType, - public_status, - created_for_user - }) - - // ذخیره کردن رکورد در پایگاه داده - await newProject.save() - } - - res.status(201).json({ - message: 'پروژه‌ها با موفقیت ساخته شدند' - }) - } else { - const project = new ProjectModel({ - title, - expertise, - sub_expertise, - gender, - age, - conversation_projects, - province: provinceFind, - city: cityFind, - offer_time, - offer_price, - description, - creator_id: user._id, // ارتباط با کاربر - project_type - }) - - // ذخیره کردن رکورد در پایگاه داده - await project.save() - - res.status(201).json({ - message: 'پروژه با موفقیت ساخته شد', - id: project._id - }) - } - } - } catch (error) { - next(error) - } -} - -module.exports = { - createProjectValidationRules, - createProject -} +/* eslint-disable camelcase */ +const { ProvinceModel, CityModel } = require('../../../models/StateCity') +const ProjectModel = require('../../../models/ProjectModel') +const UserModel = require('../../../models/UserModel') +const { check, validationResult } = require('express-validator') +const jwt = require('jsonwebtoken') +const { + canCreateProject, + respondProjectProfileIncomplete +} = require('../../../utils/projectProfile') +const createProjectValidationRules = () => { + return [ + check('title').notEmpty().withMessage('عنوان نمی‌تواند خالی باشد'), + + check('expertise').notEmpty().withMessage('تخصص نمی‌تواند خالی باشد'), + + check('gender') + .isIn(['male', 'female']) + .withMessage('جنسیت باید male یا female باشد'), + + check('age').notEmpty().withMessage('سن نمی‌تواند خالی باشد'), + + check('conversation_projects') + .notEmpty() + .withMessage('پروژه‌های مکالمه نمی‌تواند خالی باشد') + .isBoolean() + .withMessage('مقدار پروژه‌های مکالمه باید یک مقدار boolean باشد'), + + check('province').notEmpty().withMessage('استان نمی‌تواند خالی باشد'), + + check('city').notEmpty().withMessage('شهر نمی‌تواند خالی باشد'), + + check('offer_time') + .notEmpty() + .withMessage('زمان پروژه نمی‌تواند خالی باشد'), + + check('offer_price') + .notEmpty() + .withMessage('قیمت پیشنهادی نمی‌تواند خالی باشد'), + + check('description') + .notEmpty() + .withMessage('توضیحات نمی‌تواند خالی باشد'), + + check('project_type') + .notEmpty() + .withMessage('نوع پروژه نمی‌تواند خالی باشد') + .isIn(['free', 'normal', 'force', 'highlight']) + .withMessage('نوع پروژه باید normal یا force یا highlight باشد') + ] +} + +const createProject = 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(422).json({ + error: true, + message: 'شما دسترسی به این بخش ندارید' + }) + } + if (!canCreateProject(user)) { + return respondProjectProfileIncomplete(res) + } + // اعتبارسنجی درخواست + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + const { + title, + expertise, + sub_expertise, + gender, + age, + conversation_projects, + province, + city, + offer_time, + offer_price, + description, + project_type, + public_status, + created_for_user, + number_of_person + } = req.body + if ( + !title || + !expertise || + !age || + conversation_projects === null || + !province || + !city || + !offer_time || + !offer_price || + !description || + !project_type + ) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const provinceFind = await ProvinceModel.findOne( + { id: province }, + { _id: 0 } + ) + const cityFind = await CityModel.findOne({ id: city }) + // اگر پروژه برای یک کاربر خاص باید ساخته شود + if (public_status && public_status === 'private' && created_for_user) { + const createdUser = await UserModel.findById(created_for_user) + if (!createdUser) { + return res.status(422).json({ + error: true, message: 'کاربر مورد نظر برای ساخت پروژه یافت نشد' + }) + } + // ایجاد شیء پروژه + const projectData = { + title, + expertise, + sub_expertise, + gender, + age, + conversation_projects, + province, + city, + offer_time, + offer_price, + description, + project_type, + creator_id: user._id, // ارتباط با کاربر + public_status, + created_for_user + } + + // ساخت شیء پروژه + const project = new ProjectModel(projectData) + + // ذخیره کردن رکورد در پایگاه داده + await project.save() + + res.status(201).json({ message: 'پروژه با موفقیت ساخته شد', id: project._id } + ) + } else { + if (number_of_person > 1) { + for (let i = 1; i <= number_of_person; i++) { + let currentProjectType + + if (project_type === 'free' && i === 1) { + currentProjectType = 'free' + } else if (project_type === 'highlight' || project_type === 'force') { + currentProjectType = project_type + } else { + currentProjectType = 'normal' + } + + const newProject = new ProjectModel({ + title: `${title}_${i}`, + expertise, + sub_expertise, + gender, + age, + conversation_projects, + province: provinceFind, + city: cityFind, + offer_time, + offer_price, + description, + creator_id: user._id, // ارتباط با کاربر + project_type: currentProjectType, + public_status, + created_for_user + }) + + // ذخیره کردن رکورد در پایگاه داده + await newProject.save() + } + + res.status(201).json({ + message: 'پروژه‌ها با موفقیت ساخته شدند' + }) + } else { + const project = new ProjectModel({ + title, + expertise, + sub_expertise, + gender, + age, + conversation_projects, + province: provinceFind, + city: cityFind, + offer_time, + offer_price, + description, + creator_id: user._id, // ارتباط با کاربر + project_type + }) + + // ذخیره کردن رکورد در پایگاه داده + await project.save() + + res.status(201).json({ + message: 'پروژه با موفقیت ساخته شد', + id: project._id + }) + } + } + } catch (error) { + next(error) + } +} + +module.exports = { + createProjectValidationRules, + createProject +} diff --git a/controllers/application/projects/getProjectController.js b/controllers/application/projects/getProjectController.js index 9d10586..b302743 100644 --- a/controllers/application/projects/getProjectController.js +++ b/controllers/application/projects/getProjectController.js @@ -1,406 +1,406 @@ -/* eslint-disable camelcase */ -const { default: mongoose } = require('mongoose') -const ProjectModel = require('../../../models/ProjectModel') -const RequestModel = require('../../../models/RequestModel') -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') -const calculateRemainingTime = (milliseconds) => { - const days = Math.floor(milliseconds / (1000 * 60 * 60 * 24)) - const hours = Math.floor((milliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) - - return `${days} روز و ${hours} ساعت` -} -const getProjects = async (req, res, next) => { - try { - // eslint-disable-next-line no-unused-vars - const { expertise, page = 1, limit = 10, most_price, most_requests, age, gender } = req.query - // ساخت فیلتر برای استفاده در جستجوی MongoDB - const filter = { - payment_status: 'done', - status: 'accepted', - public_status: 'public', - $or: [ - { isExpired: false }, // پروژه‌هایی که isExpired برابر با false است - { isExpired: { $exists: false } } // پروژه‌هایی که فیلد isExpired وجود ندارد - ] - } - if (expertise) { - filter.expertise = expertise - } - if (age) { - filter.age = age - } - if (gender) { - filter.gender = gender - } - // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده - - if (most_price || most_requests) { - // دریافت لیست پروژه‌ها با استفاده از فیلتر و گزینه‌های صفحه‌بندی - const options = { - page: parseInt(page), // تبدیل صفحه به عدد صحیح - limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح - sort: most_price === 'true' ? { offer_price: -1 } : { requested_users: 'desc' } - } - const projects = await ProjectModel.paginate(filter, options) - - const newProj = projects.docs - res.status(200).json({ - projects: newProj, - totalPages: projects.totalPages, // ارسال تعداد کل صفحات - totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } else { - // دریافت لیست پروژه‌ها با استفاده از فیلتر و گزینه‌های صفحه‌بندی - const options = { - page: parseInt(page), // تبدیل صفحه به عدد صحیح - limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح - sort: { acceptedAt: -1, createdAt: -1 } // مرتب‌سازی بر اساس acceptedAt و سپس createdAt - } - const projects = await ProjectModel.paginate(filter, options) - const newProj = projects.docs - res.status(200).json({ - projects: newProj, - totalPages: projects.totalPages, // ارسال تعداد کل صفحات - totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } - } catch (error) { - next(error) - } - // try { - // const token = req.header('Authorization').split(' ')[1] - // if (!token) return res.status(401).send('Access Denied') - - // // eslint-disable-next-line no-unused-vars - // const decodedToken = jwt.verify(token, process.env.APP_SECRET) - - // const { expertise, page = 1, limit = 10 } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست - - // // ساخت فیلتر برای استفاده در جستجوی MongoDB - // const filter = { - // payment_status: 'done', - // status: 'accepted', - // public_status: 'public' - // } // افزودن شرط‌های دیگر برای فیلتر - - // if (expertise) { - // filter.expertise = expertise - // } - - // // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده - // const options = { - // page: parseInt(page), // تبدیل صفحه به عدد صحیح - // limit: parseInt(limit) // تبدیل محدودیت به عدد صحیح - // } - - // // دریافت لیست پروژه‌ها با استفاده از فیلتر و گزینه‌های صفحه‌بندی - // const projects = await ProjectModel.paginate(filter, options) - - // // تبدیل نتایج به فرمت مورد نیاز - // const projectsWithUserInfo = await Promise.all(projects.docs.map(async project => { - // const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') - // const projectWithUserInfo = { - // ...project.toJSON(), // تبدیل آبجکت پروژه به JSON - // creator // اضافه کردن اطلاعات کاربر به آبجکت پروژه - // } - - // // محاسبه مدت زمان باقی‌مانده برای هر پروژه - // const currentTime = new Date() - // const projectCreatedAt = new Date(project.createdAt) - // const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) - // const remainingTime = calculateRemainingTime(timeDiff) - // projectWithUserInfo.remainingTime = remainingTime - - // return projectWithUserInfo - // })) - - // res.status(200).json({ - // projects: projectsWithUserInfo, - // totalPages: projects.totalPages, // ارسال تعداد کل صفحات - // totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها - // }) - // } catch (error) { - // next(error) - // } -} -const getUserProjects = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const user_id = req.query.user_id - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - // const userReqId = decodedToken.id - - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) - } - const user = await UserModel.findById(user_id) - - if (!user) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - const response = {} - if (user.user_type === 'user') { - const options = { - page: req.query.page || 1, // صفحه پیش‌فرض ۱ - limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه - sort: { createdAt: -1 } - } - - // پیجینیشن بر روی لیست پروژه‌های کاربر اعمال می‌شود - const projects = await ProjectModel.paginate( - { selected_user: user._id, status: 'done' }, // فیلتر - options // گزینه‌های پیجینیشن - ) - response.projects = projects.docs - response.totalPages = projects.totalPages // ارسال تعداد کل صفحات - response.totalItems = projects.totalDocs // ارسال تعداد کل آیتم‌ها - response.allProjectsCount = await ProjectModel.countDocuments({ selected_user: user._id }) - const completedProjects = await Promise.all([ - ProjectModel.countDocuments({ selected_user: user._id, status: 'done' }) - ]) - response.successfulProjects = completedProjects - } else { - const options = { - page: req.query.page || 1, // صفحه پیش‌فرض ۱ - limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه - sort: { createdAt: -1 } - } - - // پیجینیشن بر روی لیست پروژه‌های کاربر اعمال می‌شود - const projects = await ProjectModel.paginate( - { creator_id: user._id, status: { $in: ['done', 'accepted'] } }, // فیلتر - options // گزینه‌های پیجینیشن - ) - response.projects = projects.docs - response.totalPages = projects.totalPages // ارسال تعداد کل صفحات - response.totalItems = projects.totalDocs - response.allProjectsCount = await ProjectModel.countDocuments({ creator_id: user._id, payment_status: 'done' }) - // تعداد پروژه‌های موفق را به دست آورید - const successfulProjects = await ProjectModel.countDocuments({ creator_id: user._id, status: 'done' }) - response.successfulProjects = successfulProjects - } - return res.status(200).json({ data: response }) - } catch (error) { - next(error) - } -} -const getUserProjectsWeb = async (req, res, next) => { - try { - const user_id = req.query.user_id - // eslint-disable-next-line no-unused-vars - - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) - } - const user = await UserModel.findById(user_id) - - if (!user) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - const data = {} - if (user.user_type === 'user') { - const options = { - page: req.query.page || 1, // صفحه پیش‌فرض ۱ - limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه - sort: { createdAt: -1 } - } - - // پیجینیشن بر روی لیست پروژه‌های کاربر اعمال می‌شود - const projects = await ProjectModel.paginate( - { selected_user: user._id, status: 'done' }, // فیلتر - options // گزینه‌های پیجینیشن - ) - data.projects = projects.docs - data.totalPages = projects.totalPages // ارسال تعداد کل صفحات - data.totalItems = projects.totalDocs // ارسال تعداد کل آیتم‌ها - data.allProjectsCount = await ProjectModel.countDocuments({ selected_user: user._id }) - const completedProjects = await Promise.all([ - ProjectModel.countDocuments({ selected_user: user._id, status: 'done' }) - ]) - data.successfulProjects = completedProjects - } else { - const options = { - page: req.query.page || 1, // صفحه پیش‌فرض ۱ - limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه - sort: { createdAt: -1 } - } - - // پیجینیشن بر روی لیست پروژه‌های کاربر اعمال می‌شود - const projects = await ProjectModel.paginate( - { creator_id: user._id, status: { $in: ['done', 'accepted'] } }, // فیلتر - options // گزینه‌های پیجینیشن - ) - data.projects = projects.docs - data.totalPages = projects.totalPages // ارسال تعداد کل صفحات - data.totalItems = projects.totalDocs - data.allProjectsCount = await ProjectModel.countDocuments({ creator_id: user._id, payment_status: 'done' }) - // تعداد پروژه‌های موفق را به دست آورید - const successfulProjects = await ProjectModel.countDocuments({ creator_id: user._id, status: 'done' }) - data.successfulProjects = successfulProjects - } - return res.status(200).json(data) - } catch (error) { - next(error) - } -} -const getSingleProjects = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const projectId = req.params.projectId - const projectRequests = await RequestModel.find({ project: projectId }) - .populate('user', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image') - .sort({ status: 1 }) - .lean() - const modifiedRequests = projectRequests.map(request => ({ - ...request, - time: request.user._id.toString() === userId ? request.time : null, - price: request.user._id.toString() === userId ? request.price : null - })) - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ message: 'پروژه مورد نظر یافت نشد' }) - } - // محاسبه مبلغ باقیمانده و مجموع مبلغ پرداخت شده - // تابع محاسبه مبلغ پرداخت شده - const getTotalPaidAmount = (installments) => { - let totalPaidAmount = 0 - installments.forEach(installment => { - totalPaidAmount += installment.amount - }) - return totalPaidAmount - } - - // تابع محاسبه مبلغ باقیمانده - const getRemainingAmount = (project) => { - const totalPaidAmount = getTotalPaidAmount(project.installments) - const remainingAmount = project.final_price - totalPaidAmount - return remainingAmount - } - - const totalPaidAmount = getTotalPaidAmount(project.installments) - const remainingAmount = getRemainingAmount(project) - // اطلاعات کاربر سازنده را نیز دریافت کنید و به آبجکت پروژه اضافه کنید - const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') - - const projectWithUserInfo = { - ...project._doc, - creator // اضافه کردن اطلاعات کاربر به آبجکت پروژه - } - const selectedUser = await UserModel.findById(project.selected_user).select('_id user_name first_name last_name user_type is_verified profile_image expertise sub_expertise province city rate user_level user_score') - projectWithUserInfo.selected_user = selectedUser - // محاسبه مدت زمان باقی‌مانده برای پروژه - const currentTime = new Date() - const projectCreatedAt = new Date(project.createdAt) - const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) - const remainingTime = calculateRemainingTime(timeDiff) - projectWithUserInfo.remainingTime = remainingTime - let projectDetails = projectWithUserInfo - if ((project.selected_user && project.selected_user.toString()) === userId || project.creator_id.toString() === userId) { - projectDetails = { - ...projectWithUserInfo, - total_paid_amount: totalPaidAmount, - remaining_amount: remainingAmount - } - } else { - projectDetails = - projectWithUserInfo - } - // } - res.status(200).json({ project: projectDetails, projectRequests: modifiedRequests }) - } catch (error) { - next(error) - } -} -const getSingleProjectsWeb = async (req, res, next) => { - try { - let userId = null - let token = req.header('Authorization') - - if (token) { - try { - token = token.split(' ')[1] - if (token) { - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - userId = decodedToken.id - } - } catch (err) { - console.warn('Invalid token:', err.message) - } - } - - const projectId = req.params.projectId - const projectRequests = await RequestModel.find({ project: projectId }) - .populate('user', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image') - .sort({ status: 1 }) - .lean() - - // اگر کاربر لاگین کرده باشد، زمان و قیمت را نشان بده، در غیر این صورت حذف کن - const modifiedRequests = projectRequests.map(request => ({ - ...request, - time: userId && request.user._id.toString() === userId ? request.time : null, - price: userId && request.user._id.toString() === userId ? request.price : null - })) - - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ message: 'پروژه مورد نظر یافت نشد' }) - } - - // محاسبه مبلغ باقیمانده و مجموع مبلغ پرداخت شده - const getTotalPaidAmount = (installments) => { - return installments.reduce((total, installment) => total + installment.amount, 0) - } - - const getRemainingAmount = (project) => { - return project.final_price - getTotalPaidAmount(project.installments) - } - - const totalPaidAmount = getTotalPaidAmount(project.installments) - const remainingAmount = getRemainingAmount(project) - - // دریافت اطلاعات سازنده پروژه - const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') - - const projectWithUserInfo = { - ...project._doc, - creator - } - - // دریافت اطلاعات کاربر منتخب در صورت وجود - const selectedUser = await UserModel.findById(project.selected_user).select('_id user_name first_name last_name user_type is_verified profile_image expertise sub_expertise province city rate user_level user_score') - projectWithUserInfo.selected_user = selectedUser - - // محاسبه زمان باقی‌مانده برای پیشنهادات پروژه - const currentTime = new Date() - const projectCreatedAt = new Date(project.createdAt) - const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) - projectWithUserInfo.remainingTime = calculateRemainingTime(timeDiff) - - let projectDetails = projectWithUserInfo - - // فقط اگر کاربر احراز هویت شده باشد و سازنده یا برنده پروژه باشد، اطلاعات مالی را برگردان - if (userId && (project.selected_user?.toString() === userId || project.creator_id.toString() === userId)) { - projectDetails = { - ...projectWithUserInfo, - total_paid_amount: totalPaidAmount, - remaining_amount: remainingAmount - } - } - - res.status(200).json({ project: projectDetails, projectRequests: modifiedRequests }) - } catch (error) { - next(error) - } -} - -// const projectDetails = await ProjectModel.findById(projectId).populate('requested_users', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image') // جزییات درخواست‌های کاربران - -module.exports = { - getProjects, getSingleProjects, getUserProjects, getUserProjectsWeb, getSingleProjectsWeb -} +/* eslint-disable camelcase */ +const { default: mongoose } = require('mongoose') +const ProjectModel = require('../../../models/ProjectModel') +const RequestModel = require('../../../models/RequestModel') +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') +const calculateRemainingTime = (milliseconds) => { + const days = Math.floor(milliseconds / (1000 * 60 * 60 * 24)) + const hours = Math.floor((milliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + + return `${days} روز و ${hours} ساعت` +} +const getProjects = async (req, res, next) => { + try { + // eslint-disable-next-line no-unused-vars + const { expertise, page = 1, limit = 10, most_price, most_requests, age, gender } = req.query + // ساخت فیلتر برای استفاده در جستجوی MongoDB + const filter = { + payment_status: 'done', + status: 'accepted', + public_status: 'public', + $or: [ + { isExpired: false }, // پروژه‌هایی که isExpired برابر با false است + { isExpired: { $exists: false } } // پروژه‌هایی که فیلد isExpired وجود ندارد + ] + } + if (expertise) { + filter.expertise = expertise + } + if (age) { + filter.age = age + } + if (gender) { + filter.gender = gender + } + // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده + + if (most_price || most_requests) { + // دریافت لیست پروژه‌ها با استفاده از فیلتر و گزینه‌های صفحه‌بندی + const options = { + page: parseInt(page), // تبدیل صفحه به عدد صحیح + limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح + sort: most_price === 'true' ? { offer_price: -1 } : { requested_users: 'desc' } + } + const projects = await ProjectModel.paginate(filter, options) + + const newProj = projects.docs + res.status(200).json({ + projects: newProj, + totalPages: projects.totalPages, // ارسال تعداد کل صفحات + totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } else { + // دریافت لیست پروژه‌ها با استفاده از فیلتر و گزینه‌های صفحه‌بندی + const options = { + page: parseInt(page), // تبدیل صفحه به عدد صحیح + limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح + sort: { acceptedAt: -1, createdAt: -1 } // مرتب‌سازی بر اساس acceptedAt و سپس createdAt + } + const projects = await ProjectModel.paginate(filter, options) + const newProj = projects.docs + res.status(200).json({ + projects: newProj, + totalPages: projects.totalPages, // ارسال تعداد کل صفحات + totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } + } catch (error) { + next(error) + } + // try { + // const token = req.header('Authorization').split(' ')[1] + // if (!token) return res.status(401).send('Access Denied') + + // // eslint-disable-next-line no-unused-vars + // const decodedToken = jwt.verify(token, process.env.APP_SECRET) + + // const { expertise, page = 1, limit = 10 } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست + + // // ساخت فیلتر برای استفاده در جستجوی MongoDB + // const filter = { + // payment_status: 'done', + // status: 'accepted', + // public_status: 'public' + // } // افزودن شرط‌های دیگر برای فیلتر + + // if (expertise) { + // filter.expertise = expertise + // } + + // // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده + // const options = { + // page: parseInt(page), // تبدیل صفحه به عدد صحیح + // limit: parseInt(limit) // تبدیل محدودیت به عدد صحیح + // } + + // // دریافت لیست پروژه‌ها با استفاده از فیلتر و گزینه‌های صفحه‌بندی + // const projects = await ProjectModel.paginate(filter, options) + + // // تبدیل نتایج به فرمت مورد نیاز + // const projectsWithUserInfo = await Promise.all(projects.docs.map(async project => { + // const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') + // const projectWithUserInfo = { + // ...project.toJSON(), // تبدیل آبجکت پروژه به JSON + // creator // اضافه کردن اطلاعات کاربر به آبجکت پروژه + // } + + // // محاسبه مدت زمان باقی‌مانده برای هر پروژه + // const currentTime = new Date() + // const projectCreatedAt = new Date(project.createdAt) + // const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) + // const remainingTime = calculateRemainingTime(timeDiff) + // projectWithUserInfo.remainingTime = remainingTime + + // return projectWithUserInfo + // })) + + // res.status(200).json({ + // projects: projectsWithUserInfo, + // totalPages: projects.totalPages, // ارسال تعداد کل صفحات + // totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها + // }) + // } catch (error) { + // next(error) + // } +} +const getUserProjects = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const user_id = req.query.user_id + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + // const userReqId = decodedToken.id + + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) + } + const user = await UserModel.findById(user_id) + + if (!user) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + const response = {} + if (user.user_type === 'user') { + const options = { + page: req.query.page || 1, // صفحه پیش‌فرض ۱ + limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه + sort: { createdAt: -1 } + } + + // پیجینیشن بر روی لیست پروژه‌های کاربر اعمال می‌شود + const projects = await ProjectModel.paginate( + { selected_user: user._id, status: 'done' }, // فیلتر + options // گزینه‌های پیجینیشن + ) + response.projects = projects.docs + response.totalPages = projects.totalPages // ارسال تعداد کل صفحات + response.totalItems = projects.totalDocs // ارسال تعداد کل آیتم‌ها + response.allProjectsCount = await ProjectModel.countDocuments({ selected_user: user._id }) + const completedProjects = await Promise.all([ + ProjectModel.countDocuments({ selected_user: user._id, status: 'done' }) + ]) + response.successfulProjects = completedProjects + } else { + const options = { + page: req.query.page || 1, // صفحه پیش‌فرض ۱ + limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه + sort: { createdAt: -1 } + } + + // پیجینیشن بر روی لیست پروژه‌های کاربر اعمال می‌شود + const projects = await ProjectModel.paginate( + { creator_id: user._id, status: { $in: ['done', 'accepted'] } }, // فیلتر + options // گزینه‌های پیجینیشن + ) + response.projects = projects.docs + response.totalPages = projects.totalPages // ارسال تعداد کل صفحات + response.totalItems = projects.totalDocs + response.allProjectsCount = await ProjectModel.countDocuments({ creator_id: user._id, payment_status: 'done' }) + // تعداد پروژه‌های موفق را به دست آورید + const successfulProjects = await ProjectModel.countDocuments({ creator_id: user._id, status: 'done' }) + response.successfulProjects = successfulProjects + } + return res.status(200).json({ data: response }) + } catch (error) { + next(error) + } +} +const getUserProjectsWeb = async (req, res, next) => { + try { + const user_id = req.query.user_id + // eslint-disable-next-line no-unused-vars + + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) + } + const user = await UserModel.findById(user_id) + + if (!user) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + const data = {} + if (user.user_type === 'user') { + const options = { + page: req.query.page || 1, // صفحه پیش‌فرض ۱ + limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه + sort: { createdAt: -1 } + } + + // پیجینیشن بر روی لیست پروژه‌های کاربر اعمال می‌شود + const projects = await ProjectModel.paginate( + { selected_user: user._id, status: 'done' }, // فیلتر + options // گزینه‌های پیجینیشن + ) + data.projects = projects.docs + data.totalPages = projects.totalPages // ارسال تعداد کل صفحات + data.totalItems = projects.totalDocs // ارسال تعداد کل آیتم‌ها + data.allProjectsCount = await ProjectModel.countDocuments({ selected_user: user._id }) + const completedProjects = await Promise.all([ + ProjectModel.countDocuments({ selected_user: user._id, status: 'done' }) + ]) + data.successfulProjects = completedProjects + } else { + const options = { + page: req.query.page || 1, // صفحه پیش‌فرض ۱ + limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه + sort: { createdAt: -1 } + } + + // پیجینیشن بر روی لیست پروژه‌های کاربر اعمال می‌شود + const projects = await ProjectModel.paginate( + { creator_id: user._id, status: { $in: ['done', 'accepted'] } }, // فیلتر + options // گزینه‌های پیجینیشن + ) + data.projects = projects.docs + data.totalPages = projects.totalPages // ارسال تعداد کل صفحات + data.totalItems = projects.totalDocs + data.allProjectsCount = await ProjectModel.countDocuments({ creator_id: user._id, payment_status: 'done' }) + // تعداد پروژه‌های موفق را به دست آورید + const successfulProjects = await ProjectModel.countDocuments({ creator_id: user._id, status: 'done' }) + data.successfulProjects = successfulProjects + } + return res.status(200).json(data) + } catch (error) { + next(error) + } +} +const getSingleProjects = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const projectId = req.params.projectId + const projectRequests = await RequestModel.find({ project: projectId }) + .populate('user', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image') + .sort({ status: 1 }) + .lean() + const modifiedRequests = projectRequests.map(request => ({ + ...request, + time: request.user._id.toString() === userId ? request.time : null, + price: request.user._id.toString() === userId ? request.price : null + })) + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ message: 'پروژه مورد نظر یافت نشد' }) + } + // محاسبه مبلغ باقیمانده و مجموع مبلغ پرداخت شده + // تابع محاسبه مبلغ پرداخت شده + const getTotalPaidAmount = (installments) => { + let totalPaidAmount = 0 + installments.forEach(installment => { + totalPaidAmount += installment.amount + }) + return totalPaidAmount + } + + // تابع محاسبه مبلغ باقیمانده + const getRemainingAmount = (project) => { + const totalPaidAmount = getTotalPaidAmount(project.installments) + const remainingAmount = project.final_price - totalPaidAmount + return remainingAmount + } + + const totalPaidAmount = getTotalPaidAmount(project.installments) + const remainingAmount = getRemainingAmount(project) + // اطلاعات کاربر سازنده را نیز دریافت کنید و به آبجکت پروژه اضافه کنید + const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') + + const projectWithUserInfo = { + ...project._doc, + creator // اضافه کردن اطلاعات کاربر به آبجکت پروژه + } + const selectedUser = await UserModel.findById(project.selected_user).select('_id user_name first_name last_name user_type is_verified profile_image expertise sub_expertise province city rate user_level user_score') + projectWithUserInfo.selected_user = selectedUser + // محاسبه مدت زمان باقی‌مانده برای پروژه + const currentTime = new Date() + const projectCreatedAt = new Date(project.createdAt) + const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) + const remainingTime = calculateRemainingTime(timeDiff) + projectWithUserInfo.remainingTime = remainingTime + let projectDetails = projectWithUserInfo + if ((project.selected_user && project.selected_user.toString()) === userId || project.creator_id.toString() === userId) { + projectDetails = { + ...projectWithUserInfo, + total_paid_amount: totalPaidAmount, + remaining_amount: remainingAmount + } + } else { + projectDetails = + projectWithUserInfo + } + // } + res.status(200).json({ project: projectDetails, projectRequests: modifiedRequests }) + } catch (error) { + next(error) + } +} +const getSingleProjectsWeb = async (req, res, next) => { + try { + let userId = null + let token = req.header('Authorization') + + if (token) { + try { + token = token.split(' ')[1] + if (token) { + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + userId = decodedToken.id + } + } catch (err) { + console.warn('Invalid token:', err.message) + } + } + + const projectId = req.params.projectId + const projectRequests = await RequestModel.find({ project: projectId }) + .populate('user', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image') + .sort({ status: 1 }) + .lean() + + // اگر کاربر لاگین کرده باشد، زمان و قیمت را نشان بده، در غیر این صورت حذف کن + const modifiedRequests = projectRequests.map(request => ({ + ...request, + time: userId && request.user._id.toString() === userId ? request.time : null, + price: userId && request.user._id.toString() === userId ? request.price : null + })) + + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ message: 'پروژه مورد نظر یافت نشد' }) + } + + // محاسبه مبلغ باقیمانده و مجموع مبلغ پرداخت شده + const getTotalPaidAmount = (installments) => { + return installments.reduce((total, installment) => total + installment.amount, 0) + } + + const getRemainingAmount = (project) => { + return project.final_price - getTotalPaidAmount(project.installments) + } + + const totalPaidAmount = getTotalPaidAmount(project.installments) + const remainingAmount = getRemainingAmount(project) + + // دریافت اطلاعات سازنده پروژه + const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') + + const projectWithUserInfo = { + ...project._doc, + creator + } + + // دریافت اطلاعات کاربر منتخب در صورت وجود + const selectedUser = await UserModel.findById(project.selected_user).select('_id user_name first_name last_name user_type is_verified profile_image expertise sub_expertise province city rate user_level user_score') + projectWithUserInfo.selected_user = selectedUser + + // محاسبه زمان باقی‌مانده برای پیشنهادات پروژه + const currentTime = new Date() + const projectCreatedAt = new Date(project.createdAt) + const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) + projectWithUserInfo.remainingTime = calculateRemainingTime(timeDiff) + + let projectDetails = projectWithUserInfo + + // فقط اگر کاربر احراز هویت شده باشد و سازنده یا برنده پروژه باشد، اطلاعات مالی را برگردان + if (userId && (project.selected_user?.toString() === userId || project.creator_id.toString() === userId)) { + projectDetails = { + ...projectWithUserInfo, + total_paid_amount: totalPaidAmount, + remaining_amount: remainingAmount + } + } + + res.status(200).json({ project: projectDetails, projectRequests: modifiedRequests }) + } catch (error) { + next(error) + } +} + +// const projectDetails = await ProjectModel.findById(projectId).populate('requested_users', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image') // جزییات درخواست‌های کاربران + +module.exports = { + getProjects, getSingleProjects, getUserProjects, getUserProjectsWeb, getSingleProjectsWeb +} diff --git a/controllers/application/projects/projectTypesController.js b/controllers/application/projects/projectTypesController.js index 5b8d600..14d9b05 100644 --- a/controllers/application/projects/projectTypesController.js +++ b/controllers/application/projects/projectTypesController.js @@ -1,41 +1,41 @@ -const TypeAndPriceModel = require('../../../models/TypeAndPriceModel') -const jwt = require('jsonwebtoken') -const UserModel = require('../../../models/UserModel') - -const getProjectTypes = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const user = await UserModel.findById(userId) - if (!user) { - return res.status(422).json({ - error: true, - message: 'کاربر یافت نشد' - }) - } - // درخواست انواع پروژه از دیتابیس - let projectTypes = await TypeAndPriceModel.find({}, 'name price') - - // بررسی برای مواردی مانند پیدا نشدن انواع پروژه - if (!projectTypes) { - return res.status(404).json({ message: 'انواع پروژه یافت نشد.' }) - } - // اگر کاربر درخواست رایگان روزانه نداشته باشد، نوع پروژه رایگان را حذف کنید - if (user.daily_free_request <= 0) { - projectTypes = projectTypes.filter(projectType => projectType.name !== 'free') - } - // ارسال انواع پروژه به کاربر - res.status(200).json({ projectTypes }) - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error('Error in getProjectTypes:', error) - res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) - } -} - -module.exports = { - getProjectTypes -} +const TypeAndPriceModel = require('../../../models/TypeAndPriceModel') +const jwt = require('jsonwebtoken') +const UserModel = require('../../../models/UserModel') + +const getProjectTypes = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const user = await UserModel.findById(userId) + if (!user) { + return res.status(422).json({ + error: true, + message: 'کاربر یافت نشد' + }) + } + // درخواست انواع پروژه از دیتابیس + let projectTypes = await TypeAndPriceModel.find({}, 'name price') + + // بررسی برای مواردی مانند پیدا نشدن انواع پروژه + if (!projectTypes) { + return res.status(404).json({ message: 'انواع پروژه یافت نشد.' }) + } + // اگر کاربر درخواست رایگان روزانه نداشته باشد، نوع پروژه رایگان را حذف کنید + if (user.daily_free_request <= 0) { + projectTypes = projectTypes.filter(projectType => projectType.name !== 'free') + } + // ارسال انواع پروژه به کاربر + res.status(200).json({ projectTypes }) + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error('Error in getProjectTypes:', error) + res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) + } +} + +module.exports = { + getProjectTypes +} diff --git a/controllers/application/projects/requestProjectController.js b/controllers/application/projects/requestProjectController.js index eb1bd5a..f28723d 100644 --- a/controllers/application/projects/requestProjectController.js +++ b/controllers/application/projects/requestProjectController.js @@ -1,209 +1,209 @@ -/* eslint-disable camelcase */ -const { default: axios } = require('axios') -const NotificationModel = require('../../../models/NotificationModel') -const ProjectModel = require('../../../models/ProjectModel') -const RequestModel = require('../../../models/RequestModel') -const UserModel = require('../../../models/UserModel') -const { check, validationResult } = require('express-validator') -const jwt = require('jsonwebtoken') -const requestProjectValidationRules = () => { - return [ - check('project_id') - .notEmpty().withMessage('آیدی پروژه نمی‌تواند خالی باشد'), - check('request_time') - .notEmpty().withMessage('زمان پروژه نمی‌تواند خالی باشد'), - - check('offer_price') - .notEmpty().withMessage('قیمت پیشنهادی نمی‌تواند خالی باشد') - ] -} - -const requestProject = 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(422).json({ - error: true, message: 'شما دسترسی به این بخش ندارید' - }) - } - if (user.is_verified !== 'verified') { - return res.status(422).json({ - error: true, message: 'مدارک شما تایید نشده است' - }) - } - // اعتبارسنجی درخواست - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - const { request_time, offer_price, project_id } = req.body - if (!project_id || !offer_price || !request_time - ) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const project = await ProjectModel.findById(project_id) - if (!project) { - return res.status(404).json({ - error: true, - message: 'پروژه یافت نشد' - }) - } - if (String(project.creator_id) === String(user._id)) { - return res.status(422).json({ - error: true, - message: 'نمی‌توانید برای پروژه خودتان درخواست ارسال کنید' - }) - } - - // چک کردن برای وجود درخواست قبلی - const existingRequest = await RequestModel.findOne({ project: project_id, user: user._id }) - if (existingRequest) { - return res.status(422).json({ - error: true, - message: 'شما قبلاً برای این پروژه درخواست داده‌اید' - }) - } - // ایجاد یک درخواست جدید - const request = new RequestModel({ - project: project_id, - user: user._id, - time: request_time, - price: offer_price - }) - await request.save() - - // ثبت اعلان برای سازنده پروژه - const creatorId = project.creator_id - - const notification = new NotificationModel({ - user_id: creatorId, - project_post_id: project_id, - type: 'request', - title: 'پیشنهاد جدید', - description: `کاربر ${user.user_name} یک پیشنهاد جدید برای پروژه شما ارسال کرده است.` - }) - await notification.save() - - const userReciver = await UserModel.findById(project.creator_id) - - const data = JSON.stringify({ - mobile: userReciver?.mobile, - templateId: '876533', - parameters: [ - { name: 'EMPLOYER', value: userReciver?.first_name + ' ' + userReciver?.last_name }, - { name: 'PROJECT', value: project?.title } - ] - }) - - const config = { - method: 'post', - url: 'https://api.sms.ir/v1/send/verify', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/plain', - 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' - }, - data - } - axios(config) - .then(function (response) { - }) - .catch(function (error) { - console.log(error) - }) - - // به روز رسانی پروژه با افزودن کاربر به لیست کاربران درخواست دهنده - const updatedProject = await ProjectModel.findOneAndUpdate( - { _id: project_id }, - { $push: { requested_users: user._id } }, - { new: true } - ) - - if (!updatedProject) { - console.error('Error adding requested user: Project not found') - return res.status(404).json({ error: true, message: 'پروژه پیدا نشد' }) - } - - res.status(201).json({ message: 'درخواست با موفقیت شد', id: project_id } - ) - } catch (error) { - next(error) - } -} - -const editRequestProject = 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(422).json({ - error: true, message: 'شما دسترسی به این بخش ندارید' - }) - } - if (user.is_verified !== 'verified') { - return res.status(422).json({ - error: true, message: 'مدارک شما تایید نشده است' - }) - } - // اعتبارسنجی درخواست - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - const { request_time, offer_price, project_id } = req.body - if (!project_id || !offer_price || !request_time - ) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - // چک کردن برای وجود درخواست قبلی - const existingRequest = await RequestModel.findOne({ project: project_id, user: user._id }) - if (!existingRequest) { - return res.status(422).json({ - error: true, - message: 'شما قبلاً برای این پروژه درخواست نداده‌اید' - }) - } - // اکنون می‌توانید اطلاعات درخواست را ویرایش کنید - existingRequest.time = request_time - existingRequest.price = offer_price - await existingRequest.save() - - // ثبت اعلان برای سازنده پروژه - const project = await ProjectModel.findById(project_id) - const creatorId = project.creator_id - - const notification = new NotificationModel({ - user_id: creatorId, - project_post_id: project_id, - type: 'request', - title: 'ویرایش پیشنهاد', - description: `کاربر ${user.user_name} پیشنهاد خود را ویرایش کرد.` - }) - await notification.save() - res.status(200).json({ - message: 'درخواست با موفقیت ویرایش شد', - id: project_id - }) - } catch (error) { - next(error) - } -} -const acceptProject = async (req, res, next) => { } -module.exports = { - requestProjectValidationRules, - requestProject, - acceptProject, - editRequestProject -} +/* eslint-disable camelcase */ +const { default: axios } = require('axios') +const NotificationModel = require('../../../models/NotificationModel') +const ProjectModel = require('../../../models/ProjectModel') +const RequestModel = require('../../../models/RequestModel') +const UserModel = require('../../../models/UserModel') +const { check, validationResult } = require('express-validator') +const jwt = require('jsonwebtoken') +const requestProjectValidationRules = () => { + return [ + check('project_id') + .notEmpty().withMessage('آیدی پروژه نمی‌تواند خالی باشد'), + check('request_time') + .notEmpty().withMessage('زمان پروژه نمی‌تواند خالی باشد'), + + check('offer_price') + .notEmpty().withMessage('قیمت پیشنهادی نمی‌تواند خالی باشد') + ] +} + +const requestProject = 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(422).json({ + error: true, message: 'شما دسترسی به این بخش ندارید' + }) + } + if (user.is_verified !== 'verified') { + return res.status(422).json({ + error: true, message: 'مدارک شما تایید نشده است' + }) + } + // اعتبارسنجی درخواست + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + const { request_time, offer_price, project_id } = req.body + if (!project_id || !offer_price || !request_time + ) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const project = await ProjectModel.findById(project_id) + if (!project) { + return res.status(404).json({ + error: true, + message: 'پروژه یافت نشد' + }) + } + if (String(project.creator_id) === String(user._id)) { + return res.status(422).json({ + error: true, + message: 'نمی‌توانید برای پروژه خودتان درخواست ارسال کنید' + }) + } + + // چک کردن برای وجود درخواست قبلی + const existingRequest = await RequestModel.findOne({ project: project_id, user: user._id }) + if (existingRequest) { + return res.status(422).json({ + error: true, + message: 'شما قبلاً برای این پروژه درخواست داده‌اید' + }) + } + // ایجاد یک درخواست جدید + const request = new RequestModel({ + project: project_id, + user: user._id, + time: request_time, + price: offer_price + }) + await request.save() + + // ثبت اعلان برای سازنده پروژه + const creatorId = project.creator_id + + const notification = new NotificationModel({ + user_id: creatorId, + project_post_id: project_id, + type: 'request', + title: 'پیشنهاد جدید', + description: `کاربر ${user.user_name} یک پیشنهاد جدید برای پروژه شما ارسال کرده است.` + }) + await notification.save() + + const userReciver = await UserModel.findById(project.creator_id) + + const data = JSON.stringify({ + mobile: userReciver?.mobile, + templateId: '876533', + parameters: [ + { name: 'EMPLOYER', value: userReciver?.first_name + ' ' + userReciver?.last_name }, + { name: 'PROJECT', value: project?.title } + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + axios(config) + .then(function (response) { + }) + .catch(function (error) { + console.log(error) + }) + + // به روز رسانی پروژه با افزودن کاربر به لیست کاربران درخواست دهنده + const updatedProject = await ProjectModel.findOneAndUpdate( + { _id: project_id }, + { $push: { requested_users: user._id } }, + { new: true } + ) + + if (!updatedProject) { + console.error('Error adding requested user: Project not found') + return res.status(404).json({ error: true, message: 'پروژه پیدا نشد' }) + } + + res.status(201).json({ message: 'درخواست با موفقیت شد', id: project_id } + ) + } catch (error) { + next(error) + } +} + +const editRequestProject = 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(422).json({ + error: true, message: 'شما دسترسی به این بخش ندارید' + }) + } + if (user.is_verified !== 'verified') { + return res.status(422).json({ + error: true, message: 'مدارک شما تایید نشده است' + }) + } + // اعتبارسنجی درخواست + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + const { request_time, offer_price, project_id } = req.body + if (!project_id || !offer_price || !request_time + ) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + // چک کردن برای وجود درخواست قبلی + const existingRequest = await RequestModel.findOne({ project: project_id, user: user._id }) + if (!existingRequest) { + return res.status(422).json({ + error: true, + message: 'شما قبلاً برای این پروژه درخواست نداده‌اید' + }) + } + // اکنون می‌توانید اطلاعات درخواست را ویرایش کنید + existingRequest.time = request_time + existingRequest.price = offer_price + await existingRequest.save() + + // ثبت اعلان برای سازنده پروژه + const project = await ProjectModel.findById(project_id) + const creatorId = project.creator_id + + const notification = new NotificationModel({ + user_id: creatorId, + project_post_id: project_id, + type: 'request', + title: 'ویرایش پیشنهاد', + description: `کاربر ${user.user_name} پیشنهاد خود را ویرایش کرد.` + }) + await notification.save() + res.status(200).json({ + message: 'درخواست با موفقیت ویرایش شد', + id: project_id + }) + } catch (error) { + next(error) + } +} +const acceptProject = async (req, res, next) => { } +module.exports = { + requestProjectValidationRules, + requestProject, + acceptProject, + editRequestProject +} diff --git a/controllers/application/projects/updateProjectController.js b/controllers/application/projects/updateProjectController.js index b1c1824..b86e22f 100644 --- a/controllers/application/projects/updateProjectController.js +++ b/controllers/application/projects/updateProjectController.js @@ -1,390 +1,390 @@ -/* eslint-disable camelcase */ -const ProjectModel = require('../../../models/ProjectModel') -const jwt = require('jsonwebtoken') -const UserModel = require('../../../models/UserModel') -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 { - canCreateProject, - respondProjectProfileIncomplete -} = require('../../../utils/projectProfile') - -const saveProjectComment = async ({ - targetUserId, - projectId, - creatorId, - comment, - rate, - commentFor -}) => { - const { ratingValue, isNewRating, requiresRating, alreadyRated } = await resolveRatingForComment({ - creatorId, - targetUserId, - rate - }) - - if (alreadyRated) { - return { error: { status: 400, message: 'شما قبلاً به این کاربر امتیاز داده‌اید.' } } - } - - 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 { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const { project_id, comment, rate, user_id } = req.body - if (!project_id || !comment || !user_id) { - return res.status(400).send({ message: 'All fields are required' }) - } - - const project = await ProjectModel.findOne({ _id: project_id, creator_id: userId }) - if (!project) { - return res.status(403).send({ message: 'Access denied: You are not the creator of this project.' }) - } - - const getTotalPaidAmount = (installments) => { - return installments.reduce((total, installment) => total + installment.amount, 0) - } - - const getRemainingAmount = (project) => { - const totalPaidAmount = getTotalPaidAmount(project.installments) - return project.final_price - totalPaidAmount - } - - const remainingAmount = getRemainingAmount(project) - if (remainingAmount !== 0) { - return res.status(422).send({ message: 'مبلغ کامل پرداخت نشده است' }) - } - - project.status = 'done' - await project.save() - - const commentResult = await saveProjectComment({ - targetUserId: user_id, - projectId: project_id, - creatorId: userId, - comment, - rate, - commentFor: 'user' - }) - - if (commentResult.error) { - return res.status(commentResult.error.status).send({ message: commentResult.error.message }) - } - - const user = await UserModel.findById(user_id) - - const notification = new NotificationModel({ - user_id: user._id, - project_post_id: project_id, - type: 'end_project', - title: 'اتمام پروژه', - description: `کارفرما پروژه ${project?.title} را به وضعیت انجام شده تغییر داد و برای شما نظر ثبت کرد(برای ثبت نظر لمس کنید) -نظر کارفرما : ${comment} - ` - }) - await notification.save() - - res.status(200).json({ message: 'پروژه با موفقیت تغییر یافت شد' }) - } catch (error) { - next(error) - } -} -const doneProjectWeb = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const { project_id, comment, rate, user_id } = req.body - if (!project_id || !comment || !user_id) { - return res.status(400).send({ message: 'All fields are required' }) - } - - const project = await ProjectModel.findOne({ _id: project_id, creator_id: userId }) - if (!project) { - return res.status(403).send({ message: 'Access denied: You are not the creator of this project.' }) - } - - project.status = 'done' - await project.save() - - const commentResult = await saveProjectComment({ - targetUserId: user_id, - projectId: project_id, - creatorId: userId, - comment, - rate, - commentFor: 'user' - }) - - if (commentResult.error) { - return res.status(commentResult.error.status).send({ message: commentResult.error.message }) - } - - const user = await UserModel.findById(user_id) - - const notification = new NotificationModel({ - user_id: user._id, - project_post_id: project_id, - type: 'end_project', - title: 'اتمام پروژه', - description: `کارفرما پروژه ${project?.title} را به وضعیت انجام شده تغییر داد و برای شما نظر ثبت کرد(برای ثبت نظر لمس کنید) -نظر کارفرما : ${comment} - ` - }) - await notification.save() - - res.status(200).json({ message: 'پروژه با موفقیت تغییر یافت شد' }) - } catch (error) { - next(error) - } -} - -const cancleProject = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const { project_id, comment, rate, user_id } = req.body - - if (!project_id || !comment || !user_id) { - return res.status(400).send({ message: 'All fields are required' }) - } - - const project = await ProjectModel.findOne({ _id: project_id, creator_id: userId }) - if (!project) { - return res.status(403).send({ message: 'Access denied: You are not the creator of this project.' }) - } - - project.status = 'cancled' - await project.save() - - const commentResult = await saveProjectComment({ - targetUserId: user_id, - projectId: project_id, - creatorId: userId, - comment, - rate, - commentFor: 'project' - }) - - if (commentResult.error) { - return res.status(commentResult.error.status).send({ message: commentResult.error.message }) - } - - res.status(200).json({ message: 'پروژه با موفقیت تغییر یافت شد' }) - } catch (error) { - next(error) - } -} - -const editProjectValidationRules = () => { - return [ - check('title').notEmpty().withMessage('عنوان نمی‌تواند خالی باشد'), - - check('expertise').notEmpty().withMessage('تخصص نمی‌تواند خالی باشد'), - - check('gender') - .isIn(['male', 'female']) - .withMessage('جنسیت باید male یا female باشد'), - - check('age').notEmpty().withMessage('سن نمی‌تواند خالی باشد'), - - check('conversation_projects') - .notEmpty() - .withMessage('پروژه‌های مکالمه نمی‌تواند خالی باشد') - .isBoolean() - .withMessage('مقدار پروژه‌های مکالمه باید یک مقدار boolean باشد'), - - check('province').notEmpty().withMessage('استان نمی‌تواند خالی باشد'), - - check('city').notEmpty().withMessage('شهر نمی‌تواند خالی باشد'), - - check('offer_time') - .notEmpty() - .withMessage('زمان پروژه نمی‌تواند خالی باشد'), - - check('offer_price') - .notEmpty() - .withMessage('قیمت پیشنهادی نمی‌تواند خالی باشد'), - - check('description') - .notEmpty() - .withMessage('توضیحات نمی‌تواند خالی باشد') - ] -} - -const editProject = 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(422).json({ - error: true, - message: 'شما دسترسی به این بخش ندارید' - }) - } - if (!canCreateProject(user)) { - return respondProjectProfileIncomplete(res) - } - - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - - const { - title, - expertise, - sub_expertise, - gender, - age, - conversation_projects, - province, - city, - offer_time, - offer_price, - description, - projectId - } = req.body - - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ - error: true, - message: 'پروژه مورد نظر یافت نشد' - }) - } - // eslint-disable-next-line eqeqeq - if (user._id.toString() != project.creator_id.toString()) { - return res.status(422).json({ - error: true, - message: 'شما نمیتوانید این پروژه را ویرایش کنید' - }) - } - if (project.status !== 'paid' && project.status !== 'pre_payment') { - return res.status(422).json({ - error: true, - message: 'شما نمیتوانید این پروژه را ویرایش کنید' - }) - } - // اعتبارسنجی اطلاعات ویرایش شده - if ( - !title || - !expertise || - !age || - conversation_projects === null || - !province || - !city || - !offer_time || - !offer_price || - !description - ) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - const provinceFind = await ProvinceModel.findOne( - { id: province }, - { _id: 0 } - ) - const cityFind = await CityModel.findOne({ id: city }) - // ویرایش یک نسخه از پروژه - project.title = title - project.expertise = expertise - project.sub_expertise = sub_expertise - project.gender = gender - project.age = age - project.conversation_projects = conversation_projects - project.province = provinceFind - project.city = cityFind - project.offer_time = offer_time - project.offer_price = offer_price - project.description = description - // ذخیره کردن تغییرات در پایگاه داده - await project.save() - res.status(200).json({ - message: 'پروژه با موفقیت ویرایش شد', - id: project._id - }) - } catch (error) { - next(error) - } -} -const setRateProject = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const { project_id, comment, rate } = req.body - const project = await ProjectModel.findOne({ _id: project_id, selected_user: userId }) - 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, - comment, - creator_id: userId - }) - await project.save() - - res.status(200).json({ message: 'نظر شما با موفقیت ثبت شد.' }) - } catch (error) { - next(error) - } -} -module.exports = { - doneProject, cancleProject, editProject, editProjectValidationRules, setRateProject, doneProjectWeb -} +/* eslint-disable camelcase */ +const ProjectModel = require('../../../models/ProjectModel') +const jwt = require('jsonwebtoken') +const UserModel = require('../../../models/UserModel') +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 { + canCreateProject, + respondProjectProfileIncomplete +} = require('../../../utils/projectProfile') + +const saveProjectComment = async ({ + targetUserId, + projectId, + creatorId, + comment, + rate, + commentFor +}) => { + const { ratingValue, isNewRating, requiresRating, alreadyRated } = await resolveRatingForComment({ + creatorId, + targetUserId, + rate + }) + + if (alreadyRated) { + return { error: { status: 400, message: 'شما قبلاً به این کاربر امتیاز داده‌اید.' } } + } + + 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 { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { project_id, comment, rate, user_id } = req.body + if (!project_id || !comment || !user_id) { + return res.status(400).send({ message: 'All fields are required' }) + } + + const project = await ProjectModel.findOne({ _id: project_id, creator_id: userId }) + if (!project) { + return res.status(403).send({ message: 'Access denied: You are not the creator of this project.' }) + } + + const getTotalPaidAmount = (installments) => { + return installments.reduce((total, installment) => total + installment.amount, 0) + } + + const getRemainingAmount = (project) => { + const totalPaidAmount = getTotalPaidAmount(project.installments) + return project.final_price - totalPaidAmount + } + + const remainingAmount = getRemainingAmount(project) + if (remainingAmount !== 0) { + return res.status(422).send({ message: 'مبلغ کامل پرداخت نشده است' }) + } + + project.status = 'done' + await project.save() + + const commentResult = await saveProjectComment({ + targetUserId: user_id, + projectId: project_id, + creatorId: userId, + comment, + rate, + commentFor: 'user' + }) + + if (commentResult.error) { + return res.status(commentResult.error.status).send({ message: commentResult.error.message }) + } + + const user = await UserModel.findById(user_id) + + const notification = new NotificationModel({ + user_id: user._id, + project_post_id: project_id, + type: 'end_project', + title: 'اتمام پروژه', + description: `کارفرما پروژه ${project?.title} را به وضعیت انجام شده تغییر داد و برای شما نظر ثبت کرد(برای ثبت نظر لمس کنید) +نظر کارفرما : ${comment} + ` + }) + await notification.save() + + res.status(200).json({ message: 'پروژه با موفقیت تغییر یافت شد' }) + } catch (error) { + next(error) + } +} +const doneProjectWeb = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { project_id, comment, rate, user_id } = req.body + if (!project_id || !comment || !user_id) { + return res.status(400).send({ message: 'All fields are required' }) + } + + const project = await ProjectModel.findOne({ _id: project_id, creator_id: userId }) + if (!project) { + return res.status(403).send({ message: 'Access denied: You are not the creator of this project.' }) + } + + project.status = 'done' + await project.save() + + const commentResult = await saveProjectComment({ + targetUserId: user_id, + projectId: project_id, + creatorId: userId, + comment, + rate, + commentFor: 'user' + }) + + if (commentResult.error) { + return res.status(commentResult.error.status).send({ message: commentResult.error.message }) + } + + const user = await UserModel.findById(user_id) + + const notification = new NotificationModel({ + user_id: user._id, + project_post_id: project_id, + type: 'end_project', + title: 'اتمام پروژه', + description: `کارفرما پروژه ${project?.title} را به وضعیت انجام شده تغییر داد و برای شما نظر ثبت کرد(برای ثبت نظر لمس کنید) +نظر کارفرما : ${comment} + ` + }) + await notification.save() + + res.status(200).json({ message: 'پروژه با موفقیت تغییر یافت شد' }) + } catch (error) { + next(error) + } +} + +const cancleProject = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { project_id, comment, rate, user_id } = req.body + + if (!project_id || !comment || !user_id) { + return res.status(400).send({ message: 'All fields are required' }) + } + + const project = await ProjectModel.findOne({ _id: project_id, creator_id: userId }) + if (!project) { + return res.status(403).send({ message: 'Access denied: You are not the creator of this project.' }) + } + + project.status = 'cancled' + await project.save() + + const commentResult = await saveProjectComment({ + targetUserId: user_id, + projectId: project_id, + creatorId: userId, + comment, + rate, + commentFor: 'project' + }) + + if (commentResult.error) { + return res.status(commentResult.error.status).send({ message: commentResult.error.message }) + } + + res.status(200).json({ message: 'پروژه با موفقیت تغییر یافت شد' }) + } catch (error) { + next(error) + } +} + +const editProjectValidationRules = () => { + return [ + check('title').notEmpty().withMessage('عنوان نمی‌تواند خالی باشد'), + + check('expertise').notEmpty().withMessage('تخصص نمی‌تواند خالی باشد'), + + check('gender') + .isIn(['male', 'female']) + .withMessage('جنسیت باید male یا female باشد'), + + check('age').notEmpty().withMessage('سن نمی‌تواند خالی باشد'), + + check('conversation_projects') + .notEmpty() + .withMessage('پروژه‌های مکالمه نمی‌تواند خالی باشد') + .isBoolean() + .withMessage('مقدار پروژه‌های مکالمه باید یک مقدار boolean باشد'), + + check('province').notEmpty().withMessage('استان نمی‌تواند خالی باشد'), + + check('city').notEmpty().withMessage('شهر نمی‌تواند خالی باشد'), + + check('offer_time') + .notEmpty() + .withMessage('زمان پروژه نمی‌تواند خالی باشد'), + + check('offer_price') + .notEmpty() + .withMessage('قیمت پیشنهادی نمی‌تواند خالی باشد'), + + check('description') + .notEmpty() + .withMessage('توضیحات نمی‌تواند خالی باشد') + ] +} + +const editProject = 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(422).json({ + error: true, + message: 'شما دسترسی به این بخش ندارید' + }) + } + if (!canCreateProject(user)) { + return respondProjectProfileIncomplete(res) + } + + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + + const { + title, + expertise, + sub_expertise, + gender, + age, + conversation_projects, + province, + city, + offer_time, + offer_price, + description, + projectId + } = req.body + + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ + error: true, + message: 'پروژه مورد نظر یافت نشد' + }) + } + // eslint-disable-next-line eqeqeq + if (user._id.toString() != project.creator_id.toString()) { + return res.status(422).json({ + error: true, + message: 'شما نمیتوانید این پروژه را ویرایش کنید' + }) + } + if (project.status !== 'paid' && project.status !== 'pre_payment') { + return res.status(422).json({ + error: true, + message: 'شما نمیتوانید این پروژه را ویرایش کنید' + }) + } + // اعتبارسنجی اطلاعات ویرایش شده + if ( + !title || + !expertise || + !age || + conversation_projects === null || + !province || + !city || + !offer_time || + !offer_price || + !description + ) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + const provinceFind = await ProvinceModel.findOne( + { id: province }, + { _id: 0 } + ) + const cityFind = await CityModel.findOne({ id: city }) + // ویرایش یک نسخه از پروژه + project.title = title + project.expertise = expertise + project.sub_expertise = sub_expertise + project.gender = gender + project.age = age + project.conversation_projects = conversation_projects + project.province = provinceFind + project.city = cityFind + project.offer_time = offer_time + project.offer_price = offer_price + project.description = description + // ذخیره کردن تغییرات در پایگاه داده + await project.save() + res.status(200).json({ + message: 'پروژه با موفقیت ویرایش شد', + id: project._id + }) + } catch (error) { + next(error) + } +} +const setRateProject = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { project_id, comment, rate } = req.body + const project = await ProjectModel.findOne({ _id: project_id, selected_user: userId }) + 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, + comment, + creator_id: userId + }) + await project.save() + + res.status(200).json({ message: 'نظر شما با موفقیت ثبت شد.' }) + } catch (error) { + next(error) + } +} +module.exports = { + doneProject, cancleProject, editProject, editProjectValidationRules, setRateProject, doneProjectWeb +} diff --git a/controllers/application/provincesController.js b/controllers/application/provincesController.js index b3ce8d6..11ab4a3 100644 --- a/controllers/application/provincesController.js +++ b/controllers/application/provincesController.js @@ -1,15 +1,15 @@ -const { ProvinceModel } = require('../../models/StateCity') - -const getProvinces = async (req, res, next) => { - try { - const provinces = await ProvinceModel.find({}) - res.status(200).json({ - provinces - }) - } catch (err) { - res.status(500).send('Internal Server Error') - } -} -module.exports = { - getProvinces -} +const { ProvinceModel } = require('../../models/StateCity') + +const getProvinces = async (req, res, next) => { + try { + const provinces = await ProvinceModel.find({}) + res.status(200).json({ + provinces + }) + } catch (err) { + res.status(500).send('Internal Server Error') + } +} +module.exports = { + getProvinces +} diff --git a/controllers/application/register/registerController.js b/controllers/application/register/registerController.js index 02af194..37d6c27 100644 --- a/controllers/application/register/registerController.js +++ b/controllers/application/register/registerController.js @@ -1,95 +1,95 @@ -const { default: axios } = require('axios') -const UserModel = require('../../../models/UserModel') -const { check, validationResult } = require('express-validator') -const { OTP_VALID_MS } = require('../../../utils/otpExpiry') -// const { Token, VerificationCode } = require('sms-ir') - -const registerValidationRules = () => { - return [ - check('mobile') - .notEmpty().withMessage('شماره موبایل نمی‌تواند خالی باشد') - .isLength({ min: 11, max: 11 }).withMessage('شماره موبایل باید دقیقاً 11 رقم باشد') - .matches(/^09[0-9]{9}$/).withMessage('فرمت شماره موبایل صحیح نیست') - ] -} - -const registerUser = async (req, res, next) => { - try { - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - - const { mobile } = req.body - if (!mobile) { - return res.status(422).json({ - error: true, - message: 'شماره موبایل نمی‌تواند خالی باشد' - }) - } - - const existingUser = await UserModel.findOne({ mobile }) - if (existingUser && existingUser.national_code !== null) { - return res.status(422).json({ error: true, message: 'شماره موبایل تکراری است' }) - } - function generateOTP () { - return Math.floor(100000 + Math.random() * 900000) - } - - const otp = generateOTP() - const otpSentAt = new Date() - // eslint-disable-next-line no-unused-vars - const user = await UserModel.findOneAndUpdate( - { mobile }, - { $set: { mobile, otp: String(otp), otpSentAt } }, - { upsert: true, new: true, lean: true } - ) - setTimeout(() => { - UserModel.findOneAndUpdate( - { mobile }, - { $set: { otp: null, otpSentAt: null } }, - { new: true } - ) - .then(() => {}) - .catch(error => console.error('Error setting OTP to null:', error)) - }, OTP_VALID_MS) - const data = JSON.stringify({ - mobile, - templateId: '930719', - parameters: [ - { name: 'CODE', value: otp.toString() } - ] - }) - - const config = { - method: 'post', - url: 'https://api.sms.ir/v1/send/verify', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/plain', - 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' - }, - data - } - - axios(config) - .then(function (response) { - // console.log(JSON.stringify(response.data)) - }) - .catch(function (error) { - console.log(error) - }) - - res.status(200).json({ - success: true, - message: 'کد تایید ارسال شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - registerValidationRules, - registerUser -} +const { default: axios } = require('axios') +const UserModel = require('../../../models/UserModel') +const { check, validationResult } = require('express-validator') +const { OTP_VALID_MS } = require('../../../utils/otpExpiry') +// const { Token, VerificationCode } = require('sms-ir') + +const registerValidationRules = () => { + return [ + check('mobile') + .notEmpty().withMessage('شماره موبایل نمی‌تواند خالی باشد') + .isLength({ min: 11, max: 11 }).withMessage('شماره موبایل باید دقیقاً 11 رقم باشد') + .matches(/^09[0-9]{9}$/).withMessage('فرمت شماره موبایل صحیح نیست') + ] +} + +const registerUser = async (req, res, next) => { + try { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + + const { mobile } = req.body + if (!mobile) { + return res.status(422).json({ + error: true, + message: 'شماره موبایل نمی‌تواند خالی باشد' + }) + } + + const existingUser = await UserModel.findOne({ mobile }) + if (existingUser && existingUser.national_code !== null) { + return res.status(422).json({ error: true, message: 'شماره موبایل تکراری است' }) + } + function generateOTP () { + return Math.floor(100000 + Math.random() * 900000) + } + + const otp = generateOTP() + const otpSentAt = new Date() + // eslint-disable-next-line no-unused-vars + const user = await UserModel.findOneAndUpdate( + { mobile }, + { $set: { mobile, otp: String(otp), otpSentAt } }, + { upsert: true, new: true, lean: true } + ) + setTimeout(() => { + UserModel.findOneAndUpdate( + { mobile }, + { $set: { otp: null, otpSentAt: null } }, + { new: true } + ) + .then(() => {}) + .catch(error => console.error('Error setting OTP to null:', error)) + }, OTP_VALID_MS) + const data = JSON.stringify({ + mobile, + templateId: '930719', + parameters: [ + { name: 'CODE', value: otp.toString() } + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + + axios(config) + .then(function (response) { + // console.log(JSON.stringify(response.data)) + }) + .catch(function (error) { + console.log(error) + }) + + res.status(200).json({ + success: true, + message: 'کد تایید ارسال شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + registerValidationRules, + registerUser +} diff --git a/controllers/application/register/userFullNameController.js b/controllers/application/register/userFullNameController.js index 412a0b8..049b35b 100644 --- a/controllers/application/register/userFullNameController.js +++ b/controllers/application/register/userFullNameController.js @@ -1,51 +1,51 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const { check, validationResult } = require('express-validator') -const jwt = require('jsonwebtoken') - -const setUserFullNameValidationRules = () => { - return [ - check('first_name') - .notEmpty().withMessage('نام نمی‌تواند خالی باشد'), - check('last_name') - .notEmpty().withMessage('نام خانوادگی نمی‌تواند خالی باشد') - ] -} - -const setUserFullName = async (req, res, next) => { - try { - const { first_name, last_name } = req.body - - // اعتبارسنجی داده‌های ورودی - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - 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: 'کاربری با این شماره موبایل یافت نشد' - }) - } - - // آپدیت نام کاربری به صورت lowercase - user.first_name = first_name.trim().toLowerCase() - user.last_name = last_name.trim().toLowerCase() - await user.save() - - return res.json({ - message: 'نام و نام خانوادگی با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - setUserFullName, setUserFullNameValidationRules -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const { check, validationResult } = require('express-validator') +const jwt = require('jsonwebtoken') + +const setUserFullNameValidationRules = () => { + return [ + check('first_name') + .notEmpty().withMessage('نام نمی‌تواند خالی باشد'), + check('last_name') + .notEmpty().withMessage('نام خانوادگی نمی‌تواند خالی باشد') + ] +} + +const setUserFullName = async (req, res, next) => { + try { + const { first_name, last_name } = req.body + + // اعتبارسنجی داده‌های ورودی + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + 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: 'کاربری با این شماره موبایل یافت نشد' + }) + } + + // آپدیت نام کاربری به صورت lowercase + user.first_name = first_name.trim().toLowerCase() + user.last_name = last_name.trim().toLowerCase() + await user.save() + + return res.json({ + message: 'نام و نام خانوادگی با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + setUserFullName, setUserFullNameValidationRules +} diff --git a/controllers/application/register/userTypeController.js b/controllers/application/register/userTypeController.js index 4c9be51..c3562a2 100644 --- a/controllers/application/register/userTypeController.js +++ b/controllers/application/register/userTypeController.js @@ -1,45 +1,45 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setUserType = async (req, res, next) => { - try { - const { user_type } = req.body - const normalizedType = 'user' - if (user_type && user_type !== 'user' && user_type !== 'employer') { - return res.status(422).json({ - error: true, - message: 'نوع یوزر معتبر نیست' - }) - } - - // جستجوی کاربر با شماره موبایل ارسالی - 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» ثبت می‌شوند - user.user_type = normalizedType - await user.save() - - return res.json({ - message: 'نوع کاربر با موفقیت به‌روزرسانی شد', - id: user._id - }) - } catch (error) { - next(error) - } -} - -module.exports = { - setUserType -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setUserType = async (req, res, next) => { + try { + const { user_type } = req.body + const normalizedType = 'user' + if (user_type && user_type !== 'user' && user_type !== 'employer') { + return res.status(422).json({ + error: true, + message: 'نوع یوزر معتبر نیست' + }) + } + + // جستجوی کاربر با شماره موبایل ارسالی + 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» ثبت می‌شوند + user.user_type = normalizedType + await user.save() + + return res.json({ + message: 'نوع کاربر با موفقیت به‌روزرسانی شد', + id: user._id + }) + } catch (error) { + next(error) + } +} + +module.exports = { + setUserType +} diff --git a/controllers/application/register/usernameController.js b/controllers/application/register/usernameController.js index 0fa941e..4157de6 100644 --- a/controllers/application/register/usernameController.js +++ b/controllers/application/register/usernameController.js @@ -1,126 +1,126 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') -const { validateUsername } = require('../../../utils/usernameValidation') -const { generateUsernameSuggestions } = require('../../../utils/usernameSuggestions') - -const setUserName = async (req, res, next) => { - try { - const { mobile, user_name } = req.body - if (!mobile || !user_name) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const user = await UserModel.findOne({ mobile }) - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شماره موبایل یافت نشد' - }) - } - - const validationError = validateUsername(user_name) - if (validationError) { - return res.status(422).json({ - error: true, - message: validationError - }) - } - - const normalizedUserName = user_name.trim().toLowerCase() - - const existingUser = await UserModel.findOne({ user_name: { $regex: new RegExp('^' + normalizedUserName + '$', 'i') } }) - if (existingUser && existingUser._id.toString() !== user._id.toString()) { - const suggestions = await generateUsernameSuggestions( - UserModel, - normalizedUserName, - user._id.toString() - ) - return res.status(422).json({ - error: true, - message: 'نام کاربری تکراری است', - suggestions - }) - } - - user.user_name = normalizedUserName - await user.save() - - return res.json({ - message: 'نام کاربری با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -const updateUserName = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const { user_name } = req.body - - if (!user_name) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const validationError = validateUsername(user_name) - if (validationError) { - return res.status(422).json({ - error: true, - message: validationError - }) - } - - const normalizedUserName = user_name.trim().toLowerCase() - - const existingUser = await UserModel.findOne({ - user_name: { $regex: new RegExp('^' + normalizedUserName + '$', 'i') } - }) - if (existingUser && existingUser._id.toString() !== userId) { - const suggestions = await generateUsernameSuggestions( - UserModel, - normalizedUserName, - userId - ) - return res.status(422).json({ - error: true, - message: 'نام کاربری تکراری است', - suggestions - }) - } - - const user = await UserModel.findByIdAndUpdate( - userId, - { user_name: normalizedUserName }, - { new: true } - ) - - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شناسه یافت نشد' - }) - } - - return res.json({ - message: 'نام کاربری با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - setUserName, updateUserName -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') +const { validateUsername } = require('../../../utils/usernameValidation') +const { generateUsernameSuggestions } = require('../../../utils/usernameSuggestions') + +const setUserName = async (req, res, next) => { + try { + const { mobile, user_name } = req.body + if (!mobile || !user_name) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const user = await UserModel.findOne({ mobile }) + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شماره موبایل یافت نشد' + }) + } + + const validationError = validateUsername(user_name) + if (validationError) { + return res.status(422).json({ + error: true, + message: validationError + }) + } + + const normalizedUserName = user_name.trim().toLowerCase() + + const existingUser = await UserModel.findOne({ user_name: { $regex: new RegExp('^' + normalizedUserName + '$', 'i') } }) + if (existingUser && existingUser._id.toString() !== user._id.toString()) { + const suggestions = await generateUsernameSuggestions( + UserModel, + normalizedUserName, + user._id.toString() + ) + return res.status(422).json({ + error: true, + message: 'نام کاربری تکراری است', + suggestions + }) + } + + user.user_name = normalizedUserName + await user.save() + + return res.json({ + message: 'نام کاربری با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +const updateUserName = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { user_name } = req.body + + if (!user_name) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const validationError = validateUsername(user_name) + if (validationError) { + return res.status(422).json({ + error: true, + message: validationError + }) + } + + const normalizedUserName = user_name.trim().toLowerCase() + + const existingUser = await UserModel.findOne({ + user_name: { $regex: new RegExp('^' + normalizedUserName + '$', 'i') } + }) + if (existingUser && existingUser._id.toString() !== userId) { + const suggestions = await generateUsernameSuggestions( + UserModel, + normalizedUserName, + userId + ) + return res.status(422).json({ + error: true, + message: 'نام کاربری تکراری است', + suggestions + }) + } + + const user = await UserModel.findByIdAndUpdate( + userId, + { user_name: normalizedUserName }, + { new: true } + ) + + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شناسه یافت نشد' + }) + } + + return res.json({ + message: 'نام کاربری با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + setUserName, updateUserName +} diff --git a/controllers/application/register/verifyController.js b/controllers/application/register/verifyController.js index a43bb71..9ac9ab0 100644 --- a/controllers/application/register/verifyController.js +++ b/controllers/application/register/verifyController.js @@ -1,55 +1,55 @@ -const UserModel = require('../../../models/UserModel') -const TokenService = require('../../../services/TokenService') -const { isOtpExpired } = require('../../../utils/otpExpiry') -// const jwt = require('jsonwebtoken') - -const verifyUser = async (req, res, next) => { - try { - const { mobile, otp } = req.body - if (!mobile || !otp) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const user = await UserModel.findOne({ mobile }) - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شماره موبایل یافت نشد' - }) - } - const token = TokenService.sign({ id: user._id }) - let step = '' - if (user.user_name === null) { step = 'user_name' } else if - (!user.password) { step = 'password' } else if - (!user.first_name) { step = 'first_name' } else if - (!user.user_type) { step = 'user_type' } else { step = 'profile_image' } - if (!user.otp || isOtpExpired(user)) { - return res.status(422).json({ - error: true, - message: 'کد تایید منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید' - }) - } - - if (String(user.otp) === String(otp)) { - return res.json({ - message: 'کد تایید صحیح بود', - token, - step - }) - } - - return res.status(422).json({ - error: true, - message: 'کد را اشتباه وارد کردید' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - verifyUser -} +const UserModel = require('../../../models/UserModel') +const TokenService = require('../../../services/TokenService') +const { isOtpExpired } = require('../../../utils/otpExpiry') +// const jwt = require('jsonwebtoken') + +const verifyUser = async (req, res, next) => { + try { + const { mobile, otp } = req.body + if (!mobile || !otp) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const user = await UserModel.findOne({ mobile }) + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شماره موبایل یافت نشد' + }) + } + const token = TokenService.sign({ id: user._id }) + let step = '' + if (user.user_name === null) { step = 'user_name' } else if + (!user.password) { step = 'password' } else if + (!user.first_name) { step = 'first_name' } else if + (!user.user_type) { step = 'user_type' } else { step = 'profile_image' } + if (!user.otp || isOtpExpired(user)) { + return res.status(422).json({ + error: true, + message: 'کد تایید منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید' + }) + } + + if (String(user.otp) === String(otp)) { + return res.json({ + message: 'کد تایید صحیح بود', + token, + step + }) + } + + return res.status(422).json({ + error: true, + message: 'کد را اشتباه وارد کردید' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + verifyUser +} diff --git a/controllers/application/search-web/searchController.js b/controllers/application/search-web/searchController.js index 05dc53c..5790ba1 100644 --- a/controllers/application/search-web/searchController.js +++ b/controllers/application/search-web/searchController.js @@ -1,52 +1,52 @@ -const ProjectModel = require('../../../models/ProjectModel') -const UserModel = require('../../../models/UserModel') -const getSearch = async (req, res, next) => { - try { - const { search, type, page = 1, limit = 10 } = req.query - let result - const selectedFields = '_id user_name first_name last_name user_type is_verified profile_image expertise rate user_level user_score' - let filter - let totalPages - let totalItems - if (search) { - if (!type) { - res.status(422).json({ success: false, message: 'نوع جستجو را مشخص کنید' }) - } else if (type === 'project') { - // جستجو در مدل Project با فیلتر کردن برای user_type === "employer" - - filter = { $or: [{ title: new RegExp(search, 'i'), status: 'accepted' }, { description: new RegExp(search, 'i'), status: 'accepted' }] } - result = await ProjectModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), sort: { createdAt: -1 } }) - totalPages = result?.totalPages - totalItems = result?.totalDocs - } else if (type === 'user') { - // جستجو در مدل User با فیلتر کردن برای user_type === "user" - filter = { user_type: 'user', $or: [{ user_name: new RegExp(search, 'i') }, { first_name: new RegExp(search, 'i') }, { last_name: new RegExp(search, 'i') }] } - result = await UserModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), select: selectedFields }) - totalPages = result?.totalPages - totalItems = result?.totalDocs - } else if (type === 'employer') { - // جستجو در مدل User با فیلتر کردن برای user_type === "employer" - filter = { user_type: 'employer', $or: [{ user_name: new RegExp(search, 'i') }, { first_name: new RegExp(search, 'i') }, { last_name: new RegExp(search, 'i') }] } - result = await UserModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), select: selectedFields }) - totalPages = result?.totalPages - totalItems = result?.totalDocs - } - - res.status(200).json({ - success: true, - result: result?.docs, - type, - totalPages, - totalItems - }) - } else { - res.status(422).json({ success: false, message: 'چیزی تایپ کنید' }) - } - } catch (error) { - next(error) - } -} - -module.exports = { - getSearch -} +const ProjectModel = require('../../../models/ProjectModel') +const UserModel = require('../../../models/UserModel') +const getSearch = async (req, res, next) => { + try { + const { search, type, page = 1, limit = 10 } = req.query + let result + const selectedFields = '_id user_name first_name last_name user_type is_verified profile_image expertise rate user_level user_score' + let filter + let totalPages + let totalItems + if (search) { + if (!type) { + res.status(422).json({ success: false, message: 'نوع جستجو را مشخص کنید' }) + } else if (type === 'project') { + // جستجو در مدل Project با فیلتر کردن برای user_type === "employer" + + filter = { $or: [{ title: new RegExp(search, 'i'), status: 'accepted' }, { description: new RegExp(search, 'i'), status: 'accepted' }] } + result = await ProjectModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), sort: { createdAt: -1 } }) + totalPages = result?.totalPages + totalItems = result?.totalDocs + } else if (type === 'user') { + // جستجو در مدل User با فیلتر کردن برای user_type === "user" + filter = { user_type: 'user', $or: [{ user_name: new RegExp(search, 'i') }, { first_name: new RegExp(search, 'i') }, { last_name: new RegExp(search, 'i') }] } + result = await UserModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), select: selectedFields }) + totalPages = result?.totalPages + totalItems = result?.totalDocs + } else if (type === 'employer') { + // جستجو در مدل User با فیلتر کردن برای user_type === "employer" + filter = { user_type: 'employer', $or: [{ user_name: new RegExp(search, 'i') }, { first_name: new RegExp(search, 'i') }, { last_name: new RegExp(search, 'i') }] } + result = await UserModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), select: selectedFields }) + totalPages = result?.totalPages + totalItems = result?.totalDocs + } + + res.status(200).json({ + success: true, + result: result?.docs, + type, + totalPages, + totalItems + }) + } else { + res.status(422).json({ success: false, message: 'چیزی تایپ کنید' }) + } + } catch (error) { + next(error) + } +} + +module.exports = { + getSearch +} diff --git a/controllers/application/search/searchController.js b/controllers/application/search/searchController.js index d04f46d..b099c3f 100644 --- a/controllers/application/search/searchController.js +++ b/controllers/application/search/searchController.js @@ -1,51 +1,51 @@ -const ProjectModel = require('../../../models/ProjectModel') -const UserModel = require('../../../models/UserModel') -const getSearch = async (req, res, next) => { - try { - const { search, type, page = 1, limit = 10 } = req.query - let result - const selectedFields = '_id user_name first_name last_name user_type is_verified profile_image expertise rate user_level user_score' - let filter - let totalPages - let totalItems - if (search) { - if (!type) { - res.status(422).json({ success: false, message: 'نوع جستجو را مشخص کنید' }) - } else if (type === 'project') { - // جستجو در مدل Project با فیلتر کردن برای user_type === "employer" - - filter = { $or: [{ title: new RegExp(search, 'i'), status: 'accepted' }, { description: new RegExp(search, 'i'), status: 'accepted' }] } - result = await ProjectModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), sort: { createdAt: -1 } }) - totalPages = result?.totalPages - totalItems = result?.totalDocs - } else if (type === 'user') { - // جستجو در مدل User با فیلتر کردن برای user_type === "user" - filter = { user_type: 'user', $or: [{ user_name: new RegExp(search, 'i') }, { first_name: new RegExp(search, 'i') }, { last_name: new RegExp(search, 'i') }] } - result = await UserModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), select: selectedFields }) - totalPages = result?.totalPages - totalItems = result?.totalDocs - } else if (type === 'employer') { - // جستجو در مدل User با فیلتر کردن برای user_type === "employer" - filter = { user_type: 'employer', $or: [{ user_name: new RegExp(search, 'i') }, { first_name: new RegExp(search, 'i') }, { last_name: new RegExp(search, 'i') }] } - result = await UserModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), select: selectedFields }) - totalPages = result?.totalPages - totalItems = result?.totalDocs - } - res.status(200).json({ - success: true, - result: result?.docs, - type, - totalPages, - totalItems - }) - } else { - res.status(422).json({ success: false, message: 'چیزی تایپ کنید' }) - } - } catch (error) { - next(error) - } -} - -module.exports = { - getSearch -} +const ProjectModel = require('../../../models/ProjectModel') +const UserModel = require('../../../models/UserModel') +const getSearch = async (req, res, next) => { + try { + const { search, type, page = 1, limit = 10 } = req.query + let result + const selectedFields = '_id user_name first_name last_name user_type is_verified profile_image expertise rate user_level user_score' + let filter + let totalPages + let totalItems + if (search) { + if (!type) { + res.status(422).json({ success: false, message: 'نوع جستجو را مشخص کنید' }) + } else if (type === 'project') { + // جستجو در مدل Project با فیلتر کردن برای user_type === "employer" + + filter = { $or: [{ title: new RegExp(search, 'i'), status: 'accepted' }, { description: new RegExp(search, 'i'), status: 'accepted' }] } + result = await ProjectModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), sort: { createdAt: -1 } }) + totalPages = result?.totalPages + totalItems = result?.totalDocs + } else if (type === 'user') { + // جستجو در مدل User با فیلتر کردن برای user_type === "user" + filter = { user_type: 'user', $or: [{ user_name: new RegExp(search, 'i') }, { first_name: new RegExp(search, 'i') }, { last_name: new RegExp(search, 'i') }] } + result = await UserModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), select: selectedFields }) + totalPages = result?.totalPages + totalItems = result?.totalDocs + } else if (type === 'employer') { + // جستجو در مدل User با فیلتر کردن برای user_type === "employer" + filter = { user_type: 'employer', $or: [{ user_name: new RegExp(search, 'i') }, { first_name: new RegExp(search, 'i') }, { last_name: new RegExp(search, 'i') }] } + result = await UserModel.paginate(filter, { page: parseInt(page), limit: parseInt(limit), select: selectedFields }) + totalPages = result?.totalPages + totalItems = result?.totalDocs + } + res.status(200).json({ + success: true, + result: result?.docs, + type, + totalPages, + totalItems + }) + } else { + res.status(422).json({ success: false, message: 'چیزی تایپ کنید' }) + } + } catch (error) { + next(error) + } +} + +module.exports = { + getSearch +} diff --git a/controllers/application/settings/settingsController.js b/controllers/application/settings/settingsController.js index 7a01630..bd76195 100644 --- a/controllers/application/settings/settingsController.js +++ b/controllers/application/settings/settingsController.js @@ -1,19 +1,19 @@ -// controllers/settingController.js - -const SettingModel = require('../../../models/SettingsModel') - -// متد برای دریافت تنظیمات بر اساس key -const getSetting = async (req, res, next) => { - try { - const { key } = req.params - const setting = await SettingModel.findOne({ key }) - if (!setting) { - return res.status(404).json({ message: 'تنظیمات مورد نظر یافت نشد' }) - } - res.status(200).json(setting) - } catch (error) { - next(error) - } -} - -module.exports = { getSetting } +// controllers/settingController.js + +const SettingModel = require('../../../models/SettingsModel') + +// متد برای دریافت تنظیمات بر اساس key +const getSetting = async (req, res, next) => { + try { + const { key } = req.params + const setting = await SettingModel.findOne({ key }) + if (!setting) { + return res.status(404).json({ message: 'تنظیمات مورد نظر یافت نشد' }) + } + res.status(200).json(setting) + } catch (error) { + next(error) + } +} + +module.exports = { getSetting } diff --git a/controllers/application/stories/storyController.js b/controllers/application/stories/storyController.js index 2185a75..38e4fdf 100644 --- a/controllers/application/stories/storyController.js +++ b/controllers/application/stories/storyController.js @@ -1,197 +1,197 @@ -/* eslint-disable camelcase */ -const path = require('path') -const fs = require('fs') -const jwt = require('jsonwebtoken') -const StoryModel = require('../../../models/StoryModel') -const UserModel = require('../../../models/UserModel') -const { getBlockerUserIds } = require('../../../utils/blockVisibility') - -const STORY_TTL_MS = 24 * 60 * 60 * 1000 - -const storyStorageDir = path.join(__dirname, '../../../storage/stories') -if (!fs.existsSync(storyStorageDir)) { - fs.mkdirSync(storyStorageDir, { recursive: true }) -} - -function storyMediaUrl (storagePath) { - if (!storagePath) return '' - const normalized = storagePath.replace(/\\/g, '/') - const idx = normalized.indexOf('/storage/') - if (idx >= 0) return normalized.slice(idx) - return normalized.replace('/root/modstagram-back/storage', '/storage') -} - -async function resolveViewerId (req) { - const token = req.header('Authorization')?.split(' ')[1] - if (!token) return null - try { - const decoded = jwt.verify(token, process.env.APP_SECRET) - return decoded?.id ? String(decoded.id) : null - } catch { - return null - } -} - -const createStoryBase64 = async (req, res) => { - try { - const userId = req.user._id - const { file } = req.body - - if (!file?.data || !file?.type) { - return res.status(400).json({ error: true, message: 'فایل استوری الزامی است' }) - } - - const mediaType = file.type.startsWith('video/') ? 'video' : 'image' - const buffer = Buffer.from(file.data.split(',')[1] || file.data, 'base64') - const ext = mediaType === 'video' ? '.mp4' : '.webp' - const filename = `${Date.now()}-${userId}${ext}` - const filePath = path.join(storyStorageDir, filename) - await fs.promises.writeFile(filePath, buffer) - - const story = await StoryModel.create({ - user_id: userId, - media_path: filePath, - media_type: mediaType, - expires_at: new Date(Date.now() + STORY_TTL_MS), - viewers: [] - }) - - return res.status(201).json({ - message: 'استوری با موفقیت منتشر شد', - storyId: story._id - }) - } catch (err) { - console.error('createStoryBase64:', err) - return res.status(500).json({ error: true, message: 'خطا در انتشار استوری' }) - } -} - -const getStoriesFeed = async (req, res, next) => { - try { - const viewerId = await resolveViewerId(req) - const now = new Date() - - let stories = await StoryModel.find({ expires_at: { $gt: now } }) - .sort({ createdAt: 1 }) - .lean() - - if (viewerId) { - const blockerIds = new Set( - (await getBlockerUserIds(UserModel, viewerId)).map(String) - ) - stories = stories.filter((s) => !blockerIds.has(String(s.user_id))) - } - - const userIds = [...new Set(stories.map((s) => String(s.user_id)))] - const users = await UserModel.find({ _id: { $in: userIds } }) - .select('_id user_name first_name last_name profile_image') - .lean() - const usersById = Object.fromEntries(users.map((u) => [String(u._id), u])) - - const grouped = {} - for (const story of stories) { - const uid = String(story.user_id) - if (!grouped[uid]) grouped[uid] = [] - grouped[uid].push({ - _id: story._id, - media_path: storyMediaUrl(story.media_path), - media_type: story.media_type, - createdAt: story.createdAt, - expires_at: story.expires_at, - viewed: viewerId - ? story.viewers?.some((v) => String(v.user_id) === viewerId) - : false - }) - } - - let feed = Object.entries(grouped).map(([uid, userStories]) => { - const user = usersById[uid] || {} - const hasUnviewed = viewerId - ? userStories.some((s) => !s.viewed) - : true - return { - user: { - _id: uid, - user_name: user.user_name || '', - first_name: user.first_name || '', - last_name: user.last_name || '', - profile_image: user.profile_image || '' - }, - stories: userStories, - has_unviewed: hasUnviewed, - latest_at: userStories[userStories.length - 1]?.createdAt - } - }) - - feed.sort((a, b) => { - if (viewerId && a.user._id === viewerId) return -1 - if (viewerId && b.user._id === viewerId) return 1 - if (a.has_unviewed !== b.has_unviewed) return a.has_unviewed ? -1 : 1 - return new Date(b.latest_at) - new Date(a.latest_at) - }) - - let myActiveStory = null - if (viewerId) { - const mine = feed.find((f) => f.user._id === viewerId) - if (mine) myActiveStory = mine - } - - return res.status(200).json({ - feed, - my_story: myActiveStory, - viewer_id: viewerId - }) - } catch (error) { - next(error) - } -} - -const markStoryViewed = async (req, res) => { - try { - const viewerId = req.user._id - const { storyId } = req.body - if (!storyId) { - return res.status(422).json({ message: 'شناسه استوری الزامی است' }) - } - - const story = await StoryModel.findById(storyId) - if (!story || story.expires_at <= new Date()) { - return res.status(404).json({ message: 'استوری یافت نشد' }) - } - - const already = story.viewers.some( - (v) => String(v.user_id) === String(viewerId) - ) - if (!already) { - story.viewers.push({ user_id: viewerId, viewed_at: new Date() }) - await story.save() - } - - return res.status(200).json({ message: 'ok' }) - } catch (err) { - console.error('markStoryViewed:', err) - return res.status(500).json({ message: 'خطا در ثبت بازدید' }) - } -} - -const deleteStory = async (req, res) => { - try { - const userId = req.user._id - const { storyId } = req.params - const story = await StoryModel.findOne({ _id: storyId, user_id: userId }) - if (!story) { - return res.status(404).json({ message: 'استوری یافت نشد' }) - } - await StoryModel.deleteOne({ _id: storyId }) - return res.status(200).json({ message: 'استوری حذف شد' }) - } catch (err) { - return res.status(500).json({ message: 'خطا در حذف استوری' }) - } -} - -module.exports = { - createStoryBase64, - getStoriesFeed, - markStoryViewed, - deleteStory -} +/* eslint-disable camelcase */ +const path = require('path') +const fs = require('fs') +const jwt = require('jsonwebtoken') +const StoryModel = require('../../../models/StoryModel') +const UserModel = require('../../../models/UserModel') +const { getBlockerUserIds } = require('../../../utils/blockVisibility') + +const STORY_TTL_MS = 24 * 60 * 60 * 1000 + +const storyStorageDir = path.join(__dirname, '../../../storage/stories') +if (!fs.existsSync(storyStorageDir)) { + fs.mkdirSync(storyStorageDir, { recursive: true }) +} + +function storyMediaUrl (storagePath) { + if (!storagePath) return '' + const normalized = storagePath.replace(/\\/g, '/') + const idx = normalized.indexOf('/storage/') + if (idx >= 0) return normalized.slice(idx) + return normalized.replace('/root/modstagram-back/storage', '/storage') +} + +async function resolveViewerId (req) { + const token = req.header('Authorization')?.split(' ')[1] + if (!token) return null + try { + const decoded = jwt.verify(token, process.env.APP_SECRET) + return decoded?.id ? String(decoded.id) : null + } catch { + return null + } +} + +const createStoryBase64 = async (req, res) => { + try { + const userId = req.user._id + const { file } = req.body + + if (!file?.data || !file?.type) { + return res.status(400).json({ error: true, message: 'فایل استوری الزامی است' }) + } + + const mediaType = file.type.startsWith('video/') ? 'video' : 'image' + const buffer = Buffer.from(file.data.split(',')[1] || file.data, 'base64') + const ext = mediaType === 'video' ? '.mp4' : '.webp' + const filename = `${Date.now()}-${userId}${ext}` + const filePath = path.join(storyStorageDir, filename) + await fs.promises.writeFile(filePath, buffer) + + const story = await StoryModel.create({ + user_id: userId, + media_path: filePath, + media_type: mediaType, + expires_at: new Date(Date.now() + STORY_TTL_MS), + viewers: [] + }) + + return res.status(201).json({ + message: 'استوری با موفقیت منتشر شد', + storyId: story._id + }) + } catch (err) { + console.error('createStoryBase64:', err) + return res.status(500).json({ error: true, message: 'خطا در انتشار استوری' }) + } +} + +const getStoriesFeed = async (req, res, next) => { + try { + const viewerId = await resolveViewerId(req) + const now = new Date() + + let stories = await StoryModel.find({ expires_at: { $gt: now } }) + .sort({ createdAt: 1 }) + .lean() + + if (viewerId) { + const blockerIds = new Set( + (await getBlockerUserIds(UserModel, viewerId)).map(String) + ) + stories = stories.filter((s) => !blockerIds.has(String(s.user_id))) + } + + const userIds = [...new Set(stories.map((s) => String(s.user_id)))] + const users = await UserModel.find({ _id: { $in: userIds } }) + .select('_id user_name first_name last_name profile_image') + .lean() + const usersById = Object.fromEntries(users.map((u) => [String(u._id), u])) + + const grouped = {} + for (const story of stories) { + const uid = String(story.user_id) + if (!grouped[uid]) grouped[uid] = [] + grouped[uid].push({ + _id: story._id, + media_path: storyMediaUrl(story.media_path), + media_type: story.media_type, + createdAt: story.createdAt, + expires_at: story.expires_at, + viewed: viewerId + ? story.viewers?.some((v) => String(v.user_id) === viewerId) + : false + }) + } + + let feed = Object.entries(grouped).map(([uid, userStories]) => { + const user = usersById[uid] || {} + const hasUnviewed = viewerId + ? userStories.some((s) => !s.viewed) + : true + return { + user: { + _id: uid, + user_name: user.user_name || '', + first_name: user.first_name || '', + last_name: user.last_name || '', + profile_image: user.profile_image || '' + }, + stories: userStories, + has_unviewed: hasUnviewed, + latest_at: userStories[userStories.length - 1]?.createdAt + } + }) + + feed.sort((a, b) => { + if (viewerId && a.user._id === viewerId) return -1 + if (viewerId && b.user._id === viewerId) return 1 + if (a.has_unviewed !== b.has_unviewed) return a.has_unviewed ? -1 : 1 + return new Date(b.latest_at) - new Date(a.latest_at) + }) + + let myActiveStory = null + if (viewerId) { + const mine = feed.find((f) => f.user._id === viewerId) + if (mine) myActiveStory = mine + } + + return res.status(200).json({ + feed, + my_story: myActiveStory, + viewer_id: viewerId + }) + } catch (error) { + next(error) + } +} + +const markStoryViewed = async (req, res) => { + try { + const viewerId = req.user._id + const { storyId } = req.body + if (!storyId) { + return res.status(422).json({ message: 'شناسه استوری الزامی است' }) + } + + const story = await StoryModel.findById(storyId) + if (!story || story.expires_at <= new Date()) { + return res.status(404).json({ message: 'استوری یافت نشد' }) + } + + const already = story.viewers.some( + (v) => String(v.user_id) === String(viewerId) + ) + if (!already) { + story.viewers.push({ user_id: viewerId, viewed_at: new Date() }) + await story.save() + } + + return res.status(200).json({ message: 'ok' }) + } catch (err) { + console.error('markStoryViewed:', err) + return res.status(500).json({ message: 'خطا در ثبت بازدید' }) + } +} + +const deleteStory = async (req, res) => { + try { + const userId = req.user._id + const { storyId } = req.params + const story = await StoryModel.findOne({ _id: storyId, user_id: userId }) + if (!story) { + return res.status(404).json({ message: 'استوری یافت نشد' }) + } + await StoryModel.deleteOne({ _id: storyId }) + return res.status(200).json({ message: 'استوری حذف شد' }) + } catch (err) { + return res.status(500).json({ message: 'خطا در حذف استوری' }) + } +} + +module.exports = { + createStoryBase64, + getStoriesFeed, + markStoryViewed, + deleteStory +} diff --git a/controllers/application/tickets/ticketsController.js b/controllers/application/tickets/ticketsController.js index 785ad59..bc67d3b 100644 --- a/controllers/application/tickets/ticketsController.js +++ b/controllers/application/tickets/ticketsController.js @@ -1,147 +1,147 @@ -const jwt = require('jsonwebtoken') -const TicketModel = require('../../../models/TicketModel') -const TicketMessageModel = require('../../../models/TicketMessageModel') -const jMoment = require('moment-jalaali') -const path = require('path') -const fs = require('fs-extra') - -const getUserTickets = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const { page = 1, limit = 10, search } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست - const filter = { user: userId } - if (search) { - filter.$or = [ - { title: { $regex: search, $options: 'i' } } - ] - } - const options = { - page: parseInt(page) || 1, - limit: parseInt(limit) || 10, - sort: { updatedAt: -1 } - } - const tickets = await TicketModel.paginate(filter, options) - const ticketList = tickets?.docs.map(ticket => { - const jDate = jMoment(ticket.createdAt).format('jYYYY-jMM-jDD HH:mm') - return { - ...ticket._doc, - createdAt: jDate - } - }) - res.json({ - tickets: ticketList, - totalPages: tickets.totalPages, // ارسال تعداد کل صفحات - totalItems: tickets.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} - -const createTicket = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const { title } = req.body - - const newTicket = new TicketModel({ - title, - user: userId - }) - await newTicket.save() - - res.status(201).json(newTicket) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} -const addUserMessageToTicket = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const { ticketId } = req.query - const { text } = req.body - const { file } = req.files - let fileUrl = null - if (file) { - // اگر فایل ارسال شده است، آن را ذخیره کنید - const uploadDir = path.join(__dirname, '../../../../storage/tickets') - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - - const uniqueFileName = `${Date.now()}-${file.name}` - const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(file.path, filePath) - - fileUrl = `/tickets/${uniqueFileName}` - } - const newMessage = new TicketMessageModel({ - text, - senderType: 'User', - senderId: userId, - ticket: ticketId, - file: fileUrl - }) - await newMessage.save() - - const ticket = await TicketModel.findById(ticketId) - if (!ticket) { - return res.status(404).json({ error: 'Ticket not found' }) - } - - ticket.status = 'Customer Response' - await ticket.save() - const newMessageWithJalaliDate = { - ...newMessage._doc, - createdAt: jMoment(newMessage.createdAt).format('jYYYY-jMM-jDD HH:mm') - } - res.status(201).json(newMessageWithJalaliDate) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} -const getTicketMessages = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { ticketId } = req.query - const { page = 1, limit = 10 } = req.query - const options = { - page: parseInt(page) || 1, - limit: parseInt(limit) || 10, - sort: { createdAt: -1 } - } - const messages = await TicketMessageModel.paginate({ ticket: ticketId }, options) - const ticket = await TicketModel.findById(ticketId) - ticket.new_message = false - await ticket.save() - const messageList = messages?.docs.map(message => { - const jDate = jMoment(message.createdAt).format('jYYYY-jMM-jDD HH:mm') - return { - ...message._doc, - createdAt: jDate - } - }) - res.json({ - messages: messageList.reverse(), - totalPages: messages.totalPages, // ارسال تعداد کل صفحات - totalItems: messages.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} -module.exports = { getUserTickets, createTicket, addUserMessageToTicket, getTicketMessages } +const jwt = require('jsonwebtoken') +const TicketModel = require('../../../models/TicketModel') +const TicketMessageModel = require('../../../models/TicketMessageModel') +const jMoment = require('moment-jalaali') +const path = require('path') +const fs = require('fs-extra') + +const getUserTickets = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const { page = 1, limit = 10, search } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست + const filter = { user: userId } + if (search) { + filter.$or = [ + { title: { $regex: search, $options: 'i' } } + ] + } + const options = { + page: parseInt(page) || 1, + limit: parseInt(limit) || 10, + sort: { updatedAt: -1 } + } + const tickets = await TicketModel.paginate(filter, options) + const ticketList = tickets?.docs.map(ticket => { + const jDate = jMoment(ticket.createdAt).format('jYYYY-jMM-jDD HH:mm') + return { + ...ticket._doc, + createdAt: jDate + } + }) + res.json({ + tickets: ticketList, + totalPages: tickets.totalPages, // ارسال تعداد کل صفحات + totalItems: tickets.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } +} + +const createTicket = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { title } = req.body + + const newTicket = new TicketModel({ + title, + user: userId + }) + await newTicket.save() + + res.status(201).json(newTicket) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } +} +const addUserMessageToTicket = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const { ticketId } = req.query + const { text } = req.body + const { file } = req.files + let fileUrl = null + if (file) { + // اگر فایل ارسال شده است، آن را ذخیره کنید + const uploadDir = path.join(__dirname, '../../../../storage/tickets') + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + + const uniqueFileName = `${Date.now()}-${file.name}` + const filePath = path.join(uploadDir, uniqueFileName) + await fs.move(file.path, filePath) + + fileUrl = `/tickets/${uniqueFileName}` + } + const newMessage = new TicketMessageModel({ + text, + senderType: 'User', + senderId: userId, + ticket: ticketId, + file: fileUrl + }) + await newMessage.save() + + const ticket = await TicketModel.findById(ticketId) + if (!ticket) { + return res.status(404).json({ error: 'Ticket not found' }) + } + + ticket.status = 'Customer Response' + await ticket.save() + const newMessageWithJalaliDate = { + ...newMessage._doc, + createdAt: jMoment(newMessage.createdAt).format('jYYYY-jMM-jDD HH:mm') + } + res.status(201).json(newMessageWithJalaliDate) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } +} +const getTicketMessages = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { ticketId } = req.query + const { page = 1, limit = 10 } = req.query + const options = { + page: parseInt(page) || 1, + limit: parseInt(limit) || 10, + sort: { createdAt: -1 } + } + const messages = await TicketMessageModel.paginate({ ticket: ticketId }, options) + const ticket = await TicketModel.findById(ticketId) + ticket.new_message = false + await ticket.save() + const messageList = messages?.docs.map(message => { + const jDate = jMoment(message.createdAt).format('jYYYY-jMM-jDD HH:mm') + return { + ...message._doc, + createdAt: jDate + } + }) + res.json({ + messages: messageList.reverse(), + totalPages: messages.totalPages, // ارسال تعداد کل صفحات + totalItems: messages.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } +} +module.exports = { getUserTickets, createTicket, addUserMessageToTicket, getTicketMessages } diff --git a/controllers/application/users/getUserController.js b/controllers/application/users/getUserController.js index 5eaca7c..61b82ad 100644 --- a/controllers/application/users/getUserController.js +++ b/controllers/application/users/getUserController.js @@ -1,1809 +1,1816 @@ -/* eslint-disable camelcase */ -const { default: mongoose } = require("mongoose"); -const LikeModel = require("../../../models/LikeModel"); -const UserModel = require("../../../models/UserModel"); -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, - viewerBlockedUser, - userBlockedViewer, - blockedProfilePayload, - getBlockerUserIds, -} = require("../../../utils/blockVisibility") -const { buildUserLevelMongoFilter, normalizeUserLevel } = require("../../../utils/userLevelFilter") -const { paginatePersonalizedExplore } = require("../../../utils/exploreAlgorithm") -const ExploreInteractionModel = require("../../../models/ExploreInteractionModel") - -const EXPLORE_NEAR_RADIUS_METERS = 2000; - -function haversineMeters(lat1, lon1, lat2, lon2) { - const toRad = (deg) => (deg * Math.PI) / 180; - const dLat = toRad(lat2 - lat1); - const dLon = toRad(lon2 - lon1); - const a = - Math.sin(dLat / 2) ** 2 + - Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; - return 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); -} - -function applyExploreFilterToUserFilter(exploreFilter, userFilter) { - if (!exploreFilter) return; - - switch (exploreFilter) { - case "model": - userFilter.expertise = "مدل"; - break; - case "photographer": - userFilter.expertise = "عکاس"; - break; - case "hairstylist": - userFilter.expertise = "آرایشگر"; - break; - case "makeup": - userFilter.sub_expertise = "میکاپ"; - break; - case "professional": - Object.assign(userFilter, buildUserLevelMongoFilter("حرفه‌ای")); - break; - default: - break; - } -} - -async function mapPostsWithUsers(posts, users, decodedToken) { - let filteredPosts = posts; - - if (decodedToken?.id) { - const blockerIds = new Set( - (await getBlockerUserIds(UserModel, decodedToken.id)).map(String) - ); - filteredPosts = filteredPosts.filter( - (post) => !blockerIds.has(String(post.user_id)) - ); - } - - const likes = decodedToken - ? await LikeModel.find({ - userId: decodedToken.id, - postId: { $in: filteredPosts.map((p) => p._id) }, - }).lean() - : []; - const likesMap = {}; - likes.forEach((l) => { - likesMap[l.postId.toString()] = true; - }); - - const comments = await CommentModel.aggregate([ - { - $match: { - post: { $in: filteredPosts.map((p) => p._id) }, - status: "accepted", - }, - }, - { $group: { _id: "$post", count: { $sum: 1 } } }, - ]); - const commentsMap = {}; - comments.forEach((c) => { - commentsMap[c._id.toString()] = c.count; - }); - - return filteredPosts.map((post) => { - const user = - users.find((u) => u._id.toString() === post.user_id.toString()) || {}; - return { - ...post, - user_name: user.user_name || "", - first_name: user.first_name || "", - last_name: user.last_name || "", - expertise: user.expertise || "", - sub_expertise: user.sub_expertise || [], - province: user.show_location ? user.province || {} : {}, - city: user.show_location ? user.city || {} : {}, - show_location: user.show_location, - user_level: user.user_level || "", - profile_image: user.profile_image || "", - likesCount: post.likes ? post.likes.length : 0, - is_liked: !!likesMap[post._id.toString()], - commentsCount: commentsMap[post._id.toString()] || 0, - }; - }); -} - -async function getRankedExplorePosts({ - windowDays, - page, - limit, - postFilter, - decodedToken, -}) { - const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000); - const postIdsWithActivity = new Set(); - - const recentLikes = await LikeModel.find({ createdAt: { $gte: since } }) - .select("postId") - .lean(); - recentLikes.forEach((like) => { - if (like.postId) postIdsWithActivity.add(String(like.postId)); - }); - - const recentComments = await CommentModel.find({ - createdAt: { $gte: since }, - status: "accepted", - comment_for: "post", - post: { $ne: null }, - }) - .select("post") - .lean(); - recentComments.forEach((comment) => { - if (comment.post) postIdsWithActivity.add(String(comment.post)); - }); - - const recentPosts = await PostModel.find({ - ...postFilter, - createdAt: { $gte: since }, - }) - .select("_id") - .lean(); - recentPosts.forEach((post) => postIdsWithActivity.add(String(post._id))); - - if (!postIdsWithActivity.size) { - return { posts: [], totalItems: 0 }; - } - - let posts = await PostModel.find({ - ...postFilter, - _id: { $in: [...postIdsWithActivity] }, - }).lean(); - - const likeCounts = {}; - recentLikes.forEach((like) => { - const id = String(like.postId); - likeCounts[id] = (likeCounts[id] || 0) + 1; - }); - - const commentCounts = {}; - recentComments.forEach((comment) => { - const id = String(comment.post); - commentCounts[id] = (commentCounts[id] || 0) + 1; - }); - - posts = posts - .map((post) => { - const id = String(post._id); - const score = - (likeCounts[id] || 0) + - (commentCounts[id] || 0) + - (post.likes ? post.likes.length : 0) * 0.1; - return { ...post, exploreScore: score }; - }) - .sort((a, b) => b.exploreScore - a.exploreScore); - - const userIds = [...new Set(posts.map((p) => String(p.user_id)))]; - const users = await UserModel.find({ _id: { $in: userIds } }) - .select( - "_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image blocked_users" - ) - .lean(); - - posts = await mapPostsWithUsers(posts, users, decodedToken); - - const totalItems = posts.length; - const startIndex = (page - 1) * limit; - const paginatedPosts = posts.slice(startIndex, startIndex + parseInt(limit, 10)); - - return { posts: paginatedPosts, totalItems }; -} - -const getAllLicenses = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - - jwt.verify(token, process.env.APP_SECRET); - - const { page = 1, limit = 10 } = req.query; - - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: { createdAt: -1 }, - populate: { - path: "userId", - model: UserModel, - select: "user_name first_name last_name is_verified profile_image", - }, - }; - - const licenses = await LicenseModel.paginate({}, options); - - const formattedLicenses = licenses.docs.map((license) => ({ - _id: license._id, - userId: license.userId, - Confirmation: license.Confirmation, - licenseImg: license.licenseImg, // چون خودش Base64 هست - createdAt: moment(license.createdAt).format("jYYYY-jMM-jDD HH:mm"), - updatedAt: moment(license.updatedAt).format("jYYYY-jMM-jDD HH:mm"), - user: license.userId, - })); - - res.status(200).json({ - licenses: formattedLicenses, - totalPages: licenses.totalPages, - totalItems: licenses.totalDocs, - currentPage: licenses.page, - }); - } catch (error) { - console.error("Error fetching licenses:", error); - next(error); - } -}; - - -// GET: دریافت License بر اساس userId -const getLicenseByUserId = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - jwt.verify(token, process.env.APP_SECRET); - - const { userId } = req.params; - - if (!mongoose.Types.ObjectId.isValid(userId)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); - } - - const license = await LicenseModel.findOne({ userId }).populate({ - path: "userId", - model: UserModel, - select: "user_name first_name last_name is_verified profile_image", - }); - - if (!license) { - return res.status(404).json({ message: "License یافت نشد" }); - } - - res.status(200).json(license); // licenseImg همان Base64 است - } catch (error) { - console.error("Error fetching license:", error); - next(error); - } -}; - - - -// POST: ذخیره License به صورت Base64 -const createLicense = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - - jwt.verify(token, process.env.APP_SECRET); - - const { userId, image } = req.body; - - if (!userId || !image) { - return res.status(400).json({ message: "userId و تصویر الزامی هستند" }); - } - - if (!mongoose.Types.ObjectId.isValid(userId)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); - } - - if (!image.startsWith("data:image/")) { - return res.status(400).json({ message: "فقط فایل‌های تصویری مجاز هستند" }); - } - - const existingLicense = await LicenseModel.findOne({ userId }); - if (existingLicense) { - existingLicense.licenseImg = image; - existingLicense.Confirmation = false; // نیاز به تأیید دوباره - await existingLicense.save(); - - return res.status(200).json({ - message: "تصویر مجوز با موفقیت به‌روزرسانی شد", - license: existingLicense, - }); - } - - const license = new LicenseModel({ - userId, - Confirmation: false, - licenseImg: image, - }); - await license.save(); - - res.status(201).json({ message: "License با موفقیت ذخیره شد", license }); - } catch (error) { - console.error("Error saving license:", error); - next(error); - } -}; - - -// PUT: به‌روزرسانی Confirmation و is_verified -const updateLicenseConfirmation = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - jwt.verify(token, process.env.APP_SECRET); - - const { userId } = req.params; - const { Confirmation } = req.body; - - if (!mongoose.Types.ObjectId.isValid(userId)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); - } - - if (typeof Confirmation !== "boolean") { - return res.status(400).json({ message: "Confirmation باید بولین باشد" }); - } - - const license = await LicenseModel.findOneAndUpdate( - { userId }, - { Confirmation }, - { new: true } - ); - - if (!license) { - return res.status(404).json({ message: "License یافت نشد" }); - } - - // اگر Confirmation به true تغییر کند، is_verified را به "true" به‌روزرسانی کن - if (Confirmation) { - await UserModel.findByIdAndUpdate(userId, { is_verified: "true" }); - } - - res.status(200).json({ message: "Confirmation به‌روزرسانی شد", license }); - } catch (error) { - console.error("Error updating license:", error); - next(error); - } -}; - - -// const getUsers = 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 { expertise, page = 1, limit = 10 } = req.query -// const filter = {} -// if (expertise) { -// filter.expertise = expertise -// } - -// let users = await UserModel.aggregate([ -// { $match: filter }, -// { $match: { last_post: { $ne: null } } }, -// { -// $addFields: { -// 'last_post.likesCount': { $size: { $ifNull: ['$last_post.likes', []] } } -// } -// }, -// { -// $lookup: { -// from: 'comments', -// let: { userId: '$_id' }, -// pipeline: [ -// { $match: { $expr: { $and: [{ $eq: ['$user', '$$userId'] }, { $eq: ['$status', 'accepted'] }] } } }, -// { $count: 'commentsCount' } -// ], -// as: 'comments' -// } -// }, -// { -// $addFields: { -// commentsCount: { $arrayElemAt: ['$comments.commentsCount', 0] } -// } -// }, -// { -// $project: { -// user_level: 1, -// first_name: 1, -// last_name: 1, -// user_name: 1, -// is_verified: 1, -// user_score: 1, -// rate: 1, -// profile_image: 1, -// last_post: { -// _id: 1, -// post_image: 1, -// caption: 1, -// status: 1, -// user_id: 1, -// updatedAt: 1, -// createdAt: 1, -// __v: 1, -// likesCount: 1, -// is_liked: 1 -// }, -// commentsCount: 1 -// } -// }, -// { $sort: { 'last_post.createdAt': -1 } } -// ]) - -// users = users.filter(user => user.last_post !== null) - -// if (!users || users.length === 0) { -// return res.status(404).json({ message: 'هیچ کاربری با این مشخصات یافت نشد' }) -// } -// users = await Promise.all(users.map(async (user) => { -// const isLikedDocument = await LikeModel.findOne({ postId: user.last_post._id, userId: decodedToken.id }) -// user.last_post.is_liked = !!isLikedDocument -// return user -// })) - -// const startIndex = (page - 1) * limit -// const endIndex = page * limit -// const totalItems = users.length - -// const paginatedUsers = users.slice(startIndex, endIndex) -// if (!users || users.length === 0) { -// return res.status(404).json({ message: 'هیچ کاربری با این مشخصات یافت نشد' }) -// } - -// return res.status(200).json({ -// users: paginatedUsers, -// totalPages: Math.ceil(totalItems / limit), -// totalItems -// }) -// } catch (error) { -// next(error) -// } -// } -const getUsers = 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 { - expertise, - page = 1, - limit = 10, - province, - city, - userLevel, - rateFilter, - } = req.query; - const filter = {}; - if (expertise) { - filter.expertise = expertise; - } - if (province) { - const provinceFind = await ProvinceModel.findOne( - { id: province }, - { _id: 0 } - ); - console.log("provinceFind:", provinceFind); - if (provinceFind) { - filter["province.id"] = provinceFind.id; - } - } - if (city) { - const cityFind = await CityModel.findOne({ id: city }); - console.log("cityFind:", cityFind); - if (cityFind) { - filter["city.id"] = cityFind.id; - } - } - if (userLevel) { - const levelFilter = buildUserLevelMongoFilter(userLevel) - if (levelFilter) Object.assign(filter, levelFilter) - } - - let users = await UserModel.aggregate([ - { $match: filter }, - { $match: { last_post: { $ne: null } } }, - { - $addFields: { - "last_post.likesCount": { - $size: { $ifNull: ["$last_post.likes", []] }, - }, - }, - }, - { - $lookup: { - from: "comments", - let: { userId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { - $and: [ - { $eq: ["$user", "$$userId"] }, - { $eq: ["$status", "accepted"] }, - ], - }, - }, - }, - { $count: "commentsCount" }, - ], - as: "comments", - }, - }, - { - $addFields: { - commentsCount: { $arrayElemAt: ["$comments.commentsCount", 0] }, - }, - }, - { - $project: { - user_level: 1, - first_name: 1, - last_name: 1, - user_name: 1, - is_verified: 1, - user_score: 1, - rate: 1, - profile_image: 1, - expertise: 1, - hair_color: 1, - eye_color: 1, - height: 1, - weight: 1, - size: 1, - services: 1, - last_post: { - _id: 1, - post_image: 1, - caption: 1, - status: 1, - user_id: 1, - updatedAt: 1, - createdAt: 1, - __v: 1, - likesCount: 1, - is_liked: 1, - }, - commentsCount: 1, - }, - }, - { $sort: { "last_post.createdAt": -1 } }, - ]); - - users = users.filter((user) => user.last_post !== null); - - if (!users || users.length === 0) { - return res - .status(404) - .json({ message: "هیچ کاربری با این مشخصات یافت نشد" }); - } - - users = await Promise.all( - users.map(async (user) => { - const isLikedDocument = await LikeModel.findOne({ - postId: user.last_post._id, - userId: decodedToken.id, - }); - user.last_post.is_liked = !!isLikedDocument; - return user; - }) - ); - - if (rateFilter) { - users.sort((a, b) => { - if (rateFilter === "کمترین") { - return a.rate - b.rate; - } else if (rateFilter === "بیشترین") { - return b.rate - a.rate; - } - return 0; - }); - } - - const startIndex = (page - 1) * limit; - const endIndex = page * limit; - const totalItems = users.length; - - const paginatedUsers = users.slice(startIndex, endIndex); - - return res.status(200).json({ - users: paginatedUsers, - totalPages: Math.ceil(totalItems / limit), - totalItems, - }); - } catch (error) { - next(error); - } -}; -// const getUsersWeb = async (req, res, next) => { -// try { -// const token = req.header('Authorization')?.split(' ')[1] -// const decodedToken = token && jwt.verify(token, process.env.APP_SECRET) - -// const { expertise, page = 1, limit = 10, province, city, userLevel, rateFilter } = req.query -// const filter = {} -// if (expertise) { -// filter.expertise = expertise -// } -// if (province) { -// const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) -// if (provinceFind) { -// filter['province.id'] = provinceFind.id -// } -// } -// if (city) { -// const cityFind = await CityModel.findOne({ id: city }) -// if (cityFind) { -// filter['city.id'] = cityFind.id -// } -// } -// if (userLevel) { -// filter.user_level = userLevel -// } - -// let users = await UserModel.aggregate([ -// { $match: filter }, -// { $match: { last_post: { $ne: null } } }, -// { -// $addFields: { -// 'last_post.likesCount': { $size: { $ifNull: ['$last_post.likes', []] } } -// } -// }, -// { -// $lookup: { -// from: 'comments', -// let: { userId: '$_id' }, -// pipeline: [ -// { $match: { $expr: { $and: [{ $eq: ['$user', '$$userId'] }, { $eq: ['$status', 'accepted'] }] } } }, -// { $count: 'commentsCount' } -// ], -// as: 'comments' -// } -// }, -// { -// $addFields: { -// commentsCount: { $arrayElemAt: ['$comments.commentsCount', 0] } -// } -// }, -// { -// $project: { -// user_level: 1, -// first_name: 1, -// last_name: 1, -// user_name: 1, -// is_verified: 1, -// user_score: 1, -// rate: 1, -// profile_image: 1, -// expertise: 1, -// hair_color: 1, -// eye_color: 1, -// height: 1, -// weight: 1, -// size: 1, -// services: 1, -// last_post: { -// _id: 1, -// post_image: 1, -// caption: 1, -// status: 1, -// user_id: 1, -// updatedAt: 1, -// createdAt: 1, -// __v: 1, -// likesCount: 1, -// is_liked: 1 -// }, -// commentsCount: 1 -// } -// }, -// { $sort: { 'last_post.createdAt': -1 } } -// ]) -// const getUsersWeb = async (req, res, next) => { -// try { -// const token = req.header('Authorization')?.split(' ')[1] -// const decodedToken = token && jwt.verify(token, process.env.APP_SECRET) - -// const { expertise, page = 1, limit = 10, province, city, userLevel, rateFilter, userId } = req.query -// const filter = {} - -// if (expertise) filter.expertise = expertise -// if (province) { -// const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) -// if (provinceFind) filter['province.id'] = provinceFind.id -// } -// if (city) { -// const cityFind = await CityModel.findOne({ id: city }) -// if (cityFind) filter['city.id'] = cityFind.id -// } -// if (userLevel) filter.user_level = userLevel - -// // ← اضافه کردن userId به فیلتر -// if (userId) { -// if (!mongoose.Types.ObjectId.isValid(userId)) { -// return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) -// } -// filter._id = mongoose.Types.ObjectId(userId) -// } - -// let users = await UserModel.aggregate([ -// { $match: filter }, -// { $match: { last_post: { $ne: null } } }, -// { -// $addFields: { -// 'last_post.likesCount': { $size: { $ifNull: ['$last_post.likes', []] } } -// } -// }, -// { -// $lookup: { -// from: 'comments', -// let: { userId: '$_id' }, -// pipeline: [ -// { $match: { $expr: { $and: [{ $eq: ['$user', '$$userId'] }, { $eq: ['$status', 'accepted'] }] } } }, -// { $count: 'commentsCount' } -// ], -// as: 'comments' -// } -// }, -// { -// $addFields: { -// commentsCount: { $arrayElemAt: ['$comments.commentsCount', 0] } -// } -// }, -// { -// $project: { -// user_level: 1, -// first_name: 1, -// last_name: 1, -// user_name: 1, -// is_verified: 1, -// user_score: 1, -// rate: 1, -// profile_image: 1, -// expertise: 1, -// hair_color: 1, -// eye_color: 1, -// height: 1, -// weight: 1, -// size: 1, -// services: 1, -// last_post: 1, -// commentsCount: 1 -// } -// }, -// { $sort: { 'last_post.createdAt': -1 } } -// ]) - -// users = users.filter(user => user.last_post !== null) - -// if (!users || users.length === 0) { -// return res.status(404).json({ message: 'هیچ کاربری با این مشخصات یافت نشد' }) -// } -// // بررسی لایک‌ها فقط در صورت وجود توکن -// if (token) { -// users = await Promise.all(users.map(async (user) => { -// const isLikedDocument = await LikeModel.findOne({ postId: user.last_post._id, userId: decodedToken.id }) -// user.last_post.is_liked = !!isLikedDocument -// return user -// })) -// } else { -// // اگر توکن وجود ندارد، مقدار پیش‌فرض را تنظیم کنید -// users = users.map(user => { -// user.last_post.is_liked = false -// return user -// }) -// } - -// if (rateFilter) { -// users.sort((a, b) => { -// if (rateFilter === 'کمترین') { -// return a.rate - b.rate -// } else if (rateFilter === 'بیشترین') { -// return b.rate - a.rate -// } -// return 0 -// }) -// } - -// const startIndex = (page - 1) * limit -// const endIndex = page * limit -// const totalItems = users.length - -// const paginatedUsers = users.slice(startIndex, endIndex) - -// return res.status(200).json({ -// users: paginatedUsers, -// totalPages: Math.ceil(totalItems / limit), -// totalItems -// }) -// } catch (error) { -// next(error) -// } -// } - -// const getUsersWeb = async (req, res, next) => { -// try { -// const token = req.header("Authorization")?.split(" ")[1]; -// const decodedToken = token && jwt.verify(token, process.env.APP_SECRET); - -// const { -// expertise, -// page = 1, -// limit = 10, -// province, -// city, -// userLevel, -// rateFilter, -// _id, -// } = req.query; - -// const filter = {}; -// if (expertise) filter.expertise = expertise; - -// if (province) { -// const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }); -// if (provinceFind) filter["province.id"] = provinceFind.id; -// } - -// if (city) { -// const cityFind = await CityModel.findOne({ id: city }); -// if (cityFind) filter["city.id"] = cityFind.id; -// } - -// if (userLevel) filter.user_level = userLevel; -// if (_id) filter._id = _id; - -// // پیدا کردن کاربران -// let users = await UserModel.find(filter) -// .select("-password -blocked_users -blocked_by") -// .lean(); - -// if (!users || users.length === 0) { -// return res.status(404).json({ message: "هیچ کاربری با این مشخصات یافت نشد" }); -// } - -// // آماده‌سازی لیست userIds برای گرفتن همه پست‌ها یکجا -// const userIds = users.map(u => u._id); - -// // گرفتن همه پست‌ها یکجا (به جای N+1 query) -// let posts = await PostModel.find({ user_id: { $in: userIds }, status: { $ne: null } }) -// .sort({ createdAt: -1 }) -// .lean(); - -// // گرفتن همه لایک‌های کاربران یکجا (برای بهبود کارایی) -// let likeFilter = {}; -// if (decodedToken) likeFilter.userId = decodedToken.id; -// const likes = decodedToken -// ? await LikeModel.find({ userId: decodedToken.id, postId: { $in: posts.map(p => p._id) } }).lean() -// : []; - -// const likesMap = {}; -// likes.forEach(l => { likesMap[l.postId.toString()] = true; }); - -// // گرفتن تعداد کامنت‌ها یکجا برای همه کاربران -// const comments = await CommentModel.aggregate([ -// { $match: { user: { $in: userIds }, status: "accepted" } }, -// { $group: { _id: "$user", count: { $sum: 1 } } } -// ]); -// const commentsMap = {}; -// comments.forEach(c => { commentsMap[c._id.toString()] = c.count; }); - -// // اتصال پست‌ها و کامنت‌ها به کاربران -// users = users.map(user => { -// const userPosts = posts.filter(p => p.user_id.toString() === user._id.toString()); -// user.posts = userPosts.map(p => ({ -// ...p, -// likesCount: p.likes ? p.likes.length : 0, -// is_liked: !!likesMap[p._id.toString()] -// })); -// user.commentsCount = commentsMap[user._id.toString()] || 0; -// return user; -// }); - -// // حذف کاربران بدون پست -// users = users.filter(u => u.posts.length > 0); - -// // مرتب‌سازی -// if (rateFilter) { -// users.sort((a, b) => { -// if (rateFilter === 'کمترین') return a.rate - b.rate; -// if (rateFilter === 'بیشترین') return b.rate - a.rate; -// return 0; -// }); -// } else { -// // مرتب‌سازی بر اساس جدیدترین پست آخر -// users.sort((a, b) => new Date(b.posts[0].createdAt) - new Date(a.posts[0].createdAt)); -// } - -// // صفحه‌بندی -// const startIndex = (page - 1) * limit; -// const endIndex = page * limit; -// const totalItems = users.length; -// const paginatedUsers = users.slice(startIndex, endIndex); - -// return res.status(200).json({ -// users: paginatedUsers, -// totalPages: Math.ceil(totalItems / limit), -// totalItems, -// }); - -// } catch (error) { -// console.error(error); -// next(error); -// } -// }; - -const getPostsWeb = async (req, res, next) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - const decodedToken = token ? jwt.verify(token, process.env.APP_SECRET) : null; - - const { - expertise, - page = 1, - limit = 10, - province, - city, - userLevel, - rateFilter, - _id, - type, - exploreFilter, - subExpertise, - lat, - lng, - feedMode, - seedPostId, - } = req.query; - - const reelsMode = feedMode === "reels" || type === "video"; - const gridMode = feedMode === "grid"; - - const postFilter = { status: { $ne: null } }; - if (type === 'video' || type === 'image') { - postFilter.type = type; - } - - const parsedLat = parseFloat(lat); - const parsedLng = parseFloat(lng); - const hasNearMeCoords = - exploreFilter === "near_me" && - Number.isFinite(parsedLat) && - Number.isFinite(parsedLng); - - // اکسپلور: همه پست‌ها (عکس/ویدئو) بدون فیلتر تخصص/مکان - const isExploreFeed = - !expertise && - !_id && - !province && - !city && - !userLevel && - !rateFilter && - !exploreFilter && - !subExpertise && - !hasNearMeCoords; - - if (exploreFilter === "trending" || exploreFilter === "best_month") { - const windowDays = exploreFilter === "trending" ? 7 : 30; - const ranked = await getRankedExplorePosts({ - windowDays, - page: parseInt(page, 10), - limit: parseInt(limit, 10), - postFilter: { ...postFilter, status: "accept" }, - decodedToken, - }); - - return res.status(200).json({ - posts: ranked.posts, - totalPages: Math.ceil(ranked.totalItems / limit) || 1, - totalItems: ranked.totalItems, - }); - } - - if (hasNearMeCoords) { - const nearbyUsers = await UserModel.find({ - show_location: true, - lat: { $nin: [null, ""] }, - lng: { $nin: [null, ""] }, - }) - .select( - "_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image lat lng blocked_users" - ) - .lean(); - - const nearbyUserIds = nearbyUsers - .filter((user) => { - const userLat = parseFloat(user.lat); - const userLng = parseFloat(user.lng); - if (!Number.isFinite(userLat) || !Number.isFinite(userLng)) return false; - return ( - haversineMeters(parsedLat, parsedLng, userLat, userLng) <= - EXPLORE_NEAR_RADIUS_METERS - ); - }) - .map((user) => user._id); - - if (!nearbyUserIds.length) { - return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); - } - - let posts = await PostModel.find({ - ...postFilter, - status: "accept", - user_id: { $in: nearbyUserIds }, - }) - .sort({ createdAt: -1 }) - .lean(); - - const users = nearbyUsers.filter((user) => - nearbyUserIds.some((id) => String(id) === String(user._id)) - ); - posts = await mapPostsWithUsers(posts, users, decodedToken); - - const totalItems = posts.length; - const startIndex = (parseInt(page, 10) - 1) * parseInt(limit, 10); - const paginatedPosts = posts.slice( - startIndex, - startIndex + parseInt(limit, 10) - ); - - return res.status(200).json({ - posts: paginatedPosts, - totalPages: Math.ceil(totalItems / limit) || 1, - totalItems, - }); - } - - if (isExploreFeed) { - let explorePosts = await PostModel.find({ ...postFilter, status: "accept" }) - .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 sub_expertise province city user_level show_location profile_image 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 = await mapPostsWithUsers(explorePosts, usersMap, decodedToken); - - const { posts: paginatedPosts, totalItems } = await paginatePersonalizedExplore({ - posts: explorePosts, - usersById, - page, - limit, - userId: decodedToken?.id, - seedPostId: reelsMode ? seedPostId : undefined, - reelsMode, - blendFresh: gridMode || (!reelsMode && !seedPostId), - }); - - return res.status(200).json({ - posts: paginatedPosts, - totalPages: Math.ceil(totalItems / limit) || 1, - totalItems - }); - } - - const userFilter = {}; - - applyExploreFilterToUserFilter(exploreFilter, userFilter); - if (expertise) userFilter.expertise = expertise; - if (subExpertise) userFilter.sub_expertise = subExpertise; - if (province) { - const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }); - if (provinceFind) userFilter["province.id"] = provinceFind.id; - } - if (city) { - const cityFind = await CityModel.findOne({ id: city }); - if (cityFind) userFilter["city.id"] = cityFind.id; - } - if (userLevel) { - const levelFilter = buildUserLevelMongoFilter(userLevel) - if (levelFilter) Object.assign(userFilter, levelFilter) - } - 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 sub_expertise province city user_level show_location profile_image") - .lean(); - - if (!users || users.length === 0) { - return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); - } - - const userIds = users.map(u => u._id); - postFilter.user_id = { $in: userIds }; - - let posts = await PostModel.find(postFilter) - .sort({ createdAt: -1 }) - .lean(); - - if (!posts || posts.length === 0) { - 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 - ? await LikeModel.find({ userId: decodedToken.id, postId: { $in: posts.map(p => p._id) } }).lean() - : []; - - const likesMap = {}; - likes.forEach(l => { likesMap[l.postId.toString()] = true; }); - - const comments = await CommentModel.aggregate([ - { $match: { post: { $in: posts.map(p => p._id) }, status: "accepted" } }, - { $group: { _id: "$post", count: { $sum: 1 } } } - ]); - const commentsMap = {}; - comments.forEach(c => { commentsMap[c._id.toString()] = c.count; }); - - posts = posts.map(post => { - const user = users.find(u => u._id.toString() === post.user_id.toString()); - return { - ...post, - user_name: user?.user_name || "", - first_name: user?.first_name || "", - last_name: user?.last_name || "", - expertise: user?.expertise || "", - sub_expertise: user?.sub_expertise || [], - province: user?.show_location ? user?.province || {} : {}, - city: user?.show_location ? user?.city || {} : {}, - show_location: user?.show_location, - user_level: user?.user_level || "", - profile_image: user?.profile_image || "", - likesCount: post.likes ? post.likes.length : 0, - is_liked: !!likesMap[post._id.toString()], - commentsCount: commentsMap[post._id.toString()] || 0, - }; - }); - - if (rateFilter) { - posts.sort((a, b) => { - if (rateFilter === 'کمترین') return (a.rate || 0) - (b.rate || 0); - if (rateFilter === 'بیشترین') return (b.rate || 0) - (a.rate || 0); - return 0; - }); - } - - const usersById = Object.fromEntries( - users.map((u) => [String(u._id), u]) - ); - - if (feedMode === "reels" && !_id && posts.length > 1) { - const { posts: paginatedPosts, totalItems } = await paginatePersonalizedExplore({ - posts, - usersById, - page, - limit, - userId: decodedToken?.id, - seedPostId, - reelsMode: true, - blendFresh: false, - }); - - return res.status(200).json({ - posts: paginatedPosts, - totalPages: Math.ceil(totalItems / limit) || 1, - totalItems, - }); - } - - if (feedMode === "reels" && _id && seedPostId && posts.length > 1) { - const seedIdx = posts.findIndex( - (p) => String(p._id) === String(seedPostId) - ); - if (seedIdx > 0) { - const seed = posts[seedIdx]; - const rest = posts.filter( - (p) => String(p._id) !== String(seedPostId) - ); - posts = [seed, ...rest]; - } - } - - const startIndex = (page - 1) * limit; - const endIndex = page * limit; - const totalItems = posts.length; - const paginatedPosts = posts.slice(startIndex, endIndex); - - return res.status(200).json({ - posts: paginatedPosts, - totalPages: Math.ceil(totalItems / limit), - totalItems, - }); - - } catch (error) { - console.error("Error in getPostsWeb:", error.message, error.stack); - return res.status(500).json({ message: "خطای سرور: " + error.message }); - } -}; - -const getSingleUser = async (req, res, next) => { - try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const user_id = req.query.user_id; - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userReqId = decodedToken.id; - - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); - } - const user = await UserModel.findById(user_id); - - if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); - } - const shamsiDate = moment(user.created_at).format("jYYYY-jMM-jDD"); - const lastOnline = moment(user.last_online).format("jYYYY-jMM-jDD"); - - // بررسی بلاک شدن - const blockedByCurrentUser = viewerBlockedUser(user, userReqId); - const blockedYou = userBlockedViewer(user, userReqId); - - if (blockedYou) { - return res.status(200).json({ user: blockedProfilePayload(user) }); - } - - // Allow chat - const offerExists = await OfferModel.exists({ - $or: [ - { sender: userReqId, receiver: user_id }, - { sender: user_id, receiver: userReqId }, - ], - }); - const response = { - is_self: user_id.toString() === userReqId.toString() ? "true" : "false", - _id: user._id, - last_online: lastOnline, - created_at: shamsiDate, - user_type: user.user_type, - user_level: user.user_level, - first_name: user.first_name, - last_name: user.last_name, - 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, - height: user.height, - weight: user.weight, - size: user.size, - eye_color: user.eye_color, - hair_color: user.hair_color, - profile_image: user.profile_image, - lat: user.show_location ? user.lat : null, - lng: user.show_location ? user.lng : null, - 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, - offer_status: !!offerExists, - monthly_free_offer: user.monthly_free_offer, - services: user.services, - expertise: user.expertise, - sub_expertise: user.sub_expertise, - }; - return res.status(200).json({ user: response }); - } catch (error) { - next(error); - } -}; -const getSingleUserWeb = async (req, res, next) => { - try { - let userReqId = null; - - // دریافت توکن و بررسی معتبر بودنش (اگر وجود داشت) - const authHeader = req.header("Authorization"); - if (authHeader) { - const token = authHeader.split(" ")[1]; - try { - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - userReqId = decodedToken.id; - } catch (err) { - console.warn("توکن نامعتبر است، ادامه بدون احراز هویت"); // فقط هشدار، نه ارور - } - } - - let user; - const { user_id, user_name } = req.query; - - if (user_id) { - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); - } - user = await UserModel.findById(user_id); - } else if (user_name) { - user = await UserModel.findOne({ user_name }); - } else { - return res - .status(400) - .json({ message: "لطفاً user_id یا user_name ارسال کنید" }); - } - - if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); - } - - const shamsiDate = moment(user.created_at).format("jYYYY-jMM-jDD"); - const lastOnline = moment(user.last_online).format("jYYYY-jMM-jDD"); - - // بررسی بلاک شدن (فقط اگر توکن معتبر داشتیم) - let blockedByCurrentUser = false; - let blockedYou = false; - let offerExists = false; - - if (userReqId) { - blockedByCurrentUser = viewerBlockedUser(user, userReqId); - blockedYou = userBlockedViewer(user, userReqId); - offerExists = await OfferModel.exists({ - sender: userReqId, - receiver: user._id, - }); - } - - if (blockedYou && userReqId) { - return res.status(200).json({ user: blockedProfilePayload(user) }); - } - - // دریافت تعداد پست‌ها و پروژه‌ها - const postsCount = await PostModel.countDocuments({ - user_id: user._id, - status: { $in: ["accept"] }, - }); - const allProjectsCount = await ProjectModel.countDocuments({ - selected_user: user._id, - }); - const successfulProjectsCount = await ProjectModel.countDocuments({ - selected_user: user._id, - status: "done", - }); - - const response = { - is_self: userReqId - ? user._id.toString() === userReqId.toString() - : "false", - _id: user._id, - last_online: lastOnline, - created_at: shamsiDate, - user_type: user.user_type, - user_level: user.user_level, - first_name: user.first_name, - last_name: user.last_name, - 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, - height: user.height, - weight: user.weight, - size: user.size, - eye_color: user.eye_color, - hair_color: user.hair_color, - profile_image: user.profile_image, - lat: user.show_location ? user.lat : null, - lng: user.show_location ? user.lng : null, - 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, - offer_status: !!offerExists, - services: user.services, - expertise: user.expertise, - sub_expertise: user.sub_expertise, - postsCount, - allProjectsCount, - successfulProjectsCount, - }; - - return res.status(200).json({ user: response }); - } catch (error) { - next(error); - } -}; -const getUserContactInfo = async (req, res, next) => { - try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const user_id = decodedToken.id; - - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); - } - const user = await UserModel.findById(user_id); - - if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); - } - - const response = { - contactInfo: user?.contactInfo, - _id: user._id, - }; - return res.status(200).json({ data: response }); - } catch (error) { - next(error); - } -}; -const { - applyAutoBlockSuspension, - clearAutoBlockSuspension -} = require('../../../utils/blockSuspension') - -const blockUser = async (req, res) => { - try { - const { user_to_block } = req.body; - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userReqId = decodedToken.id; - - 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(); - - res.status(200).json({ message: "کاربر با موفقیت بلاک شد" }); - } catch (error) { - console.error("Error blocking user:", error); - res.status(500).json({ error: "Server error" }); - } -}; - -const unblockUser = async (req, res) => { - try { - const { user_to_unblock } = req.body; - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userReqId = decodedToken.id; - - const currentUser = await UserModel.findById(userReqId); - const userToUnblock = await UserModel.findById(user_to_unblock); - - if (!currentUser || !userToUnblock) { - return res.status(404).json({ message: "کاربر یافت نشد" }); - } - - currentUser.blocked_users.pull(userToUnblock._id); - userToUnblock.blocked_by.pull(currentUser._id); - - clearAutoBlockSuspension(userToUnblock); - - await currentUser.save(); - await userToUnblock.save(); - - res.status(200).json({ message: "کاربر با موفقیت آنبلاک شد" }); - } catch (error) { - console.error("Error unblocking user:", error); - res.status(500).json({ error: "Server error" }); - } -}; - -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; - - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); - } - const user = await UserModel.findById(user_id); - const { page = 1, limit = 10 } = req.query; - if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); - } - - const filter = { user: user_id, status: "accepted" }; - if (post_id && mongoose.Types.ObjectId.isValid(post_id)) { - filter.post = post_id; - } - - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: { createdAt: -1 }, - populate: [ - { path: "creator", select: "user_name profile_image is_verified" }, - { path: "project", select: "project_name" }, - ], - }; - - const comments = await CommentModel.paginate(filter, options); - - 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); - } -}; -const createUserComment = 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 creatorId = decodedToken.id; - - const { user_id, comment, rate, post_id } = req.body; - if (!user_id || !comment) { - return res.status(400).send({ message: "All fields are required" }); - } - - const { ratingValue, isNewRating, requiresRating, alreadyRated } = - await resolveRatingForComment({ - creatorId, - targetUserId: user_id, - rate, - }); - - if (alreadyRated) { - return res.status(400).json({ - message: "شما قبلاً به این کاربر امتیاز داده‌اید.", - }); - } - - if (requiresRating) { - return res - .status(400) - .send({ message: "Rating must be between 1 and 5" }); - } - - const newComment = new CommentModel({ - user: user_id, - creator: creatorId, - rating: ratingValue, - comment, - comment_for: post_id ? "post" : "user", - post: post_id || undefined, - status: "pending", - }); - await newComment.save(); - - if (isNewRating) { - await recalculateUserRating(user_id); - } - - await createCommentNotification({ - ownerId: user_id, - commenterId: creatorId, - entityId: post_id || user_id, - type: post_id ? "post_comment" : "profile_comment", - }); - - res.status(201).json({ - message: "نظر با موفقیت ثبت شد و بعد از تایید، منتشر میشود.", - comment: newComment, - }); - } catch (error) { - next(error); - } -}; - -const inviteEmployer = async (req, res, next) => { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const user_id = req.query.user_id; - // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userReqId = decodedToken.id; - - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); - } - const user = await UserModel.findById(user_id); - const creatorUser = await UserModel.findById(userReqId); - - if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); - } - - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const existingNotification = await NotificationModel.findOne({ - user_id: user._id, - project_post_id: userReqId, - type: "invite", - createdAt: { $gte: today }, - }); - - if (existingNotification) { - return res - .status(400) - .json({ - message: "شما امروز برای این کارفرما درخواست همکاری ارسال کرده اید.", - type: "today", - }); - } - const notification = new NotificationModel({ - user_id: user._id, - project_post_id: userReqId, - type: "invite", - title: "دعوت به همکاری", - description: `کاربر ${creatorUser.user_name} شما را برای همکاری دعوت کرده است.`, - }); - await notification.save(); - - res.status(201).json({ message: "درخواست همکاری با موفقیت ثبت شد" }); -}; - -const recordExploreInteraction = 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 { targetType, targetId, authorId, contentType, action, dwellMs } = req.body; - - if (!targetType || !targetId) { - return res.status(422).json({ message: "اطلاعات ناقص است" }); - } - - const normalizedAction = action || "view"; - - if (normalizedAction === "view") { - const recentView = await ExploreInteractionModel.findOne({ - viewerId: decodedToken.id, - targetType, - targetId, - action: "view", - createdAt: { $gte: new Date(Date.now() - 30 * 60 * 1000) }, - }) - .select("_id") - .lean(); - - if (recentView) { - return res.status(201).json({ message: "ok" }); - } - } - - await ExploreInteractionModel.create({ - viewerId: decodedToken.id, - targetType, - targetId, - authorId: authorId || null, - contentType: contentType || "image", - action: normalizedAction, - dwellMs: dwellMs ?? null, - }); - - return res.status(201).json({ message: "ok" }); - } catch (error) { - console.error("recordExploreInteraction:", error); - next(error); - } -}; - -module.exports = { - getUsers, - getSingleUser, - blockUser, - unblockUser, - getComments, - inviteEmployer, - getUserContactInfo, - getPostsWeb, - getSingleUserWeb, - createUserComment, - getAllLicenses, - getLicenseByUserId, - createLicense, - updateLicenseConfirmation, - recordExploreInteraction, -}; +/* eslint-disable camelcase */ +const { default: mongoose } = require("mongoose"); +const LikeModel = require("../../../models/LikeModel"); +const UserModel = require("../../../models/UserModel"); +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, + viewerBlockedUser, + userBlockedViewer, + blockedProfilePayload, + getBlockerUserIds, +} = require("../../../utils/blockVisibility") +const { buildUserLevelMongoFilter, normalizeUserLevel } = require("../../../utils/userLevelFilter") +const { paginatePersonalizedExplore } = require("../../../utils/exploreAlgorithm") +const ExploreInteractionModel = require("../../../models/ExploreInteractionModel") + +const EXPLORE_NEAR_RADIUS_METERS = 2000; + +function haversineMeters(lat1, lon1, lat2, lon2) { + const toRad = (deg) => (deg * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; + return 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +} + +function applyExploreFilterToUserFilter(exploreFilter, userFilter) { + if (!exploreFilter) return; + + switch (exploreFilter) { + case "model": + userFilter.expertise = "مدل"; + break; + case "photographer": + userFilter.expertise = "عکاس"; + break; + case "hairstylist": + userFilter.expertise = "آرایشگر"; + break; + case "makeup": + userFilter.sub_expertise = "میکاپ"; + break; + case "professional": + Object.assign(userFilter, buildUserLevelMongoFilter("حرفه‌ای")); + break; + default: + break; + } +} + +async function mapPostsWithUsers(posts, users, decodedToken) { + let filteredPosts = posts; + + if (decodedToken?.id) { + const blockerIds = new Set( + (await getBlockerUserIds(UserModel, decodedToken.id)).map(String) + ); + filteredPosts = filteredPosts.filter( + (post) => !blockerIds.has(String(post.user_id)) + ); + } + + const likes = decodedToken + ? await LikeModel.find({ + userId: decodedToken.id, + postId: { $in: filteredPosts.map((p) => p._id) }, + }).lean() + : []; + const likesMap = {}; + likes.forEach((l) => { + likesMap[l.postId.toString()] = true; + }); + + const comments = await CommentModel.aggregate([ + { + $match: { + post: { $in: filteredPosts.map((p) => p._id) }, + status: "accepted", + }, + }, + { $group: { _id: "$post", count: { $sum: 1 } } }, + ]); + const commentsMap = {}; + comments.forEach((c) => { + commentsMap[c._id.toString()] = c.count; + }); + + return filteredPosts.map((post) => { + const user = + users.find((u) => u._id.toString() === post.user_id.toString()) || {}; + return { + ...post, + user_name: user.user_name || "", + first_name: user.first_name || "", + last_name: user.last_name || "", + expertise: user.expertise || "", + sub_expertise: user.sub_expertise || [], + province: user.show_location ? user.province || {} : {}, + city: user.show_location ? user.city || {} : {}, + show_location: user.show_location, + user_level: user.user_level || "", + profile_image: user.profile_image || "", + likesCount: post.likes ? post.likes.length : 0, + is_liked: !!likesMap[post._id.toString()], + commentsCount: commentsMap[post._id.toString()] || 0, + }; + }); +} + +async function getRankedExplorePosts({ + windowDays, + page, + limit, + postFilter, + decodedToken, +}) { + const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000); + const postIdsWithActivity = new Set(); + + const recentLikes = await LikeModel.find({ createdAt: { $gte: since } }) + .select("postId") + .lean(); + recentLikes.forEach((like) => { + if (like.postId) postIdsWithActivity.add(String(like.postId)); + }); + + const recentComments = await CommentModel.find({ + createdAt: { $gte: since }, + status: "accepted", + comment_for: "post", + post: { $ne: null }, + }) + .select("post") + .lean(); + recentComments.forEach((comment) => { + if (comment.post) postIdsWithActivity.add(String(comment.post)); + }); + + const recentPosts = await PostModel.find({ + ...postFilter, + createdAt: { $gte: since }, + }) + .select("_id") + .lean(); + recentPosts.forEach((post) => postIdsWithActivity.add(String(post._id))); + + if (!postIdsWithActivity.size) { + return { posts: [], totalItems: 0 }; + } + + let posts = await PostModel.find({ + ...postFilter, + _id: { $in: [...postIdsWithActivity] }, + }).lean(); + + const likeCounts = {}; + recentLikes.forEach((like) => { + const id = String(like.postId); + likeCounts[id] = (likeCounts[id] || 0) + 1; + }); + + const commentCounts = {}; + recentComments.forEach((comment) => { + const id = String(comment.post); + commentCounts[id] = (commentCounts[id] || 0) + 1; + }); + + posts = posts + .map((post) => { + const id = String(post._id); + const score = + (likeCounts[id] || 0) + + (commentCounts[id] || 0) + + (post.likes ? post.likes.length : 0) * 0.1; + return { ...post, exploreScore: score }; + }) + .sort((a, b) => b.exploreScore - a.exploreScore); + + const userIds = [...new Set(posts.map((p) => String(p.user_id)))]; + const users = await UserModel.find({ _id: { $in: userIds } }) + .select( + "_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image blocked_users" + ) + .lean(); + + posts = await mapPostsWithUsers(posts, users, decodedToken); + + const totalItems = posts.length; + const startIndex = (page - 1) * limit; + const paginatedPosts = posts.slice(startIndex, startIndex + parseInt(limit, 10)); + + return { posts: paginatedPosts, totalItems }; +} + +const getAllLicenses = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + + jwt.verify(token, process.env.APP_SECRET); + + const { page = 1, limit = 10 } = req.query; + + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: { createdAt: -1 }, + populate: { + path: "userId", + model: UserModel, + select: "user_name first_name last_name is_verified profile_image", + }, + }; + + const licenses = await LicenseModel.paginate({}, options); + + const formattedLicenses = licenses.docs.map((license) => ({ + _id: license._id, + userId: license.userId, + Confirmation: license.Confirmation, + licenseImg: license.licenseImg, // چون خودش Base64 هست + createdAt: moment(license.createdAt).format("jYYYY-jMM-jDD HH:mm"), + updatedAt: moment(license.updatedAt).format("jYYYY-jMM-jDD HH:mm"), + user: license.userId, + })); + + res.status(200).json({ + licenses: formattedLicenses, + totalPages: licenses.totalPages, + totalItems: licenses.totalDocs, + currentPage: licenses.page, + }); + } catch (error) { + console.error("Error fetching licenses:", error); + next(error); + } +}; + + +// GET: دریافت License بر اساس userId +const getLicenseByUserId = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + jwt.verify(token, process.env.APP_SECRET); + + const { userId } = req.params; + + if (!mongoose.Types.ObjectId.isValid(userId)) { + return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + } + + const license = await LicenseModel.findOne({ userId }).populate({ + path: "userId", + model: UserModel, + select: "user_name first_name last_name is_verified profile_image", + }); + + if (!license) { + return res.status(404).json({ message: "License یافت نشد" }); + } + + res.status(200).json(license); // licenseImg همان Base64 است + } catch (error) { + console.error("Error fetching license:", error); + next(error); + } +}; + + + +// POST: ذخیره License به صورت Base64 +const createLicense = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + + jwt.verify(token, process.env.APP_SECRET); + + const { userId, image } = req.body; + + if (!userId || !image) { + return res.status(400).json({ message: "userId و تصویر الزامی هستند" }); + } + + if (!mongoose.Types.ObjectId.isValid(userId)) { + return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + } + + if (!image.startsWith("data:image/")) { + return res.status(400).json({ message: "فقط فایل‌های تصویری مجاز هستند" }); + } + + const existingLicense = await LicenseModel.findOne({ userId }); + if (existingLicense) { + existingLicense.licenseImg = image; + existingLicense.Confirmation = false; // نیاز به تأیید دوباره + await existingLicense.save(); + + return res.status(200).json({ + message: "تصویر مجوز با موفقیت به‌روزرسانی شد", + license: existingLicense, + }); + } + + const license = new LicenseModel({ + userId, + Confirmation: false, + licenseImg: image, + }); + await license.save(); + + res.status(201).json({ message: "License با موفقیت ذخیره شد", license }); + } catch (error) { + console.error("Error saving license:", error); + next(error); + } +}; + + +// PUT: به‌روزرسانی Confirmation و is_verified +const updateLicenseConfirmation = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + jwt.verify(token, process.env.APP_SECRET); + + const { userId } = req.params; + const { Confirmation } = req.body; + + if (!mongoose.Types.ObjectId.isValid(userId)) { + return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + } + + if (typeof Confirmation !== "boolean") { + return res.status(400).json({ message: "Confirmation باید بولین باشد" }); + } + + const license = await LicenseModel.findOneAndUpdate( + { userId }, + { Confirmation }, + { new: true } + ); + + if (!license) { + return res.status(404).json({ message: "License یافت نشد" }); + } + + // اگر Confirmation به true تغییر کند، is_verified را به "true" به‌روزرسانی کن + if (Confirmation) { + await UserModel.findByIdAndUpdate(userId, { is_verified: "true" }); + } + + res.status(200).json({ message: "Confirmation به‌روزرسانی شد", license }); + } catch (error) { + console.error("Error updating license:", error); + next(error); + } +}; + + +// const getUsers = 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 { expertise, page = 1, limit = 10 } = req.query +// const filter = {} +// if (expertise) { +// filter.expertise = expertise +// } + +// let users = await UserModel.aggregate([ +// { $match: filter }, +// { $match: { last_post: { $ne: null } } }, +// { +// $addFields: { +// 'last_post.likesCount': { $size: { $ifNull: ['$last_post.likes', []] } } +// } +// }, +// { +// $lookup: { +// from: 'comments', +// let: { userId: '$_id' }, +// pipeline: [ +// { $match: { $expr: { $and: [{ $eq: ['$user', '$$userId'] }, { $eq: ['$status', 'accepted'] }] } } }, +// { $count: 'commentsCount' } +// ], +// as: 'comments' +// } +// }, +// { +// $addFields: { +// commentsCount: { $arrayElemAt: ['$comments.commentsCount', 0] } +// } +// }, +// { +// $project: { +// user_level: 1, +// first_name: 1, +// last_name: 1, +// user_name: 1, +// is_verified: 1, +// user_score: 1, +// rate: 1, +// profile_image: 1, +// last_post: { +// _id: 1, +// post_image: 1, +// caption: 1, +// status: 1, +// user_id: 1, +// updatedAt: 1, +// createdAt: 1, +// __v: 1, +// likesCount: 1, +// is_liked: 1 +// }, +// commentsCount: 1 +// } +// }, +// { $sort: { 'last_post.createdAt': -1 } } +// ]) + +// users = users.filter(user => user.last_post !== null) + +// if (!users || users.length === 0) { +// return res.status(404).json({ message: 'هیچ کاربری با این مشخصات یافت نشد' }) +// } +// users = await Promise.all(users.map(async (user) => { +// const isLikedDocument = await LikeModel.findOne({ postId: user.last_post._id, userId: decodedToken.id }) +// user.last_post.is_liked = !!isLikedDocument +// return user +// })) + +// const startIndex = (page - 1) * limit +// const endIndex = page * limit +// const totalItems = users.length + +// const paginatedUsers = users.slice(startIndex, endIndex) +// if (!users || users.length === 0) { +// return res.status(404).json({ message: 'هیچ کاربری با این مشخصات یافت نشد' }) +// } + +// return res.status(200).json({ +// users: paginatedUsers, +// totalPages: Math.ceil(totalItems / limit), +// totalItems +// }) +// } catch (error) { +// next(error) +// } +// } +const getUsers = 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 { + expertise, + page = 1, + limit = 10, + province, + city, + userLevel, + rateFilter, + } = req.query; + const filter = {}; + if (expertise) { + filter.expertise = expertise; + } + if (province) { + const provinceFind = await ProvinceModel.findOne( + { id: province }, + { _id: 0 } + ); + console.log("provinceFind:", provinceFind); + if (provinceFind) { + filter["province.id"] = provinceFind.id; + } + } + if (city) { + const cityFind = await CityModel.findOne({ id: city }); + console.log("cityFind:", cityFind); + if (cityFind) { + filter["city.id"] = cityFind.id; + } + } + if (userLevel) { + const levelFilter = buildUserLevelMongoFilter(userLevel) + if (levelFilter) Object.assign(filter, levelFilter) + } + + let users = await UserModel.aggregate([ + { $match: filter }, + { $match: { last_post: { $ne: null } } }, + { + $addFields: { + "last_post.likesCount": { + $size: { $ifNull: ["$last_post.likes", []] }, + }, + }, + }, + { + $lookup: { + from: "comments", + let: { userId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { $eq: ["$user", "$$userId"] }, + { $eq: ["$status", "accepted"] }, + ], + }, + }, + }, + { $count: "commentsCount" }, + ], + as: "comments", + }, + }, + { + $addFields: { + commentsCount: { $arrayElemAt: ["$comments.commentsCount", 0] }, + }, + }, + { + $project: { + user_level: 1, + first_name: 1, + last_name: 1, + user_name: 1, + is_verified: 1, + user_score: 1, + rate: 1, + profile_image: 1, + expertise: 1, + hair_color: 1, + eye_color: 1, + height: 1, + weight: 1, + size: 1, + services: 1, + last_post: { + _id: 1, + post_image: 1, + caption: 1, + status: 1, + user_id: 1, + updatedAt: 1, + createdAt: 1, + __v: 1, + likesCount: 1, + is_liked: 1, + }, + commentsCount: 1, + }, + }, + { $sort: { "last_post.createdAt": -1 } }, + ]); + + users = users.filter((user) => user.last_post !== null); + + if (!users || users.length === 0) { + return res + .status(404) + .json({ message: "هیچ کاربری با این مشخصات یافت نشد" }); + } + + users = await Promise.all( + users.map(async (user) => { + const isLikedDocument = await LikeModel.findOne({ + postId: user.last_post._id, + userId: decodedToken.id, + }); + user.last_post.is_liked = !!isLikedDocument; + return user; + }) + ); + + if (rateFilter) { + users.sort((a, b) => { + if (rateFilter === "کمترین") { + return a.rate - b.rate; + } else if (rateFilter === "بیشترین") { + return b.rate - a.rate; + } + return 0; + }); + } + + const startIndex = (page - 1) * limit; + const endIndex = page * limit; + const totalItems = users.length; + + const paginatedUsers = users.slice(startIndex, endIndex); + + return res.status(200).json({ + users: paginatedUsers, + totalPages: Math.ceil(totalItems / limit), + totalItems, + }); + } catch (error) { + next(error); + } +}; +// const getUsersWeb = async (req, res, next) => { +// try { +// const token = req.header('Authorization')?.split(' ')[1] +// const decodedToken = token && jwt.verify(token, process.env.APP_SECRET) + +// const { expertise, page = 1, limit = 10, province, city, userLevel, rateFilter } = req.query +// const filter = {} +// if (expertise) { +// filter.expertise = expertise +// } +// if (province) { +// const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) +// if (provinceFind) { +// filter['province.id'] = provinceFind.id +// } +// } +// if (city) { +// const cityFind = await CityModel.findOne({ id: city }) +// if (cityFind) { +// filter['city.id'] = cityFind.id +// } +// } +// if (userLevel) { +// filter.user_level = userLevel +// } + +// let users = await UserModel.aggregate([ +// { $match: filter }, +// { $match: { last_post: { $ne: null } } }, +// { +// $addFields: { +// 'last_post.likesCount': { $size: { $ifNull: ['$last_post.likes', []] } } +// } +// }, +// { +// $lookup: { +// from: 'comments', +// let: { userId: '$_id' }, +// pipeline: [ +// { $match: { $expr: { $and: [{ $eq: ['$user', '$$userId'] }, { $eq: ['$status', 'accepted'] }] } } }, +// { $count: 'commentsCount' } +// ], +// as: 'comments' +// } +// }, +// { +// $addFields: { +// commentsCount: { $arrayElemAt: ['$comments.commentsCount', 0] } +// } +// }, +// { +// $project: { +// user_level: 1, +// first_name: 1, +// last_name: 1, +// user_name: 1, +// is_verified: 1, +// user_score: 1, +// rate: 1, +// profile_image: 1, +// expertise: 1, +// hair_color: 1, +// eye_color: 1, +// height: 1, +// weight: 1, +// size: 1, +// services: 1, +// last_post: { +// _id: 1, +// post_image: 1, +// caption: 1, +// status: 1, +// user_id: 1, +// updatedAt: 1, +// createdAt: 1, +// __v: 1, +// likesCount: 1, +// is_liked: 1 +// }, +// commentsCount: 1 +// } +// }, +// { $sort: { 'last_post.createdAt': -1 } } +// ]) +// const getUsersWeb = async (req, res, next) => { +// try { +// const token = req.header('Authorization')?.split(' ')[1] +// const decodedToken = token && jwt.verify(token, process.env.APP_SECRET) + +// const { expertise, page = 1, limit = 10, province, city, userLevel, rateFilter, userId } = req.query +// const filter = {} + +// if (expertise) filter.expertise = expertise +// if (province) { +// const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) +// if (provinceFind) filter['province.id'] = provinceFind.id +// } +// if (city) { +// const cityFind = await CityModel.findOne({ id: city }) +// if (cityFind) filter['city.id'] = cityFind.id +// } +// if (userLevel) filter.user_level = userLevel + +// // ← اضافه کردن userId به فیلتر +// if (userId) { +// if (!mongoose.Types.ObjectId.isValid(userId)) { +// return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) +// } +// filter._id = mongoose.Types.ObjectId(userId) +// } + +// let users = await UserModel.aggregate([ +// { $match: filter }, +// { $match: { last_post: { $ne: null } } }, +// { +// $addFields: { +// 'last_post.likesCount': { $size: { $ifNull: ['$last_post.likes', []] } } +// } +// }, +// { +// $lookup: { +// from: 'comments', +// let: { userId: '$_id' }, +// pipeline: [ +// { $match: { $expr: { $and: [{ $eq: ['$user', '$$userId'] }, { $eq: ['$status', 'accepted'] }] } } }, +// { $count: 'commentsCount' } +// ], +// as: 'comments' +// } +// }, +// { +// $addFields: { +// commentsCount: { $arrayElemAt: ['$comments.commentsCount', 0] } +// } +// }, +// { +// $project: { +// user_level: 1, +// first_name: 1, +// last_name: 1, +// user_name: 1, +// is_verified: 1, +// user_score: 1, +// rate: 1, +// profile_image: 1, +// expertise: 1, +// hair_color: 1, +// eye_color: 1, +// height: 1, +// weight: 1, +// size: 1, +// services: 1, +// last_post: 1, +// commentsCount: 1 +// } +// }, +// { $sort: { 'last_post.createdAt': -1 } } +// ]) + +// users = users.filter(user => user.last_post !== null) + +// if (!users || users.length === 0) { +// return res.status(404).json({ message: 'هیچ کاربری با این مشخصات یافت نشد' }) +// } +// // بررسی لایک‌ها فقط در صورت وجود توکن +// if (token) { +// users = await Promise.all(users.map(async (user) => { +// const isLikedDocument = await LikeModel.findOne({ postId: user.last_post._id, userId: decodedToken.id }) +// user.last_post.is_liked = !!isLikedDocument +// return user +// })) +// } else { +// // اگر توکن وجود ندارد، مقدار پیش‌فرض را تنظیم کنید +// users = users.map(user => { +// user.last_post.is_liked = false +// return user +// }) +// } + +// if (rateFilter) { +// users.sort((a, b) => { +// if (rateFilter === 'کمترین') { +// return a.rate - b.rate +// } else if (rateFilter === 'بیشترین') { +// return b.rate - a.rate +// } +// return 0 +// }) +// } + +// const startIndex = (page - 1) * limit +// const endIndex = page * limit +// const totalItems = users.length + +// const paginatedUsers = users.slice(startIndex, endIndex) + +// return res.status(200).json({ +// users: paginatedUsers, +// totalPages: Math.ceil(totalItems / limit), +// totalItems +// }) +// } catch (error) { +// next(error) +// } +// } + +// const getUsersWeb = async (req, res, next) => { +// try { +// const token = req.header("Authorization")?.split(" ")[1]; +// const decodedToken = token && jwt.verify(token, process.env.APP_SECRET); + +// const { +// expertise, +// page = 1, +// limit = 10, +// province, +// city, +// userLevel, +// rateFilter, +// _id, +// } = req.query; + +// const filter = {}; +// if (expertise) filter.expertise = expertise; + +// if (province) { +// const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }); +// if (provinceFind) filter["province.id"] = provinceFind.id; +// } + +// if (city) { +// const cityFind = await CityModel.findOne({ id: city }); +// if (cityFind) filter["city.id"] = cityFind.id; +// } + +// if (userLevel) filter.user_level = userLevel; +// if (_id) filter._id = _id; + +// // پیدا کردن کاربران +// let users = await UserModel.find(filter) +// .select("-password -blocked_users -blocked_by") +// .lean(); + +// if (!users || users.length === 0) { +// return res.status(404).json({ message: "هیچ کاربری با این مشخصات یافت نشد" }); +// } + +// // آماده‌سازی لیست userIds برای گرفتن همه پست‌ها یکجا +// const userIds = users.map(u => u._id); + +// // گرفتن همه پست‌ها یکجا (به جای N+1 query) +// let posts = await PostModel.find({ user_id: { $in: userIds }, status: { $ne: null } }) +// .sort({ createdAt: -1 }) +// .lean(); + +// // گرفتن همه لایک‌های کاربران یکجا (برای بهبود کارایی) +// let likeFilter = {}; +// if (decodedToken) likeFilter.userId = decodedToken.id; +// const likes = decodedToken +// ? await LikeModel.find({ userId: decodedToken.id, postId: { $in: posts.map(p => p._id) } }).lean() +// : []; + +// const likesMap = {}; +// likes.forEach(l => { likesMap[l.postId.toString()] = true; }); + +// // گرفتن تعداد کامنت‌ها یکجا برای همه کاربران +// const comments = await CommentModel.aggregate([ +// { $match: { user: { $in: userIds }, status: "accepted" } }, +// { $group: { _id: "$user", count: { $sum: 1 } } } +// ]); +// const commentsMap = {}; +// comments.forEach(c => { commentsMap[c._id.toString()] = c.count; }); + +// // اتصال پست‌ها و کامنت‌ها به کاربران +// users = users.map(user => { +// const userPosts = posts.filter(p => p.user_id.toString() === user._id.toString()); +// user.posts = userPosts.map(p => ({ +// ...p, +// likesCount: p.likes ? p.likes.length : 0, +// is_liked: !!likesMap[p._id.toString()] +// })); +// user.commentsCount = commentsMap[user._id.toString()] || 0; +// return user; +// }); + +// // حذف کاربران بدون پست +// users = users.filter(u => u.posts.length > 0); + +// // مرتب‌سازی +// if (rateFilter) { +// users.sort((a, b) => { +// if (rateFilter === 'کمترین') return a.rate - b.rate; +// if (rateFilter === 'بیشترین') return b.rate - a.rate; +// return 0; +// }); +// } else { +// // مرتب‌سازی بر اساس جدیدترین پست آخر +// users.sort((a, b) => new Date(b.posts[0].createdAt) - new Date(a.posts[0].createdAt)); +// } + +// // صفحه‌بندی +// const startIndex = (page - 1) * limit; +// const endIndex = page * limit; +// const totalItems = users.length; +// const paginatedUsers = users.slice(startIndex, endIndex); + +// return res.status(200).json({ +// users: paginatedUsers, +// totalPages: Math.ceil(totalItems / limit), +// totalItems, +// }); + +// } catch (error) { +// console.error(error); +// next(error); +// } +// }; + +const getPostsWeb = async (req, res, next) => { + try { + const token = req.header("Authorization")?.split(" ")[1]; + let decodedToken = null; + if (token) { + try { + decodedToken = jwt.verify(token, process.env.APP_SECRET); + } catch (err) { + console.warn("توکن نامعتبر است، ادامه بدون احراز هویت"); + } + } + + const { + expertise, + page = 1, + limit = 10, + province, + city, + userLevel, + rateFilter, + _id, + type, + exploreFilter, + subExpertise, + lat, + lng, + feedMode, + seedPostId, + } = req.query; + + const reelsMode = feedMode === "reels" || type === "video"; + const gridMode = feedMode === "grid"; + + const postFilter = { status: { $ne: null } }; + if (type === 'video' || type === 'image') { + postFilter.type = type; + } + + const parsedLat = parseFloat(lat); + const parsedLng = parseFloat(lng); + const hasNearMeCoords = + exploreFilter === "near_me" && + Number.isFinite(parsedLat) && + Number.isFinite(parsedLng); + + // اکسپلور: همه پست‌ها (عکس/ویدئو) بدون فیلتر تخصص/مکان + const isExploreFeed = + !expertise && + !_id && + !province && + !city && + !userLevel && + !rateFilter && + !exploreFilter && + !subExpertise && + !hasNearMeCoords; + + if (exploreFilter === "trending" || exploreFilter === "best_month") { + const windowDays = exploreFilter === "trending" ? 7 : 30; + const ranked = await getRankedExplorePosts({ + windowDays, + page: parseInt(page, 10), + limit: parseInt(limit, 10), + postFilter: { ...postFilter, status: "accept" }, + decodedToken, + }); + + return res.status(200).json({ + posts: ranked.posts, + totalPages: Math.ceil(ranked.totalItems / limit) || 1, + totalItems: ranked.totalItems, + }); + } + + if (hasNearMeCoords) { + const nearbyUsers = await UserModel.find({ + show_location: true, + lat: { $nin: [null, ""] }, + lng: { $nin: [null, ""] }, + }) + .select( + "_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image lat lng blocked_users" + ) + .lean(); + + const nearbyUserIds = nearbyUsers + .filter((user) => { + const userLat = parseFloat(user.lat); + const userLng = parseFloat(user.lng); + if (!Number.isFinite(userLat) || !Number.isFinite(userLng)) return false; + return ( + haversineMeters(parsedLat, parsedLng, userLat, userLng) <= + EXPLORE_NEAR_RADIUS_METERS + ); + }) + .map((user) => user._id); + + if (!nearbyUserIds.length) { + return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); + } + + let posts = await PostModel.find({ + ...postFilter, + status: "accept", + user_id: { $in: nearbyUserIds }, + }) + .sort({ createdAt: -1 }) + .lean(); + + const users = nearbyUsers.filter((user) => + nearbyUserIds.some((id) => String(id) === String(user._id)) + ); + posts = await mapPostsWithUsers(posts, users, decodedToken); + + const totalItems = posts.length; + const startIndex = (parseInt(page, 10) - 1) * parseInt(limit, 10); + const paginatedPosts = posts.slice( + startIndex, + startIndex + parseInt(limit, 10) + ); + + return res.status(200).json({ + posts: paginatedPosts, + totalPages: Math.ceil(totalItems / limit) || 1, + totalItems, + }); + } + + if (isExploreFeed) { + let explorePosts = await PostModel.find({ ...postFilter, status: "accept" }) + .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 sub_expertise province city user_level show_location profile_image 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 = await mapPostsWithUsers(explorePosts, usersMap, decodedToken); + + const { posts: paginatedPosts, totalItems } = await paginatePersonalizedExplore({ + posts: explorePosts, + usersById, + page, + limit, + userId: decodedToken?.id, + seedPostId: reelsMode ? seedPostId : undefined, + reelsMode, + blendFresh: gridMode || (!reelsMode && !seedPostId), + }); + + return res.status(200).json({ + posts: paginatedPosts, + totalPages: Math.ceil(totalItems / limit) || 1, + totalItems + }); + } + + const userFilter = {}; + + applyExploreFilterToUserFilter(exploreFilter, userFilter); + if (expertise) userFilter.expertise = expertise; + if (subExpertise) userFilter.sub_expertise = subExpertise; + if (province) { + const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }); + if (provinceFind) userFilter["province.id"] = provinceFind.id; + } + if (city) { + const cityFind = await CityModel.findOne({ id: city }); + if (cityFind) userFilter["city.id"] = cityFind.id; + } + if (userLevel) { + const levelFilter = buildUserLevelMongoFilter(userLevel) + if (levelFilter) Object.assign(userFilter, levelFilter) + } + 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 sub_expertise province city user_level show_location profile_image") + .lean(); + + if (!users || users.length === 0) { + return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); + } + + const userIds = users.map(u => u._id); + postFilter.user_id = { $in: userIds }; + + let posts = await PostModel.find(postFilter) + .sort({ createdAt: -1 }) + .lean(); + + if (!posts || posts.length === 0) { + 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 + ? await LikeModel.find({ userId: decodedToken.id, postId: { $in: posts.map(p => p._id) } }).lean() + : []; + + const likesMap = {}; + likes.forEach(l => { likesMap[l.postId.toString()] = true; }); + + const comments = await CommentModel.aggregate([ + { $match: { post: { $in: posts.map(p => p._id) }, status: "accepted" } }, + { $group: { _id: "$post", count: { $sum: 1 } } } + ]); + const commentsMap = {}; + comments.forEach(c => { commentsMap[c._id.toString()] = c.count; }); + + posts = posts.map(post => { + const user = users.find(u => u._id.toString() === post.user_id.toString()); + return { + ...post, + user_name: user?.user_name || "", + first_name: user?.first_name || "", + last_name: user?.last_name || "", + expertise: user?.expertise || "", + sub_expertise: user?.sub_expertise || [], + province: user?.show_location ? user?.province || {} : {}, + city: user?.show_location ? user?.city || {} : {}, + show_location: user?.show_location, + user_level: user?.user_level || "", + profile_image: user?.profile_image || "", + likesCount: post.likes ? post.likes.length : 0, + is_liked: !!likesMap[post._id.toString()], + commentsCount: commentsMap[post._id.toString()] || 0, + }; + }); + + if (rateFilter) { + posts.sort((a, b) => { + if (rateFilter === 'کمترین') return (a.rate || 0) - (b.rate || 0); + if (rateFilter === 'بیشترین') return (b.rate || 0) - (a.rate || 0); + return 0; + }); + } + + const usersById = Object.fromEntries( + users.map((u) => [String(u._id), u]) + ); + + if (feedMode === "reels" && !_id && posts.length > 1) { + const { posts: paginatedPosts, totalItems } = await paginatePersonalizedExplore({ + posts, + usersById, + page, + limit, + userId: decodedToken?.id, + seedPostId, + reelsMode: true, + blendFresh: false, + }); + + return res.status(200).json({ + posts: paginatedPosts, + totalPages: Math.ceil(totalItems / limit) || 1, + totalItems, + }); + } + + if (feedMode === "reels" && _id && seedPostId && posts.length > 1) { + const seedIdx = posts.findIndex( + (p) => String(p._id) === String(seedPostId) + ); + if (seedIdx > 0) { + const seed = posts[seedIdx]; + const rest = posts.filter( + (p) => String(p._id) !== String(seedPostId) + ); + posts = [seed, ...rest]; + } + } + + const startIndex = (page - 1) * limit; + const endIndex = page * limit; + const totalItems = posts.length; + const paginatedPosts = posts.slice(startIndex, endIndex); + + return res.status(200).json({ + posts: paginatedPosts, + totalPages: Math.ceil(totalItems / limit), + totalItems, + }); + + } catch (error) { + console.error("Error in getPostsWeb:", error.message, error.stack); + return res.status(500).json({ message: "خطای سرور: " + error.message }); + } +}; + +const getSingleUser = async (req, res, next) => { + try { + const token = req.header("Authorization").split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const user_id = req.query.user_id; + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userReqId = decodedToken.id; + + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + } + const user = await UserModel.findById(user_id); + + if (!user) { + return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + } + const shamsiDate = moment(user.created_at).format("jYYYY-jMM-jDD"); + const lastOnline = moment(user.last_online).format("jYYYY-jMM-jDD"); + + // بررسی بلاک شدن + const blockedByCurrentUser = viewerBlockedUser(user, userReqId); + const blockedYou = userBlockedViewer(user, userReqId); + + if (blockedYou) { + return res.status(200).json({ user: blockedProfilePayload(user) }); + } + + // Allow chat + const offerExists = await OfferModel.exists({ + $or: [ + { sender: userReqId, receiver: user_id }, + { sender: user_id, receiver: userReqId }, + ], + }); + const response = { + is_self: user_id.toString() === userReqId.toString() ? "true" : "false", + _id: user._id, + last_online: lastOnline, + created_at: shamsiDate, + user_type: user.user_type, + user_level: user.user_level, + first_name: user.first_name, + last_name: user.last_name, + 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, + height: user.height, + weight: user.weight, + size: user.size, + eye_color: user.eye_color, + hair_color: user.hair_color, + profile_image: user.profile_image, + lat: user.show_location ? user.lat : null, + lng: user.show_location ? user.lng : null, + 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, + offer_status: !!offerExists, + monthly_free_offer: user.monthly_free_offer, + services: user.services, + expertise: user.expertise, + sub_expertise: user.sub_expertise, + }; + return res.status(200).json({ user: response }); + } catch (error) { + next(error); + } +}; +const getSingleUserWeb = async (req, res, next) => { + try { + let userReqId = null; + + // دریافت توکن و بررسی معتبر بودنش (اگر وجود داشت) + const authHeader = req.header("Authorization"); + if (authHeader) { + const token = authHeader.split(" ")[1]; + try { + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + userReqId = decodedToken.id; + } catch (err) { + console.warn("توکن نامعتبر است، ادامه بدون احراز هویت"); // فقط هشدار، نه ارور + } + } + + let user; + const { user_id, user_name } = req.query; + + if (user_id) { + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + } + user = await UserModel.findById(user_id); + } else if (user_name) { + user = await UserModel.findOne({ user_name }); + } else { + return res + .status(400) + .json({ message: "لطفاً user_id یا user_name ارسال کنید" }); + } + + if (!user) { + return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + } + + const shamsiDate = moment(user.created_at).format("jYYYY-jMM-jDD"); + const lastOnline = moment(user.last_online).format("jYYYY-jMM-jDD"); + + // بررسی بلاک شدن (فقط اگر توکن معتبر داشتیم) + let blockedByCurrentUser = false; + let blockedYou = false; + let offerExists = false; + + if (userReqId) { + blockedByCurrentUser = viewerBlockedUser(user, userReqId); + blockedYou = userBlockedViewer(user, userReqId); + offerExists = await OfferModel.exists({ + sender: userReqId, + receiver: user._id, + }); + } + + if (blockedYou && userReqId) { + return res.status(200).json({ user: blockedProfilePayload(user) }); + } + + // دریافت تعداد پست‌ها و پروژه‌ها + const postsCount = await PostModel.countDocuments({ + user_id: user._id, + status: { $in: ["accept"] }, + }); + const allProjectsCount = await ProjectModel.countDocuments({ + selected_user: user._id, + }); + const successfulProjectsCount = await ProjectModel.countDocuments({ + selected_user: user._id, + status: "done", + }); + + const response = { + is_self: userReqId + ? user._id.toString() === userReqId.toString() + : "false", + _id: user._id, + last_online: lastOnline, + created_at: shamsiDate, + user_type: user.user_type, + user_level: user.user_level, + first_name: user.first_name, + last_name: user.last_name, + 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, + height: user.height, + weight: user.weight, + size: user.size, + eye_color: user.eye_color, + hair_color: user.hair_color, + profile_image: user.profile_image, + lat: user.show_location ? user.lat : null, + lng: user.show_location ? user.lng : null, + 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, + offer_status: !!offerExists, + services: user.services, + expertise: user.expertise, + sub_expertise: user.sub_expertise, + postsCount, + allProjectsCount, + successfulProjectsCount, + }; + + return res.status(200).json({ user: response }); + } catch (error) { + next(error); + } +}; +const getUserContactInfo = async (req, res, next) => { + try { + const token = req.header("Authorization").split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const user_id = decodedToken.id; + + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + } + const user = await UserModel.findById(user_id); + + if (!user) { + return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + } + + const response = { + contactInfo: user?.contactInfo, + _id: user._id, + }; + return res.status(200).json({ data: response }); + } catch (error) { + next(error); + } +}; +const { + applyAutoBlockSuspension, + clearAutoBlockSuspension +} = require('../../../utils/blockSuspension') + +const blockUser = async (req, res) => { + try { + const { user_to_block } = req.body; + const token = req.header("Authorization").split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userReqId = decodedToken.id; + + 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(); + + res.status(200).json({ message: "کاربر با موفقیت بلاک شد" }); + } catch (error) { + console.error("Error blocking user:", error); + res.status(500).json({ error: "Server error" }); + } +}; + +const unblockUser = async (req, res) => { + try { + const { user_to_unblock } = req.body; + const token = req.header("Authorization").split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userReqId = decodedToken.id; + + const currentUser = await UserModel.findById(userReqId); + const userToUnblock = await UserModel.findById(user_to_unblock); + + if (!currentUser || !userToUnblock) { + return res.status(404).json({ message: "کاربر یافت نشد" }); + } + + currentUser.blocked_users.pull(userToUnblock._id); + userToUnblock.blocked_by.pull(currentUser._id); + + clearAutoBlockSuspension(userToUnblock); + + await currentUser.save(); + await userToUnblock.save(); + + res.status(200).json({ message: "کاربر با موفقیت آنبلاک شد" }); + } catch (error) { + console.error("Error unblocking user:", error); + res.status(500).json({ error: "Server error" }); + } +}; + +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; + + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + } + const user = await UserModel.findById(user_id); + const { page = 1, limit = 10 } = req.query; + if (!user) { + return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + } + + const filter = { user: user_id, status: "accepted" }; + if (post_id && mongoose.Types.ObjectId.isValid(post_id)) { + filter.post = post_id; + } + + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: { createdAt: -1 }, + populate: [ + { path: "creator", select: "user_name profile_image is_verified" }, + { path: "project", select: "project_name" }, + ], + }; + + const comments = await CommentModel.paginate(filter, options); + + 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); + } +}; +const createUserComment = 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 creatorId = decodedToken.id; + + const { user_id, comment, rate, post_id } = req.body; + if (!user_id || !comment) { + return res.status(400).send({ message: "All fields are required" }); + } + + const { ratingValue, isNewRating, requiresRating, alreadyRated } = + await resolveRatingForComment({ + creatorId, + targetUserId: user_id, + rate, + }); + + if (alreadyRated) { + return res.status(400).json({ + message: "شما قبلاً به این کاربر امتیاز داده‌اید.", + }); + } + + if (requiresRating) { + return res + .status(400) + .send({ message: "Rating must be between 1 and 5" }); + } + + const newComment = new CommentModel({ + user: user_id, + creator: creatorId, + rating: ratingValue, + comment, + comment_for: post_id ? "post" : "user", + post: post_id || undefined, + status: "pending", + }); + await newComment.save(); + + if (isNewRating) { + await recalculateUserRating(user_id); + } + + await createCommentNotification({ + ownerId: user_id, + commenterId: creatorId, + entityId: post_id || user_id, + type: post_id ? "post_comment" : "profile_comment", + }); + + res.status(201).json({ + message: "نظر با موفقیت ثبت شد و بعد از تایید، منتشر میشود.", + comment: newComment, + }); + } catch (error) { + next(error); + } +}; + +const inviteEmployer = async (req, res, next) => { + const token = req.header("Authorization").split(" ")[1]; + if (!token) return res.status(401).send("Access Denied"); + const user_id = req.query.user_id; + // eslint-disable-next-line no-unused-vars + const decodedToken = jwt.verify(token, process.env.APP_SECRET); + const userReqId = decodedToken.id; + + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + } + const user = await UserModel.findById(user_id); + const creatorUser = await UserModel.findById(userReqId); + + if (!user) { + return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + } + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const existingNotification = await NotificationModel.findOne({ + user_id: user._id, + project_post_id: userReqId, + type: "invite", + createdAt: { $gte: today }, + }); + + if (existingNotification) { + return res + .status(400) + .json({ + message: "شما امروز برای این کارفرما درخواست همکاری ارسال کرده اید.", + type: "today", + }); + } + const notification = new NotificationModel({ + user_id: user._id, + project_post_id: userReqId, + type: "invite", + title: "دعوت به همکاری", + description: `کاربر ${creatorUser.user_name} شما را برای همکاری دعوت کرده است.`, + }); + await notification.save(); + + res.status(201).json({ message: "درخواست همکاری با موفقیت ثبت شد" }); +}; + +const recordExploreInteraction = 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 { targetType, targetId, authorId, contentType, action, dwellMs } = req.body; + + if (!targetType || !targetId) { + return res.status(422).json({ message: "اطلاعات ناقص است" }); + } + + const normalizedAction = action || "view"; + + if (normalizedAction === "view") { + const recentView = await ExploreInteractionModel.findOne({ + viewerId: decodedToken.id, + targetType, + targetId, + action: "view", + createdAt: { $gte: new Date(Date.now() - 30 * 60 * 1000) }, + }) + .select("_id") + .lean(); + + if (recentView) { + return res.status(201).json({ message: "ok" }); + } + } + + await ExploreInteractionModel.create({ + viewerId: decodedToken.id, + targetType, + targetId, + authorId: authorId || null, + contentType: contentType || "image", + action: normalizedAction, + dwellMs: dwellMs ?? null, + }); + + return res.status(201).json({ message: "ok" }); + } catch (error) { + console.error("recordExploreInteraction:", error); + next(error); + } +}; + +module.exports = { + getUsers, + getSingleUser, + blockUser, + unblockUser, + getComments, + inviteEmployer, + getUserContactInfo, + getPostsWeb, + getSingleUserWeb, + createUserComment, + getAllLicenses, + getLicenseByUserId, + createLicense, + updateLicenseConfirmation, + recordExploreInteraction, +}; diff --git a/controllers/application/verify/addressController.js b/controllers/application/verify/addressController.js index b8217f9..c5e9a4d 100644 --- a/controllers/application/verify/addressController.js +++ b/controllers/application/verify/addressController.js @@ -1,136 +1,136 @@ -/* eslint-disable camelcase */ -const { ProvinceModel, CityModel } = require('../../../models/StateCity') -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setAddress = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - 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 === undefined || - lat === null || - lat === '' || - lng === undefined || - lng === null || - lng === '' || - show_location === undefined - ) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی ناقص است' - }) - } - - // یافتن کاربر با شماره موبایل - 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: 'کاربری با این شماره موبایل یافت نشد' - }) - } - - // یافتن استان و شهر - const province = await ProvinceModel.findOne({ id: province_id }) - const city = await CityModel.findOne({ id: city_id }) - if (!province || !city) { - return res.status(404).json({ - error: true, - message: 'استان یا شهر موردنظر یافت نشد' - }) - } - - user.province = province - user.city = city - user.address = address - user.lat = lat - user.lng = lng - user.show_location = - show_location === true || show_location === 'true' - - await user.save() - - return res.json({ - message: 'آدرس با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} -const updateAddress = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - 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 === undefined || - lat === null || - lat === '' || - lng === undefined || - lng === null || - lng === '' || - show_location === undefined - ) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی ناقص است' - }) - } - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - // یافتن کاربر با استفاده از شناسه کاربر - const user = await UserModel.findById(userId) - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شناسه یافت نشد' - }) - } - - // یافتن استان و شهر با استفاده از شناسه استان و شهر - const province = await ProvinceModel.findOne({ id: province_id }) - const city = await CityModel.findOne({ id: city_id }) - if (!province || !city) { - return res.status(404).json({ - error: true, - message: 'استان یا شهر مورد نظر یافت نشد' - }) - } - - // به‌روزرسانی اطلاعات آدرس کاربر - user.province = province - user.city = city - user.address = address - user.lat = lat - user.lng = lng - user.show_location = - show_location === true || show_location === 'true' - - await user.save() - - return res.json({ - message: 'آدرس با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - setAddress, updateAddress -} +/* eslint-disable camelcase */ +const { ProvinceModel, CityModel } = require('../../../models/StateCity') +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setAddress = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + 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 === undefined || + lat === null || + lat === '' || + lng === undefined || + lng === null || + lng === '' || + show_location === undefined + ) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی ناقص است' + }) + } + + // یافتن کاربر با شماره موبایل + 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: 'کاربری با این شماره موبایل یافت نشد' + }) + } + + // یافتن استان و شهر + const province = await ProvinceModel.findOne({ id: province_id }) + const city = await CityModel.findOne({ id: city_id }) + if (!province || !city) { + return res.status(404).json({ + error: true, + message: 'استان یا شهر موردنظر یافت نشد' + }) + } + + user.province = province + user.city = city + user.address = address + user.lat = lat + user.lng = lng + user.show_location = + show_location === true || show_location === 'true' + + await user.save() + + return res.json({ + message: 'آدرس با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} +const updateAddress = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + 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 === undefined || + lat === null || + lat === '' || + lng === undefined || + lng === null || + lng === '' || + show_location === undefined + ) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی ناقص است' + }) + } + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + // یافتن کاربر با استفاده از شناسه کاربر + const user = await UserModel.findById(userId) + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شناسه یافت نشد' + }) + } + + // یافتن استان و شهر با استفاده از شناسه استان و شهر + const province = await ProvinceModel.findOne({ id: province_id }) + const city = await CityModel.findOne({ id: city_id }) + if (!province || !city) { + return res.status(404).json({ + error: true, + message: 'استان یا شهر مورد نظر یافت نشد' + }) + } + + // به‌روزرسانی اطلاعات آدرس کاربر + user.province = province + user.city = city + user.address = address + user.lat = lat + user.lng = lng + user.show_location = + show_location === true || show_location === 'true' + + await user.save() + + return res.json({ + message: 'آدرس با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + setAddress, updateAddress +} diff --git a/controllers/application/verify/authController.js b/controllers/application/verify/authController.js index 31bbde6..e3756e6 100644 --- a/controllers/application/verify/authController.js +++ b/controllers/application/verify/authController.js @@ -1,108 +1,108 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const { check, validationResult } = require('express-validator') -const jwt = require('jsonwebtoken') - -const saveAuthValidationRules = () => { - return [ - check('shaba').notEmpty().withMessage('شماره شبا نمی‌تواند خالی باشد') - .matches(/^(?=.{24}$)[0-9]*$/).withMessage('فرمت شماره شبا صحیح نیست'), - check('birthday').notEmpty().withMessage('تاریخ تولد نمی‌تواند خالی باشد'), - check('national_code').notEmpty().withMessage('کد ملی نمی‌تواند خالی باشد') - .isLength({ min: 10, max: 10 }).withMessage('کد ملی باید 10 رقم باشد') - ] -} - -const saveAuth = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - const { shaba, birthday, national_code } = req.body - if (!shaba || !birthday || !national_code) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - // جستجوی کاربر با شماره موبایل ارسالی - 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: 'کاربری با این شماره موبایل یافت نشد' - }) - } - - // بررسی تکراری بودن کد ملی - const existingUserWithNationalCode = await UserModel.findOne({ national_code }) - if (existingUserWithNationalCode && existingUserWithNationalCode._id.toString() !== user._id.toString()) { - return res.status(409).json({ - error: true, - message: 'کد ملی تکراری است' - }) - } - user.shaba = shaba - user.birthday = birthday - user.national_code = national_code - await user.save() - - return res.json({ - message: 'اطلاعات با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} -const updateShaba = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }) - } - - const { shaba } = req.body - - if (!shaba) { - return res.status(422).json({ - error: true, - message: 'شبا ارسال نشده است' - }) - } - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - // جستجوی کاربر با استفاده از شناسه کاربر - const user = await UserModel.findById(userId) - - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شناسه یافت نشد' - }) - } - - // فقط شبا را به‌روزرسانی کنید - user.shaba = shaba - await user.save() - - return res.json({ - message: 'شبا با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - saveAuth, saveAuthValidationRules, updateShaba -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const { check, validationResult } = require('express-validator') +const jwt = require('jsonwebtoken') + +const saveAuthValidationRules = () => { + return [ + check('shaba').notEmpty().withMessage('شماره شبا نمی‌تواند خالی باشد') + .matches(/^(?=.{24}$)[0-9]*$/).withMessage('فرمت شماره شبا صحیح نیست'), + check('birthday').notEmpty().withMessage('تاریخ تولد نمی‌تواند خالی باشد'), + check('national_code').notEmpty().withMessage('کد ملی نمی‌تواند خالی باشد') + .isLength({ min: 10, max: 10 }).withMessage('کد ملی باید 10 رقم باشد') + ] +} + +const saveAuth = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + const { shaba, birthday, national_code } = req.body + if (!shaba || !birthday || !national_code) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + // جستجوی کاربر با شماره موبایل ارسالی + 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: 'کاربری با این شماره موبایل یافت نشد' + }) + } + + // بررسی تکراری بودن کد ملی + const existingUserWithNationalCode = await UserModel.findOne({ national_code }) + if (existingUserWithNationalCode && existingUserWithNationalCode._id.toString() !== user._id.toString()) { + return res.status(409).json({ + error: true, + message: 'کد ملی تکراری است' + }) + } + user.shaba = shaba + user.birthday = birthday + user.national_code = national_code + await user.save() + + return res.json({ + message: 'اطلاعات با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} +const updateShaba = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }) + } + + const { shaba } = req.body + + if (!shaba) { + return res.status(422).json({ + error: true, + message: 'شبا ارسال نشده است' + }) + } + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + // جستجوی کاربر با استفاده از شناسه کاربر + const user = await UserModel.findById(userId) + + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شناسه یافت نشد' + }) + } + + // فقط شبا را به‌روزرسانی کنید + user.shaba = shaba + await user.save() + + return res.json({ + message: 'شبا با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + saveAuth, saveAuthValidationRules, updateShaba +} diff --git a/controllers/application/verify/colorsController.js b/controllers/application/verify/colorsController.js index a6be8e6..6b1f040 100644 --- a/controllers/application/verify/colorsController.js +++ b/controllers/application/verify/colorsController.js @@ -1,80 +1,80 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setColors = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { eye_color, hair_color } = req.body - if (!eye_color || !hair_color) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - // جستجوی کاربر با شماره موبایل ارسالی - 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.eye_color = eye_color - user.hair_color = hair_color - await user.save() - - return res.json({ - message: 'رنگ ها با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} -const updateColors = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { eye_color, hair_color } = req.body - - // اعتبارسنجی داده‌ها - if (!eye_color || !hair_color) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی ناقص است' - }) - } - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - // یافتن کاربر با استفاده از شناسه کاربر - const user = await UserModel.findById(userId) - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شناسه یافت نشد' - }) - } - - // به‌روزرسانی اطلاعات رنگ‌ها - user.eye_color = eye_color - user.hair_color = hair_color - await user.save() - - return res.json({ - message: 'رنگ‌ها با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - setColors, updateColors -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setColors = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { eye_color, hair_color } = req.body + if (!eye_color || !hair_color) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + // جستجوی کاربر با شماره موبایل ارسالی + 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.eye_color = eye_color + user.hair_color = hair_color + await user.save() + + return res.json({ + message: 'رنگ ها با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} +const updateColors = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { eye_color, hair_color } = req.body + + // اعتبارسنجی داده‌ها + if (!eye_color || !hair_color) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی ناقص است' + }) + } + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + // یافتن کاربر با استفاده از شناسه کاربر + const user = await UserModel.findById(userId) + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شناسه یافت نشد' + }) + } + + // به‌روزرسانی اطلاعات رنگ‌ها + user.eye_color = eye_color + user.hair_color = hair_color + await user.save() + + return res.json({ + message: 'رنگ‌ها با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + setColors, updateColors +} diff --git a/controllers/application/verify/completeRegistrationController.js b/controllers/application/verify/completeRegistrationController.js index 5555ef5..9a30ba5 100644 --- a/controllers/application/verify/completeRegistrationController.js +++ b/controllers/application/verify/completeRegistrationController.js @@ -1,39 +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 -} +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/conversationController.js b/controllers/application/verify/conversationController.js index 0db287a..95bc845 100644 --- a/controllers/application/verify/conversationController.js +++ b/controllers/application/verify/conversationController.js @@ -1,74 +1,74 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setConversation = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { bio, conversation_projects } = req.body - if (!bio || conversation_projects === undefined) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - // جستجوی کاربر با شماره موبایل ارسالی - 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.bio = bio - user.conversation_projects = conversation_projects - await user.save() - - return res.json({ - message: 'روابط عمومی با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} -const updateConversation = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { bio, conversation_projects } = req.body - - if (!bio || conversation_projects === undefined) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - // جستجوی کاربر با استفاده از شناسه کاربر - const user = await UserModel.findById(userId) - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شناسه یافت نشد' - }) - } - user.bio = bio - user.conversation_projects = conversation_projects - await user.save() - - return res.json({ - message: 'روابط عمومی با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} -module.exports = { - setConversation, updateConversation -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setConversation = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { bio, conversation_projects } = req.body + if (!bio || conversation_projects === undefined) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + // جستجوی کاربر با شماره موبایل ارسالی + 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.bio = bio + user.conversation_projects = conversation_projects + await user.save() + + return res.json({ + message: 'روابط عمومی با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} +const updateConversation = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { bio, conversation_projects } = req.body + + if (!bio || conversation_projects === undefined) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + // جستجوی کاربر با استفاده از شناسه کاربر + const user = await UserModel.findById(userId) + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شناسه یافت نشد' + }) + } + user.bio = bio + user.conversation_projects = conversation_projects + await user.save() + + return res.json({ + message: 'روابط عمومی با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} +module.exports = { + setConversation, updateConversation +} diff --git a/controllers/application/verify/cooperationTypeController.js b/controllers/application/verify/cooperationTypeController.js index 87351d3..2debfbe 100644 --- a/controllers/application/verify/cooperationTypeController.js +++ b/controllers/application/verify/cooperationTypeController.js @@ -1,93 +1,93 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setCooperationType = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { cooperation_type, cooperation_abroad } = req.body - if (!cooperation_type || cooperation_abroad === undefined) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - if (!['all', 'verified'].includes(cooperation_type)) { - return res.status(422).json({ - error: true, - message: 'نوع همکاری نیست' - }) - } - - // جستجوی کاربر با شماره موبایل ارسالی - 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.cooperation_type = cooperation_type - user.cooperation_abroad = cooperation_abroad - await user.save() - - return res.json({ - message: 'نوع همکاری با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} -const updateCooperationType = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { cooperation_type, cooperation_abroad } = req.body - - if (!cooperation_type || cooperation_abroad === undefined) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - if (!['all', 'verified'].includes(cooperation_type)) { - return res.status(422).json({ - error: true, - message: 'نوع همکاری نامعتبر است' - }) - } - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - // جستجوی کاربر با استفاده از شناسه کاربر - const user = await UserModel.findById(userId) - - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شناسه یافت نشد' - }) - } - - user.cooperation_type = cooperation_type - user.cooperation_abroad = cooperation_abroad - await user.save() - - return res.json({ - message: 'نوع همکاری با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - setCooperationType, - updateCooperationType -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setCooperationType = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { cooperation_type, cooperation_abroad } = req.body + if (!cooperation_type || cooperation_abroad === undefined) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + if (!['all', 'verified'].includes(cooperation_type)) { + return res.status(422).json({ + error: true, + message: 'نوع همکاری نیست' + }) + } + + // جستجوی کاربر با شماره موبایل ارسالی + 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.cooperation_type = cooperation_type + user.cooperation_abroad = cooperation_abroad + await user.save() + + return res.json({ + message: 'نوع همکاری با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} +const updateCooperationType = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { cooperation_type, cooperation_abroad } = req.body + + if (!cooperation_type || cooperation_abroad === undefined) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + if (!['all', 'verified'].includes(cooperation_type)) { + return res.status(422).json({ + error: true, + message: 'نوع همکاری نامعتبر است' + }) + } + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + // جستجوی کاربر با استفاده از شناسه کاربر + const user = await UserModel.findById(userId) + + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شناسه یافت نشد' + }) + } + + user.cooperation_type = cooperation_type + user.cooperation_abroad = cooperation_abroad + await user.save() + + return res.json({ + message: 'نوع همکاری با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + setCooperationType, + updateCooperationType +} diff --git a/controllers/application/verify/expertiseController.js b/controllers/application/verify/expertiseController.js index 5f1f5d8..3c88796 100644 --- a/controllers/application/verify/expertiseController.js +++ b/controllers/application/verify/expertiseController.js @@ -1,81 +1,81 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setExpertise = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { expertise, sub_expertise } = req.body - if (!expertise || !sub_expertise) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - // جستجوی کاربر با شماره موبایل ارسالی - 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: 'کاربری با این شماره موبایل یافت نشد' - }) - } - - // آپدیت نام کاربری به صورت lowercase - user.expertise = expertise - user.sub_expertise = sub_expertise - await user.save() - - return res.json({ - message: 'تخصص با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -const updateExpertise = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { expertise, sub_expertise } = req.body - - // اعتبارسنجی داده‌ها - if (!expertise || !sub_expertise) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی ناقص است' - }) - } - - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - // یافتن کاربر با استفاده از شناسه کاربر - const user = await UserModel.findById(userId) - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شناسه یافت نشد' - }) - } - - // به‌روزرسانی اطلاعات تخصص و زیرتخصص - user.expertise = expertise - user.sub_expertise = sub_expertise - await user.save() - - return res.json({ - message: 'تخصص با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} -module.exports = { - setExpertise, updateExpertise -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setExpertise = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { expertise, sub_expertise } = req.body + if (!expertise || !sub_expertise) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + // جستجوی کاربر با شماره موبایل ارسالی + 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: 'کاربری با این شماره موبایل یافت نشد' + }) + } + + // آپدیت نام کاربری به صورت lowercase + user.expertise = expertise + user.sub_expertise = sub_expertise + await user.save() + + return res.json({ + message: 'تخصص با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +const updateExpertise = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { expertise, sub_expertise } = req.body + + // اعتبارسنجی داده‌ها + if (!expertise || !sub_expertise) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی ناقص است' + }) + } + + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + // یافتن کاربر با استفاده از شناسه کاربر + const user = await UserModel.findById(userId) + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شناسه یافت نشد' + }) + } + + // به‌روزرسانی اطلاعات تخصص و زیرتخصص + user.expertise = expertise + user.sub_expertise = sub_expertise + await user.save() + + return res.json({ + message: 'تخصص با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} +module.exports = { + setExpertise, updateExpertise +} diff --git a/controllers/application/verify/genderController.js b/controllers/application/verify/genderController.js index 50a9e3e..2371eca 100644 --- a/controllers/application/verify/genderController.js +++ b/controllers/application/verify/genderController.js @@ -1,46 +1,46 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setGender = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { gender } = req.body - if (!gender) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ورودی اشتباه است' - }) - } - if (!['male', 'female'].includes(gender)) { - return res.status(422).json({ - error: true, - message: 'جنسیت معتبر نیست' - }) - } - // جستجوی کاربر با شماره موبایل ارسالی - 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.gender = gender.toLowerCase() - await user.save() - - return res.json({ - message: 'جنسیت با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - setGender -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setGender = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { gender } = req.body + if (!gender) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ورودی اشتباه است' + }) + } + if (!['male', 'female'].includes(gender)) { + return res.status(422).json({ + error: true, + message: 'جنسیت معتبر نیست' + }) + } + // جستجوی کاربر با شماره موبایل ارسالی + 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.gender = gender.toLowerCase() + await user.save() + + return res.json({ + message: 'جنسیت با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + setGender +} diff --git a/controllers/application/verify/servicesController.js b/controllers/application/verify/servicesController.js index afae1a7..6ce2826 100644 --- a/controllers/application/verify/servicesController.js +++ b/controllers/application/verify/servicesController.js @@ -1,107 +1,107 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') -const fs = require('fs-extra') -const path = require('path') - -const setServices = 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(422).json({ - error: true, - message: 'شما دسترسی به این بخش ندارید' - }) - } - const { services } = req.body - if (!services) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - // مدیریت تصاویر برای خدمات - const servicesWithImages = [] - const parsedServices = JSON.parse(services) // پارس کردن رشته JSON به آرایه جاوا اسکریپت - if (parsedServices && parsedServices.length > 0) { - for (const service of parsedServices) { - const serviceImages = [] - const serviceId = service.id - if (req.files && req.files.serviceImages && req.files.serviceImages[serviceId]) { - const imageFiles = Array.isArray(req.files.serviceImages[serviceId]) ? req.files.serviceImages[serviceId] : [req.files.serviceImages[serviceId]] - const uploadDir = path.join(__dirname, '../../../../storage/services') - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - for (const imageFile of imageFiles) { - const uniqueFileName = `${user.user_name}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(imageFile.name)}` - const filePath = path.join(uploadDir, uniqueFileName) - // await fs.promises.rename(imageFile.path, filePath) - await fs.move(imageFile.path, filePath) - const imageUrl = `/services/${uniqueFileName}` - serviceImages.push(imageUrl) - // به‌روزرسانی مسیر تصویر در آبجکت service - service.image = imageUrl - } - } else { - console.log('sss') - } - servicesWithImages.push({ ...service, images: serviceImages }) - } - } - - user.services = servicesWithImages - await user.save() - - return res.json({ - message: 'خدمات با موفقیت ثبت شد' - }) - } catch (error) { - next(error) - } -} - -const updateServices = async (req, res, next) => { - try { - const { height, weight, size } = req.body - - if (!height || !weight || !size) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - // جستجوی کاربر - const user = await UserModel.findById(userId) - - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربر یافت نشد' - }) - } - - user.height = height - user.weight = weight - user.size = size - await user.save() - - return res.json({ - message: 'سایز با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} -module.exports = { - setServices, updateServices -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') +const fs = require('fs-extra') +const path = require('path') + +const setServices = 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(422).json({ + error: true, + message: 'شما دسترسی به این بخش ندارید' + }) + } + const { services } = req.body + if (!services) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + // مدیریت تصاویر برای خدمات + const servicesWithImages = [] + const parsedServices = JSON.parse(services) // پارس کردن رشته JSON به آرایه جاوا اسکریپت + if (parsedServices && parsedServices.length > 0) { + for (const service of parsedServices) { + const serviceImages = [] + const serviceId = service.id + if (req.files && req.files.serviceImages && req.files.serviceImages[serviceId]) { + const imageFiles = Array.isArray(req.files.serviceImages[serviceId]) ? req.files.serviceImages[serviceId] : [req.files.serviceImages[serviceId]] + const uploadDir = path.join(__dirname, '../../../../storage/services') + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + for (const imageFile of imageFiles) { + const uniqueFileName = `${user.user_name}-${Date.now()}${Math.random().toString().slice(2, 11)}${path.extname(imageFile.name)}` + const filePath = path.join(uploadDir, uniqueFileName) + // await fs.promises.rename(imageFile.path, filePath) + await fs.move(imageFile.path, filePath) + const imageUrl = `/services/${uniqueFileName}` + serviceImages.push(imageUrl) + // به‌روزرسانی مسیر تصویر در آبجکت service + service.image = imageUrl + } + } else { + console.log('sss') + } + servicesWithImages.push({ ...service, images: serviceImages }) + } + } + + user.services = servicesWithImages + await user.save() + + return res.json({ + message: 'خدمات با موفقیت ثبت شد' + }) + } catch (error) { + next(error) + } +} + +const updateServices = async (req, res, next) => { + try { + const { height, weight, size } = req.body + + if (!height || !weight || !size) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + // جستجوی کاربر + const user = await UserModel.findById(userId) + + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربر یافت نشد' + }) + } + + user.height = height + user.weight = weight + user.size = size + await user.save() + + return res.json({ + message: 'سایز با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} +module.exports = { + setServices, updateServices +} diff --git a/controllers/application/verify/setNationalCardImageController.js b/controllers/application/verify/setNationalCardImageController.js index 3547e14..d054ae5 100644 --- a/controllers/application/verify/setNationalCardImageController.js +++ b/controllers/application/verify/setNationalCardImageController.js @@ -1,56 +1,56 @@ -/* eslint-disable camelcase */ -const fs = require('fs-extra') -const path = require('path') -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setNationalCardImage = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { national_card_image } = req.files - // جستجوی کاربر با شماره موبایل ارسالی - 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: 'کاربری با این شماره موبایل یافت نشد' - }) - } - // بررسی آیا فایل آپلود شده است - if (!national_card_image) { - return res.status(400).json({ - error: true, - message: 'لطفاً عکس کارت ملی را انتخاب کنید' - }) - } - - // ذخیره فایل عکس پروفایل - const uploadDir = path.join(__dirname, '../../../../storage/carts') - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - - const uniqueFileName = `${user.user_name}-${Date.now()}${path.extname(national_card_image.name)}` - const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(national_card_image.path, filePath) - - // ذخیره مسیر فایل در دیتابیس - user.national_card_image = `/carts/${uniqueFileName}` - user.is_Register = true - user.is_verified = 'pending' - await user.save() - - return res.json({ - message: 'عکس کارت ملی با موفقیت ذخیره شد', - national_card_image: user.national_card_image - }) - } catch (error) { - next(error) - } -} - -module.exports = { - setNationalCardImage -} +/* eslint-disable camelcase */ +const fs = require('fs-extra') +const path = require('path') +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setNationalCardImage = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { national_card_image } = req.files + // جستجوی کاربر با شماره موبایل ارسالی + 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: 'کاربری با این شماره موبایل یافت نشد' + }) + } + // بررسی آیا فایل آپلود شده است + if (!national_card_image) { + return res.status(400).json({ + error: true, + message: 'لطفاً عکس کارت ملی را انتخاب کنید' + }) + } + + // ذخیره فایل عکس پروفایل + const uploadDir = path.join(__dirname, '../../../../storage/carts') + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + + const uniqueFileName = `${user.user_name}-${Date.now()}${path.extname(national_card_image.name)}` + const filePath = path.join(uploadDir, uniqueFileName) + await fs.move(national_card_image.path, filePath) + + // ذخیره مسیر فایل در دیتابیس + user.national_card_image = `/carts/${uniqueFileName}` + user.is_Register = true + user.is_verified = 'pending' + await user.save() + + return res.json({ + message: 'عکس کارت ملی با موفقیت ذخیره شد', + national_card_image: user.national_card_image + }) + } catch (error) { + next(error) + } +} + +module.exports = { + setNationalCardImage +} diff --git a/controllers/application/verify/setProfileImageController.js b/controllers/application/verify/setProfileImageController.js index c32e9a9..20b1077 100644 --- a/controllers/application/verify/setProfileImageController.js +++ b/controllers/application/verify/setProfileImageController.js @@ -1,105 +1,105 @@ -/* eslint-disable camelcase */ -const fs = require('fs-extra') -const path = require('path') -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setProfileImage = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { mobile } = req.body - const { profile_image } = req.files - if (!mobile) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - 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: 'کاربری با این شماره موبایل یافت نشد' - }) - } - // بررسی آیا فایل آپلود شده است - if (!profile_image) { - return res.status(400).json({ - error: true, - message: 'لطفاً عکس پروفایل را انتخاب کنید' - }) - } - - // ذخیره فایل عکس پروفایل - const uploadDir = path.join(__dirname, '../../../storage/profiles') - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - const uniqueFileName = `${user.user_name}-${Date.now()}${path.extname(profile_image.name)}` - const filePath = path.join(uploadDir, uniqueFileName) - - await fs.move(profile_image.path, filePath) - // ذخیره مسیر فایل در دیتابیس - user.profile_image = `/profiles/${uniqueFileName}` - await user.save() - - return res.json({ - message: 'عکس پروفایل با موفقیت ذخیره شد', - profile_image: user.profile_image - }) - } catch (error) { - next(error) - } -} - -const updateProfileImage = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const { profile_image } = req.files - - if (!profile_image) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const user = await UserModel.findById(userId) - - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این شماره موبایل یافت نشد' - }) - } - - const uploadDir = path.join(__dirname, '../../../storage/profiles') - - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - - const uniqueFileName = `${user.user_name}-${Date.now()}${path.extname(profile_image.name)}` - const filePath = path.join(uploadDir, uniqueFileName) - - await fs.move(profile_image.path, filePath) - - user.profile_image = `/profiles/${uniqueFileName}` - await user.save() - - return res.json({ - message: 'عکس پروفایل با موفقیت به‌روزرسانی شد', - profile_image: user.profile_image - }) - } catch (error) { - next(error) - } -} -module.exports = { - setProfileImage, updateProfileImage -} +/* eslint-disable camelcase */ +const fs = require('fs-extra') +const path = require('path') +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setProfileImage = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { mobile } = req.body + const { profile_image } = req.files + if (!mobile) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + 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: 'کاربری با این شماره موبایل یافت نشد' + }) + } + // بررسی آیا فایل آپلود شده است + if (!profile_image) { + return res.status(400).json({ + error: true, + message: 'لطفاً عکس پروفایل را انتخاب کنید' + }) + } + + // ذخیره فایل عکس پروفایل + const uploadDir = path.join(__dirname, '../../../storage/profiles') + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + const uniqueFileName = `${user.user_name}-${Date.now()}${path.extname(profile_image.name)}` + const filePath = path.join(uploadDir, uniqueFileName) + + await fs.move(profile_image.path, filePath) + // ذخیره مسیر فایل در دیتابیس + user.profile_image = `/profiles/${uniqueFileName}` + await user.save() + + return res.json({ + message: 'عکس پروفایل با موفقیت ذخیره شد', + profile_image: user.profile_image + }) + } catch (error) { + next(error) + } +} + +const updateProfileImage = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const { profile_image } = req.files + + if (!profile_image) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const user = await UserModel.findById(userId) + + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این شماره موبایل یافت نشد' + }) + } + + const uploadDir = path.join(__dirname, '../../../storage/profiles') + + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + + const uniqueFileName = `${user.user_name}-${Date.now()}${path.extname(profile_image.name)}` + const filePath = path.join(uploadDir, uniqueFileName) + + await fs.move(profile_image.path, filePath) + + user.profile_image = `/profiles/${uniqueFileName}` + await user.save() + + return res.json({ + message: 'عکس پروفایل با موفقیت به‌روزرسانی شد', + profile_image: user.profile_image + }) + } catch (error) { + next(error) + } +} +module.exports = { + setProfileImage, updateProfileImage +} diff --git a/controllers/application/verify/sizesController.js b/controllers/application/verify/sizesController.js index d056251..3516069 100644 --- a/controllers/application/verify/sizesController.js +++ b/controllers/application/verify/sizesController.js @@ -1,78 +1,78 @@ -/* eslint-disable camelcase */ -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') - -const setSizes = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { height, weight, size } = req.body - if (!height || !weight || !size) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - // جستجوی کاربر با شماره موبایل ارسالی - 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.height = height - user.weight = weight - user.size = size - await user.save() - - return res.json({ - message: 'سایز با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} - -const updateSizes = async (req, res, next) => { - try { - const { height, weight, size } = req.body - - if (!height || !weight || !size) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - // جستجوی کاربر - const user = await UserModel.findById(userId) - - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربر یافت نشد' - }) - } - - user.height = height - user.weight = weight - user.size = size - await user.save() - - return res.json({ - message: 'سایز با موفقیت به‌روزرسانی شد' - }) - } catch (error) { - next(error) - } -} -module.exports = { - setSizes, updateSizes -} +/* eslint-disable camelcase */ +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') + +const setSizes = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { height, weight, size } = req.body + if (!height || !weight || !size) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + // جستجوی کاربر با شماره موبایل ارسالی + 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.height = height + user.weight = weight + user.size = size + await user.save() + + return res.json({ + message: 'سایز با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} + +const updateSizes = async (req, res, next) => { + try { + const { height, weight, size } = req.body + + if (!height || !weight || !size) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + // جستجوی کاربر + const user = await UserModel.findById(userId) + + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربر یافت نشد' + }) + } + + user.height = height + user.weight = weight + user.size = size + await user.save() + + return res.json({ + message: 'سایز با موفقیت به‌روزرسانی شد' + }) + } catch (error) { + next(error) + } +} +module.exports = { + setSizes, updateSizes +} diff --git a/controllers/application/version/versionController.js b/controllers/application/version/versionController.js index 62eaf2d..6b557f3 100644 --- a/controllers/application/version/versionController.js +++ b/controllers/application/version/versionController.js @@ -1,16 +1,16 @@ -const VersionModel = require('../../../models/VersionModel') - -// Endpoint برای دریافت نسخه فعلی -const getVersion = async (req, res, next) => { - try { - const currentVersion = await VersionModel.findOne().sort({ _id: -1 }).exec() - if (!currentVersion) { - return res.status(200).json({ message: 'نسخه‌ای پیدا نشد' }) - } - res.json(currentVersion) - } catch (error) { - res.status(500).json({ message: 'خطای سرور', error }) - } -} - -module.exports = { getVersion } +const VersionModel = require('../../../models/VersionModel') + +// Endpoint برای دریافت نسخه فعلی +const getVersion = async (req, res, next) => { + try { + const currentVersion = await VersionModel.findOne().sort({ _id: -1 }).exec() + if (!currentVersion) { + return res.status(200).json({ message: 'نسخه‌ای پیدا نشد' }) + } + res.json(currentVersion) + } catch (error) { + res.status(500).json({ message: 'خطای سرور', error }) + } +} + +module.exports = { getVersion } diff --git a/controllers/application/workroom/workroomController.js b/controllers/application/workroom/workroomController.js index df4926d..3d53d32 100644 --- a/controllers/application/workroom/workroomController.js +++ b/controllers/application/workroom/workroomController.js @@ -1,222 +1,222 @@ -/* eslint-disable camelcase */ -const ProjectModel = require('../../../models/ProjectModel') -const RequestModel = require('../../../models/RequestModel') -const UserModel = require('../../../models/UserModel') -const jwt = require('jsonwebtoken') -const calculateRemainingTime = (milliseconds) => { - const days = Math.floor(milliseconds / (1000 * 60 * 60 * 24)) - const hours = Math.floor((milliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) - - return `${days} روز و ${hours} ساعت` -} -const getProjects = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const { status_filter, page = 1, limit = 10 } = req.query - - const participantFilter = { - $or: [ - { creator_id: userId }, - { selected_user: userId }, - { requested_users: userId }, - { created_for_user: userId } - ] - } - - let filter = participantFilter - if (status_filter) { - if (status_filter === 'دریافتی') { - filter = { created_for_user: userId, status: 'accepted' } - } else if (status_filter === 'ارسال شده') { - filter = { requested_users: userId, status: 'accepted' } - } else if (status_filter === 'منتشر شده') { - filter = { creator_id: userId, status: 'accepted' } - } else if (status_filter === 'در دست اقدام') { - filter = { - $or: [ - { creator_id: userId, status: 'ongoing' }, - { selected_user: userId, status: 'ongoing' } - ] - } - } else if (status_filter === 'اتمام پروژه') { - filter = { - $or: [ - { creator_id: userId, status: 'done' }, - { selected_user: userId, status: 'done' } - ] - } - } else if (status_filter === 'کنسل شده') { - filter = { - $or: [ - { creator_id: userId, status: 'cancled' }, - { selected_user: userId, status: 'cancled' } - ] - } - } else if (status_filter === 'در دست بررسی') { - filter = { creator_id: userId, status: { $in: ['paid', 'rejected'] } } - } else if (status_filter === 'پرداخت نشده') { - filter = { creator_id: userId, status: 'pre_payment' } - } - } - // paid - // دریافت لیست پروژه‌ها با استفاده از فیلتر - - // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده - const options = { - page: parseInt(page), // تبدیل صفحه به عدد صحیح - limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح - sort: { createdAt: -1 } - } - const projects = await ProjectModel.paginate(filter, options) - const projectsWithUserInfo = await Promise.all(projects?.docs.map(async project => { - const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') - const projectWithUserInfo = { - ...project._doc, - creator, - status_filter - } - const currentTime = new Date() - const projectCreatedAt = new Date(project.createdAt) - const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) - const remainingTime = calculateRemainingTime(timeDiff) - projectWithUserInfo.remainingTime = remainingTime - return projectWithUserInfo - })) - res.status(200).json({ - projects: projectsWithUserInfo, - totalPages: projects.totalPages, // ارسال تعداد کل صفحات - totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - next(error) - } -} -const getSingleProject = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - const projectId = req.query.projectId - const projectRequests = await RequestModel.find({ project: projectId }) - .populate('user', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image') - .sort({ status: 1 }) - .lean() - const modifiedRequests = projectRequests.map(request => ({ - ...request, - time: request?.user?._id.toString() === userId ? request.time : null, - price: request?.user?._id.toString() === userId ? request.price : null - })) - const project = await ProjectModel.findById(projectId) - if (!project) { - return res.status(404).json({ message: 'پروژه مورد نظر یافت نشد' }) - } - // محاسبه مبلغ باقیمانده و مجموع مبلغ پرداخت شده - // تابع محاسبه مبلغ پرداخت شده - const getTotalPaidAmount = (installments) => { - let totalPaidAmount = 0 - installments.forEach(installment => { - totalPaidAmount += installment.amount - }) - return totalPaidAmount - } - - // تابع محاسبه مبلغ باقیمانده - const getRemainingAmount = (project) => { - const totalPaidAmount = getTotalPaidAmount(project.installments) - const remainingAmount = project.final_price - totalPaidAmount - return remainingAmount - } - - const totalPaidAmount = getTotalPaidAmount(project.installments) - const remainingAmount = getRemainingAmount(project) - // اطلاعات کاربر سازنده را نیز دریافت کنید و به آبجکت پروژه اضافه کنید - const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') - - const projectWithUserInfo = { - ...project._doc, - creator // اضافه کردن اطلاعات کاربر به آبجکت پروژه - } - const selectedUser = await UserModel.findById(project.selected_user).select('_id user_name first_name last_name user_type is_verified profile_image expertise sub_expertise province city rate user_level user_score') - projectWithUserInfo.selected_user = selectedUser - // محاسبه مدت زمان باقی‌مانده برای پروژه - const currentTime = new Date() - const projectCreatedAt = new Date(project.createdAt) - const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) - const remainingTime = calculateRemainingTime(timeDiff) - projectWithUserInfo.remainingTime = remainingTime - let projectDetails = projectWithUserInfo - if ((project.selected_user && project.selected_user.toString()) === userId || project.creator_id.toString() === userId) { - projectDetails = { - ...projectWithUserInfo, - total_paid_amount: totalPaidAmount, - remaining_amount: remainingAmount - } - } else { - projectDetails = - projectWithUserInfo - } - // } - res.status(200).json({ project: projectDetails, projectRequests: modifiedRequests }) - } catch (error) { - next(error) - } -} - -const getProjectRequests = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - // بررسی اینکه آیا کاربر ایجاد کننده پروژه است یا خیر - const projectId = req.params.projectId - const project = await ProjectModel.findOne({ _id: projectId, creator_id: userId }) - if (!project) { - return res.status(403).send({ message: 'Access denied: You are not the creator of this project.' }) - } - - // تابع محاسبه مبلغ پرداخت شده - const getTotalPaidAmount = (installments) => { - let totalPaidAmount = 0 - installments.forEach(installment => { - totalPaidAmount += installment.amount - }) - return totalPaidAmount - } - - // تابع محاسبه مبلغ باقیمانده - const getRemainingAmount = (project) => { - const totalPaidAmount = getTotalPaidAmount(project.installments) - const remainingAmount = project.final_price - totalPaidAmount - return remainingAmount - } - - // محاسبه مبلغ باقیمانده - const remainingAmount = getRemainingAmount(project) - - // یافتن جزییات پروژه - const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') - const selectedUser = await UserModel.findById(project.selected_user).select('_id user_name first_name last_name user_type is_verified profile_image expertise sub_expertise province city rate user_level user_score') - const totalPaidAmount = getTotalPaidAmount(project.installments) - - const projectRequests = await RequestModel.find({ project: projectId }).populate('user', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image').sort({ status: 1 }) - const projectinfo = await ProjectModel.findById(projectId) // جزییات درخواست‌های کاربران - projectinfo.selected_user = selectedUser - const projectDetails = { - ...projectinfo._doc, - creator, - total_paid_amount: totalPaidAmount, - remaining_amount: remainingAmount - } - res.status(200).json({ projectRequests, projectDetails }) - } catch (error) { - next(error) - } -} -module.exports = { - getProjects, getSingleProject, getProjectRequests -} +/* eslint-disable camelcase */ +const ProjectModel = require('../../../models/ProjectModel') +const RequestModel = require('../../../models/RequestModel') +const UserModel = require('../../../models/UserModel') +const jwt = require('jsonwebtoken') +const calculateRemainingTime = (milliseconds) => { + const days = Math.floor(milliseconds / (1000 * 60 * 60 * 24)) + const hours = Math.floor((milliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + + return `${days} روز و ${hours} ساعت` +} +const getProjects = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const { status_filter, page = 1, limit = 10 } = req.query + + const participantFilter = { + $or: [ + { creator_id: userId }, + { selected_user: userId }, + { requested_users: userId }, + { created_for_user: userId } + ] + } + + let filter = participantFilter + if (status_filter) { + if (status_filter === 'دریافتی') { + filter = { created_for_user: userId, status: 'accepted' } + } else if (status_filter === 'ارسال شده') { + filter = { requested_users: userId, status: 'accepted' } + } else if (status_filter === 'منتشر شده') { + filter = { creator_id: userId, status: 'accepted' } + } else if (status_filter === 'در دست اقدام') { + filter = { + $or: [ + { creator_id: userId, status: 'ongoing' }, + { selected_user: userId, status: 'ongoing' } + ] + } + } else if (status_filter === 'اتمام پروژه') { + filter = { + $or: [ + { creator_id: userId, status: 'done' }, + { selected_user: userId, status: 'done' } + ] + } + } else if (status_filter === 'کنسل شده') { + filter = { + $or: [ + { creator_id: userId, status: 'cancled' }, + { selected_user: userId, status: 'cancled' } + ] + } + } else if (status_filter === 'در دست بررسی') { + filter = { creator_id: userId, status: { $in: ['paid', 'rejected'] } } + } else if (status_filter === 'پرداخت نشده') { + filter = { creator_id: userId, status: 'pre_payment' } + } + } + // paid + // دریافت لیست پروژه‌ها با استفاده از فیلتر + + // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده + const options = { + page: parseInt(page), // تبدیل صفحه به عدد صحیح + limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح + sort: { createdAt: -1 } + } + const projects = await ProjectModel.paginate(filter, options) + const projectsWithUserInfo = await Promise.all(projects?.docs.map(async project => { + const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') + const projectWithUserInfo = { + ...project._doc, + creator, + status_filter + } + const currentTime = new Date() + const projectCreatedAt = new Date(project.createdAt) + const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) + const remainingTime = calculateRemainingTime(timeDiff) + projectWithUserInfo.remainingTime = remainingTime + return projectWithUserInfo + })) + res.status(200).json({ + projects: projectsWithUserInfo, + totalPages: projects.totalPages, // ارسال تعداد کل صفحات + totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + next(error) + } +} +const getSingleProject = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const projectId = req.query.projectId + const projectRequests = await RequestModel.find({ project: projectId }) + .populate('user', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image') + .sort({ status: 1 }) + .lean() + const modifiedRequests = projectRequests.map(request => ({ + ...request, + time: request?.user?._id.toString() === userId ? request.time : null, + price: request?.user?._id.toString() === userId ? request.price : null + })) + const project = await ProjectModel.findById(projectId) + if (!project) { + return res.status(404).json({ message: 'پروژه مورد نظر یافت نشد' }) + } + // محاسبه مبلغ باقیمانده و مجموع مبلغ پرداخت شده + // تابع محاسبه مبلغ پرداخت شده + const getTotalPaidAmount = (installments) => { + let totalPaidAmount = 0 + installments.forEach(installment => { + totalPaidAmount += installment.amount + }) + return totalPaidAmount + } + + // تابع محاسبه مبلغ باقیمانده + const getRemainingAmount = (project) => { + const totalPaidAmount = getTotalPaidAmount(project.installments) + const remainingAmount = project.final_price - totalPaidAmount + return remainingAmount + } + + const totalPaidAmount = getTotalPaidAmount(project.installments) + const remainingAmount = getRemainingAmount(project) + // اطلاعات کاربر سازنده را نیز دریافت کنید و به آبجکت پروژه اضافه کنید + const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') + + const projectWithUserInfo = { + ...project._doc, + creator // اضافه کردن اطلاعات کاربر به آبجکت پروژه + } + const selectedUser = await UserModel.findById(project.selected_user).select('_id user_name first_name last_name user_type is_verified profile_image expertise sub_expertise province city rate user_level user_score') + projectWithUserInfo.selected_user = selectedUser + // محاسبه مدت زمان باقی‌مانده برای پروژه + const currentTime = new Date() + const projectCreatedAt = new Date(project.createdAt) + const timeDiff = project.offer_time * 24 * 60 * 60 * 1000 - (currentTime - projectCreatedAt) + const remainingTime = calculateRemainingTime(timeDiff) + projectWithUserInfo.remainingTime = remainingTime + let projectDetails = projectWithUserInfo + if ((project.selected_user && project.selected_user.toString()) === userId || project.creator_id.toString() === userId) { + projectDetails = { + ...projectWithUserInfo, + total_paid_amount: totalPaidAmount, + remaining_amount: remainingAmount + } + } else { + projectDetails = + projectWithUserInfo + } + // } + res.status(200).json({ project: projectDetails, projectRequests: modifiedRequests }) + } catch (error) { + next(error) + } +} + +const getProjectRequests = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + // بررسی اینکه آیا کاربر ایجاد کننده پروژه است یا خیر + const projectId = req.params.projectId + const project = await ProjectModel.findOne({ _id: projectId, creator_id: userId }) + if (!project) { + return res.status(403).send({ message: 'Access denied: You are not the creator of this project.' }) + } + + // تابع محاسبه مبلغ پرداخت شده + const getTotalPaidAmount = (installments) => { + let totalPaidAmount = 0 + installments.forEach(installment => { + totalPaidAmount += installment.amount + }) + return totalPaidAmount + } + + // تابع محاسبه مبلغ باقیمانده + const getRemainingAmount = (project) => { + const totalPaidAmount = getTotalPaidAmount(project.installments) + const remainingAmount = project.final_price - totalPaidAmount + return remainingAmount + } + + // محاسبه مبلغ باقیمانده + const remainingAmount = getRemainingAmount(project) + + // یافتن جزییات پروژه + const creator = await UserModel.findById(project.creator_id).select('first_name last_name user_name user_type is_verified profile_image gender province city rate') + const selectedUser = await UserModel.findById(project.selected_user).select('_id user_name first_name last_name user_type is_verified profile_image expertise sub_expertise province city rate user_level user_score') + const totalPaidAmount = getTotalPaidAmount(project.installments) + + const projectRequests = await RequestModel.find({ project: projectId }).populate('user', 'price time user_level first_name last_name user_name is_verified user_score rate profile_image').sort({ status: 1 }) + const projectinfo = await ProjectModel.findById(projectId) // جزییات درخواست‌های کاربران + projectinfo.selected_user = selectedUser + const projectDetails = { + ...projectinfo._doc, + creator, + total_paid_amount: totalPaidAmount, + remaining_amount: remainingAmount + } + res.status(200).json({ projectRequests, projectDetails }) + } catch (error) { + next(error) + } +} +module.exports = { + getProjects, getSingleProject, getProjectRequests +} diff --git a/controllers/panel/advertising/academyCategoryController.js b/controllers/panel/advertising/academyCategoryController.js index 4f821ba..9e52065 100644 --- a/controllers/panel/advertising/academyCategoryController.js +++ b/controllers/panel/advertising/academyCategoryController.js @@ -1,50 +1,50 @@ -const AcademyCategoryModel = require('../../models/AcademyCategoryModel'); - -// دریافت همه دسته‌بندی‌های اکادمی -exports.getAll = async (req, res) => { - try { - const categories = await AcademyCategoryModel.find({}).sort({ createdAt: -1 }); - res.json({ success: true, data: categories }); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } -}; - -// ایجاد دسته‌بندی جدید -exports.create = async (req, res) => { - try { - const { title } = req.body; - if (!title) return res.status(400).json({ success: false, message: "عنوان الزامی است" }); - - const category = await AcademyCategoryModel.create({ title }); - res.status(201).json({ success: true, message: "دسته‌بندی ایجاد شد", data: category }); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } -}; - -// ویرایش دسته‌بندی -exports.update = async (req, res) => { - try { - const { id } = req.params; - const { title, status } = req.body; - - const category = await AcademyCategoryModel.findByIdAndUpdate(id, { title, status }, { new: true }); - if (!category) return res.status(404).json({ success: false, message: "دسته‌بندی یافت نشد" }); - - res.json({ success: true, message: "دسته‌بندی ویرایش شد", data: category }); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } -}; - -// حذف دسته‌بندی -exports.remove = async (req, res) => { - try { - const { id } = req.params; - await AcademyCategoryModel.findByIdAndDelete(id); - res.json({ success: true, message: "دسته‌بندی حذف شد" }); - } catch (error) { - res.status(500).json({ success: false, message: error.message }); - } +const AcademyCategoryModel = require('../../models/AcademyCategoryModel'); + +// دریافت همه دسته‌بندی‌های اکادمی +exports.getAll = async (req, res) => { + try { + const categories = await AcademyCategoryModel.find({}).sort({ createdAt: -1 }); + res.json({ success: true, data: categories }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}; + +// ایجاد دسته‌بندی جدید +exports.create = async (req, res) => { + try { + const { title } = req.body; + if (!title) return res.status(400).json({ success: false, message: "عنوان الزامی است" }); + + const category = await AcademyCategoryModel.create({ title }); + res.status(201).json({ success: true, message: "دسته‌بندی ایجاد شد", data: category }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}; + +// ویرایش دسته‌بندی +exports.update = async (req, res) => { + try { + const { id } = req.params; + const { title, status } = req.body; + + const category = await AcademyCategoryModel.findByIdAndUpdate(id, { title, status }, { new: true }); + if (!category) return res.status(404).json({ success: false, message: "دسته‌بندی یافت نشد" }); + + res.json({ success: true, message: "دسته‌بندی ویرایش شد", data: category }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}; + +// حذف دسته‌بندی +exports.remove = async (req, res) => { + try { + const { id } = req.params; + await AcademyCategoryModel.findByIdAndDelete(id); + res.json({ success: true, message: "دسته‌بندی حذف شد" }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } }; \ No newline at end of file diff --git a/controllers/panel/advertising/advertisingController.js b/controllers/panel/advertising/advertisingController.js index 75bb440..63831b2 100644 --- a/controllers/panel/advertising/advertisingController.js +++ b/controllers/panel/advertising/advertisingController.js @@ -1,350 +1,350 @@ -/* eslint-disable eqeqeq */ -/* eslint-disable camelcase */ -const { ProvinceModel, CityModel } = require('../../../models/StateCity') -const AdvertisingModel = require('../../../models/AdvertisingModel') -// const AdvertisingLikeModel = require('../../../models/AdvertisingLikeModel') -// const AdvertisingComment = require('../../../models/AdvertisingCommentModel') -const AdvertisingProfileModel = require('../../../models/AdvertisingProfile') -const NotificationModel = require('../../../models/NotificationModel') -const AdvertisingCategoryModel = require('../../../models/AdvertisingCategoryModel') - - -const getAdvertisings = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { - page = 1, limit = 10, province, city, category, search, - startDate, - endDate, - status - } = req.query - - const filter = { - } - if (province) { - const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) - filter.province = provinceFind - } - if (city) { - const cityFind = await CityModel.findOne({ id: city }) - filter.city = cityFind - } - if (category) { - filter.category = category - } - if (search) { - filter.$or = [ - { title: { $regex: search, $options: 'i' } }, - { category: { $regex: search, $options: 'i' } } - ] - } - - // Date - if (startDate || endDate) { - filter.createdAt = {} - - if (startDate) { - filter.createdAt.$gte = new Date(startDate) // تاریخ شروع - } - - if (endDate) { - filter.createdAt.$lte = new Date(endDate) // تاریخ پایان - } - } - - if (status) { - filter.status = status - } - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: { createdAt: -1 }, - populate: [ - { path: 'creator_id', select: 'user_name first_name last_name' } - ] - } - const advertisings = await AdvertisingModel.paginate(filter, options) - - const newAds = advertisings.docs.map(ad => { - return { - // فیلدهای مشخص‌شده از ad._doc - _id: ad._doc._id, - creator_id: ad._doc.creator_id, - category: ad._doc.category, - title: ad._doc.title, - province: ad._doc.province, - city: ad._doc.city, - status: ad._doc.status, - neighbourhood: ad._doc.neighbourhood - } - }) - res.status(200).json({ - advertisings: newAds, - totalPages: advertisings.totalPages, - totalItems: advertisings.totalDocs - }) - } catch (error) { - next(error) - } -} -const getSingleAdvertising = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const advertisingId = req.query.advertisingId - const advertising = await AdvertisingModel.findById(advertisingId) - .populate('creator_id', '_id user_name first_name last_name') - - if (!advertising) { - return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) - } - - const creator = await AdvertisingProfileModel.findOne({ user: advertising.creator_id._id }) - - // اضافه کردن تعداد لایک‌ها، تعداد کامنت‌ها و وضعیت لایک به تبلیغ - const advertisingData = { - ...advertising._doc - } - - res.status(200).json({ - advertising: advertisingData, - creator - }) - } catch (error) { - next(error) - } -} -const acceptAdvertising = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { advertisingId } = req.body - - // پیدا کردن تبلیغ - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - return res.status(404).json({ message: 'تبلیغ مورد نظر یافت نشد' }) - } - - // تغییر وضعیت به accepted و ذخیره زمان قبول شدن - advertising.status = 'accepted' - advertising.acceptedAt = new Date() - await advertising.save() - const notification = new NotificationModel({ - user_id: advertising?.creator_id, - project_post_id: advertising?._id, - type: 'vitrine', - title: 'تایید ویترین', - description: `ویترین شما با عنوان: ${advertising?.title} تایید و منتشر شد ` - }) - await notification.save() - - res.status(200).json({ message: 'تبلیغ اکسپت شد و پس از 15 روز منقضی خواهد شد' }) - } catch (error) { - next(error) - } -} -const rejectAdvertising = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { advertisingId, rejectReason } = req.body - - // پیدا کردن تبلیغ - const advertising = await AdvertisingModel.findById(advertisingId) - if (!advertising) { - return res.status(404).json({ message: 'تبلیغ مورد نظر یافت نشد' }) - } - - // تغییر وضعیت به rejected و ذخیره دلیل رد شدن - advertising.status = 'rejected' - advertising.reject_reason = rejectReason - await advertising.save() - const notification = new NotificationModel({ - user_id: advertising?.creator_id, - project_post_id: advertising?._id, - type: 'vitrine', - title: 'رد ویترین', - description: `ویترین شما با عنوان: ${advertising?.title} رد شد، برای دیدن علت رد، لمس کنید ` - }) - await notification.save() - res.status(200).json({ message: 'تبلیغ رد شد', advertising }) - } catch (error) { - next(error) - } -} - - -const deleteAdvertisingCategory = async (req, res, next) => { - try { - const { id } = req.params - - // چک کنه که ID معتبره - if (!id.match(/^[0-9a-fA-F]{24}$/)) { - return res.status(400).json({ - success: false, - message: 'شناسه معتبر نمی‌باشد' - }) - } - - // پیدا کنه دسته‌بندی رو - const category = await AdvertisingCategoryModel.findById(id) - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی مورد نظر یافت نشد' - }) - } - - // حذف کنه - await category.deleteOne() - - return res.status(200).json({ - success: true, - message: 'دسته‌بندی تبلیغات با موفقیت حذف شد', - data: { id } - }) - } catch (error) { - next(error) - } -} - -const createAdvertisingCategory = async (req, res, next) => { - try { - const { title, status } = req.body - - // اعتبارسنجی ورودی‌ها - if (!title || title.trim().length < 1) { - return res.status(400).json({ - success: false, - message: 'عنوان دسته‌بندی الزامی است' - }) - } - - if (title.length > 255) { - return res.status(400).json({ - success: false, - message: 'عنوان دسته‌بندی نباید بیشتر از ۲۵۵ کاراکتر باشد' - }) - } - - // بررسی اینکه دسته‌بندی تکراری نباشد - const exists = await AdvertisingCategoryModel.findOne({ title: title.trim() }) - if (exists) { - return res.status(409).json({ - success: false, - message: 'این دسته‌بندی از قبل وجود دارد' - }) - } - - // ایجاد دسته‌بندی جدید - const newCategory = await AdvertisingCategoryModel.create({ - title: title.trim(), - status: status ?? true - }) - - return res.status(201).json({ - success: true, - message: 'دسته‌بندی تبلیغات با موفقیت ایجاد شد', - data: newCategory - }) - } catch (error) { - next(error) // میره به errorHandler مرکزی - } -} - -const getAdvertisingCategories = async (req, res, next) => { - try { - const page = Math.max(parseInt(req.query.page || '1', 10), 1); - const limit = Math.max(Math.min(parseInt(req.query.limit || '20', 10), 100), 1); - const { status, q, sort } = req.query; - - const query = {}; - if (typeof status !== 'undefined') { - // accept 'true'/'false' or boolean - if (status === 'true' || status === 'false') query.status = status === 'true'; - else if (status === '1' || status === '0') query.status = status === '1'; - } - if (q) { - query.title = { $regex: q.trim(), $options: 'i' }; - } - - const options = { - page, - limit, - sort: sort || '-createdAt', - lean: true, - }; - - // اگر mongoose-paginate-v2 نصب و فعال است: - if (typeof AdvertisingCategoryModel.paginate === 'function') { - const result = await AdvertisingCategoryModel.paginate(query, options); - return res.status(200).json({ - success: true, - totalDocs: result.totalDocs, - totalPages: result.totalPages, - page: result.page, - limit: result.limit, - docs: result.docs, - }); - } - - // fallback بدون paginate - const docs = await AdvertisingCategoryModel.find(query) - .sort(options.sort) - .skip((page - 1) * limit) - .limit(limit) - .lean(); - - const totalDocs = await AdvertisingCategoryModel.countDocuments(query); - const totalPages = Math.ceil(totalDocs / limit); - - return res.status(200).json({ - success: true, - totalDocs, - totalPages, - page, - limit, - docs, - }); - } catch (error) { - next(error); - } -}; - -/** - * GET /advertising/advertising-categories/:id - */ -const getAdvertisingCategoryById = async (req, res, next) => { - try { - const { id } = req.params; - if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) { - return res.status(400).json({ success: false, message: 'شناسه معتبر نیست' }); - } - - const category = await AdvertisingCategoryModel.findById(id).lean(); - if (!category) { - return res.status(404).json({ success: false, message: 'دسته‌بندی یافت نشد' }); - } - - return res.status(200).json({ success: true, data: category }); - } catch (error) { - next(error); - } -}; - - -module.exports = { - getSingleAdvertising, - getAdvertisings, - deleteAdvertisingCategory, - createAdvertisingCategory, - getAdvertisingCategories, - getAdvertisingCategoryById, - acceptAdvertising, - rejectAdvertising -} +/* eslint-disable eqeqeq */ +/* eslint-disable camelcase */ +const { ProvinceModel, CityModel } = require('../../../models/StateCity') +const AdvertisingModel = require('../../../models/AdvertisingModel') +// const AdvertisingLikeModel = require('../../../models/AdvertisingLikeModel') +// const AdvertisingComment = require('../../../models/AdvertisingCommentModel') +const AdvertisingProfileModel = require('../../../models/AdvertisingProfile') +const NotificationModel = require('../../../models/NotificationModel') +const AdvertisingCategoryModel = require('../../../models/AdvertisingCategoryModel') + + +const getAdvertisings = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { + page = 1, limit = 10, province, city, category, search, + startDate, + endDate, + status + } = req.query + + const filter = { + } + if (province) { + const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) + filter.province = provinceFind + } + if (city) { + const cityFind = await CityModel.findOne({ id: city }) + filter.city = cityFind + } + if (category) { + filter.category = category + } + if (search) { + filter.$or = [ + { title: { $regex: search, $options: 'i' } }, + { category: { $regex: search, $options: 'i' } } + ] + } + + // Date + if (startDate || endDate) { + filter.createdAt = {} + + if (startDate) { + filter.createdAt.$gte = new Date(startDate) // تاریخ شروع + } + + if (endDate) { + filter.createdAt.$lte = new Date(endDate) // تاریخ پایان + } + } + + if (status) { + filter.status = status + } + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: { createdAt: -1 }, + populate: [ + { path: 'creator_id', select: 'user_name first_name last_name' } + ] + } + const advertisings = await AdvertisingModel.paginate(filter, options) + + const newAds = advertisings.docs.map(ad => { + return { + // فیلدهای مشخص‌شده از ad._doc + _id: ad._doc._id, + creator_id: ad._doc.creator_id, + category: ad._doc.category, + title: ad._doc.title, + province: ad._doc.province, + city: ad._doc.city, + status: ad._doc.status, + neighbourhood: ad._doc.neighbourhood + } + }) + res.status(200).json({ + advertisings: newAds, + totalPages: advertisings.totalPages, + totalItems: advertisings.totalDocs + }) + } catch (error) { + next(error) + } +} +const getSingleAdvertising = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const advertisingId = req.query.advertisingId + const advertising = await AdvertisingModel.findById(advertisingId) + .populate('creator_id', '_id user_name first_name last_name') + + if (!advertising) { + return res.status(404).json({ message: 'ویترین مورد نظر یافت نشد' }) + } + + const creator = await AdvertisingProfileModel.findOne({ user: advertising.creator_id._id }) + + // اضافه کردن تعداد لایک‌ها، تعداد کامنت‌ها و وضعیت لایک به تبلیغ + const advertisingData = { + ...advertising._doc + } + + res.status(200).json({ + advertising: advertisingData, + creator + }) + } catch (error) { + next(error) + } +} +const acceptAdvertising = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { advertisingId } = req.body + + // پیدا کردن تبلیغ + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + return res.status(404).json({ message: 'تبلیغ مورد نظر یافت نشد' }) + } + + // تغییر وضعیت به accepted و ذخیره زمان قبول شدن + advertising.status = 'accepted' + advertising.acceptedAt = new Date() + await advertising.save() + const notification = new NotificationModel({ + user_id: advertising?.creator_id, + project_post_id: advertising?._id, + type: 'vitrine', + title: 'تایید ویترین', + description: `ویترین شما با عنوان: ${advertising?.title} تایید و منتشر شد ` + }) + await notification.save() + + res.status(200).json({ message: 'تبلیغ اکسپت شد و پس از 15 روز منقضی خواهد شد' }) + } catch (error) { + next(error) + } +} +const rejectAdvertising = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { advertisingId, rejectReason } = req.body + + // پیدا کردن تبلیغ + const advertising = await AdvertisingModel.findById(advertisingId) + if (!advertising) { + return res.status(404).json({ message: 'تبلیغ مورد نظر یافت نشد' }) + } + + // تغییر وضعیت به rejected و ذخیره دلیل رد شدن + advertising.status = 'rejected' + advertising.reject_reason = rejectReason + await advertising.save() + const notification = new NotificationModel({ + user_id: advertising?.creator_id, + project_post_id: advertising?._id, + type: 'vitrine', + title: 'رد ویترین', + description: `ویترین شما با عنوان: ${advertising?.title} رد شد، برای دیدن علت رد، لمس کنید ` + }) + await notification.save() + res.status(200).json({ message: 'تبلیغ رد شد', advertising }) + } catch (error) { + next(error) + } +} + + +const deleteAdvertisingCategory = async (req, res, next) => { + try { + const { id } = req.params + + // چک کنه که ID معتبره + if (!id.match(/^[0-9a-fA-F]{24}$/)) { + return res.status(400).json({ + success: false, + message: 'شناسه معتبر نمی‌باشد' + }) + } + + // پیدا کنه دسته‌بندی رو + const category = await AdvertisingCategoryModel.findById(id) + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی مورد نظر یافت نشد' + }) + } + + // حذف کنه + await category.deleteOne() + + return res.status(200).json({ + success: true, + message: 'دسته‌بندی تبلیغات با موفقیت حذف شد', + data: { id } + }) + } catch (error) { + next(error) + } +} + +const createAdvertisingCategory = async (req, res, next) => { + try { + const { title, status } = req.body + + // اعتبارسنجی ورودی‌ها + if (!title || title.trim().length < 1) { + return res.status(400).json({ + success: false, + message: 'عنوان دسته‌بندی الزامی است' + }) + } + + if (title.length > 255) { + return res.status(400).json({ + success: false, + message: 'عنوان دسته‌بندی نباید بیشتر از ۲۵۵ کاراکتر باشد' + }) + } + + // بررسی اینکه دسته‌بندی تکراری نباشد + const exists = await AdvertisingCategoryModel.findOne({ title: title.trim() }) + if (exists) { + return res.status(409).json({ + success: false, + message: 'این دسته‌بندی از قبل وجود دارد' + }) + } + + // ایجاد دسته‌بندی جدید + const newCategory = await AdvertisingCategoryModel.create({ + title: title.trim(), + status: status ?? true + }) + + return res.status(201).json({ + success: true, + message: 'دسته‌بندی تبلیغات با موفقیت ایجاد شد', + data: newCategory + }) + } catch (error) { + next(error) // میره به errorHandler مرکزی + } +} + +const getAdvertisingCategories = async (req, res, next) => { + try { + const page = Math.max(parseInt(req.query.page || '1', 10), 1); + const limit = Math.max(Math.min(parseInt(req.query.limit || '20', 10), 100), 1); + const { status, q, sort } = req.query; + + const query = {}; + if (typeof status !== 'undefined') { + // accept 'true'/'false' or boolean + if (status === 'true' || status === 'false') query.status = status === 'true'; + else if (status === '1' || status === '0') query.status = status === '1'; + } + if (q) { + query.title = { $regex: q.trim(), $options: 'i' }; + } + + const options = { + page, + limit, + sort: sort || '-createdAt', + lean: true, + }; + + // اگر mongoose-paginate-v2 نصب و فعال است: + if (typeof AdvertisingCategoryModel.paginate === 'function') { + const result = await AdvertisingCategoryModel.paginate(query, options); + return res.status(200).json({ + success: true, + totalDocs: result.totalDocs, + totalPages: result.totalPages, + page: result.page, + limit: result.limit, + docs: result.docs, + }); + } + + // fallback بدون paginate + const docs = await AdvertisingCategoryModel.find(query) + .sort(options.sort) + .skip((page - 1) * limit) + .limit(limit) + .lean(); + + const totalDocs = await AdvertisingCategoryModel.countDocuments(query); + const totalPages = Math.ceil(totalDocs / limit); + + return res.status(200).json({ + success: true, + totalDocs, + totalPages, + page, + limit, + docs, + }); + } catch (error) { + next(error); + } +}; + +/** + * GET /advertising/advertising-categories/:id + */ +const getAdvertisingCategoryById = async (req, res, next) => { + try { + const { id } = req.params; + if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) { + return res.status(400).json({ success: false, message: 'شناسه معتبر نیست' }); + } + + const category = await AdvertisingCategoryModel.findById(id).lean(); + if (!category) { + return res.status(404).json({ success: false, message: 'دسته‌بندی یافت نشد' }); + } + + return res.status(200).json({ success: true, data: category }); + } catch (error) { + next(error); + } +}; + + +module.exports = { + getSingleAdvertising, + getAdvertisings, + deleteAdvertisingCategory, + createAdvertisingCategory, + getAdvertisingCategories, + getAdvertisingCategoryById, + acceptAdvertising, + rejectAdvertising +} diff --git a/controllers/panel/comments/advertisingCommentController.js b/controllers/panel/comments/advertisingCommentController.js index be52612..f992c03 100644 --- a/controllers/panel/comments/advertisingCommentController.js +++ b/controllers/panel/comments/advertisingCommentController.js @@ -1,195 +1,195 @@ -/* eslint-disable camelcase */ -const AdvertisingComment = require('../../../models/AdvertisingCommentModel') -const jMoment = require('moment-jalaali') -const mongoose = require('mongoose') -const AdvertisingModel = require('../../../models/AdvertisingModel') -const NotificationModel = require('../../../models/NotificationModel') - -// دریافت کامنت‌ها -const getAdvertisingComments = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { - page = 1, limit = 10, - startDate, - endDate, - status - } = req.query - const filter = {} - - // Date - if (startDate || endDate) { - filter.createdAt = {} - - if (startDate) { - filter.createdAt.$gte = new Date(startDate) // تاریخ شروع - } - - if (endDate) { - filter.createdAt.$lte = new Date(endDate) // تاریخ پایان - } - } - - if (status) { - filter.status = status - } - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: { createdAt: -1 }, - populate: [ - { path: 'userId', select: 'user_name first_name last_name profile_image' }, - { path: 'advertisingId', select: 'title', populate: { path: 'creator_id', select: 'user_name first_name last_name' } } - // اضافه کردن اطلاعات سازنده - ] - } - - const comments = await AdvertisingComment.paginate(filter, options) - const commentList = comments.docs.map(comment => ({ - _id: comment._id, - createdAt: jMoment(comment.createdAt).format('jYYYY-jMM-jDD HH:mm'), - status: comment.status, - text: comment.text, - advertising: comment.advertisingId ? comment.advertisingId.title : null, - user: comment.userId ? { - _id: comment.userId._id, - user_name: comment.userId.user_name, - first_name: comment.userId.first_name, - last_name: comment.userId.last_name, - profile_image: comment.userId.profile_image - } : null, - creator: (comment.advertisingId && comment.advertisingId.creator_id) ? { - _id: comment.advertisingId.creator_id._id, - user_name: comment.advertisingId.creator_id.user_name, - first_name: comment.advertisingId.creator_id.first_name, - last_name: comment.advertisingId.creator_id.last_name - } : null - })); - - - res.status(200).json({ - comments: commentList, - totalPages: comments.totalPages, - totalItems: comments.totalDocs - }) - } catch (error) { - next(error) - } -} - -// دریافت جزئیات یک کامنت -const getAdvertisingCommentDetail = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const comment_id = req.query.comment_id - - if (!mongoose.Types.ObjectId.isValid(comment_id)) { - return res.status(400).json({ message: 'شناسه کامنت معتبر نیست' }) - } - - const comment = await AdvertisingComment.findById(comment_id) - .populate('userId', 'user_name first_name last_name profile_image') - .populate({ - path: 'advertisingId', - select: 'title creator_id', - populate: { - path: 'creator_id', - select: 'user_name first_name last_name' - } - }) - if (!comment) { - return res.status(404).json({ message: 'کامنت مورد نظر یافت نشد' }) - } - - const response = { - _id: comment._id, - text: comment.text, - createdAt: jMoment(comment.createdAt).format('jYYYY-jMM-jDD HH:mm'), - status: comment.status, - advertising: comment.advertisingId ? comment.advertisingId.title : null, - creator: { - _id: comment?.advertisingId?.creator_id._id, - user_name: comment?.advertisingId?.creator_id.user_name, - first_name: comment?.advertisingId?.creator_id.first_name, - last_name: comment?.advertisingId?.creator_id.last_name - }, - user: { - _id: comment.userId._id, - user_name: comment.userId.user_name, - first_name: comment.userId.first_name, - last_name: comment.userId.last_name, - profile_image: comment.userId.profile_image - } - } - - res.status(200).json({ comment: response }) - } catch (error) { - next(error) - } -} - -// تایید یک کامنت -const acceptAdvertisingComment = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { id, status, text } = req.body - - if (!id || !status) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const comment = await AdvertisingComment.findById(id) - // افزایش تعداد کامنت‌های تبلیغ - await AdvertisingModel.findByIdAndUpdate(comment?.advertisingId, { $inc: { commentsCount: 1 } }) - if (!comment) { - return res.status(404).json({ - error: true, - message: 'کامنت مورد نظر یافت نشد' - }) - } - - if (text) { - comment.text = text - } - comment.status = status - await comment.save() - - if (status === 'accepted') { - const notification = new NotificationModel({ - user_id: comment?.userId, - project_post_id: comment?.advertisingId, - type: 'vitrine-comment', - title: 'انتشار نظر', - description: 'نظر شما منتشر شد.' - }) - await notification.save() - } else { - const notification = new NotificationModel({ - user_id: comment?.userId, - project_post_id: comment?.advertisingId, - type: 'vitrine-comment', - title: 'رد نظر', - description: 'نظر شما رد شد.' - }) - await notification.save() - } - res.status(201).json({ message: 'کامنت با موفقیت تایید شد' }) - } catch (error) { - next(error) - } -} - -module.exports = { - getAdvertisingComments, - getAdvertisingCommentDetail, - acceptAdvertisingComment -} +/* eslint-disable camelcase */ +const AdvertisingComment = require('../../../models/AdvertisingCommentModel') +const jMoment = require('moment-jalaali') +const mongoose = require('mongoose') +const AdvertisingModel = require('../../../models/AdvertisingModel') +const NotificationModel = require('../../../models/NotificationModel') + +// دریافت کامنت‌ها +const getAdvertisingComments = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { + page = 1, limit = 10, + startDate, + endDate, + status + } = req.query + const filter = {} + + // Date + if (startDate || endDate) { + filter.createdAt = {} + + if (startDate) { + filter.createdAt.$gte = new Date(startDate) // تاریخ شروع + } + + if (endDate) { + filter.createdAt.$lte = new Date(endDate) // تاریخ پایان + } + } + + if (status) { + filter.status = status + } + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: { createdAt: -1 }, + populate: [ + { path: 'userId', select: 'user_name first_name last_name profile_image' }, + { path: 'advertisingId', select: 'title', populate: { path: 'creator_id', select: 'user_name first_name last_name' } } + // اضافه کردن اطلاعات سازنده + ] + } + + const comments = await AdvertisingComment.paginate(filter, options) + const commentList = comments.docs.map(comment => ({ + _id: comment._id, + createdAt: jMoment(comment.createdAt).format('jYYYY-jMM-jDD HH:mm'), + status: comment.status, + text: comment.text, + advertising: comment.advertisingId ? comment.advertisingId.title : null, + user: comment.userId ? { + _id: comment.userId._id, + user_name: comment.userId.user_name, + first_name: comment.userId.first_name, + last_name: comment.userId.last_name, + profile_image: comment.userId.profile_image + } : null, + creator: (comment.advertisingId && comment.advertisingId.creator_id) ? { + _id: comment.advertisingId.creator_id._id, + user_name: comment.advertisingId.creator_id.user_name, + first_name: comment.advertisingId.creator_id.first_name, + last_name: comment.advertisingId.creator_id.last_name + } : null + })); + + + res.status(200).json({ + comments: commentList, + totalPages: comments.totalPages, + totalItems: comments.totalDocs + }) + } catch (error) { + next(error) + } +} + +// دریافت جزئیات یک کامنت +const getAdvertisingCommentDetail = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const comment_id = req.query.comment_id + + if (!mongoose.Types.ObjectId.isValid(comment_id)) { + return res.status(400).json({ message: 'شناسه کامنت معتبر نیست' }) + } + + const comment = await AdvertisingComment.findById(comment_id) + .populate('userId', 'user_name first_name last_name profile_image') + .populate({ + path: 'advertisingId', + select: 'title creator_id', + populate: { + path: 'creator_id', + select: 'user_name first_name last_name' + } + }) + if (!comment) { + return res.status(404).json({ message: 'کامنت مورد نظر یافت نشد' }) + } + + const response = { + _id: comment._id, + text: comment.text, + createdAt: jMoment(comment.createdAt).format('jYYYY-jMM-jDD HH:mm'), + status: comment.status, + advertising: comment.advertisingId ? comment.advertisingId.title : null, + creator: { + _id: comment?.advertisingId?.creator_id._id, + user_name: comment?.advertisingId?.creator_id.user_name, + first_name: comment?.advertisingId?.creator_id.first_name, + last_name: comment?.advertisingId?.creator_id.last_name + }, + user: { + _id: comment.userId._id, + user_name: comment.userId.user_name, + first_name: comment.userId.first_name, + last_name: comment.userId.last_name, + profile_image: comment.userId.profile_image + } + } + + res.status(200).json({ comment: response }) + } catch (error) { + next(error) + } +} + +// تایید یک کامنت +const acceptAdvertisingComment = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { id, status, text } = req.body + + if (!id || !status) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const comment = await AdvertisingComment.findById(id) + // افزایش تعداد کامنت‌های تبلیغ + await AdvertisingModel.findByIdAndUpdate(comment?.advertisingId, { $inc: { commentsCount: 1 } }) + if (!comment) { + return res.status(404).json({ + error: true, + message: 'کامنت مورد نظر یافت نشد' + }) + } + + if (text) { + comment.text = text + } + comment.status = status + await comment.save() + + if (status === 'accepted') { + const notification = new NotificationModel({ + user_id: comment?.userId, + project_post_id: comment?.advertisingId, + type: 'vitrine-comment', + title: 'انتشار نظر', + description: 'نظر شما منتشر شد.' + }) + await notification.save() + } else { + const notification = new NotificationModel({ + user_id: comment?.userId, + project_post_id: comment?.advertisingId, + type: 'vitrine-comment', + title: 'رد نظر', + description: 'نظر شما رد شد.' + }) + await notification.save() + } + res.status(201).json({ message: 'کامنت با موفقیت تایید شد' }) + } catch (error) { + next(error) + } +} + +module.exports = { + getAdvertisingComments, + getAdvertisingCommentDetail, + acceptAdvertisingComment +} diff --git a/controllers/panel/comments/commentController.js b/controllers/panel/comments/commentController.js index 4ae3f7b..c0548e9 100644 --- a/controllers/panel/comments/commentController.js +++ b/controllers/panel/comments/commentController.js @@ -1,172 +1,172 @@ -/* eslint-disable camelcase */ -const CommentModel = require('../../../models/CommentModel') -const jMoment = require('moment-jalaali') -const { default: mongoose } = require('mongoose') - -// دریافت لیست کامنت‌ها با پیجینیشن -const getComments = async (req, res, next) => { - try { - console.log("gg6"); - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { - page = 1, limit = 10, - startDate, - endDate, - status - } = req.query - const filter = {} // فیلتر بر اساس وضعیت کامنت - - // Date - if (startDate || endDate) { - filter.createdAt = {} - - if (startDate) { - filter.createdAt.$gte = new Date(startDate) // تاریخ شروع - } - - if (endDate) { - filter.createdAt.$lte = new Date(endDate) // تاریخ پایان - } - } - console.log("gg8"); - if (status) { - filter.status = status - } - const options = { - page: parseInt(page), // تبدیل صفحه به عدد صحیح - limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح - sort: { createdAt: -1 }, - populate: [ - { path: 'creator', select: 'user_name first_name last_name profile_image ' }, - { path: 'user', select: 'user_name first_name last_name' }, - { path: 'project', select: 'title' } - ] - } - - const comments = await CommentModel.paginate(filter, options) - const commentList = comments.docs.map(comment => ({ - _id: comment._id, - createdAt: jMoment(comment.createdAt).format('jYYYY-jMM-jDD HH:mm'), - status: comment.status, - comment_for: comment.comment_for, - project: comment.project ? comment.project.title : null, - creator: comment.creator ? { - _id: comment.creator._id, - user_name: comment.creator.user_name, - first_name: comment.creator.first_name, - last_name: comment.creator.last_name, - profile_image: comment.creator.profile_image - } : null, - user: comment.user ? { - _id: comment.user._id, - user_name: comment.user.user_name, - first_name: comment.user.first_name, - last_name: comment.user.last_name - } : null - })); - - console.log("gg9"); - res.status(200).json({ - comments: commentList, - totalPages: comments.totalPages, - totalItems: comments.totalDocs - }) - } catch (error) { - console.log("gg"); - - next(error) - } -} - -// دریافت جزئیات یک کامنت -const getCommentDetail = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const comment_id = req.query.comment_id - - if (!mongoose.Types.ObjectId.isValid(comment_id)) { - return res.status(400).json({ message: 'شناسه کامنت معتبر نیست' }) - } - - const comment = await CommentModel.findById(comment_id) - .populate('creator', 'user_name first_name last_name profile_image ') - .populate('user', 'user_name first_name last_name') - .populate('project', 'title') - - if (!comment) { - return res.status(404).json({ message: 'کامنت مورد نظر یافت نشد' }) - } - - const response = { - _id: comment._id, - comment: comment.comment, - rating: comment.rating, - createdAt: jMoment(comment.createdAt).format('jYYYY-jMM-jDD HH:mm'), - status: comment.status, - comment_for: comment.comment_for, - project: comment.project ? comment.project.title : null, - creator: { - _id: comment.creator._id, - user_name: comment.creator.user_name, - first_name: comment.creator.first_name, - last_name: comment.creator.last_name, - profile_image: comment.creator.profile_image - }, - user: { - _id: comment.user._id, - user_name: comment.user.user_name, - first_name: comment.user.first_name, - last_name: comment.user.last_name - } - } - - res.status(200).json({ comment: response }) - } catch (error) { - next(error) - } -} - -// تایید یک کامنت -const acceptComment = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { id, status, text } = req.body - - if (!id || !status) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const comment = await CommentModel.findById(id) - - if (!comment) { - return res.status(404).json({ - error: true, - message: 'کامنت مورد نظر یافت نشد' - }) - } - if (text) { - comment.comment = text - } - comment.status = status - await comment.save() - - res.status(201).json({ message: 'کامنت با موفقیت تایید شد' }) - } catch (error) { - next(error) - } -} - -module.exports = { - getComments, - getCommentDetail, - acceptComment -} +/* eslint-disable camelcase */ +const CommentModel = require('../../../models/CommentModel') +const jMoment = require('moment-jalaali') +const { default: mongoose } = require('mongoose') + +// دریافت لیست کامنت‌ها با پیجینیشن +const getComments = async (req, res, next) => { + try { + console.log("gg6"); + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { + page = 1, limit = 10, + startDate, + endDate, + status + } = req.query + const filter = {} // فیلتر بر اساس وضعیت کامنت + + // Date + if (startDate || endDate) { + filter.createdAt = {} + + if (startDate) { + filter.createdAt.$gte = new Date(startDate) // تاریخ شروع + } + + if (endDate) { + filter.createdAt.$lte = new Date(endDate) // تاریخ پایان + } + } + console.log("gg8"); + if (status) { + filter.status = status + } + const options = { + page: parseInt(page), // تبدیل صفحه به عدد صحیح + limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح + sort: { createdAt: -1 }, + populate: [ + { path: 'creator', select: 'user_name first_name last_name profile_image ' }, + { path: 'user', select: 'user_name first_name last_name' }, + { path: 'project', select: 'title' } + ] + } + + const comments = await CommentModel.paginate(filter, options) + const commentList = comments.docs.map(comment => ({ + _id: comment._id, + createdAt: jMoment(comment.createdAt).format('jYYYY-jMM-jDD HH:mm'), + status: comment.status, + comment_for: comment.comment_for, + project: comment.project ? comment.project.title : null, + creator: comment.creator ? { + _id: comment.creator._id, + user_name: comment.creator.user_name, + first_name: comment.creator.first_name, + last_name: comment.creator.last_name, + profile_image: comment.creator.profile_image + } : null, + user: comment.user ? { + _id: comment.user._id, + user_name: comment.user.user_name, + first_name: comment.user.first_name, + last_name: comment.user.last_name + } : null + })); + + console.log("gg9"); + res.status(200).json({ + comments: commentList, + totalPages: comments.totalPages, + totalItems: comments.totalDocs + }) + } catch (error) { + console.log("gg"); + + next(error) + } +} + +// دریافت جزئیات یک کامنت +const getCommentDetail = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const comment_id = req.query.comment_id + + if (!mongoose.Types.ObjectId.isValid(comment_id)) { + return res.status(400).json({ message: 'شناسه کامنت معتبر نیست' }) + } + + const comment = await CommentModel.findById(comment_id) + .populate('creator', 'user_name first_name last_name profile_image ') + .populate('user', 'user_name first_name last_name') + .populate('project', 'title') + + if (!comment) { + return res.status(404).json({ message: 'کامنت مورد نظر یافت نشد' }) + } + + const response = { + _id: comment._id, + comment: comment.comment, + rating: comment.rating, + createdAt: jMoment(comment.createdAt).format('jYYYY-jMM-jDD HH:mm'), + status: comment.status, + comment_for: comment.comment_for, + project: comment.project ? comment.project.title : null, + creator: { + _id: comment.creator._id, + user_name: comment.creator.user_name, + first_name: comment.creator.first_name, + last_name: comment.creator.last_name, + profile_image: comment.creator.profile_image + }, + user: { + _id: comment.user._id, + user_name: comment.user.user_name, + first_name: comment.user.first_name, + last_name: comment.user.last_name + } + } + + res.status(200).json({ comment: response }) + } catch (error) { + next(error) + } +} + +// تایید یک کامنت +const acceptComment = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { id, status, text } = req.body + + if (!id || !status) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const comment = await CommentModel.findById(id) + + if (!comment) { + return res.status(404).json({ + error: true, + message: 'کامنت مورد نظر یافت نشد' + }) + } + if (text) { + comment.comment = text + } + comment.status = status + await comment.save() + + res.status(201).json({ message: 'کامنت با موفقیت تایید شد' }) + } catch (error) { + next(error) + } +} + +module.exports = { + getComments, + getCommentDetail, + acceptComment +} diff --git a/controllers/panel/expertise/expertiseController.js b/controllers/panel/expertise/expertiseController.js index d6e7d66..e1cf9b4 100644 --- a/controllers/panel/expertise/expertiseController.js +++ b/controllers/panel/expertise/expertiseController.js @@ -1,111 +1,111 @@ -/* eslint-disable camelcase */ -const ExpertiseModel = require('../../../models/ExpertiseModel') - -const createExpertise = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { expertise } = req.body - if (!expertise - ) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - // بررسی وجود تخصص با این نام در پایگاه داده - const existingExpertise = await ExpertiseModel.findOne({ expertise }) - - if (existingExpertise) { - return res.status(400).json({ message: 'تخصص با این نام قبلاً ثبت شده است' }) - } - - // ایجاد یک نمونه جدید از مدل ExpertiseModel - const newExpertise = new ExpertiseModel({ - expertise - }) - - // ذخیره تخصص جدید در پایگاه داده - await newExpertise.save() - - res.json({ message: 'تخصص با موفقیت اضافه شد' }) - } catch (error) { - next(error) - } -} - -const createSubExpertise = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const expertiseId = req.query.id - const { name } = req.body - if (!expertiseId || !name) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - // یافتن تخصص موردنظر از طریق آیدی - const expertise = await ExpertiseModel.findById(expertiseId) - - if (!expertise) { - return res.status(404).json({ message: 'تخصص موردنظر یافت نشد' }) - } - - // اضافه کردن زیرتخصص جدید به آرایه sub_expertise تخصص موجود - expertise.sub_expertise.push({ name }) - await expertise.save() - - res.json({ message: 'زیرتخصص با موفقیت به تخصص اضافه شد' }) - } catch (error) { - next(error) - } -} -const getExpertise = async (req, res, next) => { - try { - const expertises = await ExpertiseModel.find().select('expertise sub_expertise') - res.json({ expertises }) - } catch (error) { - next(error) - } -} - -// ویرایش تخصص -const editExpertise = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { expertise } = req.body - const expertiseId = req.params.id - - if (!expertise || !expertiseId) { - return res.status(422).json({ message: 'اطلاعات ارسالی اشتباه است' }) - } - - const updatedExpertise = await ExpertiseModel.findByIdAndUpdate( - expertiseId, - { expertise }, - { new: true } - ) - - if (!updatedExpertise) { - return res.status(404).json({ message: 'تخصص یافت نشد' }) - } - - res.json({ message: 'تخصص با موفقیت ویرایش شد' }) - } catch (error) { - next(error) - } -} - -module.exports = { - createExpertise, - createSubExpertise, - getExpertise, - editExpertise - -} +/* eslint-disable camelcase */ +const ExpertiseModel = require('../../../models/ExpertiseModel') + +const createExpertise = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { expertise } = req.body + if (!expertise + ) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + // بررسی وجود تخصص با این نام در پایگاه داده + const existingExpertise = await ExpertiseModel.findOne({ expertise }) + + if (existingExpertise) { + return res.status(400).json({ message: 'تخصص با این نام قبلاً ثبت شده است' }) + } + + // ایجاد یک نمونه جدید از مدل ExpertiseModel + const newExpertise = new ExpertiseModel({ + expertise + }) + + // ذخیره تخصص جدید در پایگاه داده + await newExpertise.save() + + res.json({ message: 'تخصص با موفقیت اضافه شد' }) + } catch (error) { + next(error) + } +} + +const createSubExpertise = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const expertiseId = req.query.id + const { name } = req.body + if (!expertiseId || !name) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + // یافتن تخصص موردنظر از طریق آیدی + const expertise = await ExpertiseModel.findById(expertiseId) + + if (!expertise) { + return res.status(404).json({ message: 'تخصص موردنظر یافت نشد' }) + } + + // اضافه کردن زیرتخصص جدید به آرایه sub_expertise تخصص موجود + expertise.sub_expertise.push({ name }) + await expertise.save() + + res.json({ message: 'زیرتخصص با موفقیت به تخصص اضافه شد' }) + } catch (error) { + next(error) + } +} +const getExpertise = async (req, res, next) => { + try { + const expertises = await ExpertiseModel.find().select('expertise sub_expertise') + res.json({ expertises }) + } catch (error) { + next(error) + } +} + +// ویرایش تخصص +const editExpertise = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { expertise } = req.body + const expertiseId = req.params.id + + if (!expertise || !expertiseId) { + return res.status(422).json({ message: 'اطلاعات ارسالی اشتباه است' }) + } + + const updatedExpertise = await ExpertiseModel.findByIdAndUpdate( + expertiseId, + { expertise }, + { new: true } + ) + + if (!updatedExpertise) { + return res.status(404).json({ message: 'تخصص یافت نشد' }) + } + + res.json({ message: 'تخصص با موفقیت ویرایش شد' }) + } catch (error) { + next(error) + } +} + +module.exports = { + createExpertise, + createSubExpertise, + getExpertise, + editExpertise + +} diff --git a/controllers/panel/financial/financialController.js b/controllers/panel/financial/financialController.js index 7e6c3ce..c87c796 100644 --- a/controllers/panel/financial/financialController.js +++ b/controllers/panel/financial/financialController.js @@ -1,108 +1,108 @@ -/* eslint-disable camelcase */ -const { default: mongoose } = require('mongoose') -const PaymentModel = require('../../../models/PaymentModel') -const jMoment = require('moment-jalaali') - -const getFinancial = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { - page = 1, limit = 10, - startDate, - endDate, - status - } = req.query - const filter = {} // افزودن شرط‌های دیگر برای فیلتر - - // Date - if (startDate || endDate) { - filter.createdAt = {} - - if (startDate) { - filter.createdAt.$gte = new Date(startDate) // تاریخ شروع - } - - if (endDate) { - filter.createdAt.$lte = new Date(endDate) // تاریخ پایان - } - } - - if (status) { - filter.status = status - } - - const options = { - page: parseInt(page), // تبدیل صفحه به عدد صحیح - limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح - sort: { createdAt: -1 }, - populate: [ - { - path: 'user_id', - select: '_id user_name first_name mobile last_name ' - }, - { - path: 'project_id', - select: '_id title project_type' - } - ] - } - const financial = await PaymentModel.paginate( - filter, // فیلتر - options // گزینه‌های پیجینیشن - ) - const financialList = financial?.docs.map(financia => { - const jDate = jMoment(financia.createdAt).format('jYYYY-jMM-jDD HH:mm') - return { - ...financia._doc, - createdAt: jDate - } - }) - res.json({ - financials: financialList, - totalPages: financial.totalPages, // ارسال تعداد کل صفحات - totalItems: financial.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -const getFinancialDetail = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const financial_id = req.query.financial_id - if (!mongoose.Types.ObjectId.isValid(financial_id)) { - return res.status(400).json({ message: 'شناسه تراکنش معتبر نیست' }) - } - const financial = await PaymentModel.findById(financial_id).populate([ - { - path: 'user_id', - select: '_id user_name first_name mobile last_name shaba' - }, - { - path: 'project_id', - select: '_id title project_type', - populate: { - path: 'selected_user', - select: '_id user_name first_name mobile last_name shaba' - } - } - ]) - - if (!financial) { - return res.status(404).json({ message: 'تراکنش مورد نظر یافت نشد' }) - } - const newfinancial = { - ...financial._doc, - createdAt: jMoment(financial.createdAt).format('jYYYY-jMM-jDD HH:mm') - } - return res.status(200).json({ financial: newfinancial }) - } catch (error) { - console.error('Error occurred:', error) - res.status(500).json({ error: 'Internal server error' }) - } -} -module.exports = { getFinancial, getFinancialDetail } +/* eslint-disable camelcase */ +const { default: mongoose } = require('mongoose') +const PaymentModel = require('../../../models/PaymentModel') +const jMoment = require('moment-jalaali') + +const getFinancial = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { + page = 1, limit = 10, + startDate, + endDate, + status + } = req.query + const filter = {} // افزودن شرط‌های دیگر برای فیلتر + + // Date + if (startDate || endDate) { + filter.createdAt = {} + + if (startDate) { + filter.createdAt.$gte = new Date(startDate) // تاریخ شروع + } + + if (endDate) { + filter.createdAt.$lte = new Date(endDate) // تاریخ پایان + } + } + + if (status) { + filter.status = status + } + + const options = { + page: parseInt(page), // تبدیل صفحه به عدد صحیح + limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح + sort: { createdAt: -1 }, + populate: [ + { + path: 'user_id', + select: '_id user_name first_name mobile last_name ' + }, + { + path: 'project_id', + select: '_id title project_type' + } + ] + } + const financial = await PaymentModel.paginate( + filter, // فیلتر + options // گزینه‌های پیجینیشن + ) + const financialList = financial?.docs.map(financia => { + const jDate = jMoment(financia.createdAt).format('jYYYY-jMM-jDD HH:mm') + return { + ...financia._doc, + createdAt: jDate + } + }) + res.json({ + financials: financialList, + totalPages: financial.totalPages, // ارسال تعداد کل صفحات + totalItems: financial.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +const getFinancialDetail = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const financial_id = req.query.financial_id + if (!mongoose.Types.ObjectId.isValid(financial_id)) { + return res.status(400).json({ message: 'شناسه تراکنش معتبر نیست' }) + } + const financial = await PaymentModel.findById(financial_id).populate([ + { + path: 'user_id', + select: '_id user_name first_name mobile last_name shaba' + }, + { + path: 'project_id', + select: '_id title project_type', + populate: { + path: 'selected_user', + select: '_id user_name first_name mobile last_name shaba' + } + } + ]) + + if (!financial) { + return res.status(404).json({ message: 'تراکنش مورد نظر یافت نشد' }) + } + const newfinancial = { + ...financial._doc, + createdAt: jMoment(financial.createdAt).format('jYYYY-jMM-jDD HH:mm') + } + return res.status(200).json({ financial: newfinancial }) + } catch (error) { + console.error('Error occurred:', error) + res.status(500).json({ error: 'Internal server error' }) + } +} +module.exports = { getFinancial, getFinancialDetail } diff --git a/controllers/panel/login/loginController.js b/controllers/panel/login/loginController.js index 0d631b2..1b38717 100644 --- a/controllers/panel/login/loginController.js +++ b/controllers/panel/login/loginController.js @@ -1,94 +1,94 @@ -/* eslint-disable camelcase */ -const AdminModel = require('../../../models/AdminModel') -const bcrypt = require('bcryptjs') -const TokenService = require('../../../services/TokenService') - -const adminLogin = async (req, res, next) => { - try { - // اعتبارسنجی ورودی ها - const { user_name, password } = req.body - if (!user_name || !password) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - // یافتن کاربر با نام کاربری - const userLow = user_name.toLowerCase() - const user = await AdminModel.findOne({ user_name: userLow }) - - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این نام کاربری یافت نشد' - }) - } - if (!user.password) { - return res.status(422).json({ - error: true, - message: 'رمز عبور برای این کاربر تنظیم نشده است' - }) - } - const isPasswordValid = await bcrypt.compare(password, user.password) - if (!isPasswordValid) { - return res.status(422).json({ - error: true, - message: 'نام کاربری یا رمز عبور اشتباه است' - }) - } - - // اگر هم نام کاربری و هم رمز عبور درست بود، ارسال پیام موفقیت آمیز - const token = TokenService.sign({ id: user._id }) - return res.json({ - message: 'کد تایید صحیح بود', - token, - id: user._id - }) - } catch (error) { - next(error) - } -} -const changePassword = async (req, res, next) => { - try { - // اعتبارسنجی ورودی ها - const { user_name, password, new_password } = req.body - if (!user_name || !password || !new_password) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - // یافتن کاربر با نام کاربری - const userLow = user_name.toLowerCase() - const user = await AdminModel.findOne({ user_name: userLow }) - - if (!user) { - return res.status(404).json({ - error: true, - message: 'کاربری با این نام کاربری یا رمز عبور یافت نشد' - }) - } - const isPasswordValid = await bcrypt.compare(password, user.password) - if (!isPasswordValid) { - return res.status(422).json({ - error: true, - message: 'نام کاربری یا رمز عبور اشتباه است' - }) - } - - // هش کردن پسورد جدید و ذخیره در دیتابیس - const hashedPassword = await bcrypt.hash(new_password, 10) - user.password = hashedPassword - await user.save() - - return res.json({ - message: 'کلمه عبور با موفقیت ذخیره شد' - }) - } catch (error) { - next(error) - } -} - -module.exports = { - adminLogin, changePassword -} +/* eslint-disable camelcase */ +const AdminModel = require('../../../models/AdminModel') +const bcrypt = require('bcryptjs') +const TokenService = require('../../../services/TokenService') + +const adminLogin = async (req, res, next) => { + try { + // اعتبارسنجی ورودی ها + const { user_name, password } = req.body + if (!user_name || !password) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + // یافتن کاربر با نام کاربری + const userLow = user_name.toLowerCase() + const user = await AdminModel.findOne({ user_name: userLow }) + + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این نام کاربری یافت نشد' + }) + } + if (!user.password) { + return res.status(422).json({ + error: true, + message: 'رمز عبور برای این کاربر تنظیم نشده است' + }) + } + const isPasswordValid = await bcrypt.compare(password, user.password) + if (!isPasswordValid) { + return res.status(422).json({ + error: true, + message: 'نام کاربری یا رمز عبور اشتباه است' + }) + } + + // اگر هم نام کاربری و هم رمز عبور درست بود، ارسال پیام موفقیت آمیز + const token = TokenService.sign({ id: user._id }) + return res.json({ + message: 'کد تایید صحیح بود', + token, + id: user._id + }) + } catch (error) { + next(error) + } +} +const changePassword = async (req, res, next) => { + try { + // اعتبارسنجی ورودی ها + const { user_name, password, new_password } = req.body + if (!user_name || !password || !new_password) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + // یافتن کاربر با نام کاربری + const userLow = user_name.toLowerCase() + const user = await AdminModel.findOne({ user_name: userLow }) + + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری با این نام کاربری یا رمز عبور یافت نشد' + }) + } + const isPasswordValid = await bcrypt.compare(password, user.password) + if (!isPasswordValid) { + return res.status(422).json({ + error: true, + message: 'نام کاربری یا رمز عبور اشتباه است' + }) + } + + // هش کردن پسورد جدید و ذخیره در دیتابیس + const hashedPassword = await bcrypt.hash(new_password, 10) + user.password = hashedPassword + await user.save() + + return res.json({ + message: 'کلمه عبور با موفقیت ذخیره شد' + }) + } catch (error) { + next(error) + } +} + +module.exports = { + adminLogin, changePassword +} diff --git a/controllers/panel/posts/postController.js b/controllers/panel/posts/postController.js index b66fb5c..9f7ff85 100644 --- a/controllers/panel/posts/postController.js +++ b/controllers/panel/posts/postController.js @@ -1,176 +1,176 @@ -/* eslint-disable camelcase */ -const NotificationModel = require('../../../models/NotificationModel') -const PostModel = require('../../../models/PostModel') -const UserModel = require('../../../models/UserModel') -const jMoment = require('moment-jalaali') -const { default: mongoose } = require('mongoose') - -const getPosts = async (req, res, next) => { - try { - const authHeader = req.header('Authorization'); - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ message: 'Access Denied: No token provided' }); - } - const token = authHeader.split(' ')[1]; - - const { - page = 1, - limit = 10, - startDate, - endDate, - status, - } = req.query; - - const filter = {}; - if (startDate || endDate) { - filter.createdAt = {}; - if (startDate) { - filter.createdAt.$gte = new Date(startDate); - } - if (endDate) { - filter.createdAt.$lte = new Date(endDate); - } - } - if (status) { - filter.status = status; - } - - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: { createdAt: -1 }, - populate: { - path: 'user_id', - select: '_id user_name first_name mobile last_name national_code user_type expertise', - }, - }; - - const posts = await PostModel.paginate(filter, options); - const postList = posts.docs.map((post) => ({ - _id: post._id, - post_image: post.post_images && post.post_images.length > 0 ? post.post_images[0] : post.post_video || null, // استفاده از اولین تصویر یا ویدئو - status: post.status, - post_images: post.post_images, - post_video: post.post_video, - type: post.type, - files: post.files, - user_id: post.user_id?._id || null, - user_name: post.user_id?.user_name || '-', - user_type: post.user_id?.user_type || '-', - expertise: post.user_id?.expertise || '-', - first_name: post.user_id?.first_name || '-', - last_name: post.user_id?.last_name || '-', - mobile: post.user_id?.mobile || '-', - national_code: post.user_id?.national_code || '-', - createdAt: post.createdAt ? jMoment(post.createdAt).format('jYYYY-jMM-jDD HH:mm') : '-', - })); - - res.status(200).json({ - posts: postList, - totalPages: posts.totalPages, - totalItems: posts.totalDocs, - }); - } catch (error) { - console.error('Error in getPosts:', error); - res.status(500).json({ message: 'خطا در دریافت پست‌ها.' }); - } -}; -const getPostDetail = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const post_id = req.query.post_id - // eslint-disable-next-line no-unused-vars - - if (!mongoose.Types.ObjectId.isValid(post_id)) { - return res.status(400).json({ message: 'شناسه پست معتبر نیست' }) - } - const post = await PostModel.findById(post_id).populate('user_id', '_id user_name first_name last_name profile_image user_level') - - if (!post) { - return res.status(404).json({ message: 'پست مورد نظر یافت نشد' }) - } - const response = { - _id: post._id, - post_image: post.post_images && post.post_images.length > 0 ? post.post_images[0] : post.post_video || null, // استفاده از اولین تصویر یا ویدئو - status: post.status, - caption: post.caption, - comments: post.comments, - post_images: post.post_images, - post_video: post.post_video, - type: post.type, - files: post.files, - user_id: post.user_id?._id || null, - user_name: post.user_id?.user_name || '-', - user_type: post.user_id?.user_type || '-', - expertise: post.user_id?.expertise || '-', - first_name: post.user_id?.first_name || '-', - last_name: post.user_id?.last_name || '-', - mobile: post.user_id?.mobile || '-', - national_code: post.user_id?.national_code || '-', - createdAt: post.createdAt ? jMoment(post.createdAt).format('jYYYY-jMM-jDD HH:mm') : '-', - } - return res.status(200).json({ post: response }) - } catch (error) { - next(error) - } -} - -const acceptPost = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { id, user_id, status } = req.body - - if (!id || !user_id || !status) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const user = await UserModel.findById(user_id) - const post = await PostModel.findById(id) - - if (!post) { - return res.status(404).json({ - error: true, - message: 'پست مورد نظر یافت نشد' - }) - } - - post.status = status - await post.save() - if (status === 'accept') { - user.last_post = post - await user.save() - - const notification = new NotificationModel({ - user_id: user?._id, - project_post_id: post?._id, - type: 'accept-post', - title: 'انتشار پست', - description: 'پست شما مورد تایید قرار گرفت.' - }) - await notification.save() - } else { - const notification = new NotificationModel({ - user_id: user?._id, - project_post_id: post?._id, - type: 'reject-post', - title: 'رد پست', - description: 'پست شما مورد تایید قرار نگرفت.' - }) - await notification.save() - } - - res.status(201).json({ message: 'پست با موفقیت تایید شد' }) - } catch (error) { - next(error) - } -} - -module.exports = { - acceptPost, getPosts, getPostDetail -} +/* eslint-disable camelcase */ +const NotificationModel = require('../../../models/NotificationModel') +const PostModel = require('../../../models/PostModel') +const UserModel = require('../../../models/UserModel') +const jMoment = require('moment-jalaali') +const { default: mongoose } = require('mongoose') + +const getPosts = async (req, res, next) => { + try { + const authHeader = req.header('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ message: 'Access Denied: No token provided' }); + } + const token = authHeader.split(' ')[1]; + + const { + page = 1, + limit = 10, + startDate, + endDate, + status, + } = req.query; + + const filter = {}; + if (startDate || endDate) { + filter.createdAt = {}; + if (startDate) { + filter.createdAt.$gte = new Date(startDate); + } + if (endDate) { + filter.createdAt.$lte = new Date(endDate); + } + } + if (status) { + filter.status = status; + } + + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: { createdAt: -1 }, + populate: { + path: 'user_id', + select: '_id user_name first_name mobile last_name national_code user_type expertise', + }, + }; + + const posts = await PostModel.paginate(filter, options); + const postList = posts.docs.map((post) => ({ + _id: post._id, + post_image: post.post_images && post.post_images.length > 0 ? post.post_images[0] : post.post_video || null, // استفاده از اولین تصویر یا ویدئو + status: post.status, + post_images: post.post_images, + post_video: post.post_video, + type: post.type, + files: post.files, + user_id: post.user_id?._id || null, + user_name: post.user_id?.user_name || '-', + user_type: post.user_id?.user_type || '-', + expertise: post.user_id?.expertise || '-', + first_name: post.user_id?.first_name || '-', + last_name: post.user_id?.last_name || '-', + mobile: post.user_id?.mobile || '-', + national_code: post.user_id?.national_code || '-', + createdAt: post.createdAt ? jMoment(post.createdAt).format('jYYYY-jMM-jDD HH:mm') : '-', + })); + + res.status(200).json({ + posts: postList, + totalPages: posts.totalPages, + totalItems: posts.totalDocs, + }); + } catch (error) { + console.error('Error in getPosts:', error); + res.status(500).json({ message: 'خطا در دریافت پست‌ها.' }); + } +}; +const getPostDetail = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const post_id = req.query.post_id + // eslint-disable-next-line no-unused-vars + + if (!mongoose.Types.ObjectId.isValid(post_id)) { + return res.status(400).json({ message: 'شناسه پست معتبر نیست' }) + } + const post = await PostModel.findById(post_id).populate('user_id', '_id user_name first_name last_name profile_image user_level') + + if (!post) { + return res.status(404).json({ message: 'پست مورد نظر یافت نشد' }) + } + const response = { + _id: post._id, + post_image: post.post_images && post.post_images.length > 0 ? post.post_images[0] : post.post_video || null, // استفاده از اولین تصویر یا ویدئو + status: post.status, + caption: post.caption, + comments: post.comments, + post_images: post.post_images, + post_video: post.post_video, + type: post.type, + files: post.files, + user_id: post.user_id?._id || null, + user_name: post.user_id?.user_name || '-', + user_type: post.user_id?.user_type || '-', + expertise: post.user_id?.expertise || '-', + first_name: post.user_id?.first_name || '-', + last_name: post.user_id?.last_name || '-', + mobile: post.user_id?.mobile || '-', + national_code: post.user_id?.national_code || '-', + createdAt: post.createdAt ? jMoment(post.createdAt).format('jYYYY-jMM-jDD HH:mm') : '-', + } + return res.status(200).json({ post: response }) + } catch (error) { + next(error) + } +} + +const acceptPost = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { id, user_id, status } = req.body + + if (!id || !user_id || !status) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const user = await UserModel.findById(user_id) + const post = await PostModel.findById(id) + + if (!post) { + return res.status(404).json({ + error: true, + message: 'پست مورد نظر یافت نشد' + }) + } + + post.status = status + await post.save() + if (status === 'accept') { + user.last_post = post + await user.save() + + const notification = new NotificationModel({ + user_id: user?._id, + project_post_id: post?._id, + type: 'accept-post', + title: 'انتشار پست', + description: 'پست شما مورد تایید قرار گرفت.' + }) + await notification.save() + } else { + const notification = new NotificationModel({ + user_id: user?._id, + project_post_id: post?._id, + type: 'reject-post', + title: 'رد پست', + description: 'پست شما مورد تایید قرار نگرفت.' + }) + await notification.save() + } + + res.status(201).json({ message: 'پست با موفقیت تایید شد' }) + } catch (error) { + next(error) + } +} + +module.exports = { + acceptPost, getPosts, getPostDetail +} diff --git a/controllers/panel/projects/projectController.js b/controllers/panel/projects/projectController.js index 1a0cc0c..6ae52a9 100644 --- a/controllers/panel/projects/projectController.js +++ b/controllers/panel/projects/projectController.js @@ -1,256 +1,256 @@ -/* eslint-disable camelcase */ -const ProjectModel = require('../../../models/ProjectModel') -const jMoment = require('moment-jalaali') -const RequestModel = require('../../../models/RequestModel') -const NotificationModel = require('../../../models/NotificationModel') - -const getProjects = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - // eslint-disable-next-line no-unused-vars - const { - page = 1, limit = 10, - startDate, - endDate, - status - } = req.query - // ساخت فیلتر برای استفاده در جستجوی MongoDB - const filter = { - } // افزودن شرط‌های دیگر برای فیلتر - // Date - if (startDate || endDate) { - filter.createdAt = {} - - if (startDate) { - filter.createdAt.$gte = new Date(startDate) // تاریخ شروع - } - - if (endDate) { - filter.createdAt.$lte = new Date(endDate) // تاریخ پایان - } - } - - if (status) { - filter.status = status - } - - // دریافت لیست پروژه‌ها با استفاده از فیلتر و گزینه‌های صفحه‌بندی - const options = { - page: parseInt(page), // تبدیل صفحه به عدد صحیح - limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح - sort: { createdAt: -1 }, - populate: [ - { path: 'creator_id', select: '_id user_name first_name last_name' }, - { path: 'selected_user', select: '_id user_name first_name last_name' } - ] - } - const projects = await ProjectModel.paginate(filter, options) - const projectsList = projects.docs.map(project => ({ - _id: project._id, - title: project.title, - expertise: project.expertise, - sub_expertise: project.sub_expertise, - offer_time: project.offer_time, - offer_price: project.offer_price, - final_price: project.final_price, - final_time: project.final_time, - createdAt: jMoment(project.createdAt).format('jYYYY-jMM-jDD HH:mm'), - status: project.status, - creator: project.creator_id - ? { - _id: project.creator_id._id, - user_name: project.creator_id.user_name, - first_name: project.creator_id.first_name, - last_name: project.creator_id.last_name - } - : null, - selected_user: project.selected_user - ? { - _id: project.selected_user._id, - user_name: project.selected_user.user_name, - first_name: project.selected_user.first_name, - last_name: project.selected_user.last_name - } - : null - })) - - res.status(200).json({ - projects: projectsList, - totalPages: projects.totalPages, // ارسال تعداد کل صفحات - totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - next(error) - } -} -const getProjectDetails = async (req, res, next) => { - try { - const { project_id } = req.query - - // بررسی وجود projectId - if (!project_id) { - return res.status(400).send('Project ID is required') - } - - // یافتن پروژه و جمع‌آوری اطلاعات مرتبط با سازنده و کاربر انتخاب شده - const project = await ProjectModel.findById(project_id) - .populate('creator_id', '_id user_name first_name last_name mobile') - .populate('selected_user', '_id user_name first_name last_name mobile') - .lean() - - // بررسی وجود پروژه - if (!project) { - return res.status(404).send('Project not found') - } - - // دریافت لیست درخواست‌های پروژه - const projectRequests = await RequestModel.find({ project: project_id }) - .populate('user', '_id first_name last_name user_name rate profile_image') - .sort({ status: 1 }) - .lean() - - // ساختاردهی پاسخ نهایی - const projectDetails = { - _id: project._id, - title: project.title, - description: project.description, - creator: project.creator_id - ? { - _id: project.creator_id._id, - first_name: project.creator_id.first_name, - last_name: project.creator_id.last_name, - user_name: project.creator_id.user_name, - mobile: project.creator_id.mobile - } - : null, - selected_user: project.selected_user - ? { - _id: project.selected_user._id, - first_name: project.selected_user.first_name, - last_name: project.creator_id.last_name, - user_name: project.selected_user.user_name, - mobile: project.selected_user.mobile - } - : null, - province: project.province, - city: project.city, - createdAt: jMoment(project.createdAt).format('jYYYY-jMM-jDD HH:mm'), - expertise: project.expertise, - sub_expertise: project.sub_expertise, - offer_time: project.offer_time, - offer_price: project.offer_price, - final_price: project.final_price, - final_time: project.final_time, - status: project.status, - reject_reason: project.reject_reason, - requested_users: projectRequests.map(request => ({ - _id: request.user._id, - first_name: request.user.first_name, - last_name: request.user.last_name, - user_name: request.user.user_name, - profile_image: request.user.profile_image, - time: request.time, - price: request.price - })) - } - - res.status(200).json({ project: projectDetails }) - } catch (error) { - next(error) - } -} -const acceptProject = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { id, user_name } = req.body - - if (!id) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const project = await ProjectModel.findById(id) - - if (!project) { - return res.status(404).json({ - error: true, - message: 'پست مورد نظر یافت نشد' - }) - } - project.acceptedAt = new Date() - project.status = 'accepted' - await project.save() - const notification = new NotificationModel({ - user_id: project?.creator_id, - project_post_id: project?._id, - type: 'accept-project', - title: 'انتشار درخواست', - description: `درخواست شما با عنوان: ${project?.title} منتشر شد ` - }) - await notification.save() - if (project?.public_status === 'private' && project?.created_for_user) { - const notification = new NotificationModel({ - user_id: project?.created_for_user, - project_post_id: project?._id, - type: 'recive-project', - title: 'دعوت به همکاری', - description: `کاربر ${user_name} برای شما پروژه ایجاد کرده است ، برای نمایش جزئیات لمس کنید` - }) - await notification.save() - } - res.status(201).json({ message: 'پروژه با موفقیت تایید شد' }) - } catch (error) { - next(error) - } -} -const rejectProject = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { id, rejectReason } = req.body - - if (!id) { - return res.status(422).json({ - error: true, - message: 'اطلاعات ارسالی اشتباه است' - }) - } - - const project = await ProjectModel.findById(id) - - if (!project) { - return res.status(404).json({ - error: true, - message: 'پست مورد نظر یافت نشد' - }) - } - - project.status = 'rejected' - project.reject_reason = rejectReason - await project.save() - const notification = new NotificationModel({ - user_id: project?.creator_id, - project_post_id: project?._id, - type: 'reject-project', - title: 'رد درخواست', - description: `درخواست شما با عنوان: ${project?.title} رد شد، برای دیدن علت رد، لمس کنید ` - }) - await notification.save() - res.status(201).json({ message: 'پروژه با موفقیت رد شد' }) - } catch (error) { - next(error) - } -} -module.exports = { - getProjects, - getProjectDetails, - acceptProject, - rejectProject -} +/* eslint-disable camelcase */ +const ProjectModel = require('../../../models/ProjectModel') +const jMoment = require('moment-jalaali') +const RequestModel = require('../../../models/RequestModel') +const NotificationModel = require('../../../models/NotificationModel') + +const getProjects = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + // eslint-disable-next-line no-unused-vars + const { + page = 1, limit = 10, + startDate, + endDate, + status + } = req.query + // ساخت فیلتر برای استفاده در جستجوی MongoDB + const filter = { + } // افزودن شرط‌های دیگر برای فیلتر + // Date + if (startDate || endDate) { + filter.createdAt = {} + + if (startDate) { + filter.createdAt.$gte = new Date(startDate) // تاریخ شروع + } + + if (endDate) { + filter.createdAt.$lte = new Date(endDate) // تاریخ پایان + } + } + + if (status) { + filter.status = status + } + + // دریافت لیست پروژه‌ها با استفاده از فیلتر و گزینه‌های صفحه‌بندی + const options = { + page: parseInt(page), // تبدیل صفحه به عدد صحیح + limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح + sort: { createdAt: -1 }, + populate: [ + { path: 'creator_id', select: '_id user_name first_name last_name' }, + { path: 'selected_user', select: '_id user_name first_name last_name' } + ] + } + const projects = await ProjectModel.paginate(filter, options) + const projectsList = projects.docs.map(project => ({ + _id: project._id, + title: project.title, + expertise: project.expertise, + sub_expertise: project.sub_expertise, + offer_time: project.offer_time, + offer_price: project.offer_price, + final_price: project.final_price, + final_time: project.final_time, + createdAt: jMoment(project.createdAt).format('jYYYY-jMM-jDD HH:mm'), + status: project.status, + creator: project.creator_id + ? { + _id: project.creator_id._id, + user_name: project.creator_id.user_name, + first_name: project.creator_id.first_name, + last_name: project.creator_id.last_name + } + : null, + selected_user: project.selected_user + ? { + _id: project.selected_user._id, + user_name: project.selected_user.user_name, + first_name: project.selected_user.first_name, + last_name: project.selected_user.last_name + } + : null + })) + + res.status(200).json({ + projects: projectsList, + totalPages: projects.totalPages, // ارسال تعداد کل صفحات + totalItems: projects.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + next(error) + } +} +const getProjectDetails = async (req, res, next) => { + try { + const { project_id } = req.query + + // بررسی وجود projectId + if (!project_id) { + return res.status(400).send('Project ID is required') + } + + // یافتن پروژه و جمع‌آوری اطلاعات مرتبط با سازنده و کاربر انتخاب شده + const project = await ProjectModel.findById(project_id) + .populate('creator_id', '_id user_name first_name last_name mobile') + .populate('selected_user', '_id user_name first_name last_name mobile') + .lean() + + // بررسی وجود پروژه + if (!project) { + return res.status(404).send('Project not found') + } + + // دریافت لیست درخواست‌های پروژه + const projectRequests = await RequestModel.find({ project: project_id }) + .populate('user', '_id first_name last_name user_name rate profile_image') + .sort({ status: 1 }) + .lean() + + // ساختاردهی پاسخ نهایی + const projectDetails = { + _id: project._id, + title: project.title, + description: project.description, + creator: project.creator_id + ? { + _id: project.creator_id._id, + first_name: project.creator_id.first_name, + last_name: project.creator_id.last_name, + user_name: project.creator_id.user_name, + mobile: project.creator_id.mobile + } + : null, + selected_user: project.selected_user + ? { + _id: project.selected_user._id, + first_name: project.selected_user.first_name, + last_name: project.creator_id.last_name, + user_name: project.selected_user.user_name, + mobile: project.selected_user.mobile + } + : null, + province: project.province, + city: project.city, + createdAt: jMoment(project.createdAt).format('jYYYY-jMM-jDD HH:mm'), + expertise: project.expertise, + sub_expertise: project.sub_expertise, + offer_time: project.offer_time, + offer_price: project.offer_price, + final_price: project.final_price, + final_time: project.final_time, + status: project.status, + reject_reason: project.reject_reason, + requested_users: projectRequests.map(request => ({ + _id: request.user._id, + first_name: request.user.first_name, + last_name: request.user.last_name, + user_name: request.user.user_name, + profile_image: request.user.profile_image, + time: request.time, + price: request.price + })) + } + + res.status(200).json({ project: projectDetails }) + } catch (error) { + next(error) + } +} +const acceptProject = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { id, user_name } = req.body + + if (!id) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const project = await ProjectModel.findById(id) + + if (!project) { + return res.status(404).json({ + error: true, + message: 'پست مورد نظر یافت نشد' + }) + } + project.acceptedAt = new Date() + project.status = 'accepted' + await project.save() + const notification = new NotificationModel({ + user_id: project?.creator_id, + project_post_id: project?._id, + type: 'accept-project', + title: 'انتشار درخواست', + description: `درخواست شما با عنوان: ${project?.title} منتشر شد ` + }) + await notification.save() + if (project?.public_status === 'private' && project?.created_for_user) { + const notification = new NotificationModel({ + user_id: project?.created_for_user, + project_post_id: project?._id, + type: 'recive-project', + title: 'دعوت به همکاری', + description: `کاربر ${user_name} برای شما پروژه ایجاد کرده است ، برای نمایش جزئیات لمس کنید` + }) + await notification.save() + } + res.status(201).json({ message: 'پروژه با موفقیت تایید شد' }) + } catch (error) { + next(error) + } +} +const rejectProject = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { id, rejectReason } = req.body + + if (!id) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ارسالی اشتباه است' + }) + } + + const project = await ProjectModel.findById(id) + + if (!project) { + return res.status(404).json({ + error: true, + message: 'پست مورد نظر یافت نشد' + }) + } + + project.status = 'rejected' + project.reject_reason = rejectReason + await project.save() + const notification = new NotificationModel({ + user_id: project?.creator_id, + project_post_id: project?._id, + type: 'reject-project', + title: 'رد درخواست', + description: `درخواست شما با عنوان: ${project?.title} رد شد، برای دیدن علت رد، لمس کنید ` + }) + await notification.save() + res.status(201).json({ message: 'پروژه با موفقیت رد شد' }) + } catch (error) { + next(error) + } +} +module.exports = { + getProjects, + getProjectDetails, + acceptProject, + rejectProject +} diff --git a/controllers/panel/projects/projectTypesController.js b/controllers/panel/projects/projectTypesController.js index 784712e..5f50f47 100644 --- a/controllers/panel/projects/projectTypesController.js +++ b/controllers/panel/projects/projectTypesController.js @@ -1,63 +1,63 @@ -const TypeAndPriceModel = require('../../../models/TypeAndPriceModel') -const jwt = require('jsonwebtoken') -const UserModel = require('../../../models/UserModel') - -const getProjectTypes = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET) - const userId = decodedToken.id - - const user = await UserModel.findById(userId) - if (!user) { - return res.status(422).json({ - error: true, - message: 'کاربر یافت نشد' - }) - } - // درخواست انواع پروژه از دیتابیس - let projectTypes = await TypeAndPriceModel.find({}, 'name price') - - // بررسی برای مواردی مانند پیدا نشدن انواع پروژه - if (!projectTypes) { - return res.status(404).json({ message: 'انواع پروژه یافت نشد.' }) - } - // اگر کاربر درخواست رایگان روزانه نداشته باشد، نوع پروژه رایگان را حذف کنید - if (user.daily_free_request <= 0) { - projectTypes = projectTypes.filter(projectType => projectType.name !== 'free') - } - // ارسال انواع پروژه به کاربر - res.status(200).json({ projectTypes }) - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error('Error in getProjectTypes:', error) - res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) - } -} - -const createProjectTypes = async (req, res, next) => { - try { - const { name, price } = req.body - // بررسی اعتبار ورودی‌ها - if (!name || price == null || price === undefined) { - return res.status(400).json({ message: 'لطفا نام و قیمت پروژه را وارد کنید.' }) - } - - // ایجاد نوع پروژه جدید - const newType = new TypeAndPriceModel({ name, price }) - await newType.save() - - // ارسال پیام موفقیت آمیز به کاربر - res.status(201).json({ message: 'نوع پروژه جدید با موفقیت ایجاد شد.' }) - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error('Error in createProjectTypes:', error) - res.status(500).json({ message: 'خطا در ایجاد نوع پروژه.' }) - } -} - -module.exports = { - getProjectTypes, - createProjectTypes -} +const TypeAndPriceModel = require('../../../models/TypeAndPriceModel') +const jwt = require('jsonwebtoken') +const UserModel = require('../../../models/UserModel') + +const getProjectTypes = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + + const user = await UserModel.findById(userId) + if (!user) { + return res.status(422).json({ + error: true, + message: 'کاربر یافت نشد' + }) + } + // درخواست انواع پروژه از دیتابیس + let projectTypes = await TypeAndPriceModel.find({}, 'name price') + + // بررسی برای مواردی مانند پیدا نشدن انواع پروژه + if (!projectTypes) { + return res.status(404).json({ message: 'انواع پروژه یافت نشد.' }) + } + // اگر کاربر درخواست رایگان روزانه نداشته باشد، نوع پروژه رایگان را حذف کنید + if (user.daily_free_request <= 0) { + projectTypes = projectTypes.filter(projectType => projectType.name !== 'free') + } + // ارسال انواع پروژه به کاربر + res.status(200).json({ projectTypes }) + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error('Error in getProjectTypes:', error) + res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) + } +} + +const createProjectTypes = async (req, res, next) => { + try { + const { name, price } = req.body + // بررسی اعتبار ورودی‌ها + if (!name || price == null || price === undefined) { + return res.status(400).json({ message: 'لطفا نام و قیمت پروژه را وارد کنید.' }) + } + + // ایجاد نوع پروژه جدید + const newType = new TypeAndPriceModel({ name, price }) + await newType.save() + + // ارسال پیام موفقیت آمیز به کاربر + res.status(201).json({ message: 'نوع پروژه جدید با موفقیت ایجاد شد.' }) + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error('Error in createProjectTypes:', error) + res.status(500).json({ message: 'خطا در ایجاد نوع پروژه.' }) + } +} + +module.exports = { + getProjectTypes, + createProjectTypes +} diff --git a/controllers/panel/settings/cdnController.js b/controllers/panel/settings/cdnController.js index 3d4cee0..dbdd376 100644 --- a/controllers/panel/settings/cdnController.js +++ b/controllers/panel/settings/cdnController.js @@ -1,211 +1,211 @@ -const { exec } = require('child_process') -const fs = require('fs') -const path = require('path') -const SettingModel = require('../../../models/SettingsModel') - -const CDN_KEYS = [ - 'cdn_enabled', - 'cdn_domain', - 'cdn_origin_url', - 'cdn_ns1', - 'cdn_ns2', - 'cdn_cache_ttl', - 'ssl_enabled', - 'ssl_auto_renew', - 'ssl_status', - 'ssl_expires_at', - 'rate_limit_requests', - 'rate_limit_window_minutes', -] - -const DEFAULT_CDN_SETTINGS = { - cdn_enabled: 'false', - cdn_domain: 'cdn.modstagram.com', - cdn_origin_url: 'https://api.modstagram.com', - cdn_ns1: 'ns1.modstagram.com', - cdn_ns2: 'ns2.modstagram.com', - cdn_cache_ttl: '3600', - ssl_enabled: 'false', - ssl_auto_renew: 'true', - ssl_status: 'none', - ssl_expires_at: '', - rate_limit_requests: '3000', - rate_limit_window_minutes: '3', -} - -const upsertSetting = async (key, value) => { - await SettingModel.findOneAndUpdate( - { key }, - { value: String(value) }, - { new: true, upsert: true } - ) -} - -const getSettingsMap = async () => { - const settings = await SettingModel.find({ key: { $in: CDN_KEYS } }) - const map = { ...DEFAULT_CDN_SETTINGS } - settings.forEach((item) => { - map[item.key] = item.value - }) - return map -} - -const getCdnSettings = async (req, res, next) => { - try { - const settings = await getSettingsMap() - res.status(200).json({ settings }) - } catch (error) { - next(error) - } -} - -const updateCdnSettings = async (req, res, next) => { - try { - const { settings } = req.body - if (!settings || typeof settings !== 'object') { - return res.status(400).json({ message: 'داده‌های تنظیمات نامعتبر است' }) - } - - const updates = Object.entries(settings).filter(([key]) => CDN_KEYS.includes(key)) - await Promise.all(updates.map(([key, value]) => upsertSetting(key, value))) - - res.status(200).json({ - message: 'تنظیمات CDN با موفقیت ذخیره شد', - settings: await getSettingsMap(), - }) - } catch (error) { - next(error) - } -} - -const readCertInfo = (domain) => - new Promise((resolve) => { - const certPath = - process.env.SSL_CERT_PATH || - `/etc/letsencrypt/live/${domain}/fullchain.pem` - - if (!fs.existsSync(certPath)) { - resolve({ found: false, certPath }) - return - } - - exec( - `openssl x509 -in "${certPath}" -noout -enddate`, - { timeout: 10000 }, - (error, stdout) => { - if (error) { - resolve({ found: true, certPath, valid: false }) - return - } - - const match = stdout.match(/notAfter=(.+)/) - const expiresAt = match ? new Date(match[1].trim()).toISOString() : null - const valid = expiresAt ? new Date(expiresAt) > new Date() : false - resolve({ found: true, certPath, valid, expiresAt }) - } - ) - }) - -const getSslStatus = async (req, res, next) => { - try { - const settings = await getSettingsMap() - const domain = settings.cdn_domain - const certInfo = await readCertInfo(domain) - - let status = settings.ssl_status - if (certInfo.found && certInfo.valid) { - status = 'active' - if (certInfo.expiresAt) { - await upsertSetting('ssl_expires_at', certInfo.expiresAt) - await upsertSetting('ssl_status', 'active') - await upsertSetting('ssl_enabled', 'true') - } - } else if (certInfo.found && !certInfo.valid) { - status = 'expired' - } - - res.status(200).json({ - domain, - status, - ssl_enabled: settings.ssl_enabled === 'true', - ssl_auto_renew: settings.ssl_auto_renew === 'true', - expires_at: certInfo.expiresAt || settings.ssl_expires_at || null, - cert_path: certInfo.certPath, - cert_found: certInfo.found, - }) - } catch (error) { - next(error) - } -} - -const activateSsl = async (req, res, next) => { - try { - const settings = await getSettingsMap() - const domain = req.body?.domain || settings.cdn_domain - const email = req.body?.email || process.env.SSL_ADMIN_EMAIL || 'admin@modstagram.com' - - if (!domain) { - return res.status(400).json({ message: 'دامنه CDN تنظیم نشده است' }) - } - - await upsertSetting('ssl_status', 'pending') - - const scriptPath = path.join(__dirname, '../../../scripts/cdn-ssl.sh') - const command = `bash "${scriptPath}" "${domain}" "${email}"` - - exec(command, { timeout: 180000 }, async (error, stdout, stderr) => { - if (error) { - await upsertSetting('ssl_status', 'failed') - return res.status(500).json({ - message: 'خطا در فعال‌سازی SSL', - details: stderr || error.message, - output: stdout, - }) - } - - const certInfo = await readCertInfo(domain) - await upsertSetting('ssl_enabled', 'true') - await upsertSetting('ssl_status', certInfo.valid ? 'active' : 'pending') - if (certInfo.expiresAt) { - await upsertSetting('ssl_expires_at', certInfo.expiresAt) - } - - return res.status(200).json({ - message: 'SSL رایگان با موفقیت فعال شد', - status: certInfo.valid ? 'active' : 'pending', - expires_at: certInfo.expiresAt || null, - output: stdout, - }) - }) - } catch (error) { - next(error) - } -} - -const getPublicCdnConfig = async (req, res, next) => { - try { - const settings = await getSettingsMap() - const cdnEnabled = settings.cdn_enabled === 'true' - const cdnDomain = settings.cdn_domain - const originUrl = settings.cdn_origin_url - - res.status(200).json({ - cdn_enabled: cdnEnabled, - cdn_domain: cdnDomain, - image_base_url: cdnEnabled - ? `https://${cdnDomain.replace(/^https?:\/\//, '')}/storage` - : `${originUrl}/storage`, - ssl_enabled: settings.ssl_enabled === 'true', - }) - } catch (error) { - next(error) - } -} - -module.exports = { - getCdnSettings, - updateCdnSettings, - getSslStatus, - activateSsl, - getPublicCdnConfig, -} +const { exec } = require('child_process') +const fs = require('fs') +const path = require('path') +const SettingModel = require('../../../models/SettingsModel') + +const CDN_KEYS = [ + 'cdn_enabled', + 'cdn_domain', + 'cdn_origin_url', + 'cdn_ns1', + 'cdn_ns2', + 'cdn_cache_ttl', + 'ssl_enabled', + 'ssl_auto_renew', + 'ssl_status', + 'ssl_expires_at', + 'rate_limit_requests', + 'rate_limit_window_minutes', +] + +const DEFAULT_CDN_SETTINGS = { + cdn_enabled: 'false', + cdn_domain: 'cdn.modstagram.com', + cdn_origin_url: 'https://api.modstagram.com', + cdn_ns1: 'ns1.modstagram.com', + cdn_ns2: 'ns2.modstagram.com', + cdn_cache_ttl: '3600', + ssl_enabled: 'false', + ssl_auto_renew: 'true', + ssl_status: 'none', + ssl_expires_at: '', + rate_limit_requests: '3000', + rate_limit_window_minutes: '3', +} + +const upsertSetting = async (key, value) => { + await SettingModel.findOneAndUpdate( + { key }, + { value: String(value) }, + { new: true, upsert: true } + ) +} + +const getSettingsMap = async () => { + const settings = await SettingModel.find({ key: { $in: CDN_KEYS } }) + const map = { ...DEFAULT_CDN_SETTINGS } + settings.forEach((item) => { + map[item.key] = item.value + }) + return map +} + +const getCdnSettings = async (req, res, next) => { + try { + const settings = await getSettingsMap() + res.status(200).json({ settings }) + } catch (error) { + next(error) + } +} + +const updateCdnSettings = async (req, res, next) => { + try { + const { settings } = req.body + if (!settings || typeof settings !== 'object') { + return res.status(400).json({ message: 'داده‌های تنظیمات نامعتبر است' }) + } + + const updates = Object.entries(settings).filter(([key]) => CDN_KEYS.includes(key)) + await Promise.all(updates.map(([key, value]) => upsertSetting(key, value))) + + res.status(200).json({ + message: 'تنظیمات CDN با موفقیت ذخیره شد', + settings: await getSettingsMap(), + }) + } catch (error) { + next(error) + } +} + +const readCertInfo = (domain) => + new Promise((resolve) => { + const certPath = + process.env.SSL_CERT_PATH || + `/etc/letsencrypt/live/${domain}/fullchain.pem` + + if (!fs.existsSync(certPath)) { + resolve({ found: false, certPath }) + return + } + + exec( + `openssl x509 -in "${certPath}" -noout -enddate`, + { timeout: 10000 }, + (error, stdout) => { + if (error) { + resolve({ found: true, certPath, valid: false }) + return + } + + const match = stdout.match(/notAfter=(.+)/) + const expiresAt = match ? new Date(match[1].trim()).toISOString() : null + const valid = expiresAt ? new Date(expiresAt) > new Date() : false + resolve({ found: true, certPath, valid, expiresAt }) + } + ) + }) + +const getSslStatus = async (req, res, next) => { + try { + const settings = await getSettingsMap() + const domain = settings.cdn_domain + const certInfo = await readCertInfo(domain) + + let status = settings.ssl_status + if (certInfo.found && certInfo.valid) { + status = 'active' + if (certInfo.expiresAt) { + await upsertSetting('ssl_expires_at', certInfo.expiresAt) + await upsertSetting('ssl_status', 'active') + await upsertSetting('ssl_enabled', 'true') + } + } else if (certInfo.found && !certInfo.valid) { + status = 'expired' + } + + res.status(200).json({ + domain, + status, + ssl_enabled: settings.ssl_enabled === 'true', + ssl_auto_renew: settings.ssl_auto_renew === 'true', + expires_at: certInfo.expiresAt || settings.ssl_expires_at || null, + cert_path: certInfo.certPath, + cert_found: certInfo.found, + }) + } catch (error) { + next(error) + } +} + +const activateSsl = async (req, res, next) => { + try { + const settings = await getSettingsMap() + const domain = req.body?.domain || settings.cdn_domain + const email = req.body?.email || process.env.SSL_ADMIN_EMAIL || 'admin@modstagram.com' + + if (!domain) { + return res.status(400).json({ message: 'دامنه CDN تنظیم نشده است' }) + } + + await upsertSetting('ssl_status', 'pending') + + const scriptPath = path.join(__dirname, '../../../scripts/cdn-ssl.sh') + const command = `bash "${scriptPath}" "${domain}" "${email}"` + + exec(command, { timeout: 180000 }, async (error, stdout, stderr) => { + if (error) { + await upsertSetting('ssl_status', 'failed') + return res.status(500).json({ + message: 'خطا در فعال‌سازی SSL', + details: stderr || error.message, + output: stdout, + }) + } + + const certInfo = await readCertInfo(domain) + await upsertSetting('ssl_enabled', 'true') + await upsertSetting('ssl_status', certInfo.valid ? 'active' : 'pending') + if (certInfo.expiresAt) { + await upsertSetting('ssl_expires_at', certInfo.expiresAt) + } + + return res.status(200).json({ + message: 'SSL رایگان با موفقیت فعال شد', + status: certInfo.valid ? 'active' : 'pending', + expires_at: certInfo.expiresAt || null, + output: stdout, + }) + }) + } catch (error) { + next(error) + } +} + +const getPublicCdnConfig = async (req, res, next) => { + try { + const settings = await getSettingsMap() + const cdnEnabled = settings.cdn_enabled === 'true' + const cdnDomain = settings.cdn_domain + const originUrl = settings.cdn_origin_url + + res.status(200).json({ + cdn_enabled: cdnEnabled, + cdn_domain: cdnDomain, + image_base_url: cdnEnabled + ? `https://${cdnDomain.replace(/^https?:\/\//, '')}/storage` + : `${originUrl}/storage`, + ssl_enabled: settings.ssl_enabled === 'true', + }) + } catch (error) { + next(error) + } +} + +module.exports = { + getCdnSettings, + updateCdnSettings, + getSslStatus, + activateSsl, + getPublicCdnConfig, +} diff --git a/controllers/panel/settings/settingsController.js b/controllers/panel/settings/settingsController.js index 5bab1eb..b6ee8bc 100644 --- a/controllers/panel/settings/settingsController.js +++ b/controllers/panel/settings/settingsController.js @@ -1,36 +1,36 @@ -// controllers/settingController.js - -const SettingModel = require('../../../models/SettingsModel') - -// متد برای دریافت تنظیمات بر اساس key -const getSetting = async (req, res, next) => { - try { - const { key } = req.params - const setting = await SettingModel.findOne({ key }) - if (!setting) { - return res.status(404).json({ message: 'تنظیمات مورد نظر یافت نشد' }) - } - res.status(200).json(setting) - } catch (error) { - next(error) - } -} - -// متد برای به‌روزرسانی تنظیمات بر اساس key -const updateSetting = async (req, res, next) => { - try { - const { key } = req.params - const { value } = req.body - // eslint-disable-next-line no-unused-vars - const setting = await SettingModel.findOneAndUpdate( - { key }, - { value }, - { new: true, upsert: true } // اگر وجود نداشت، آن را ایجاد کن - ) - res.status(200).json({ message: 'تنظیمات با موفقیت بروز شد' }) - } catch (error) { - next(error) - } -} - -module.exports = { getSetting, updateSetting } +// controllers/settingController.js + +const SettingModel = require('../../../models/SettingsModel') + +// متد برای دریافت تنظیمات بر اساس key +const getSetting = async (req, res, next) => { + try { + const { key } = req.params + const setting = await SettingModel.findOne({ key }) + if (!setting) { + return res.status(404).json({ message: 'تنظیمات مورد نظر یافت نشد' }) + } + res.status(200).json(setting) + } catch (error) { + next(error) + } +} + +// متد برای به‌روزرسانی تنظیمات بر اساس key +const updateSetting = async (req, res, next) => { + try { + const { key } = req.params + const { value } = req.body + // eslint-disable-next-line no-unused-vars + const setting = await SettingModel.findOneAndUpdate( + { key }, + { value }, + { new: true, upsert: true } // اگر وجود نداشت، آن را ایجاد کن + ) + res.status(200).json({ message: 'تنظیمات با موفقیت بروز شد' }) + } catch (error) { + next(error) + } +} + +module.exports = { getSetting, updateSetting } diff --git a/controllers/panel/shops/shopsController.js b/controllers/panel/shops/shopsController.js index cb471c9..b9bb591 100644 --- a/controllers/panel/shops/shopsController.js +++ b/controllers/panel/shops/shopsController.js @@ -1,161 +1,161 @@ -/* eslint-disable camelcase */ -const { default: mongoose } = require('mongoose') -const jMoment = require('moment-jalaali') -const AdvertisingProfileModel = require('../../../models/AdvertisingProfile') -const AdvertisingModel = require('../../../models/AdvertisingModel') - -const getShops = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - // eslint-disable-next-line no-unused-vars - const { page = 1, limit = 10, expertise, most_price, most_requests, age, gender } = req.query - const filter = {} // افزودن شرط‌های دیگر برای فیلتر - - // if (expertise) { - // filter.expertise = expertise - // } - // if (age) { - // filter.age = age - // } - // if (gender) { - // filter.gender = gender - // } - // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده - const options = { - page: parseInt(page), // تبدیل صفحه به عدد صحیح - limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح - sort: { createdAt: -1 }, - populate: [ - { path: 'user', select: '_id user_name first_name last_name' } - ] - } - const shops = await AdvertisingProfileModel.paginate(filter, options) - const newShops = shops.docs.map(user => ({ - _id: user._id, - user_name: user?.user?.user_name, - first_name: user?.user?.first_name, - last_name: user?.user?.last_name, - user_id: user?.user?._id, - profile_image: user.profile_image, - category: user.category, - vitrine_name: user.vitrine_name, - province: user.province, - city: user.city, - neighbourhood: user.neighbourhood, - createdAt: jMoment(user.createdAt).format('jYYYY-jMM-jDD HH:mm') - })) - res.status(200).json({ - shops: newShops, - totalPages: shops.totalPages, // ارسال تعداد کل صفحات - totalItems: shops.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error('Error in getProjectTypes:', error) - res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) - } -} -const getShopDetail = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const profile_id = req.query.profile_id - // eslint-disable-next-line no-unused-vars - // دریافت جزئیات فروشگاه با استفاده از profile_id - const shop = await AdvertisingProfileModel.findById(profile_id) - .populate('user', '_id user_name first_name last_name') - .exec() - const userAds = await AdvertisingModel.find({ creator_id: shop?.user }) - // جمع‌آوری اطلاعات از تبلیغات‌ها - let allImages = [] - let allServices = [] - let allFeatures = [] - for (const ad of userAds) { - allImages = allImages.concat(ad.images) - allServices = allServices.concat(ad.services) - allFeatures = allFeatures.concat(ad.features) - } - // حذف آیتم‌های null و تکراری از allFeatures - const uniqueFeatures = [] - const featureTitles = new Set() - for (const feature of allFeatures) { - if (feature && !featureTitles.has(feature.title)) { - uniqueFeatures.push(feature) - featureTitles.add(feature.title) - } - } - if (!shop) { - return res.status(404).json({ message: 'فروشگاه مورد نظر یافت نشد' }) - } - const response = { - _id: shop?._id, - user_name: shop?.user?.user_name, - first_name: shop?.user?.first_name, - last_name: shop?.user?.last_name, - user_id: shop?.user?._id, - profile_image: shop?.profile_image, - category: shop?.category, - vitrine_name: shop?.vitrine_name, - province: shop?.province, - city: shop?.city, - neighbourhood: shop?.neighbourhood, - contactInfo: shop?.contactInfo, - address: shop?.address, - allImages: allImages?.reverse(), - allServices, - allFeatures: uniqueFeatures, - about_us: shop?.about_us - // اطلاعات اضافی که بعداً به شما می‌گویم - } - return res.status(200).json({ shop: response }) - } catch (error) { - next(error) - } -} -const updateShop = async (req, res, next) => { - try { - const shopId = req.query.shopId // دریافت شناسه کاربر از درخواست - - // چک کردن اعتبار شناسه کاربر - if (!mongoose.Types.ObjectId.isValid(shopId)) { - return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) - } - - // فیلدهایی که می‌خواهید آپدیت شوند را از درخواست دریافت کنید - const updateFields = req.body - - // اجرای آپدیت و بازگشت مقدار آپدیت شده - const updatedUser = await AdvertisingProfileModel.findByIdAndUpdate( - shopId, - updateFields, - { new: true } - ) - - // بررسی آیا کاربر وجود دارد یا نه - if (!updatedUser) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - - // ارسال پاسخ با مقادیر آپدیت شده - res.status(200).json({ message: 'کاربر با موفقیت ویرایش شد', id: updatedUser._id }) - } catch (error) { - const errors = [] - - if (error.code === 11000) { - // Duplicate key error - for (const key in error.keyValue) { - errors.push({ message: `ارور در فیلد ${key}` }) - } - } else { - errors.push({ message: 'خطا در آپدیت کاربر' }) - } - - res.status(500).json({ errors }) - } -} - -module.exports = { - getShops, getShopDetail, updateShop -} +/* eslint-disable camelcase */ +const { default: mongoose } = require('mongoose') +const jMoment = require('moment-jalaali') +const AdvertisingProfileModel = require('../../../models/AdvertisingProfile') +const AdvertisingModel = require('../../../models/AdvertisingModel') + +const getShops = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + // eslint-disable-next-line no-unused-vars + const { page = 1, limit = 10, expertise, most_price, most_requests, age, gender } = req.query + const filter = {} // افزودن شرط‌های دیگر برای فیلتر + + // if (expertise) { + // filter.expertise = expertise + // } + // if (age) { + // filter.age = age + // } + // if (gender) { + // filter.gender = gender + // } + // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده + const options = { + page: parseInt(page), // تبدیل صفحه به عدد صحیح + limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح + sort: { createdAt: -1 }, + populate: [ + { path: 'user', select: '_id user_name first_name last_name' } + ] + } + const shops = await AdvertisingProfileModel.paginate(filter, options) + const newShops = shops.docs.map(user => ({ + _id: user._id, + user_name: user?.user?.user_name, + first_name: user?.user?.first_name, + last_name: user?.user?.last_name, + user_id: user?.user?._id, + profile_image: user.profile_image, + category: user.category, + vitrine_name: user.vitrine_name, + province: user.province, + city: user.city, + neighbourhood: user.neighbourhood, + createdAt: jMoment(user.createdAt).format('jYYYY-jMM-jDD HH:mm') + })) + res.status(200).json({ + shops: newShops, + totalPages: shops.totalPages, // ارسال تعداد کل صفحات + totalItems: shops.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + // در صورت بروز خطا، ارسال پیام خطا به کاربر + console.error('Error in getProjectTypes:', error) + res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) + } +} +const getShopDetail = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const profile_id = req.query.profile_id + // eslint-disable-next-line no-unused-vars + // دریافت جزئیات فروشگاه با استفاده از profile_id + const shop = await AdvertisingProfileModel.findById(profile_id) + .populate('user', '_id user_name first_name last_name') + .exec() + const userAds = await AdvertisingModel.find({ creator_id: shop?.user }) + // جمع‌آوری اطلاعات از تبلیغات‌ها + let allImages = [] + let allServices = [] + let allFeatures = [] + for (const ad of userAds) { + allImages = allImages.concat(ad.images) + allServices = allServices.concat(ad.services) + allFeatures = allFeatures.concat(ad.features) + } + // حذف آیتم‌های null و تکراری از allFeatures + const uniqueFeatures = [] + const featureTitles = new Set() + for (const feature of allFeatures) { + if (feature && !featureTitles.has(feature.title)) { + uniqueFeatures.push(feature) + featureTitles.add(feature.title) + } + } + if (!shop) { + return res.status(404).json({ message: 'فروشگاه مورد نظر یافت نشد' }) + } + const response = { + _id: shop?._id, + user_name: shop?.user?.user_name, + first_name: shop?.user?.first_name, + last_name: shop?.user?.last_name, + user_id: shop?.user?._id, + profile_image: shop?.profile_image, + category: shop?.category, + vitrine_name: shop?.vitrine_name, + province: shop?.province, + city: shop?.city, + neighbourhood: shop?.neighbourhood, + contactInfo: shop?.contactInfo, + address: shop?.address, + allImages: allImages?.reverse(), + allServices, + allFeatures: uniqueFeatures, + about_us: shop?.about_us + // اطلاعات اضافی که بعداً به شما می‌گویم + } + return res.status(200).json({ shop: response }) + } catch (error) { + next(error) + } +} +const updateShop = async (req, res, next) => { + try { + const shopId = req.query.shopId // دریافت شناسه کاربر از درخواست + + // چک کردن اعتبار شناسه کاربر + if (!mongoose.Types.ObjectId.isValid(shopId)) { + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) + } + + // فیلدهایی که می‌خواهید آپدیت شوند را از درخواست دریافت کنید + const updateFields = req.body + + // اجرای آپدیت و بازگشت مقدار آپدیت شده + const updatedUser = await AdvertisingProfileModel.findByIdAndUpdate( + shopId, + updateFields, + { new: true } + ) + + // بررسی آیا کاربر وجود دارد یا نه + if (!updatedUser) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + + // ارسال پاسخ با مقادیر آپدیت شده + res.status(200).json({ message: 'کاربر با موفقیت ویرایش شد', id: updatedUser._id }) + } catch (error) { + const errors = [] + + if (error.code === 11000) { + // Duplicate key error + for (const key in error.keyValue) { + errors.push({ message: `ارور در فیلد ${key}` }) + } + } else { + errors.push({ message: 'خطا در آپدیت کاربر' }) + } + + res.status(500).json({ errors }) + } +} + +module.exports = { + getShops, getShopDetail, updateShop +} diff --git a/controllers/panel/tickets/ticketsController.js b/controllers/panel/tickets/ticketsController.js index fba19d7..5a71ef2 100644 --- a/controllers/panel/tickets/ticketsController.js +++ b/controllers/panel/tickets/ticketsController.js @@ -1,153 +1,153 @@ -const NotificationModel = require('../../../models/NotificationModel') -const TicketMessageModel = require('../../../models/TicketMessageModel') -const TicketModel = require('../../../models/TicketModel') -const jwt = require('jsonwebtoken') -const jMoment = require('moment-jalaali') - -const getAllTickets = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { - page = 1, limit = 10, - startDate, - endDate, - status - } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست - - const filter = {} // افزودن شرط‌های دیگر برای فیلتر - - // Date - if (startDate || endDate) { - filter.createdAt = {} - - if (startDate) { - filter.createdAt.$gte = new Date(startDate) // تاریخ شروع - } - - if (endDate) { - filter.createdAt.$lte = new Date(endDate) // تاریخ پایان - } - } - - if (status) { - filter.status = status - } - - const options = { - page: parseInt(page, 10) || 1, - limit: parseInt(limit, 10) || 10, - populate: [ - { path: 'user', select: '_id user_name first_name last_name mobile expertise' } - ], - sort: { updatedAt: -1 } - } - const tickets = await TicketModel.paginate(filter, options) - const ticketList = tickets?.docs.map(ticket => { - const jDate = jMoment(ticket.createdAt).format('jYYYY-jMM-jDD HH:mm') - return { - ...ticket._doc, - createdAt: jDate - } - }) - res.json({ - tickets: ticketList, - totalPages: tickets.totalPages, // ارسال تعداد کل صفحات - totalItems: tickets.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} -const addAdminMessageToTicket = async (req, res) => { - 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 adminId = decodedToken.id - - const { text, ticketId } = req.body - - const newMessage = new TicketMessageModel({ - text, - senderType: 'Admin', - senderId: adminId, - ticket: ticketId - }) - await newMessage.save() - - const ticket = await TicketModel.findById(ticketId) - if (!ticket) { - return res.status(404).json({ error: 'Ticket not found' }) - } - ticket.status = 'Answered' - ticket.new_message = true - await ticket.save() - const notification = new NotificationModel({ - user_id: ticket?.user, - project_post_id: ticket?._id, - type: 'ticket-message', - title: 'پاسخ پشتیبانی', - description: `پاسخ جدید در تیکت: ${ticket?.title}` - }) - await notification.save() - res.status(201).json(newMessage) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} -// senderId -const getTicketMessages = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const { ticketId } = req.query - const { page = 1, limit = 10 } = req.query - const options = { - page: parseInt(page) || 1, - limit: parseInt(limit) || 10, - populate: [ - { path: 'senderId', select: '_id user_name first_name last_name mobile expertise' } - ], - sort: { createdAt: -1 } - } - const messages = await TicketMessageModel.paginate({ ticket: ticketId }, options) - const messageList = messages?.docs.map(message => { - const jDate = jMoment(message.createdAt).format('jYYYY-jMM-jDD HH:mm') - return { - ...message._doc, - createdAt: jDate - } - }) - res.json({ - messages: messageList.reverse(), - totalPages: messages.totalPages, // ارسال تعداد کل صفحات - totalItems: messages.totalDocs // ارسال تعداد کل آیتم‌ها - }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} -const closeTicket = async (req, res) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - - const { ticketId } = req.body - if (!ticketId) return res.status(400).send('Ticket ID is required') - - const ticket = await TicketModel.findById(ticketId) - if (!ticket) return res.status(404).send('Ticket not found') - - ticket.status = 'Closed' - await ticket.save() - - res.status(200).json({ message: 'Ticket has been closed successfully' }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) - } -} - -module.exports = { getAllTickets, addAdminMessageToTicket, getTicketMessages, closeTicket } +const NotificationModel = require('../../../models/NotificationModel') +const TicketMessageModel = require('../../../models/TicketMessageModel') +const TicketModel = require('../../../models/TicketModel') +const jwt = require('jsonwebtoken') +const jMoment = require('moment-jalaali') + +const getAllTickets = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { + page = 1, limit = 10, + startDate, + endDate, + status + } = req.query // افزودن پارامترهای صفحه‌بندی به درخواست + + const filter = {} // افزودن شرط‌های دیگر برای فیلتر + + // Date + if (startDate || endDate) { + filter.createdAt = {} + + if (startDate) { + filter.createdAt.$gte = new Date(startDate) // تاریخ شروع + } + + if (endDate) { + filter.createdAt.$lte = new Date(endDate) // تاریخ پایان + } + } + + if (status) { + filter.status = status + } + + const options = { + page: parseInt(page, 10) || 1, + limit: parseInt(limit, 10) || 10, + populate: [ + { path: 'user', select: '_id user_name first_name last_name mobile expertise' } + ], + sort: { updatedAt: -1 } + } + const tickets = await TicketModel.paginate(filter, options) + const ticketList = tickets?.docs.map(ticket => { + const jDate = jMoment(ticket.createdAt).format('jYYYY-jMM-jDD HH:mm') + return { + ...ticket._doc, + createdAt: jDate + } + }) + res.json({ + tickets: ticketList, + totalPages: tickets.totalPages, // ارسال تعداد کل صفحات + totalItems: tickets.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } +} +const addAdminMessageToTicket = async (req, res) => { + 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 adminId = decodedToken.id + + const { text, ticketId } = req.body + + const newMessage = new TicketMessageModel({ + text, + senderType: 'Admin', + senderId: adminId, + ticket: ticketId + }) + await newMessage.save() + + const ticket = await TicketModel.findById(ticketId) + if (!ticket) { + return res.status(404).json({ error: 'Ticket not found' }) + } + ticket.status = 'Answered' + ticket.new_message = true + await ticket.save() + const notification = new NotificationModel({ + user_id: ticket?.user, + project_post_id: ticket?._id, + type: 'ticket-message', + title: 'پاسخ پشتیبانی', + description: `پاسخ جدید در تیکت: ${ticket?.title}` + }) + await notification.save() + res.status(201).json(newMessage) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } +} +// senderId +const getTicketMessages = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { ticketId } = req.query + const { page = 1, limit = 10 } = req.query + const options = { + page: parseInt(page) || 1, + limit: parseInt(limit) || 10, + populate: [ + { path: 'senderId', select: '_id user_name first_name last_name mobile expertise' } + ], + sort: { createdAt: -1 } + } + const messages = await TicketMessageModel.paginate({ ticket: ticketId }, options) + const messageList = messages?.docs.map(message => { + const jDate = jMoment(message.createdAt).format('jYYYY-jMM-jDD HH:mm') + return { + ...message._doc, + createdAt: jDate + } + }) + res.json({ + messages: messageList.reverse(), + totalPages: messages.totalPages, // ارسال تعداد کل صفحات + totalItems: messages.totalDocs // ارسال تعداد کل آیتم‌ها + }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } +} +const closeTicket = async (req, res) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const { ticketId } = req.body + if (!ticketId) return res.status(400).send('Ticket ID is required') + + const ticket = await TicketModel.findById(ticketId) + if (!ticket) return res.status(404).send('Ticket not found') + + ticket.status = 'Closed' + await ticket.save() + + res.status(200).json({ message: 'Ticket has been closed successfully' }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) + } +} + +module.exports = { getAllTickets, addAdminMessageToTicket, getTicketMessages, closeTicket } diff --git a/controllers/panel/users/usersController.js b/controllers/panel/users/usersController.js index cf480f0..f0c2761 100644 --- a/controllers/panel/users/usersController.js +++ b/controllers/panel/users/usersController.js @@ -1,410 +1,410 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable camelcase */ -const { default: mongoose } = require('mongoose') -const UserModel = require('../../../models/UserModel') -const jMoment = require('moment-jalaali') -const NotificationModel = require('../../../models/NotificationModel') -const { default: axios } = require('axios') - -const getUsers = async (req, res, next) => { - try { - // بررسی توکن - const token = req.header('Authorization')?.split(' ')[1]; - if (!token) return res.status(401).send('Access Denied'); - - // پارامترهای query - const { - page = 1, - limit = 20, - expertise, - most_price, - most_requests, - age, - gender, - startDate, - endDate, - status, - search - } = req.query; - - const filter = {}; // شرط‌های فیلتر - - // فیلتر تاریخ - if (startDate || endDate) { - filter.createdAt = {}; - if (startDate) filter.createdAt.$gte = new Date(startDate); - if (endDate) filter.createdAt.$lte = new Date(endDate); - } - - // فیلتر وضعیت - if (status) { - filter.is_verified = status; - } - - // فیلتر سرچ - if (search) { - const regex = new RegExp(search, "i"); // i = ignore case - filter.$or = [ - { first_name: regex }, - { last_name: regex }, - { user_name: regex }, - { mobile: regex } - ]; - } - - // فیلترهای دیگر (اختیاری) - if (expertise) filter.expertise = expertise; - if (age) filter.age = age; - if (gender) filter.gender = gender; - - // گزینه‌های صفحه‌بندی - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: { createdAt: -1 } - }; - - // گرفتن کاربران با paginate - const users = await UserModel.paginate(filter, options); - - // آماده‌سازی خروجی - const newUsers = users.docs.map(user => ({ - _id: user._id, - mobile: user.mobile, - user_name: user.user_name, - first_name: user.first_name, - last_name: user.last_name, - user_type: user.user_type, - is_verified: user.is_verified, - profile_image: user.profile_image, - expertise: user.expertise, - sub_expertise: user.sub_expertise, - gender: user.gender, - national_code: user.national_code, - province: user.province, - city: user.city, - last_online: jMoment(user.last_online).format('jYYYY-jMM-jDD HH:mm'), - createdAt: jMoment(user.createdAt).format('jYYYY-jMM-jDD HH:mm'), - block_status: user.block_status - })); - - res.status(200).json({ - users: newUsers, - totalPages: users.totalPages, - totalItems: users.totalDocs - }); - } catch (error) { - console.error('Error in getUsers:', error); - res.status(500).json({ message: 'خطا در دریافت کاربران.' }); - } -}; - - - -// const getUsers = async (req, res, next) => { -// try { -// const token = req.header('Authorization').split(' ')[1] -// if (!token) return res.status(401).send('Access Denied') - -// // eslint-disable-next-line no-unused-vars -// const { -// page = 1, -// limit = 10, -// expertise, -// most_price, -// most_requests, -// age, -// gender, -// startDate, -// endDate, -// status -// } = req.query -// const filter = {} // افزودن شرط‌های دیگر برای فیلتر - -// // Date -// if (startDate || endDate) { -// filter.createdAt = {} - -// if (startDate) { -// filter.createdAt.$gte = new Date(startDate) // تاریخ شروع -// } - -// if (endDate) { -// filter.createdAt.$lte = new Date(endDate) // تاریخ پایان -// } -// } - -// if (status) { -// filter.is_verified = status -// } - -// // if (expertise) { -// // filter.expertise = expertise -// // } -// // if (age) { -// // filter.age = age -// // } -// // if (gender) { -// // filter.gender = gender -// // } -// // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده -// const options = { -// page: parseInt(page), // تبدیل صفحه به عدد صحیح -// limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح -// sort: { createdAt: -1 } -// } -// const users = await UserModel.paginate(filter, options) -// const newUsers = users.docs.map(user => ({ -// _id: user._id, -// mobile: user.mobile, -// user_name: user.user_name, -// first_name: user.first_name, -// last_name: user.last_name, -// user_type: user.user_type, -// is_verified: user.is_verified, -// profile_image: user.profile_image, -// expertise: user.expertise, -// sub_expertise: user.sub_expertise, -// gender: user.gender, -// national_code: user.national_code, -// province: user.province, -// city: user.city, -// last_online: jMoment(user.last_online).format('jYYYY-jMM-jDD HH:mm'), -// createdAt: jMoment(user.createdAt).format('jYYYY-jMM-jDD HH:mm'), -// block_status: user.block_status -// })) -// res.status(200).json({ -// users: newUsers, -// totalPages: users.totalPages, // ارسال تعداد کل صفحات -// totalItems: users.totalDocs // ارسال تعداد کل آیتم‌ها -// }) -// } catch (error) { -// // در صورت بروز خطا، ارسال پیام خطا به کاربر -// console.error('Error in getProjectTypes:', error) -// res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) -// } -// } -const getUserDetail = async (req, res, next) => { - try { - const token = req.header('Authorization').split(' ')[1] - if (!token) return res.status(401).send('Access Denied') - const user_id = req.query.user_id - // eslint-disable-next-line no-unused-vars - - if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) - } - const user = await UserModel.findById(user_id) - - if (!user) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - - const response = { - _id: user?._id, - profile_image: user?.profile_image, - first_name: user?.first_name, - last_name: user?.last_name, - user_name: user?.user_name, - mobile: user?.mobile, - national_code: user?.national_code, - province: user?.province, - city: user?.city, - address: user?.address, - height: user?.height, - weight: user?.weight, - size: user?.size, - birthday: user?.birthday, - show_location: user?.show_location, - cooperation_abroad: user?.cooperation_abroad, - conversation_projects: user?.conversation_projects, - expertise: user?.expertise, - sub_expertise: user?.sub_expertise, - shaba: user?.shaba, - national_card_image: user?.national_card_image, - last_online: jMoment(user?.last_online).format('jYYYY-jMM-jDD HH:mm'), - createdAt: jMoment(user?.createdAt).format('jYYYY-jMM-jDD HH:mm'), - block_status: user?.block_status, - user_level: user?.user_level, - user_type: user?.user_type, - user_score: user?.user_score, - services :user?.services, - is_verified: user?.is_verified, - rate: user?.rate, - bio: user?.bio, - eye_color: user?.eye_color, - hair_color: user?.hair_color, - lat: user?.lat, - lng: user?.lng - } - return res.status(200).json({ user: response }) - } catch (error) { - next(error) - } -} -const updateUser = async (req, res, next) => { - try { - const userId = req.query.userId // دریافت شناسه کاربر از درخواست - - // چک کردن اعتبار شناسه کاربر - if (!mongoose.Types.ObjectId.isValid(userId)) { - return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) - } - - // فیلدهایی که می‌خواهید آپدیت شوند را از درخواست دریافت کنید - const updateFields = req.body - - // اجرای آپدیت و بازگشت مقدار آپدیت شده - const updatedUser = await UserModel.findByIdAndUpdate( - userId, - updateFields, - { new: true } - ) - - // بررسی آیا کاربر وجود دارد یا نه - if (!updatedUser) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - - // ارسال پاسخ با مقادیر آپدیت شده - res.status(200).json({ message: 'کاربر با موفقیت ویرایش شد', id: updatedUser._id }) - } catch (error) { - const errors = [] - - if (error.code === 11000) { - // Duplicate key error - for (const key in error.keyValue) { - errors.push({ message: `ارور در فیلد ${key}` }) - } - } else { - errors.push({ message: 'خطا در آپدیت کاربر' }) - } - - res.status(500).json({ errors }) - } -} -const verifyUser = async (req, res, next) => { - try { - const userId = req.query.userId // دریافت شناسه کاربر از درخواست - const user = await UserModel.findById(userId) - if (!user) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - user.is_verified = 'verified'; - user.is_Register = true; - await user.save(); - - - const notification = new NotificationModel({ - user_id: userId, - type: 'verify', - title: 'تایید مدارک', - description: 'مدارک ارسالی شما با موفقیت تایید شد' - }) - await notification.save() - - // Sms - - const userReciver = await UserModel.findById(userId) - - const data = JSON.stringify({ - mobile: userReciver?.mobile, - templateId: '632057', - parameters: [ - { name: 'USERS', value: userReciver?.first_name + ' ' + userReciver?.last_name } - ] - }) - - const config = { - method: 'post', - url: 'https://api.sms.ir/v1/send/verify', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/plain', - 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' - }, - data - } - axios(config) - .then(function (response) { - }) - .catch(function (error) { - console.log(error) - }) - - // End Sms - - // ارسال پاسخ با مقادیر آپدیت شده - res.status(200).json({ message: 'مدارک کاربر با موفقیت تایید شد' }) - } catch (error) { - next(error) - } -} - -const updateUserStatus = async (req, res, next) => { - try { - const { userId, block_status } = req.body; - - - if (!mongoose.Types.ObjectId.isValid(userId)) { - return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }); - } - - const user = await UserModel.findByIdAndUpdate( - userId, - { block_status }, - { new: true } - ); - - if (!user) return res.status(404).json({ message: 'کاربر یافت نشد' }); - - res.status(200).json({ message: 'وضعیت کاربر با موفقیت تغییر کرد', block_status: user.block_status }); - } catch (error) { - console.error(error); - res.status(500).json({ message: 'خطای سرور' }); - } -}; - -const rejectUser = async (req, res, next) => { - try { - const userId = req.query.userId // دریافت شناسه کاربر از درخواست - const user = await UserModel.findById(userId) - if (!user) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - user.is_verified = 'rejected' - user.is_Register = false - await user.save() - const notification = new NotificationModel({ - user_id: userId, - type: 'reject-user', - title: 'رد مدارک', - description: 'مدارک ارسالی شما رد شد، برای اطلاعات بیشتر از بخش تیکت به پشتیبانی پیام دهید.' - }) - await notification.save() - - // ارسال پاسخ با مقادیر آپدیت شده - res.status(200).json({ message: 'مدارک کاربر با موفقیت رد شد' }) - } catch (error) { - next(error) - } -} -const changePass = async (req, res, next) => { - try { - const { userId, password } = req.body; - const user = await UserModel.findById(userId) - if (!user) { - return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) - } - user.password = password - await user.save() - - // ارسال پاسخ با مقادیر آپدیت شده - res.status(200).json({ message: 'رمز عبور با موفقیت تغییر کرد' }); - } catch (error) { - next(error) - } -} -module.exports = { - getUsers, getUserDetail, updateUser, verifyUser, rejectUser, updateUserStatus, changePass -} +/* eslint-disable no-unused-vars */ +/* eslint-disable camelcase */ +const { default: mongoose } = require('mongoose') +const UserModel = require('../../../models/UserModel') +const jMoment = require('moment-jalaali') +const NotificationModel = require('../../../models/NotificationModel') +const { default: axios } = require('axios') + +const getUsers = async (req, res, next) => { + try { + // بررسی توکن + const token = req.header('Authorization')?.split(' ')[1]; + if (!token) return res.status(401).send('Access Denied'); + + // پارامترهای query + const { + page = 1, + limit = 20, + expertise, + most_price, + most_requests, + age, + gender, + startDate, + endDate, + status, + search + } = req.query; + + const filter = {}; // شرط‌های فیلتر + + // فیلتر تاریخ + if (startDate || endDate) { + filter.createdAt = {}; + if (startDate) filter.createdAt.$gte = new Date(startDate); + if (endDate) filter.createdAt.$lte = new Date(endDate); + } + + // فیلتر وضعیت + if (status) { + filter.is_verified = status; + } + + // فیلتر سرچ + if (search) { + const regex = new RegExp(search, "i"); // i = ignore case + filter.$or = [ + { first_name: regex }, + { last_name: regex }, + { user_name: regex }, + { mobile: regex } + ]; + } + + // فیلترهای دیگر (اختیاری) + if (expertise) filter.expertise = expertise; + if (age) filter.age = age; + if (gender) filter.gender = gender; + + // گزینه‌های صفحه‌بندی + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: { createdAt: -1 } + }; + + // گرفتن کاربران با paginate + const users = await UserModel.paginate(filter, options); + + // آماده‌سازی خروجی + const newUsers = users.docs.map(user => ({ + _id: user._id, + mobile: user.mobile, + user_name: user.user_name, + first_name: user.first_name, + last_name: user.last_name, + user_type: user.user_type, + is_verified: user.is_verified, + profile_image: user.profile_image, + expertise: user.expertise, + sub_expertise: user.sub_expertise, + gender: user.gender, + national_code: user.national_code, + province: user.province, + city: user.city, + last_online: jMoment(user.last_online).format('jYYYY-jMM-jDD HH:mm'), + createdAt: jMoment(user.createdAt).format('jYYYY-jMM-jDD HH:mm'), + block_status: user.block_status + })); + + res.status(200).json({ + users: newUsers, + totalPages: users.totalPages, + totalItems: users.totalDocs + }); + } catch (error) { + console.error('Error in getUsers:', error); + res.status(500).json({ message: 'خطا در دریافت کاربران.' }); + } +}; + + + +// const getUsers = async (req, res, next) => { +// try { +// const token = req.header('Authorization').split(' ')[1] +// if (!token) return res.status(401).send('Access Denied') + +// // eslint-disable-next-line no-unused-vars +// const { +// page = 1, +// limit = 10, +// expertise, +// most_price, +// most_requests, +// age, +// gender, +// startDate, +// endDate, +// status +// } = req.query +// const filter = {} // افزودن شرط‌های دیگر برای فیلتر + +// // Date +// if (startDate || endDate) { +// filter.createdAt = {} + +// if (startDate) { +// filter.createdAt.$gte = new Date(startDate) // تاریخ شروع +// } + +// if (endDate) { +// filter.createdAt.$lte = new Date(endDate) // تاریخ پایان +// } +// } + +// if (status) { +// filter.is_verified = status +// } + +// // if (expertise) { +// // filter.expertise = expertise +// // } +// // if (age) { +// // filter.age = age +// // } +// // if (gender) { +// // filter.gender = gender +// // } +// // استفاده از مدل پیجینیت شده برای دریافت نتایج صفحه‌بندی شده +// const options = { +// page: parseInt(page), // تبدیل صفحه به عدد صحیح +// limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح +// sort: { createdAt: -1 } +// } +// const users = await UserModel.paginate(filter, options) +// const newUsers = users.docs.map(user => ({ +// _id: user._id, +// mobile: user.mobile, +// user_name: user.user_name, +// first_name: user.first_name, +// last_name: user.last_name, +// user_type: user.user_type, +// is_verified: user.is_verified, +// profile_image: user.profile_image, +// expertise: user.expertise, +// sub_expertise: user.sub_expertise, +// gender: user.gender, +// national_code: user.national_code, +// province: user.province, +// city: user.city, +// last_online: jMoment(user.last_online).format('jYYYY-jMM-jDD HH:mm'), +// createdAt: jMoment(user.createdAt).format('jYYYY-jMM-jDD HH:mm'), +// block_status: user.block_status +// })) +// res.status(200).json({ +// users: newUsers, +// totalPages: users.totalPages, // ارسال تعداد کل صفحات +// totalItems: users.totalDocs // ارسال تعداد کل آیتم‌ها +// }) +// } catch (error) { +// // در صورت بروز خطا، ارسال پیام خطا به کاربر +// console.error('Error in getProjectTypes:', error) +// res.status(500).json({ message: 'خطا در دریافت انواع پروژه.' }) +// } +// } +const getUserDetail = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const user_id = req.query.user_id + // eslint-disable-next-line no-unused-vars + + if (!mongoose.Types.ObjectId.isValid(user_id)) { + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) + } + const user = await UserModel.findById(user_id) + + if (!user) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + + const response = { + _id: user?._id, + profile_image: user?.profile_image, + first_name: user?.first_name, + last_name: user?.last_name, + user_name: user?.user_name, + mobile: user?.mobile, + national_code: user?.national_code, + province: user?.province, + city: user?.city, + address: user?.address, + height: user?.height, + weight: user?.weight, + size: user?.size, + birthday: user?.birthday, + show_location: user?.show_location, + cooperation_abroad: user?.cooperation_abroad, + conversation_projects: user?.conversation_projects, + expertise: user?.expertise, + sub_expertise: user?.sub_expertise, + shaba: user?.shaba, + national_card_image: user?.national_card_image, + last_online: jMoment(user?.last_online).format('jYYYY-jMM-jDD HH:mm'), + createdAt: jMoment(user?.createdAt).format('jYYYY-jMM-jDD HH:mm'), + block_status: user?.block_status, + user_level: user?.user_level, + user_type: user?.user_type, + user_score: user?.user_score, + services :user?.services, + is_verified: user?.is_verified, + rate: user?.rate, + bio: user?.bio, + eye_color: user?.eye_color, + hair_color: user?.hair_color, + lat: user?.lat, + lng: user?.lng + } + return res.status(200).json({ user: response }) + } catch (error) { + next(error) + } +} +const updateUser = async (req, res, next) => { + try { + const userId = req.query.userId // دریافت شناسه کاربر از درخواست + + // چک کردن اعتبار شناسه کاربر + if (!mongoose.Types.ObjectId.isValid(userId)) { + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) + } + + // فیلدهایی که می‌خواهید آپدیت شوند را از درخواست دریافت کنید + const updateFields = req.body + + // اجرای آپدیت و بازگشت مقدار آپدیت شده + const updatedUser = await UserModel.findByIdAndUpdate( + userId, + updateFields, + { new: true } + ) + + // بررسی آیا کاربر وجود دارد یا نه + if (!updatedUser) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + + // ارسال پاسخ با مقادیر آپدیت شده + res.status(200).json({ message: 'کاربر با موفقیت ویرایش شد', id: updatedUser._id }) + } catch (error) { + const errors = [] + + if (error.code === 11000) { + // Duplicate key error + for (const key in error.keyValue) { + errors.push({ message: `ارور در فیلد ${key}` }) + } + } else { + errors.push({ message: 'خطا در آپدیت کاربر' }) + } + + res.status(500).json({ errors }) + } +} +const verifyUser = async (req, res, next) => { + try { + const userId = req.query.userId // دریافت شناسه کاربر از درخواست + const user = await UserModel.findById(userId) + if (!user) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + user.is_verified = 'verified'; + user.is_Register = true; + await user.save(); + + + const notification = new NotificationModel({ + user_id: userId, + type: 'verify', + title: 'تایید مدارک', + description: 'مدارک ارسالی شما با موفقیت تایید شد' + }) + await notification.save() + + // Sms + + const userReciver = await UserModel.findById(userId) + + const data = JSON.stringify({ + mobile: userReciver?.mobile, + templateId: '632057', + parameters: [ + { name: 'USERS', value: userReciver?.first_name + ' ' + userReciver?.last_name } + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + axios(config) + .then(function (response) { + }) + .catch(function (error) { + console.log(error) + }) + + // End Sms + + // ارسال پاسخ با مقادیر آپدیت شده + res.status(200).json({ message: 'مدارک کاربر با موفقیت تایید شد' }) + } catch (error) { + next(error) + } +} + +const updateUserStatus = async (req, res, next) => { + try { + const { userId, block_status } = req.body; + + + if (!mongoose.Types.ObjectId.isValid(userId)) { + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }); + } + + const user = await UserModel.findByIdAndUpdate( + userId, + { block_status }, + { new: true } + ); + + if (!user) return res.status(404).json({ message: 'کاربر یافت نشد' }); + + res.status(200).json({ message: 'وضعیت کاربر با موفقیت تغییر کرد', block_status: user.block_status }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'خطای سرور' }); + } +}; + +const rejectUser = async (req, res, next) => { + try { + const userId = req.query.userId // دریافت شناسه کاربر از درخواست + const user = await UserModel.findById(userId) + if (!user) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + user.is_verified = 'rejected' + user.is_Register = false + await user.save() + const notification = new NotificationModel({ + user_id: userId, + type: 'reject-user', + title: 'رد مدارک', + description: 'مدارک ارسالی شما رد شد، برای اطلاعات بیشتر از بخش تیکت به پشتیبانی پیام دهید.' + }) + await notification.save() + + // ارسال پاسخ با مقادیر آپدیت شده + res.status(200).json({ message: 'مدارک کاربر با موفقیت رد شد' }) + } catch (error) { + next(error) + } +} +const changePass = async (req, res, next) => { + try { + const { userId, password } = req.body; + const user = await UserModel.findById(userId) + if (!user) { + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) + } + user.password = password + await user.save() + + // ارسال پاسخ با مقادیر آپدیت شده + res.status(200).json({ message: 'رمز عبور با موفقیت تغییر کرد' }); + } catch (error) { + next(error) + } +} +module.exports = { + getUsers, getUserDetail, updateUser, verifyUser, rejectUser, updateUserStatus, changePass +} diff --git a/controllers/panel/version/versionController.js b/controllers/panel/version/versionController.js index c3fd223..fca01d6 100644 --- a/controllers/panel/version/versionController.js +++ b/controllers/panel/version/versionController.js @@ -1,37 +1,37 @@ -const VersionModel = require('../../../models/VersionModel') - -// Endpoint برای دریافت نسخه فعلی -const getVersion = async (req, res, next) => { - try { - const currentVersion = await VersionModel.findOne().sort({ _id: -1 }).exec() - if (!currentVersion) { - return res.status(200).json({ message: 'نسخه‌ای پیدا نشد' }) - } - res.json(currentVersion) - } catch (error) { - res.status(500).json({ message: 'خطای سرور', error }) - } -} -// متد برای ایجاد یا ویرایش نسخه -const upsertVersion = async (req, res, next) => { - try { - const { version, mandatory, releaseNotes, updateUrls } = req.body - - if (!version || !Array.isArray(updateUrls) || updateUrls.length === 0) { - return res.status(400).json({ message: 'اطلاعات ناقص است' }) - } - - // پیدا کردن نسخه‌ای که مشابه باشد و ویرایش یا ایجاد - const updatedVersion = await VersionModel.findOneAndUpdate( - { version }, // شرط برای پیدا کردن نسخه با همین شماره - { mandatory, releaseNotes, updateUrls }, // به‌روزرسانی مقادیر - { new: true, upsert: true } // ایجاد نسخه جدید اگر وجود ندارد - ) - - res.status(200).json({ message: 'نسخه به‌روزرسانی شد', version: updatedVersion }) - } catch (error) { - res.status(500).json({ message: 'خطای سرور', error }) - } -} - -module.exports = { getVersion, upsertVersion } +const VersionModel = require('../../../models/VersionModel') + +// Endpoint برای دریافت نسخه فعلی +const getVersion = async (req, res, next) => { + try { + const currentVersion = await VersionModel.findOne().sort({ _id: -1 }).exec() + if (!currentVersion) { + return res.status(200).json({ message: 'نسخه‌ای پیدا نشد' }) + } + res.json(currentVersion) + } catch (error) { + res.status(500).json({ message: 'خطای سرور', error }) + } +} +// متد برای ایجاد یا ویرایش نسخه +const upsertVersion = async (req, res, next) => { + try { + const { version, mandatory, releaseNotes, updateUrls } = req.body + + if (!version || !Array.isArray(updateUrls) || updateUrls.length === 0) { + return res.status(400).json({ message: 'اطلاعات ناقص است' }) + } + + // پیدا کردن نسخه‌ای که مشابه باشد و ویرایش یا ایجاد + const updatedVersion = await VersionModel.findOneAndUpdate( + { version }, // شرط برای پیدا کردن نسخه با همین شماره + { mandatory, releaseNotes, updateUrls }, // به‌روزرسانی مقادیر + { new: true, upsert: true } // ایجاد نسخه جدید اگر وجود ندارد + ) + + res.status(200).json({ message: 'نسخه به‌روزرسانی شد', version: updatedVersion }) + } catch (error) { + res.status(500).json({ message: 'خطای سرور', error }) + } +} + +module.exports = { getVersion, upsertVersion } diff --git a/index.js b/index.js index bf9b23a..4cae0bb 100644 --- a/index.js +++ b/index.js @@ -1,493 +1,493 @@ -const express = require('express'); -const app = express(); -const path = require('path'); -const http = require('http').createServer(app); -const cors = require('cors'); -const moment = require('moment-jalaali'); -const MessageModel = require('./models/MessageModel'); -const fs = require('fs-extra'); -const cron = require('node-cron'); -const UserModel = require('./models/UserModel'); -const blockCheck = require('./middlewares/blockCheck'); -const rateLimit = require('express-rate-limit'); -const { default: axios } = require('axios'); - -const allowedOrigins = [ - 'http://localhost:3000', - 'http://localhost:3001', // اضافه شد — مهم برای توسعه فرانت - 'http://localhost:3002', - 'http://localhost:3003', - 'http://localhost:3004', - 'http://localhost', - 'https://modstagram.com', - 'http://modstagram.com', - 'http://193.151.143.243:3004', - 'https://www.modstagram.com', - 'https://api.modstagram.com', - 'https://panel.modstagram.com', - 'https://www.panel.modstagram.com', - 'https://panel.modstagram.ir', - 'https://www.panel.modstagram.ir', - 'ionic://localhost', - 'capacitor://localhost', - // IPهای محلی (برای تست در شبکه داخلی)/ - /^http:\/\/192\.168\.\d{1,3}\.\d{1,3}:\d+$/, - /^http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$/, -]; - -app.use(cors({ - origin: (origin, callback) => { - // در محیط توسعه، همه originها مجاز باشن - if (process.env.NODE_ENV !== 'production') { - return callback(null, true); - } - - // اگر origin وجود نداشته باشه (مثل درخواست از موبایل یا Postman) اجازه بده - if (!origin) { - return callback(null, true); - } - - // چک لیست یا RegExp - const isAllowed = allowedOrigins.some(item => { - if (typeof item === 'string') return item === origin; - if (item instanceof RegExp) return item.test(origin); - return false; - }); - - if (isAllowed) { - callback(null, true); - } else { - callback(new Error('Not allowed by CORS')); - } - }, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-no-toast'], - credentials: true, - optionsSuccessStatus: 200 // برای مرورگرهای قدیمی -})); - -// Socket.IO CORS -const io = require('socket.io')(http, { - cors: { - origin: (origin, callback) => { - if (process.env.NODE_ENV !== 'production') { - return callback(null, true); - } - - if (!origin) { - return callback(null, true); - } - - const isAllowed = allowedOrigins.some(item => { - if (typeof item === 'string') return item === origin; - if (item instanceof RegExp) return item.test(origin); - return false; - }); - - if (isAllowed) { - callback(null, true); - } else { - callback(new Error('Not allowed by CORS')); - } - }, - methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'], - credentials: true - } -}); - -// افزایش limit برای Base64 (1000mb کافیه) -app.use(express.json({ limit: '1000mb' })); -app.use(express.urlencoded({ limit: '1000mb', extended: true })); - -// مسیرهای مشخص قبل از /storage عمومی — پروفایل از هر دو محل (قدیم و جدید) سرو می‌شود -app.use('/storage/profiles', express.static(path.join(__dirname, '../storage/profiles'))); -app.use('/storage/profiles', express.static(path.join(__dirname, 'storage/profiles'))); -app.use('/storage/carts', express.static(path.join(__dirname, '../storage/carts'))); -app.use('/storage/carts', express.static(path.join(__dirname, 'storage/carts'))); -app.use('/storage/posts', express.static(path.join(__dirname, 'storage/posts'))); -app.use('/storage/posts', express.static(path.join(__dirname, '../storage/posts'))); -app.use('/storage/messages', express.static(path.join(__dirname, '../storage/messages'))); -app.use('/storage/messages', express.static(path.join(__dirname, 'storage/messages'))); -app.use('/storage/tickets', express.static(path.join(__dirname, '../storage/tickets'))); -app.use('/storage/tickets', express.static(path.join(__dirname, 'storage/tickets'))); -app.use('/storage/advertising', express.static(path.join(__dirname, '../storage/advertising'))); -app.use('/storage/advertising', express.static(path.join(__dirname, 'storage/advertising'))); -app.use('/storage/services', express.static(path.join(__dirname, '../storage/services'))); -app.use('/storage/services', express.static(path.join(__dirname, 'storage/services'))); -app.use('/storage', express.static(path.join(__dirname, 'storage'))); - -require('./boot'); - -// غیرفعال کردن پارس‌کننده‌های بدنه برای روت‌های آپلود فایل -app.use((req, res, next) => { - if (req.path === '/api/v1/posts/create' && req.method === 'POST') { - console.log('DEBUG: Skipping body parsers for /api/v1/posts/create'); - return next(); - } - express.json()(req, res, next); -}); -app.use((req, res, next) => { - if (req.path === '/api/v1/posts/create' && req.method === 'POST') { - return next(); - } - express.urlencoded({ extended: true })(req, res, next); -}); - -require('./middlewares')(app); - -const limiter = rateLimit({ - windowMs: 3 * 60 * 1000, - max: 3000, - standardHeaders: true, - legacyHeaders: false, - message: 'تعداد درخواست‌های شما بیشتر از حد مجاز است، لطفاً بعداً دوباره تلاش کنید.', - handler: (req, res) => { - const retryAfter = Math.ceil((req.rateLimit.resetTime - Date.now()) / 1000); - res.set('Retry-After', retryAfter); - res.status(429).json({ - error: `شما بیش از حد مجاز درخواست ارسال کرده‌اید. لطفاً بعد از ${retryAfter} ثانیه دوباره تلاش کنید.` - }); - } -}); -app.use(limiter); -app.set('trust proxy', 'loopback'); - -require('./routes')(app); - -const { enrichMessage, emitChatEvents, parseForwardedContent } = require('./helpers/chatHelpers') -const { - parseSelfDestructSeconds, - buildExpiresAt, - activeMessageFilter, - purgeExpiredMessages, - scheduleMessageExpiry, - deleteMessagesAndNotify -} = require('./helpers/expiredMessages') -const { - parseViewOnce, - isViewOnceMediaType, - sanitizeMessageForViewer, - openViewOnceMessage, - completeViewOnceMessage -} = require('./helpers/viewOnceMessages') - -const mongoose = require('mongoose') - -app.get('/api/v1/chat', async (req, res) => { - try { - const { senderId, receiverId, limit, page } = req.query - - if (!senderId || !receiverId) { - return res.status(400).json({ error: 'senderId and receiverId are required' }) - } - - if ( - !mongoose.Types.ObjectId.isValid(String(senderId)) || - !mongoose.Types.ObjectId.isValid(String(receiverId)) - ) { - return res.status(400).json({ error: 'Invalid senderId or receiverId' }) - } - - const sid = new mongoose.Types.ObjectId(String(senderId)) - const rid = new mongoose.Types.ObjectId(String(receiverId)) - const pageNumber = parseInt(page) || 1 - const limitPerPage = parseInt(limit) || 40 - - const filter = activeMessageFilter({ - $or: [ - { senderId: sid, receiverId: rid }, - { senderId: rid, receiverId: sid } - ] - }) - - const totalMessagesCount = await MessageModel.countDocuments(filter) - const totalPages = Math.ceil(totalMessagesCount / limitPerPage) || 1 - const skip = (pageNumber - 1) * limitPerPage - - const messages = await MessageModel.find(filter) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limitPerPage) - - await MessageModel.updateMany( - { _id: { $in: messages.map((m) => m._id) }, receiverId: sid }, - { $set: { readStatus: 1 } } - ) - - const enriched = await Promise.all( - messages.map((m) => - enrichMessage(sanitizeMessageForViewer(m, sid)) - ) - ) - res.json({ messages: enriched.reverse(), totalPages }) - } catch (error) { - console.error('Error fetching messages:', error) - res.status(500).json({ error: 'Server error' }) - } -}) - -app.post('/api/v1/chat', [blockCheck], async (req, res) => { - try { - const { senderId, receiverId, content, replyToId, forwardedFrom: fwdBody, selfDestructSeconds } = req.body - const { body, forwardedFrom: fwdParsed } = parseForwardedContent(content) - const destructSec = parseSelfDestructSeconds(selfDestructSeconds) - - const payload = { - senderId, - receiverId, - content: body || content, - replyToId: replyToId || undefined, - forwardedFrom: fwdBody || fwdParsed || undefined, - expiresAt: buildExpiresAt(destructSec) - } - - const newMessage = new MessageModel(payload) - await newMessage.save() - const formatted = await enrichMessage(newMessage) - - res.status(201).json({ data: formatted }) - emitChatEvents(io, formatted, senderId, receiverId) - scheduleMessageExpiry(io, newMessage) - } catch (error) { - console.error('Error sending message:', error) - res.status(500).json({ error: 'Server error' }) - } -}) -app.get('/api/v1/chat/read', async (req, res) => { - try { - const { senderId, receiverId } = req.query // شناسه فرستنده و گیرنده از درخواست دریافت شود - // پیدا کردن تمام پیام‌هایی که کاربر دوم مشاهده کرده است و کاربر اول آنها را ارسال کرده است - const messages = await MessageModel.find( - { senderId: receiverId, receiverId: senderId } // برای مواقعی که شناسه‌ها معکوس باشند - ) - // به روزرسانی وضعیت خوانده شده یا نشده بودن پیام‌ها به "خوانده شده" - await MessageModel.updateMany({ _id: { $in: messages.map(message => message._id) } }, { $set: { readStatus: 1 } }) - res.status(200).json({ message: 'Message read status updated successfully' }) - } catch (error) { - console.error('Error updating message read status:', error) - res.status(500).json({ error: 'Server error' }) - } -}) - -app.post("/api/v1/notification/send-sms", async (req, res) => { - const { receiverId, message } = req.body; - const user = await UserModel.findById(receiverId); - if (!user || !user.mobile) return res.status(404).json({ error: "User not found" }); - console.log("3"); - - const mobile = user.mobile - const user_name = user.user_name - const data = JSON.stringify({ - mobile, - templateId: '876533', - parameters: [ - { name: 'USER', value: user_name } - ] - }) - - const config = { - method: 'post', - url: 'https://api.sms.ir/v1/send/verify', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/plain', - 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' - }, - data - } - - axios(config) - .then(function (response) { - console.log(response); - - }) - .catch(function (error) { - console.log(error) - }) - - res.status(200).json({ success: true }); -}); - -app.post('/api/v1/chat/file', [blockCheck], async (req, res) => { - try { - const { senderId, receiverId, content, replyToId, fileType, selfDestructSeconds, viewOnce } = req.body - const destructSec = parseSelfDestructSeconds(selfDestructSeconds) - const viewOnceFlag = parseViewOnce(viewOnce) - const resolvedFileType = fileType || undefined - - if (viewOnceFlag && !isViewOnceMediaType(resolvedFileType)) { - return res.status(422).json({ error: 'View once is only for image, video, and voice' }) - } - const { file } = req.files - let fileUrl = null - if (file) { - const uploadDir = path.join(__dirname, '../storage/messages') - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }) - } - const uniqueFileName = `${Date.now()}-${file.name}` - const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(file.path, filePath) - fileUrl = `/messages/${uniqueFileName}` - } - - const newMessage = new MessageModel({ - senderId, - receiverId, - content: content || '', - file: fileUrl, - fileType: resolvedFileType, - replyToId: replyToId || undefined, - expiresAt: buildExpiresAt(destructSec), - viewOnce: viewOnceFlag - }) - await newMessage.save() - const formatted = await enrichMessage(newMessage) - - res.status(201).json({ data: formatted }) - emitChatEvents(io, formatted, senderId, receiverId) - scheduleMessageExpiry(io, newMessage) - } catch (error) { - console.error('Error sending message:', error) - res.status(500).json({ error: 'Server error' }) - } -}) - -app.post('/api/v1/chat/view-once/open', [blockCheck], async (req, res) => { - try { - const userId = String(req.user._id) - const { messageId } = req.body - if (!messageId) { - return res.status(422).json({ error: 'messageId is required' }) - } - - const result = await openViewOnceMessage(messageId, userId) - if (result.error) { - return res.status(result.status || 400).json({ error: result.error }) - } - - res.json({ - file: result.file, - fileType: result.fileType, - messageId: String(result.message._id) - }) - } catch (error) { - console.error('view-once open error:', error) - res.status(500).json({ error: 'Server error' }) - } -}) - -app.post('/api/v1/chat/view-once/complete', [blockCheck], async (req, res) => { - try { - const userId = String(req.user._id) - const { messageId } = req.body - if (!messageId) { - return res.status(422).json({ error: 'messageId is required' }) - } - - const result = await completeViewOnceMessage(io, messageId, userId) - if (result.error) { - return res.status(result.status || 400).json({ error: result.error }) - } - - res.json({ deletedIds: result.deletedIds }) - } catch (error) { - console.error('view-once complete error:', error) - 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: 'فقط پیام‌های خودتان قابل حذف هستند' }) - } - - const deletedIds = await deleteMessagesAndNotify(io, messages, { 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) -io.on('connection', (socket) => { - socket.on('joinUser', ({ userId }) => { - if (userId) socket.join(`user:${userId}`) - }) - - socket.on('joinChat', ({ userId, receiverId }) => { - if (userId && receiverId) { - socket.join(`chat:${userId}:${receiverId}`) - socket.join(`chat:${receiverId}:${userId}`) - } - }) - - socket.on('typing', ({ senderId, receiverId }) => { - io.to(`chat:${receiverId}:${senderId}`).emit('userTyping', { userId: senderId }) - }) - - socket.on('stopTyping', ({ senderId, receiverId }) => { - io.to(`chat:${receiverId}:${senderId}`).emit('userStoppedTyping', { userId: senderId }) - }) - - socket.on('messageSeen', async ({ messageId, senderId, receiverId }) => { - try { - if (messageId) { - await MessageModel.findByIdAndUpdate(messageId, { readStatus: 1 }) - io.to(`chat:${receiverId}:${senderId}`).emit('messageStatusUpdate', { - messageId, - status: 'seen' - }) - } - } catch (e) { - console.error('messageSeen error', e) - } - }) -}) - -// Free Project -require('./services/AdvertisingCron') -require('./services/ProjectCron') - -cron.schedule('*/30 * * * * *', () => { - purgeExpiredMessages(io).catch((err) => console.error('expired messages cron:', err)) -}) - -cron.schedule('0 0 1 * *', async () => { - try { - await UserModel.updateMany({}, { - daily_free_request: 1, - last_free_request_date: new Date(), - monthly_free_offer: 1, - last_free_offer_date: new Date() - }) - console.log('Monthly free requests reset for all users.') - } catch (error) { - console.error('Error resetting daily free requests:', error) - } -}, { - scheduled: true, - timezone: 'Asia/Tehran' -}) -module.exports = (port) => { - http.listen(port, () => { - console.log(`HTTP server is running on port ${port}`) - }) -} +require('dotenv').config(); +const express = require('express'); +const app = express(); +const path = require('path'); +const http = require('http').createServer(app); +const cors = require('cors'); +const moment = require('moment-jalaali'); +const MessageModel = require('./models/MessageModel'); +const fs = require('fs-extra'); +const cron = require('node-cron'); +const UserModel = require('./models/UserModel'); +const blockCheck = require('./middlewares/blockCheck'); +const rateLimit = require('express-rate-limit'); +const { default: axios } = require('axios'); + +const allowedOrigins = [ + 'http://localhost:3000', + 'http://localhost:3001', // اضافه شد — مهم برای توسعه فرانت + 'http://localhost:3002', + 'http://localhost:3003', + 'http://localhost:3004', + 'http://localhost', + 'https://modstagram.com', + 'http://modstagram.com', + 'http://193.151.143.243:3004', + 'https://www.modstagram.com', + 'https://api.modstagram.com', + 'https://panel.modstagram.com', + 'https://www.panel.modstagram.com', + 'https://panel.modstagram.ir', + 'https://www.panel.modstagram.ir', + 'ionic://localhost', + 'capacitor://localhost', + // IPهای محلی (برای تست در شبکه داخلی)/ + /^http:\/\/192\.168\.\d{1,3}\.\d{1,3}:\d+$/, + /^http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$/, +]; + +app.use(cors({ + origin: (origin, callback) => { + // در محیط توسعه، همه originها مجاز باشن + if (process.env.NODE_ENV !== 'production') { + return callback(null, true); + } + + // اگر origin وجود نداشته باشه (مثل درخواست از موبایل یا Postman) اجازه بده + if (!origin) { + return callback(null, true); + } + + // چک لیست یا RegExp + const isAllowed = allowedOrigins.some(item => { + if (typeof item === 'string') return item === origin; + if (item instanceof RegExp) return item.test(origin); + return false; + }); + + if (isAllowed) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-no-toast'], + credentials: true, + optionsSuccessStatus: 200 // برای مرورگرهای قدیمی +})); + +// Socket.IO CORS +const io = require('socket.io')(http, { + cors: { + origin: (origin, callback) => { + if (process.env.NODE_ENV !== 'production') { + return callback(null, true); + } + + if (!origin) { + return callback(null, true); + } + + const isAllowed = allowedOrigins.some(item => { + if (typeof item === 'string') return item === origin; + if (item instanceof RegExp) return item.test(origin); + return false; + }); + + if (isAllowed) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'], + credentials: true + } +}); + +// افزایش limit برای Base64 (1000mb کافیه) +app.use(express.json({ limit: '1000mb' })); +app.use(express.urlencoded({ limit: '1000mb', extended: true })); + +// مسیرهای مشخص قبل از /storage عمومی — پروفایل از هر دو محل (قدیم و جدید) سرو می‌شود +app.use('/storage/profiles', express.static(path.join(__dirname, '../storage/profiles'))); +app.use('/storage/profiles', express.static(path.join(__dirname, 'storage/profiles'))); +app.use('/storage/carts', express.static(path.join(__dirname, '../storage/carts'))); +app.use('/storage/carts', express.static(path.join(__dirname, 'storage/carts'))); +app.use('/storage/posts', express.static(path.join(__dirname, 'storage/posts'))); +app.use('/storage/posts', express.static(path.join(__dirname, '../storage/posts'))); +app.use('/storage/messages', express.static(path.join(__dirname, '../storage/messages'))); +app.use('/storage/messages', express.static(path.join(__dirname, 'storage/messages'))); +app.use('/storage/tickets', express.static(path.join(__dirname, '../storage/tickets'))); +app.use('/storage/tickets', express.static(path.join(__dirname, 'storage/tickets'))); +app.use('/storage/advertising', express.static(path.join(__dirname, '../storage/advertising'))); +app.use('/storage/advertising', express.static(path.join(__dirname, 'storage/advertising'))); +app.use('/storage/services', express.static(path.join(__dirname, '../storage/services'))); +app.use('/storage/services', express.static(path.join(__dirname, 'storage/services'))); +app.use('/storage', express.static(path.join(__dirname, 'storage'))); + +require('./boot'); + +// غیرفعال کردن پارس‌کننده‌های بدنه برای روت‌های آپلود فایل +app.use((req, res, next) => { + if (req.path === '/api/v1/posts/create' && req.method === 'POST') { + console.log('DEBUG: Skipping body parsers for /api/v1/posts/create'); + return next(); + } + express.json()(req, res, next); +}); +app.use((req, res, next) => { + if (req.path === '/api/v1/posts/create' && req.method === 'POST') { + return next(); + } + express.urlencoded({ extended: true })(req, res, next); +}); + +require('./middlewares')(app); + +const limiter = rateLimit({ + windowMs: 3 * 60 * 1000, + max: 3000, + standardHeaders: true, + legacyHeaders: false, + message: 'تعداد درخواست‌های شما بیشتر از حد مجاز است، لطفاً بعداً دوباره تلاش کنید.', + handler: (req, res) => { + const retryAfter = Math.ceil((req.rateLimit.resetTime - Date.now()) / 1000); + res.set('Retry-After', retryAfter); + res.status(429).json({ + error: `شما بیش از حد مجاز درخواست ارسال کرده‌اید. لطفاً بعد از ${retryAfter} ثانیه دوباره تلاش کنید.` + }); + } +}); +app.use(limiter); +app.set('trust proxy', 'loopback'); + +require('./routes')(app); + +const { enrichMessage, emitChatEvents, parseForwardedContent } = require('./helpers/chatHelpers') +const { + parseSelfDestructSeconds, + buildExpiresAt, + activeMessageFilter, + purgeExpiredMessages, + scheduleMessageExpiry, + deleteMessagesAndNotify +} = require('./helpers/expiredMessages') +const { + parseViewOnce, + isViewOnceMediaType, + sanitizeMessageForViewer, + openViewOnceMessage, + completeViewOnceMessage +} = require('./helpers/viewOnceMessages') + +const mongoose = require('mongoose') + +app.get('/api/v1/chat', async (req, res) => { + try { + const { senderId, receiverId, limit, page } = req.query + + if (!senderId || !receiverId) { + return res.status(400).json({ error: 'senderId and receiverId are required' }) + } + + if ( + !mongoose.Types.ObjectId.isValid(String(senderId)) || + !mongoose.Types.ObjectId.isValid(String(receiverId)) + ) { + return res.status(400).json({ error: 'Invalid senderId or receiverId' }) + } + + const sid = new mongoose.Types.ObjectId(String(senderId)) + const rid = new mongoose.Types.ObjectId(String(receiverId)) + const pageNumber = parseInt(page) || 1 + const limitPerPage = parseInt(limit) || 40 + + const filter = activeMessageFilter({ + $or: [ + { senderId: sid, receiverId: rid }, + { senderId: rid, receiverId: sid } + ] + }) + + const totalMessagesCount = await MessageModel.countDocuments(filter) + const totalPages = Math.ceil(totalMessagesCount / limitPerPage) || 1 + const skip = (pageNumber - 1) * limitPerPage + + const messages = await MessageModel.find(filter) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limitPerPage) + + await MessageModel.updateMany( + { _id: { $in: messages.map((m) => m._id) }, receiverId: sid }, + { $set: { readStatus: 1 } } + ) + + const enriched = await Promise.all( + messages.map((m) => + enrichMessage(sanitizeMessageForViewer(m, sid)) + ) + ) + res.json({ messages: enriched.reverse(), totalPages }) + } catch (error) { + console.error('Error fetching messages:', error) + res.status(500).json({ error: 'Server error' }) + } +}) + +app.post('/api/v1/chat', [blockCheck], async (req, res) => { + try { + const { senderId, receiverId, content, replyToId, forwardedFrom: fwdBody, selfDestructSeconds } = req.body + const { body, forwardedFrom: fwdParsed } = parseForwardedContent(content) + const destructSec = parseSelfDestructSeconds(selfDestructSeconds) + + const payload = { + senderId, + receiverId, + content: body || content, + replyToId: replyToId || undefined, + forwardedFrom: fwdBody || fwdParsed || undefined, + expiresAt: buildExpiresAt(destructSec) + } + + const newMessage = new MessageModel(payload) + await newMessage.save() + const formatted = await enrichMessage(newMessage) + + res.status(201).json({ data: formatted }) + emitChatEvents(io, formatted, senderId, receiverId) + scheduleMessageExpiry(io, newMessage) + } catch (error) { + console.error('Error sending message:', error) + res.status(500).json({ error: 'Server error' }) + } +}) +app.get('/api/v1/chat/read', async (req, res) => { + try { + const { senderId, receiverId } = req.query // شناسه فرستنده و گیرنده از درخواست دریافت شود + // پیدا کردن تمام پیام‌هایی که کاربر دوم مشاهده کرده است و کاربر اول آنها را ارسال کرده است + const messages = await MessageModel.find( + { senderId: receiverId, receiverId: senderId } // برای مواقعی که شناسه‌ها معکوس باشند + ) + // به روزرسانی وضعیت خوانده شده یا نشده بودن پیام‌ها به "خوانده شده" + await MessageModel.updateMany({ _id: { $in: messages.map(message => message._id) } }, { $set: { readStatus: 1 } }) + res.status(200).json({ message: 'Message read status updated successfully' }) + } catch (error) { + console.error('Error updating message read status:', error) + res.status(500).json({ error: 'Server error' }) + } +}) + +app.post("/api/v1/notification/send-sms", async (req, res) => { + const { receiverId, message } = req.body; + const user = await UserModel.findById(receiverId); + if (!user || !user.mobile) return res.status(404).json({ error: "User not found" }); + console.log("3"); + + const mobile = user.mobile + const user_name = user.user_name + const data = JSON.stringify({ + mobile, + templateId: '876533', + parameters: [ + { name: 'USER', value: user_name } + ] + }) + + const config = { + method: 'post', + url: 'https://api.sms.ir/v1/send/verify', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/plain', + 'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt' + }, + data + } + + axios(config) + .then(function (response) { + console.log(response); + + }) + .catch(function (error) { + console.log(error) + }) + + res.status(200).json({ success: true }); +}); + +app.post('/api/v1/chat/file', [blockCheck], async (req, res) => { + try { + const { senderId, receiverId, content, replyToId, fileType, selfDestructSeconds, viewOnce } = req.body + const destructSec = parseSelfDestructSeconds(selfDestructSeconds) + const viewOnceFlag = parseViewOnce(viewOnce) + const resolvedFileType = fileType || undefined + + if (viewOnceFlag && !isViewOnceMediaType(resolvedFileType)) { + return res.status(422).json({ error: 'View once is only for image, video, and voice' }) + } + const { file } = req.files + let fileUrl = null + if (file) { + const uploadDir = path.join(__dirname, '../storage/messages') + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + const uniqueFileName = `${Date.now()}-${file.name}` + const filePath = path.join(uploadDir, uniqueFileName) + await fs.move(file.path, filePath) + fileUrl = `/messages/${uniqueFileName}` + } + + const newMessage = new MessageModel({ + senderId, + receiverId, + content: content || '', + file: fileUrl, + fileType: resolvedFileType, + replyToId: replyToId || undefined, + expiresAt: buildExpiresAt(destructSec), + viewOnce: viewOnceFlag + }) + await newMessage.save() + const formatted = await enrichMessage(newMessage) + + res.status(201).json({ data: formatted }) + emitChatEvents(io, formatted, senderId, receiverId) + scheduleMessageExpiry(io, newMessage) + } catch (error) { + console.error('Error sending message:', error) + res.status(500).json({ error: 'Server error' }) + } +}) + +app.post('/api/v1/chat/view-once/open', [blockCheck], async (req, res) => { + try { + const userId = String(req.user._id) + const { messageId } = req.body + if (!messageId) { + return res.status(422).json({ error: 'messageId is required' }) + } + + const result = await openViewOnceMessage(messageId, userId) + if (result.error) { + return res.status(result.status || 400).json({ error: result.error }) + } + + res.json({ + file: result.file, + fileType: result.fileType, + messageId: String(result.message._id) + }) + } catch (error) { + console.error('view-once open error:', error) + res.status(500).json({ error: 'Server error' }) + } +}) + +app.post('/api/v1/chat/view-once/complete', [blockCheck], async (req, res) => { + try { + const userId = String(req.user._id) + const { messageId } = req.body + if (!messageId) { + return res.status(422).json({ error: 'messageId is required' }) + } + + const result = await completeViewOnceMessage(io, messageId, userId) + if (result.error) { + return res.status(result.status || 400).json({ error: result.error }) + } + + res.json({ deletedIds: result.deletedIds }) + } catch (error) { + console.error('view-once complete error:', error) + 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: 'فقط پیام‌های خودتان قابل حذف هستند' }) + } + + const deletedIds = await deleteMessagesAndNotify(io, messages, { 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) +io.on('connection', (socket) => { + socket.on('joinUser', ({ userId }) => { + if (userId) socket.join(`user:${userId}`) + }) + + socket.on('joinChat', ({ userId, receiverId }) => { + if (userId && receiverId) { + socket.join(`chat:${userId}:${receiverId}`) + socket.join(`chat:${receiverId}:${userId}`) + } + }) + + socket.on('typing', ({ senderId, receiverId }) => { + io.to(`chat:${receiverId}:${senderId}`).emit('userTyping', { userId: senderId }) + }) + + socket.on('stopTyping', ({ senderId, receiverId }) => { + io.to(`chat:${receiverId}:${senderId}`).emit('userStoppedTyping', { userId: senderId }) + }) + + socket.on('messageSeen', async ({ messageId, senderId, receiverId }) => { + try { + if (messageId) { + await MessageModel.findByIdAndUpdate(messageId, { readStatus: 1 }) + io.to(`chat:${receiverId}:${senderId}`).emit('messageStatusUpdate', { + messageId, + status: 'seen' + }) + } + } catch (e) { + console.error('messageSeen error', e) + } + }) +}) + +// Free Project +require('./services/AdvertisingCron') +require('./services/ProjectCron') + +cron.schedule('*/30 * * * * *', () => { + purgeExpiredMessages(io).catch((err) => console.error('expired messages cron:', err)) +}) + +cron.schedule('0 0 1 * *', async () => { + try { + await UserModel.updateMany({}, { + daily_free_request: 1, + last_free_request_date: new Date(), + monthly_free_offer: 1, + last_free_offer_date: new Date() + }) + console.log('Monthly free requests reset for all users.') + } catch (error) { + console.error('Error resetting daily free requests:', error) + } +}, { + scheduled: true, + timezone: 'Asia/Tehran' +}) +const port = process.env.APP_PORT || 3002; +http.listen(port, () => { + console.log(`HTTP server is running on port ${port}`); +}); diff --git a/storage/posts/images/1783480489898-0-pngtree-mystic-blackberry-a-textured-design-on-an-abstract-dark-purple-background-image_13879614.webp b/storage/posts/images/1783480489898-0-pngtree-mystic-blackberry-a-textured-design-on-an-abstract-dark-purple-background-image_13879614.webp new file mode 100644 index 0000000..22cbb4f Binary files /dev/null and b/storage/posts/images/1783480489898-0-pngtree-mystic-blackberry-a-textured-design-on-an-abstract-dark-purple-background-image_13879614.webp differ diff --git a/storage/posts/images/1783493607061-0-IMG_20241230_133023.webp b/storage/posts/images/1783493607061-0-IMG_20241230_133023.webp new file mode 100644 index 0000000..a458aa2 Binary files /dev/null and b/storage/posts/images/1783493607061-0-IMG_20241230_133023.webp differ diff --git a/storage/posts/images/1783630511019-0-1000359068.webp b/storage/posts/images/1783630511019-0-1000359068.webp new file mode 100644 index 0000000..d4f5f69 Binary files /dev/null and b/storage/posts/images/1783630511019-0-1000359068.webp differ diff --git a/storage/posts/images/1783630511019-1-1000359064.webp b/storage/posts/images/1783630511019-1-1000359064.webp new file mode 100644 index 0000000..e70c341 Binary files /dev/null and b/storage/posts/images/1783630511019-1-1000359064.webp differ diff --git a/storage/profiles/amirreza-1783480544685.jpg b/storage/profiles/amirreza-1783480544685.jpg new file mode 100644 index 0000000..31f1a8e Binary files /dev/null and b/storage/profiles/amirreza-1783480544685.jpg differ diff --git a/storage/profiles/mahaki-1783583191589.jpg b/storage/profiles/mahaki-1783583191589.jpg new file mode 100644 index 0000000..48c0f15 Binary files /dev/null and b/storage/profiles/mahaki-1783583191589.jpg differ diff --git a/storage/profiles/modstagram-1783493542394.png b/storage/profiles/modstagram-1783493542394.png new file mode 100644 index 0000000..3649566 Binary files /dev/null and b/storage/profiles/modstagram-1783493542394.png differ diff --git a/utils/blockSuspension.js b/utils/blockSuspension.js index 0d503e3..0af4af0 100644 --- a/utils/blockSuspension.js +++ b/utils/blockSuspension.js @@ -1,77 +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 -} +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 index 6642052..636941b 100644 --- a/utils/blockVisibility.js +++ b/utils/blockVisibility.js @@ -1,49 +1,49 @@ -function listIncludesUserId(list, userId) { - if (!userId || !list?.length) return false - return list.some((id) => String(id) === String(userId)) -} - -/** Viewer has blocked the profile owner */ -function viewerBlockedUser(user, viewerId) { - if (!user || !viewerId) return false - return listIncludesUserId(user.blocked_by, viewerId) -} - -/** Profile owner has blocked the viewer */ -function userBlockedViewer(user, viewerId) { - if (!user || !viewerId) return false - return listIncludesUserId(user.blocked_users, viewerId) -} - -/** Viewer (blocked person) cannot see profile/posts of user who blocked them */ -function viewerIsBlockedBy(user, viewerId) { - return userBlockedViewer(user, 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 = { - listIncludesUserId, - viewerBlockedUser, - userBlockedViewer, - viewerIsBlockedBy, - blockedProfilePayload, - getBlockerUserIds, -} +function listIncludesUserId(list, userId) { + if (!userId || !list?.length) return false + return list.some((id) => String(id) === String(userId)) +} + +/** Viewer has blocked the profile owner */ +function viewerBlockedUser(user, viewerId) { + if (!user || !viewerId) return false + return listIncludesUserId(user.blocked_by, viewerId) +} + +/** Profile owner has blocked the viewer */ +function userBlockedViewer(user, viewerId) { + if (!user || !viewerId) return false + return listIncludesUserId(user.blocked_users, viewerId) +} + +/** Viewer (blocked person) cannot see profile/posts of user who blocked them */ +function viewerIsBlockedBy(user, viewerId) { + return userBlockedViewer(user, 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 = { + listIncludesUserId, + viewerBlockedUser, + userBlockedViewer, + viewerIsBlockedBy, + blockedProfilePayload, + getBlockerUserIds, +} diff --git a/utils/commentNotification.js b/utils/commentNotification.js index fe1bf52..0772798 100644 --- a/utils/commentNotification.js +++ b/utils/commentNotification.js @@ -1,57 +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 -} +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 index 4755545..dfad111 100644 --- a/utils/commentRating.js +++ b/utils/commentRating.js @@ -1,91 +1,91 @@ -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) { - if (rate && rate >= 1 && rate <= 5) { - return { - ratingValue: null, - isNewRating: false, - alreadyRated: true, - } - } - 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 -} +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) { + if (rate && rate >= 1 && rate <= 5) { + return { + ratingValue: null, + isNewRating: false, + alreadyRated: true, + } + } + 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/exploreAlgorithm.js b/utils/exploreAlgorithm.js index c2548be..f1e9299 100644 --- a/utils/exploreAlgorithm.js +++ b/utils/exploreAlgorithm.js @@ -1,543 +1,543 @@ -const LikeModel = require('../models/LikeModel') -const CommentModel = require('../models/CommentModel') -const OfferModel = require('../models/OfferModel') -const UserModel = require('../models/UserModel') -const PostModel = require('../models/PostModel') -const ExploreInteractionModel = require('../models/ExploreInteractionModel') - -const EXPERTISE_ALIASES = { - مدل: ['مدل', 'مدلینگ'], - مدلینگ: ['مدل', 'مدلینگ'], - عکاس: ['عکاس', 'عکاسی'], - عکاسی: ['عکاس', 'عکاسی'], - آرایشگر: ['آرایشگر', 'زیبایی'], - زیبایی: ['آرایشگر', 'زیبایی'] -} - -const ACTION_WEIGHTS = { - offer: 10, - profile_visit: 6, - share: 5, - comment: 4, - like: 3, - watch_complete: 3.5, - view: 2, - skip: -1.5, - dwell: 1.5 -} - -function expertiseMatches(userExpertise, authorExpertise) { - if (!userExpertise || !authorExpertise) return false - const aliases = EXPERTISE_ALIASES[userExpertise] || [userExpertise] - return aliases.includes(authorExpertise) -} - -function interactionDecay(createdAt) { - const ageDays = - (Date.now() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24) - return Math.exp(-ageDays / 21) -} - -function engagementVelocity(likesCount, commentsCount, ageHours) { - const raw = likesCount * 0.55 + commentsCount * 1.1 - const ageFactor = Math.max(1, ageHours / 24) - return raw / Math.sqrt(ageFactor) -} - -function shuffleArray(items, seed = Date.now()) { - const copy = [...items] - let state = seed % 2147483647 || 1 - const random = () => { - state = (state * 16807) % 2147483647 - return (state - 1) / 2147483646 - } - for (let i = copy.length - 1; i > 0; i -= 1) { - const j = Math.floor(random() * (i + 1)) - ;[copy[i], copy[j]] = [copy[j], copy[i]] - } - return copy -} - -function interleaveWeighted(primary, secondary, primaryRatio = 0.7) { - if (!primary.length) return shuffleArray(secondary) - if (!secondary.length) return shuffleArray(primary) - - const result = [] - let primaryIdx = 0 - let secondaryIdx = 0 - const primaryBurst = Math.max(2, Math.round(primaryRatio * 10)) - const secondaryBurst = Math.max(1, 10 - primaryBurst) - let primaryCount = 0 - let secondaryCount = 0 - - while (primaryIdx < primary.length || secondaryIdx < secondary.length) { - const shouldTakeSecondary = - secondaryIdx < secondary.length && - (primaryIdx >= primary.length || - (secondaryCount < secondaryBurst && - primaryCount >= primaryBurst)) - - if (shouldTakeSecondary) { - result.push(secondary[secondaryIdx]) - secondaryIdx += 1 - secondaryCount += 1 - primaryCount = 0 - continue - } - - if (primaryIdx < primary.length) { - result.push(primary[primaryIdx]) - primaryIdx += 1 - primaryCount += 1 - secondaryCount = 0 - continue - } - - if (secondaryIdx < secondary.length) { - result.push(secondary[secondaryIdx]) - secondaryIdx += 1 - } - } - - return result -} - -function matchesSeedCategory(author, seedAuthor) { - if (!seedAuthor || !author) return false - - if ( - seedAuthor.expertise && - expertiseMatches(seedAuthor.expertise, author.expertise) - ) { - return true - } - - if ( - seedAuthor.sub_expertise?.length && - author.sub_expertise?.some((s) => seedAuthor.sub_expertise.includes(s)) - ) { - return true - } - - return false -} - -function hasEnoughUserSignals(signals) { - const meaningful = - (signals.likedPostIds?.length || 0) + - (signals.commentedPostIds?.length || 0) + - Object.keys(signals.offerAuthors || {}).length - - const affinityStrength = Object.values(signals.authorAffinity || {}).reduce( - (sum, value) => sum + value, - 0 - ) - - return meaningful >= 8 || affinityStrength >= 22 -} - -function scoreColdStartPost(post, author, seedPost, seedAuthor) { - let score = Math.random() * 80 - - if (matchesSeedCategory(author, seedAuthor)) { - score += 50 - } - - if ( - seedAuthor?.sub_expertise?.length && - author?.sub_expertise?.some((s) => seedAuthor.sub_expertise.includes(s)) - ) { - score += 22 - } - - if (seedPost?.type && post.type === seedPost.type) { - score += 14 - } - - const likesCount = post.likes ? post.likes.length : post.likesCount || 0 - const commentsCount = post.commentsCount || 0 - const ageHours = - (Date.now() - new Date(post.createdAt).getTime()) / (1000 * 60 * 60) - score += engagementVelocity(likesCount, commentsCount, ageHours) * 0.25 - - return score -} - -function rankColdStartReels(pool, usersById, seedAuthor, seedPost) { - const sameCategory = [] - const others = [] - - pool.forEach((post) => { - const author = usersById[String(post.user_id)] || {} - if (matchesSeedCategory(author, seedAuthor)) { - sameCategory.push(post) - } else { - others.push(post) - } - }) - - const seedKey = seedPost?._id ? String(seedPost._id) : '0' - const shuffledSame = shuffleArray(sameCategory, seedKey.length * 997) - const shuffledOthers = shuffleArray(others, seedKey.length * 499) - - return interleaveWeighted(shuffledSame, shuffledOthers, 0.72) -} - -function diversifyByAuthor(posts, maxPerPage = 2) { - const authorCounts = {} - const picked = [] - const deferred = [] - - for (const post of posts) { - const authorId = String(post.user_id) - const count = authorCounts[authorId] || 0 - if (count < maxPerPage) { - picked.push(post) - authorCounts[authorId] = count + 1 - } else { - deferred.push(post) - } - } - - return [...picked, ...deferred] -} - -function blendFreshIntoRanked(rankedPosts, allPosts, ratio = 0.12) { - if (!rankedPosts.length) return rankedPosts - - const rankedIds = new Set(rankedPosts.map((p) => String(p._id))) - const fresh = [...allPosts] - .filter((p) => !rankedIds.has(String(p._id))) - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) - - if (!fresh.length) return rankedPosts - - const result = [...rankedPosts] - const step = Math.max(4, Math.round(1 / ratio)) - - let freshIdx = 0 - for (let i = step - 1; i < result.length && freshIdx < fresh.length; i += step) { - result.splice(i, 0, fresh[freshIdx]) - freshIdx += 1 - } - - while (freshIdx < fresh.length && result.length < rankedPosts.length + fresh.length) { - result.push(fresh[freshIdx]) - freshIdx += 1 - } - - return result -} - -async function buildUserExploreSignals(userId) { - if (!userId) { - return { - userExpertise: null, - userSubExpertise: [], - contentTypeWeight: { image: 0, video: 0, academy: 0 }, - authorAffinity: {}, - offerAuthors: {}, - viewedPosts: {}, - viewedAuthors: {}, - likedPostIds: [], - commentedPostIds: [], - isColdStart: true - } - } - - const user = await UserModel.findById(userId) - .select('expertise sub_expertise') - .lean() - - const [likes, comments, offers, interactions] = await Promise.all([ - LikeModel.find({ userId }).select('postId createdAt').lean(), - CommentModel.find({ - creator: userId, - comment_for: 'post', - post: { $ne: null } - }) - .select('post user createdAt') - .lean(), - OfferModel.find({ sender: userId }).select('receiver createdAt').lean(), - ExploreInteractionModel.find({ viewerId: userId }) - .sort({ createdAt: -1 }) - .limit(1200) - .lean() - ]) - - const likedPostIds = likes.map((l) => String(l.postId)) - const likedPosts = likedPostIds.length - ? await PostModel.find({ _id: { $in: likedPostIds } }) - .select('user_id type') - .lean() - : [] - - const contentTypeWeight = { image: 0, video: 0, academy: 0 } - const authorAffinity = {} - const offerAuthors = {} - const viewedPosts = {} - const viewedAuthors = {} - - const bumpAuthor = (authorId, weight) => { - if (!authorId) return - const key = String(authorId) - authorAffinity[key] = (authorAffinity[key] || 0) + weight - } - - const bumpContent = (type, weight) => { - if (!type) return - contentTypeWeight[type] = (contentTypeWeight[type] || 0) + weight - } - - const bumpViewedPost = (postId, weight) => { - if (!postId) return - const key = String(postId) - viewedPosts[key] = Math.max(viewedPosts[key] || 0, weight) - } - - const bumpViewedAuthor = (authorId, weight) => { - if (!authorId) return - const key = String(authorId) - viewedAuthors[key] = Math.max(viewedAuthors[key] || 0, weight) - } - - offers.forEach((offer) => { - const decay = interactionDecay(offer.createdAt) - const key = String(offer.receiver) - offerAuthors[key] = (offerAuthors[key] || 0) + 12 * decay - bumpAuthor(offer.receiver, 8 * decay) - }) - - likedPosts.forEach((post) => { - bumpAuthor(post.user_id, 4) - bumpContent(post.type, 2) - }) - - comments.forEach((comment) => { - const decay = interactionDecay(comment.createdAt) - bumpAuthor(comment.user, 5 * decay) - }) - - interactions.forEach((item) => { - const decay = interactionDecay(item.createdAt) - const base = - ACTION_WEIGHTS[item.action] ?? ACTION_WEIGHTS.view - const weight = base * decay - - if (item.contentType) bumpContent(item.contentType, weight * 0.5) - if (item.authorId) bumpAuthor(item.authorId, weight) - - if ( - item.targetType === 'post' && - ['view', 'watch_complete', 'skip', 'dwell'].includes(item.action) - ) { - const viewWeight = - item.action === 'watch_complete' - ? 1 - : item.action === 'skip' - ? 0.85 - : item.action === 'dwell' - ? 0.55 - : 0.35 - bumpViewedPost(item.targetId, viewWeight * decay) - if (item.authorId) bumpViewedAuthor(item.authorId, viewWeight * decay * 0.6) - } - }) - - return { - userExpertise: user?.expertise || null, - userSubExpertise: user?.sub_expertise || [], - contentTypeWeight, - authorAffinity, - offerAuthors, - viewedPosts, - viewedAuthors, - likedPostIds, - commentedPostIds: comments.map((c) => String(c.post)), - isColdStart: !hasEnoughUserSignals({ - likedPostIds, - commentedPostIds: comments.map((c) => String(c.post)), - offerAuthors, - authorAffinity - }) - } -} - -function scorePost(post, author, signals, options = {}) { - const { reelsMode = false, seedPost = null, seedAuthor = null } = options - let score = Math.random() * 4 - - const postId = String(post._id) - const authorId = String(post.user_id) - const likesCount = post.likes ? post.likes.length : post.likesCount || 0 - const commentsCount = post.commentsCount || 0 - - const ageHours = - (Date.now() - new Date(post.createdAt).getTime()) / (1000 * 60 * 60) - - score += engagementVelocity(likesCount, commentsCount, ageHours) - - if (signals.userExpertise && expertiseMatches(signals.userExpertise, author?.expertise)) { - score += 24 - } - - if ( - signals.userSubExpertise?.length && - author?.sub_expertise?.some((s) => signals.userSubExpertise.includes(s)) - ) { - score += 16 - } - - if (post.type && signals.contentTypeWeight[post.type]) { - score += signals.contentTypeWeight[post.type] * 2 - } - - score += signals.authorAffinity[authorId] || 0 - score += signals.offerAuthors[authorId] || 0 - - if (signals.likedPostIds?.includes(postId)) score -= 35 - if (signals.commentedPostIds?.includes(postId)) score -= 28 - - const viewedWeight = signals.viewedPosts?.[postId] || 0 - if (viewedWeight > 0) score -= 8 + viewedWeight * 22 - - const authorFatigue = signals.viewedAuthors?.[authorId] || 0 - if (authorFatigue > 0) score -= authorFatigue * 6 - - if (ageHours < 24) score += 6 - else if (ageHours < 48) score += 4 - else if (ageHours < 168) score += 2 - - if (reelsMode && post.type === 'video') score += 14 - - if (seedPost) { - if (String(seedPost.user_id) === authorId) score += 20 - if ( - seedAuthor?.expertise && - expertiseMatches(seedAuthor.expertise, author?.expertise) - ) { - score += 18 - } - if ( - seedAuthor?.sub_expertise?.length && - author?.sub_expertise?.some((s) => seedAuthor.sub_expertise.includes(s)) - ) { - score += 8 - } - if (seedPost.type && post.type === seedPost.type) score += 6 - } - - return score -} - -async function rankPostsForFeed({ - posts, - usersById, - userId, - seedPostId, - reelsMode = false, - blendFresh = false -}) { - const signals = await buildUserExploreSignals(userId) - const coldStart = signals.isColdStart - - let seedPost = null - let seedAuthor = null - if (seedPostId) { - seedPost = posts.find((p) => String(p._id) === String(seedPostId)) || null - if (!seedPost) { - seedPost = await PostModel.findById(seedPostId).lean() - } - if (seedPost) { - seedAuthor = - usersById[String(seedPost.user_id)] || - (await UserModel.findById(seedPost.user_id) - .select( - '_id expertise sub_expertise user_name first_name last_name profile_image' - ) - .lean()) - if (seedAuthor && !usersById[String(seedPost.user_id)]) { - usersById[String(seedPost.user_id)] = seedAuthor - } - } - } - - const pool = seedPost - ? posts.filter((p) => String(p._id) !== String(seedPostId)) - : posts - - let ranked = [] - - if (coldStart && reelsMode && seedAuthor) { - ranked = rankColdStartReels(pool, usersById, seedAuthor, seedPost) - } else if (coldStart && reelsMode && seedPost) { - ranked = shuffleArray(pool, String(seedPostId).length * 131) - } else if (coldStart) { - ranked = shuffleArray(pool) - if (blendFresh) { - ranked = blendFreshIntoRanked(ranked, pool, 0.2) - } - } else { - const scored = pool - .map((post) => ({ - post, - score: scorePost(post, usersById[String(post.user_id)] || {}, signals, { - reelsMode, - seedPost, - seedAuthor - }) - })) - .sort((a, b) => b.score - a.score) - .map((item) => item.post) - - ranked = diversifyByAuthor(scored, reelsMode ? 1 : 2) - - if (blendFresh) { - ranked = blendFreshIntoRanked(ranked, pool) - } - } - - if (seedPost) { - ranked = [seedPost, ...ranked] - } - - return ranked -} - -async function paginatePersonalizedExplore({ - posts, - usersById, - page, - limit, - userId, - seedPostId, - reelsMode = false, - blendFresh = false -}) { - const pageNum = Math.max(1, parseInt(page, 10) || 1) - const limitNum = Math.max(1, parseInt(limit, 10) || 10) - - const ranked = await rankPostsForFeed({ - posts, - usersById, - userId, - seedPostId, - reelsMode, - blendFresh: blendFresh && !seedPostId && !reelsMode - }) - - const totalItems = ranked.length - const startIndex = (pageNum - 1) * limitNum - - return { - posts: ranked.slice(startIndex, startIndex + limitNum), - totalItems - } -} - -module.exports = { - buildUserExploreSignals, - paginatePersonalizedExplore, - rankPostsForFeed, - scorePost -} +const LikeModel = require('../models/LikeModel') +const CommentModel = require('../models/CommentModel') +const OfferModel = require('../models/OfferModel') +const UserModel = require('../models/UserModel') +const PostModel = require('../models/PostModel') +const ExploreInteractionModel = require('../models/ExploreInteractionModel') + +const EXPERTISE_ALIASES = { + مدل: ['مدل', 'مدلینگ'], + مدلینگ: ['مدل', 'مدلینگ'], + عکاس: ['عکاس', 'عکاسی'], + عکاسی: ['عکاس', 'عکاسی'], + آرایشگر: ['آرایشگر', 'زیبایی'], + زیبایی: ['آرایشگر', 'زیبایی'] +} + +const ACTION_WEIGHTS = { + offer: 10, + profile_visit: 6, + share: 5, + comment: 4, + like: 3, + watch_complete: 3.5, + view: 2, + skip: -1.5, + dwell: 1.5 +} + +function expertiseMatches(userExpertise, authorExpertise) { + if (!userExpertise || !authorExpertise) return false + const aliases = EXPERTISE_ALIASES[userExpertise] || [userExpertise] + return aliases.includes(authorExpertise) +} + +function interactionDecay(createdAt) { + const ageDays = + (Date.now() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24) + return Math.exp(-ageDays / 21) +} + +function engagementVelocity(likesCount, commentsCount, ageHours) { + const raw = likesCount * 0.55 + commentsCount * 1.1 + const ageFactor = Math.max(1, ageHours / 24) + return raw / Math.sqrt(ageFactor) +} + +function shuffleArray(items, seed = Date.now()) { + const copy = [...items] + let state = seed % 2147483647 || 1 + const random = () => { + state = (state * 16807) % 2147483647 + return (state - 1) / 2147483646 + } + for (let i = copy.length - 1; i > 0; i -= 1) { + const j = Math.floor(random() * (i + 1)) + ;[copy[i], copy[j]] = [copy[j], copy[i]] + } + return copy +} + +function interleaveWeighted(primary, secondary, primaryRatio = 0.7) { + if (!primary.length) return shuffleArray(secondary) + if (!secondary.length) return shuffleArray(primary) + + const result = [] + let primaryIdx = 0 + let secondaryIdx = 0 + const primaryBurst = Math.max(2, Math.round(primaryRatio * 10)) + const secondaryBurst = Math.max(1, 10 - primaryBurst) + let primaryCount = 0 + let secondaryCount = 0 + + while (primaryIdx < primary.length || secondaryIdx < secondary.length) { + const shouldTakeSecondary = + secondaryIdx < secondary.length && + (primaryIdx >= primary.length || + (secondaryCount < secondaryBurst && + primaryCount >= primaryBurst)) + + if (shouldTakeSecondary) { + result.push(secondary[secondaryIdx]) + secondaryIdx += 1 + secondaryCount += 1 + primaryCount = 0 + continue + } + + if (primaryIdx < primary.length) { + result.push(primary[primaryIdx]) + primaryIdx += 1 + primaryCount += 1 + secondaryCount = 0 + continue + } + + if (secondaryIdx < secondary.length) { + result.push(secondary[secondaryIdx]) + secondaryIdx += 1 + } + } + + return result +} + +function matchesSeedCategory(author, seedAuthor) { + if (!seedAuthor || !author) return false + + if ( + seedAuthor.expertise && + expertiseMatches(seedAuthor.expertise, author.expertise) + ) { + return true + } + + if ( + seedAuthor.sub_expertise?.length && + author.sub_expertise?.some((s) => seedAuthor.sub_expertise.includes(s)) + ) { + return true + } + + return false +} + +function hasEnoughUserSignals(signals) { + const meaningful = + (signals.likedPostIds?.length || 0) + + (signals.commentedPostIds?.length || 0) + + Object.keys(signals.offerAuthors || {}).length + + const affinityStrength = Object.values(signals.authorAffinity || {}).reduce( + (sum, value) => sum + value, + 0 + ) + + return meaningful >= 8 || affinityStrength >= 22 +} + +function scoreColdStartPost(post, author, seedPost, seedAuthor) { + let score = Math.random() * 80 + + if (matchesSeedCategory(author, seedAuthor)) { + score += 50 + } + + if ( + seedAuthor?.sub_expertise?.length && + author?.sub_expertise?.some((s) => seedAuthor.sub_expertise.includes(s)) + ) { + score += 22 + } + + if (seedPost?.type && post.type === seedPost.type) { + score += 14 + } + + const likesCount = post.likes ? post.likes.length : post.likesCount || 0 + const commentsCount = post.commentsCount || 0 + const ageHours = + (Date.now() - new Date(post.createdAt).getTime()) / (1000 * 60 * 60) + score += engagementVelocity(likesCount, commentsCount, ageHours) * 0.25 + + return score +} + +function rankColdStartReels(pool, usersById, seedAuthor, seedPost) { + const sameCategory = [] + const others = [] + + pool.forEach((post) => { + const author = usersById[String(post.user_id)] || {} + if (matchesSeedCategory(author, seedAuthor)) { + sameCategory.push(post) + } else { + others.push(post) + } + }) + + const seedKey = seedPost?._id ? String(seedPost._id) : '0' + const shuffledSame = shuffleArray(sameCategory, seedKey.length * 997) + const shuffledOthers = shuffleArray(others, seedKey.length * 499) + + return interleaveWeighted(shuffledSame, shuffledOthers, 0.72) +} + +function diversifyByAuthor(posts, maxPerPage = 2) { + const authorCounts = {} + const picked = [] + const deferred = [] + + for (const post of posts) { + const authorId = String(post.user_id) + const count = authorCounts[authorId] || 0 + if (count < maxPerPage) { + picked.push(post) + authorCounts[authorId] = count + 1 + } else { + deferred.push(post) + } + } + + return [...picked, ...deferred] +} + +function blendFreshIntoRanked(rankedPosts, allPosts, ratio = 0.12) { + if (!rankedPosts.length) return rankedPosts + + const rankedIds = new Set(rankedPosts.map((p) => String(p._id))) + const fresh = [...allPosts] + .filter((p) => !rankedIds.has(String(p._id))) + .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + + if (!fresh.length) return rankedPosts + + const result = [...rankedPosts] + const step = Math.max(4, Math.round(1 / ratio)) + + let freshIdx = 0 + for (let i = step - 1; i < result.length && freshIdx < fresh.length; i += step) { + result.splice(i, 0, fresh[freshIdx]) + freshIdx += 1 + } + + while (freshIdx < fresh.length && result.length < rankedPosts.length + fresh.length) { + result.push(fresh[freshIdx]) + freshIdx += 1 + } + + return result +} + +async function buildUserExploreSignals(userId) { + if (!userId) { + return { + userExpertise: null, + userSubExpertise: [], + contentTypeWeight: { image: 0, video: 0, academy: 0 }, + authorAffinity: {}, + offerAuthors: {}, + viewedPosts: {}, + viewedAuthors: {}, + likedPostIds: [], + commentedPostIds: [], + isColdStart: true + } + } + + const user = await UserModel.findById(userId) + .select('expertise sub_expertise') + .lean() + + const [likes, comments, offers, interactions] = await Promise.all([ + LikeModel.find({ userId }).select('postId createdAt').lean(), + CommentModel.find({ + creator: userId, + comment_for: 'post', + post: { $ne: null } + }) + .select('post user createdAt') + .lean(), + OfferModel.find({ sender: userId }).select('receiver createdAt').lean(), + ExploreInteractionModel.find({ viewerId: userId }) + .sort({ createdAt: -1 }) + .limit(1200) + .lean() + ]) + + const likedPostIds = likes.map((l) => String(l.postId)) + const likedPosts = likedPostIds.length + ? await PostModel.find({ _id: { $in: likedPostIds } }) + .select('user_id type') + .lean() + : [] + + const contentTypeWeight = { image: 0, video: 0, academy: 0 } + const authorAffinity = {} + const offerAuthors = {} + const viewedPosts = {} + const viewedAuthors = {} + + const bumpAuthor = (authorId, weight) => { + if (!authorId) return + const key = String(authorId) + authorAffinity[key] = (authorAffinity[key] || 0) + weight + } + + const bumpContent = (type, weight) => { + if (!type) return + contentTypeWeight[type] = (contentTypeWeight[type] || 0) + weight + } + + const bumpViewedPost = (postId, weight) => { + if (!postId) return + const key = String(postId) + viewedPosts[key] = Math.max(viewedPosts[key] || 0, weight) + } + + const bumpViewedAuthor = (authorId, weight) => { + if (!authorId) return + const key = String(authorId) + viewedAuthors[key] = Math.max(viewedAuthors[key] || 0, weight) + } + + offers.forEach((offer) => { + const decay = interactionDecay(offer.createdAt) + const key = String(offer.receiver) + offerAuthors[key] = (offerAuthors[key] || 0) + 12 * decay + bumpAuthor(offer.receiver, 8 * decay) + }) + + likedPosts.forEach((post) => { + bumpAuthor(post.user_id, 4) + bumpContent(post.type, 2) + }) + + comments.forEach((comment) => { + const decay = interactionDecay(comment.createdAt) + bumpAuthor(comment.user, 5 * decay) + }) + + interactions.forEach((item) => { + const decay = interactionDecay(item.createdAt) + const base = + ACTION_WEIGHTS[item.action] ?? ACTION_WEIGHTS.view + const weight = base * decay + + if (item.contentType) bumpContent(item.contentType, weight * 0.5) + if (item.authorId) bumpAuthor(item.authorId, weight) + + if ( + item.targetType === 'post' && + ['view', 'watch_complete', 'skip', 'dwell'].includes(item.action) + ) { + const viewWeight = + item.action === 'watch_complete' + ? 1 + : item.action === 'skip' + ? 0.85 + : item.action === 'dwell' + ? 0.55 + : 0.35 + bumpViewedPost(item.targetId, viewWeight * decay) + if (item.authorId) bumpViewedAuthor(item.authorId, viewWeight * decay * 0.6) + } + }) + + return { + userExpertise: user?.expertise || null, + userSubExpertise: user?.sub_expertise || [], + contentTypeWeight, + authorAffinity, + offerAuthors, + viewedPosts, + viewedAuthors, + likedPostIds, + commentedPostIds: comments.map((c) => String(c.post)), + isColdStart: !hasEnoughUserSignals({ + likedPostIds, + commentedPostIds: comments.map((c) => String(c.post)), + offerAuthors, + authorAffinity + }) + } +} + +function scorePost(post, author, signals, options = {}) { + const { reelsMode = false, seedPost = null, seedAuthor = null } = options + let score = Math.random() * 4 + + const postId = String(post._id) + const authorId = String(post.user_id) + const likesCount = post.likes ? post.likes.length : post.likesCount || 0 + const commentsCount = post.commentsCount || 0 + + const ageHours = + (Date.now() - new Date(post.createdAt).getTime()) / (1000 * 60 * 60) + + score += engagementVelocity(likesCount, commentsCount, ageHours) + + if (signals.userExpertise && expertiseMatches(signals.userExpertise, author?.expertise)) { + score += 24 + } + + if ( + signals.userSubExpertise?.length && + author?.sub_expertise?.some((s) => signals.userSubExpertise.includes(s)) + ) { + score += 16 + } + + if (post.type && signals.contentTypeWeight[post.type]) { + score += signals.contentTypeWeight[post.type] * 2 + } + + score += signals.authorAffinity[authorId] || 0 + score += signals.offerAuthors[authorId] || 0 + + if (signals.likedPostIds?.includes(postId)) score -= 35 + if (signals.commentedPostIds?.includes(postId)) score -= 28 + + const viewedWeight = signals.viewedPosts?.[postId] || 0 + if (viewedWeight > 0) score -= 8 + viewedWeight * 22 + + const authorFatigue = signals.viewedAuthors?.[authorId] || 0 + if (authorFatigue > 0) score -= authorFatigue * 6 + + if (ageHours < 24) score += 6 + else if (ageHours < 48) score += 4 + else if (ageHours < 168) score += 2 + + if (reelsMode && post.type === 'video') score += 14 + + if (seedPost) { + if (String(seedPost.user_id) === authorId) score += 20 + if ( + seedAuthor?.expertise && + expertiseMatches(seedAuthor.expertise, author?.expertise) + ) { + score += 18 + } + if ( + seedAuthor?.sub_expertise?.length && + author?.sub_expertise?.some((s) => seedAuthor.sub_expertise.includes(s)) + ) { + score += 8 + } + if (seedPost.type && post.type === seedPost.type) score += 6 + } + + return score +} + +async function rankPostsForFeed({ + posts, + usersById, + userId, + seedPostId, + reelsMode = false, + blendFresh = false +}) { + const signals = await buildUserExploreSignals(userId) + const coldStart = signals.isColdStart + + let seedPost = null + let seedAuthor = null + if (seedPostId) { + seedPost = posts.find((p) => String(p._id) === String(seedPostId)) || null + if (!seedPost) { + seedPost = await PostModel.findById(seedPostId).lean() + } + if (seedPost) { + seedAuthor = + usersById[String(seedPost.user_id)] || + (await UserModel.findById(seedPost.user_id) + .select( + '_id expertise sub_expertise user_name first_name last_name profile_image' + ) + .lean()) + if (seedAuthor && !usersById[String(seedPost.user_id)]) { + usersById[String(seedPost.user_id)] = seedAuthor + } + } + } + + const pool = seedPost + ? posts.filter((p) => String(p._id) !== String(seedPostId)) + : posts + + let ranked = [] + + if (coldStart && reelsMode && seedAuthor) { + ranked = rankColdStartReels(pool, usersById, seedAuthor, seedPost) + } else if (coldStart && reelsMode && seedPost) { + ranked = shuffleArray(pool, String(seedPostId).length * 131) + } else if (coldStart) { + ranked = shuffleArray(pool) + if (blendFresh) { + ranked = blendFreshIntoRanked(ranked, pool, 0.2) + } + } else { + const scored = pool + .map((post) => ({ + post, + score: scorePost(post, usersById[String(post.user_id)] || {}, signals, { + reelsMode, + seedPost, + seedAuthor + }) + })) + .sort((a, b) => b.score - a.score) + .map((item) => item.post) + + ranked = diversifyByAuthor(scored, reelsMode ? 1 : 2) + + if (blendFresh) { + ranked = blendFreshIntoRanked(ranked, pool) + } + } + + if (seedPost) { + ranked = [seedPost, ...ranked] + } + + return ranked +} + +async function paginatePersonalizedExplore({ + posts, + usersById, + page, + limit, + userId, + seedPostId, + reelsMode = false, + blendFresh = false +}) { + const pageNum = Math.max(1, parseInt(page, 10) || 1) + const limitNum = Math.max(1, parseInt(limit, 10) || 10) + + const ranked = await rankPostsForFeed({ + posts, + usersById, + userId, + seedPostId, + reelsMode, + blendFresh: blendFresh && !seedPostId && !reelsMode + }) + + const totalItems = ranked.length + const startIndex = (pageNum - 1) * limitNum + + return { + posts: ranked.slice(startIndex, startIndex + limitNum), + totalItems + } +} + +module.exports = { + buildUserExploreSignals, + paginatePersonalizedExplore, + rankPostsForFeed, + scorePost +} diff --git a/utils/likeNotification.js b/utils/likeNotification.js index 87ad0a6..6e3a050 100644 --- a/utils/likeNotification.js +++ b/utils/likeNotification.js @@ -1,47 +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 -} +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 +} diff --git a/utils/otpExpiry.js b/utils/otpExpiry.js index f30e7fe..1b4d22e 100644 --- a/utils/otpExpiry.js +++ b/utils/otpExpiry.js @@ -1,16 +1,16 @@ -const OTP_VALID_MS = 3 * 60 * 1000 - -const isOtpExpired = (user) => { - if (!user?.otp) return true - if (!user?.otpSentAt) return false - return Date.now() - new Date(user.otpSentAt).getTime() > OTP_VALID_MS -} - -const clearOtp = (UserModel, mobile) => - UserModel.updateOne({ mobile }, { $set: { otp: null, otpSentAt: null } }) - -module.exports = { - OTP_VALID_MS, - isOtpExpired, - clearOtp -} +const OTP_VALID_MS = 3 * 60 * 1000 + +const isOtpExpired = (user) => { + if (!user?.otp) return true + if (!user?.otpSentAt) return false + return Date.now() - new Date(user.otpSentAt).getTime() > OTP_VALID_MS +} + +const clearOtp = (UserModel, mobile) => + UserModel.updateOne({ mobile }, { $set: { otp: null, otpSentAt: null } }) + +module.exports = { + OTP_VALID_MS, + isOtpExpired, + clearOtp +} diff --git a/utils/projectProfile.js b/utils/projectProfile.js index 6c2ae8f..3309aef 100644 --- a/utils/projectProfile.js +++ b/utils/projectProfile.js @@ -1,20 +1,20 @@ -function canCreateProject(user) { - return Boolean( - user?.user_name?.trim() && - user?.first_name?.trim() && - user?.last_name?.trim() - ); -} - -function respondProjectProfileIncomplete(res) { - return res.status(422).json({ - error: true, - message: - "برای ثبت پروژه، تکمیل نام کاربری، نام و نام خانوادگی در تنظیمات الزامی است", - }); -} - -module.exports = { - canCreateProject, - respondProjectProfileIncomplete, -}; +function canCreateProject(user) { + return Boolean( + user?.user_name?.trim() && + user?.first_name?.trim() && + user?.last_name?.trim() + ); +} + +function respondProjectProfileIncomplete(res) { + return res.status(422).json({ + error: true, + message: + "برای ثبت پروژه، تکمیل نام کاربری، نام و نام خانوادگی در تنظیمات الزامی است", + }); +} + +module.exports = { + canCreateProject, + respondProjectProfileIncomplete, +}; diff --git a/utils/userLevelFilter.js b/utils/userLevelFilter.js index 1a95363..f64a964 100644 --- a/utils/userLevelFilter.js +++ b/utils/userLevelFilter.js @@ -1,48 +1,48 @@ -const USER_LEVELS = { - NEW: 'تازه وارد', - STANDARD: 'استاندارد', - PRO: 'حرفه‌ای', - MASTER: 'استاد', -} - -/** Normalize level strings from URL/query (handles ی / ZWNJ variants). */ -function normalizeUserLevel(raw) { - if (raw == null || raw === '') return null - - const text = String(raw) - .trim() - .replace(/\u200c/g, '') - .replace(/\s+/g, ' ') - - if (text === USER_LEVELS.NEW) return USER_LEVELS.NEW - if (text === USER_LEVELS.STANDARD) return USER_LEVELS.STANDARD - if (text === USER_LEVELS.MASTER) return USER_LEVELS.MASTER - if (text === USER_LEVELS.PRO || text === 'حرفه ای') return USER_LEVELS.PRO - - return text -} - -/** Build Mongo filter for user_level (includes unset levels for تازه وارد). */ -function buildUserLevelMongoFilter(rawLevel) { - const level = normalizeUserLevel(rawLevel) - if (!level) return null - - if (level === USER_LEVELS.NEW) { - return { - $or: [ - { user_level: USER_LEVELS.NEW }, - { user_level: null }, - { user_level: '' }, - { user_level: { $exists: false } }, - ], - } - } - - return { user_level: level } -} - -module.exports = { - USER_LEVELS, - normalizeUserLevel, - buildUserLevelMongoFilter, -} +const USER_LEVELS = { + NEW: 'تازه وارد', + STANDARD: 'استاندارد', + PRO: 'حرفه‌ای', + MASTER: 'استاد', +} + +/** Normalize level strings from URL/query (handles ی / ZWNJ variants). */ +function normalizeUserLevel(raw) { + if (raw == null || raw === '') return null + + const text = String(raw) + .trim() + .replace(/\u200c/g, '') + .replace(/\s+/g, ' ') + + if (text === USER_LEVELS.NEW) return USER_LEVELS.NEW + if (text === USER_LEVELS.STANDARD) return USER_LEVELS.STANDARD + if (text === USER_LEVELS.MASTER) return USER_LEVELS.MASTER + if (text === USER_LEVELS.PRO || text === 'حرفه ای') return USER_LEVELS.PRO + + return text +} + +/** Build Mongo filter for user_level (includes unset levels for تازه وارد). */ +function buildUserLevelMongoFilter(rawLevel) { + const level = normalizeUserLevel(rawLevel) + if (!level) return null + + if (level === USER_LEVELS.NEW) { + return { + $or: [ + { user_level: USER_LEVELS.NEW }, + { user_level: null }, + { user_level: '' }, + { user_level: { $exists: false } }, + ], + } + } + + return { user_level: level } +} + +module.exports = { + USER_LEVELS, + normalizeUserLevel, + buildUserLevelMongoFilter, +} diff --git a/utils/usernameSuggestions.js b/utils/usernameSuggestions.js index 51e26a7..51484da 100644 --- a/utils/usernameSuggestions.js +++ b/utils/usernameSuggestions.js @@ -1,56 +1,56 @@ -const { validateUsername, USERNAME_MAX_LENGTH } = require('./usernameValidation') - -const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - -const isUsernameTaken = async (UserModel, userName, excludeUserId) => { - const existing = await UserModel.findOne({ - user_name: { $regex: new RegExp(`^${escapeRegex(userName)}$`, 'i') } - }) - - if (!existing) return false - if (excludeUserId && existing._id.toString() === excludeUserId) return false - return true -} - -const buildCandidates = (base, attempt) => { - const random2 = () => String(Math.floor(Math.random() * 90 + 10)) - const random3 = () => String(Math.floor(Math.random() * 900 + 100)) - const random4 = () => String(Math.floor(Math.random() * 9000 + 1000)) - - const builders = [ - () => `${base}_${random2()}`, - () => `${base}.${random2()}`, - () => `${base}${random3()}`, - () => `${base}-${random2()}`, - () => `${base}_${random3()}`, - () => `${base}${random4()}`, - ] - - return builders[attempt % builders.length]() -} - -const trimToMax = (value) => - value.length > USERNAME_MAX_LENGTH ? value.slice(0, USERNAME_MAX_LENGTH) : value - -const generateUsernameSuggestions = async (UserModel, baseUserName, excludeUserId) => { - const base = trimToMax(baseUserName.trim().toLowerCase()) - const suggestions = [] - let attempt = 0 - - while (suggestions.length < 3 && attempt < 30) { - const candidate = trimToMax(buildCandidates(base, attempt)) - attempt += 1 - - if (validateUsername(candidate)) continue - if (suggestions.includes(candidate)) continue - if (await isUsernameTaken(UserModel, candidate, excludeUserId)) continue - - suggestions.push(candidate) - } - - return suggestions -} - -module.exports = { - generateUsernameSuggestions, -} +const { validateUsername, USERNAME_MAX_LENGTH } = require('./usernameValidation') + +const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + +const isUsernameTaken = async (UserModel, userName, excludeUserId) => { + const existing = await UserModel.findOne({ + user_name: { $regex: new RegExp(`^${escapeRegex(userName)}$`, 'i') } + }) + + if (!existing) return false + if (excludeUserId && existing._id.toString() === excludeUserId) return false + return true +} + +const buildCandidates = (base, attempt) => { + const random2 = () => String(Math.floor(Math.random() * 90 + 10)) + const random3 = () => String(Math.floor(Math.random() * 900 + 100)) + const random4 = () => String(Math.floor(Math.random() * 9000 + 1000)) + + const builders = [ + () => `${base}_${random2()}`, + () => `${base}.${random2()}`, + () => `${base}${random3()}`, + () => `${base}-${random2()}`, + () => `${base}_${random3()}`, + () => `${base}${random4()}`, + ] + + return builders[attempt % builders.length]() +} + +const trimToMax = (value) => + value.length > USERNAME_MAX_LENGTH ? value.slice(0, USERNAME_MAX_LENGTH) : value + +const generateUsernameSuggestions = async (UserModel, baseUserName, excludeUserId) => { + const base = trimToMax(baseUserName.trim().toLowerCase()) + const suggestions = [] + let attempt = 0 + + while (suggestions.length < 3 && attempt < 30) { + const candidate = trimToMax(buildCandidates(base, attempt)) + attempt += 1 + + if (validateUsername(candidate)) continue + if (suggestions.includes(candidate)) continue + if (await isUsernameTaken(UserModel, candidate, excludeUserId)) continue + + suggestions.push(candidate) + } + + return suggestions +} + +module.exports = { + generateUsernameSuggestions, +} diff --git a/utils/usernameValidation.js b/utils/usernameValidation.js index be99b38..aa47639 100644 --- a/utils/usernameValidation.js +++ b/utils/usernameValidation.js @@ -1,33 +1,33 @@ -/** حروف انگلیسی، اعداد و . _ - — بدون فارسی و کاراکترهای غیرمعمول */ -const USERNAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]{5,99}$/ - -const USERNAME_MIN_LENGTH = 6 -const USERNAME_MAX_LENGTH = 100 - -const validateUsername = (user_name) => { - if (!user_name || typeof user_name !== 'string') { - return 'نام کاربری الزامی است' - } - - const trimmed = user_name.trim() - if (trimmed.length < USERNAME_MIN_LENGTH) { - return `نام کاربری باید حداقل ${USERNAME_MIN_LENGTH} کاراکتر باشد` - } - - if (trimmed.length > USERNAME_MAX_LENGTH) { - return `نام کاربری نمی‌تواند بیشتر از ${USERNAME_MAX_LENGTH} کاراکتر باشد` - } - - if (!USERNAME_REGEX.test(trimmed)) { - return 'نام کاربری فقط می‌تواند شامل حروف انگلیسی، اعداد و کاراکترهای . _ - باشد' - } - - return null -} - -module.exports = { - USERNAME_REGEX, - USERNAME_MIN_LENGTH, - USERNAME_MAX_LENGTH, - validateUsername -} +/** حروف انگلیسی، اعداد و . _ - — بدون فارسی و کاراکترهای غیرمعمول */ +const USERNAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]{5,99}$/ + +const USERNAME_MIN_LENGTH = 6 +const USERNAME_MAX_LENGTH = 100 + +const validateUsername = (user_name) => { + if (!user_name || typeof user_name !== 'string') { + return 'نام کاربری الزامی است' + } + + const trimmed = user_name.trim() + if (trimmed.length < USERNAME_MIN_LENGTH) { + return `نام کاربری باید حداقل ${USERNAME_MIN_LENGTH} کاراکتر باشد` + } + + if (trimmed.length > USERNAME_MAX_LENGTH) { + return `نام کاربری نمی‌تواند بیشتر از ${USERNAME_MAX_LENGTH} کاراکتر باشد` + } + + if (!USERNAME_REGEX.test(trimmed)) { + return 'نام کاربری فقط می‌تواند شامل حروف انگلیسی، اعداد و کاراکترهای . _ - باشد' + } + + return null +} + +module.exports = { + USERNAME_REGEX, + USERNAME_MIN_LENGTH, + USERNAME_MAX_LENGTH, + validateUsername +}