const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const passport = require('passport'); const GoogleStrategy = require('passport-google-oauth20').Strategy; const FacebookStrategy = require('passport-facebook').Strategy; const GitHubStrategy = require('passport-github2').Strategy; const nodemailer = require('nodemailer'); const axios = require('axios'); const { CloudinaryStorage } = require('multer-storage-cloudinary'); const cloudinary = require('cloudinary').v2; const multer = require('multer'); const MGZonStrategy = require('passport-mgzon'); const { jsPDF } = require('jspdf'); const Jimp = require('jimp'); const fs = require('fs'); const fetch = global.fetch || require('node-fetch'); const pdfParse = require('pdf-parse'); const { createHash } = require('crypto'); const crypto = require('crypto'); const path = require('path'); require('jspdf-autotable'); require('dotenv').config(); const AdmZip = require('adm-zip'); const winston = require('winston'); // ✅ انقل تعريف logger هنا (قبل استخدامه) const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), ] }); // ✅ الآن logger موجود، يقدر يستخدم const allowedRedirectUris = process.env.ALLOWED_REDIRECT_URIS ? process.env.ALLOWED_REDIRECT_URIS.split(',') : []; if (!allowedRedirectUris.length) { logger.error('ALLOWED_REDIRECT_URIS is not defined in .env'); process.exit(1); } // ✅ طباعة قيمة ALLOWED_REDIRECT_URIS في الـ Console (للتأكد من قراءتها بشكل صحيح) console.log('========================================'); console.log('🔍 ALLOWED_REDIRECT_URIS from env:', process.env.ALLOWED_REDIRECT_URIS); console.log('📋 ALLOWED_REDIRECT_URIS array:', allowedRedirectUris); console.log('========================================'); const sharp = require('sharp'); // const { body, validationResult } = require('express-validator'); const swaggerJsDoc = require('swagger-jsdoc'); const { body, validationResult, param } = require('express-validator'); const swaggerUi = require('swagger-ui-express'); // const { body, validationResult, param } = require('express-validator'); const csurf = require('csurf'); const Sentry = require('@sentry/node'); const morgan = require('morgan'); const cookieParser = require('cookie-parser'); const timeout = require('express-timeout-handler'); const compression = require('compression'); const SentryTracing = require('@sentry/tracing'); const helmet = require('helmet'); const app = express(); app.set('trust proxy', true); const getSocketClient = require('./utils/socket-client'); app.use(cors({ origin: '*', // ⚠️ للاختبار فقط credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token', 'X-New-Token', 'x-refresh-token'] })); const cron = require('node-cron'); const { google } = require('googleapis'); const { Handlers } = require('@sentry/node'); const rateLimit = require('express-rate-limit'); const OAuth2Strategy = require('passport-oauth2').Strategy; // const jsPDF = require('jspdf'); const webpush = require('web-push'); const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, WidthType } = require('docx'); const ShortcodeService = require('./services/ShortcodeService'); const aiService = require('./services/aiService'); const StoreShortcode = require('./models/StoreShortcode'); const TemplateStore = require('./models/TemplateStore'); // const { buildContributionGraph, getDayActivities } = require('./services/contribution.service'); // ❌ احذف هذا السطر لأنه مكرر (logger عرفته فوق) // const logger = winston.createLogger({...}); Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 0.2, // تتبع 20% من الطلبات environment: process.env.NODE_ENV || 'development', }); // Endpoint لتحديث وجلب عدد الزوار app.post('/api/visits', async (req, res) => { try { let visit = await Visit.findOne(); if (!visit) { visit = new Visit({ count: 1930537 }); } visit.count += 1; await visit.save(); res.json({ visitCount: visit.count }); } catch (error) { logger.error(`Error updating visit count: ${error.message}`); Sentry.captureException(error); res.status(500).json({ error: 'Failed to update visit count' }); } }); // GET /api/projects/:projectId/og-image - صورة ديناميكية للمشروع app.get('/api/projects/:projectId/og-image', async (req, res) => { try { const { projectId } = req.params; // جلب بيانات المشروع const project = await Project.findById(projectId); if (!project) { return res.status(404).json({ error: 'Project not found' }); } // جلب بيانات صاحب المشروع const user = await User.findById(project.userId); const ownerName = user?.profile?.nickname || user?.username || 'User'; // إنشاء صورة باستخدام Sharp const width = 1200; const height = 630; // نسبة OG image standard (1.91:1) // إنشاء SVG template const svg = ` 📁 ${escapeXml(project.title.substring(0, 60))} ${project.description ? ` ${escapeXml(project.description.substring(0, 100))}${project.description.length > 100 ? '...' : ''} ` : ''} ${Array.from({ length: Math.min(5, project.stars || 0) }, (_, i) => ` `).join('')} ${Array.from({ length: 5 - Math.min(5, project.stars || 0) }, (_, i) => ` `).join('')} ${escapeXml(ownerName)} MGZon `; // تحويل SVG إلى PNG const imageBuffer = await sharp(Buffer.from(svg)) .png() .toBuffer(); res.setHeader('Content-Type', 'image/png'); res.setHeader('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours res.send(imageBuffer); } catch (error) { console.error('Error generating project image:', error); // Return default image res.sendFile(path.join(__dirname, 'public', 'assets', 'img', 'default-project.png')); } }); // Helper function to escape XML function escapeXml(str) { if (!str) return ''; return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } const visitSchema = new mongoose.Schema({ count: { type: Number, default: 1930537 } // القيمة الابتدائية }); const Visit = mongoose.model('Visit', visitSchema); app.use(express.json({ type: ['application/json', 'text/plain'] })); app.use(cookieParser()); app.use(Handlers.requestHandler()); app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } })); app.use(compression({ level: 6, threshold: 1024 })); app.use(timeout.handler({ timeout: 10000, onTimeout: (req, res) => { logger.error(`Request timed out: ${req.originalUrl}`); res.status(504).json({ error: 'Request timed out' }); }})); // ============================================ // 🛡️ GitHub Actions Bypass Middleware // ============================================ const MGZON_ACTIONS_TOKEN = process.env.MGZON_ACTIONS_TOKEN; app.use((req, res, next) => { const token = req.headers['x-github-token']; // لو الطلب جاي من GitHub Actions بالتوكن الصحيح if (token && token === MGZON_ACTIONS_TOKEN) { logger.info(`✅ GitHub Actions bypass for: ${req.originalUrl}`); return next(); // تخطي كل الحماية (CSRF, CORS, إلخ) } next(); }); // ✅ حماية CSRF مع استثناء مسار تسجيل الدخول const csrfProtection = csurf({ cookie: true }); // قائمة المسارات المستثناة من CSRF const excludedPaths = [ '/api/login', '/graphql', '/api/register', '/api/forgot-password', '/api/reset-password', '/api/verify-email', '/api/resend-verification', '/api/projects', '/api/skills', '/api/comments', '/api/users', '/api/users/search', '/api/upload', '/api/files/list', '/api/files/delete', '/api/visits', '/api/profile/me', '/api/profile/:nickname', '/api/check-nickname', '/api/store/settings', '/api/store/enable', '/api/store/products', '/api/store/:username/products', '/api/store/products/:productId', '/api/store/theme', '/api/store/theme/reset', '/api/store/shortcodes/parse', '/api/store/shortcodes', '/api/store/shortcodes/:id', '/api/store/shortcodes/preview', '/api/cart', '/api/cart/add', '/api/cart/update', '/api/cart/remove', '/api/cart/clear-store', '/api/cart/store/:storeId', '/api/cart/clear-all', '/api/cart/summary', '/api/checkout/payment-info/:storeId', '/api/checkout/:storeId', '/api/templates/debug/:id', '/api/store/orders/buyer', '/api/store/orders/seller', '/api/store/orders/:orderId/status', '/api/store/earnings', '/api/store/cart', '/api/admin/stores', '/api/store/products/:productId/review', '/api/store/:storeId/follow', '/api/store/:storeId/followers', '/api/store/coupons', '/api/store/validate-coupon', '/api/store/analytics', '/api/webhooks/paypal', '/api/webhooks/stripe', '/api/store/orders/:orderId', '/api/store/orders/:orderId/download/:productId', '/api/store/orders/:orderId/upload', '/api/store/orders/:orderId/file-url', '/api/admin/subscription/plans', '/api/admin/subscription/plans/:planId', '/api/subscription/plans', '/api/admin/subscription/payment-methods', '/api/admin/subscription/payment-methods/:methodId', '/api/subscription/payment-methods', '/api/subscription/subscribe', '/api/subscription/my', '/api/admin/subscriptions', '/api/admin/subscriptions/:subId/approve', '/api/admin/subscriptions/:subId/reject', '/api/admin/subscriptions/:subId/cancel', '/api/users/:userId/subscription-status', '/api/admin/subscription/stats', '/api/store/:storeId/follow-status', '/api/store/orders/:orderId/cancel', '/api/subscription/settings', '/api/subscription/check', '/api/subscription/renew', '/api/subscription/cancel', '/api/users/:userId/badges', '/api/profile/:userId/view', '/api/achievements/:slug', '/api/users/:userId', '/api/admin/stores/:userId/toggle', '/api/admin/stores/:userId', '/api/admin/products', '/api/admin/products/:productId/toggle', '/api/admin/products/:productId', '/api/admin/orders', '/api/admin/orders/:orderId/status', '/api/admin/platform-banks', '/api/admin/platform-banks/:bankId', '/api/admin/users/all', '/api/admin/users/:userId/make-admin', '/api/admin/logs', '/api/admin/export/users', '/api/admin/export/orders', '/api/admin/backup', '/shop/preview', '/api/store/preview-data', '/api/store/preview/update', '/shop/preview/product/:productId', '/api/store/preview/product/:productId', '/api/store/:username', '/api/store/:username/follow-status', '/api/store/preview/products', '/api/store/preview', '/api/store/preview/settings', '/api/store/preview/sections', '/api/store/search/products', '/api/store/search/stores', '/api/store/search', '/api/store/sections/order', '/api/store/contact', '/api/store/:username/analytics', '/api/analytics/track-store-view', '/api/store/me', '/api/store/:username/pages', '/api/store/:username/page/:slug', '/api/store/my/pages', '/api/store/my/pages/:pageId', '/api/store/preview/page/:slug', '/api/store/preview/pages', '/api/store/my/pages/upload', '/api/store/my/pages/upload-zip', '/api/store/my/pages/reorder', '/api/store/my/pages/preview', '/api/store/:username/:slug', '/api/store/my/pages/stats', '/api/backgrounds', '/api/backgrounds/categories', '/api/backgrounds/:id', '/api/store/background', '/api/store/backgrounds/upload', '/api/store/backgrounds/:id', '/api/store/background/current', '/api/store/background/history', '/api/store/theme/upload', '/api/store/theme/preview', '/api/store/my/pages/:pageId/duplicate', '/api/templates/serve/:slug/*', '/api/templates/serve/:slug', '/api/templates/serve/:slug/file/*', '/api/templates/serve/:slug/file', '/api/store/my/pages/:pageId/export', '/api/store/preview/follow-status', '/api/visual-builder/sections', '/api/visual-builder/section', '/api/visual-builder/section/:sectionId', '/api/visual-builder/section-types', '/api/visual-builder/preview', '/api/visual-builder/duplicate/:sectionId', '/api/store/reset-template', '/api/ai/generate-layout', '/api/ai/generate-shortcode', '/api/ai/suggest-design', '/api/ai/enhance-description', '/api/ai/suggest-keywords', '/api/ai/generate-store-description', '/api/ai/clear-cache', '/api/store/:username/full-template', '/api/ai/generate-template', '/api/ai/admin/stats', '/api/ai/admin/clear-cache', '/api/store/layout-template', '/api/store/layout-template/reset', '/api/store/apply-layout/:userId', '/api/store/layout-templates', '/api/templates', '/api/templates/featured', '/api/templates/popular', '/api/templates/categories', '/api/templates/:id', '/api/templates/:id/preview', '/api/templates/:id/apply', '/api/templates/:id/review', '/api/templates/my/installed', '/api/profile/project-progress', '/api/user-interactions', '/api/projects/:projectId/og-image', '/api/comments/:projectId', '/api/educations', '/api/profile/contact', '/api/conversations', '/api/conversations/:conversationId/messages', '/api/users/me/projects/:projectId', '/api/notifications', '/api/facebook/posts', '/api/github/repos', '/api/verify-token', // ✅ أضف هذه المسارات الجديدة '/api/profile/appearance', '/api/profile/seo', '/api/profile', '/api/refresh-token', '/api/logout', '/api/parse-resume', '/api/comments/:commentId/report', '/api/profile/resume/:nickname', '/api/profile/pdf/:nickname', // ✅ مسارات التفاعل مع التعليقات (جديدة) '/api/comments/:commentId/like', '/api/comments/:commentId/reply', '/api/posts/:postId/comments/:commentId/reply', '/api/posts/comments/:commentId/reply/:replyId/like', '/api/posts/:postId/comments/:commentId/report', '/api/comments/:commentId', '/api/user/interactions/privacy', '/api/profile/:nickname/interactions', '/api/github/status', '/api/github/import-repo', '/api/user/education', '/api/github/disconnect', '/api/github-projects', '/api/admin/comments/reported', '/api/admin/comments/:commentId/resolve-report', '/api/notifications/read-all', '/api/educations/:id', '/api/notifications/:id/read', '/api/notifications/:id', '/api/notifications/unread-count', '/api/site-settings', '/api/site-settings', '/api/profile/id/:userId', '/api/reports', '/api/posts/feed', '/api/users/me/pin/:postId', '/api/users/:userId/stats', '/api/posts/search', '/api/posts/suggested', '/api/users/explore', '/api/posts/saved', '/api/stories', '/api/stories/feed', '/api/stories/:storyId/view', '/api/stories/:storyId/reaction', '/api/stories/:storyId', '/api/stories/user/:userId', '/api/stories/all', '/api/stories/discover', '/api/stories/stats', '/api/stories/expired', '/api/jobs', '/api/jobs/my', '/api/admin/messages/reports', '/api/jobs/:jobId', '/api/posts/share-to-profile', '/api/messages/:messageId/report', '/api/jobs/:jobId/apply', '/api/jobs/applications/my', '/api/jobs/applications/:applicationId/withdraw', '/api/admin/messages/reports/:reportId/resolve', '/api/jobs/:jobId/applications', '/api/jobs/applications/:applicationId/status', '/api/jobs/applications/:applicationId/interview', '/api/jobs/:jobId/save', '/api/jobs/saved', '/api/jobs/categories', '/api/jobs/stats', '/api/jobs/:jobId/duplicate', '/api/jobs/my/applications', '/api/users/me/followers', '/api/users/me/following', '/api/jobs/applications/:applicationId', '/api/comments/:postId/:commentId', '/api/users/:userId/following', '/api/users/me/following/ids', '/api/users/me/following/check', '/api/users/trending', '/api/users/suggestions', '/api/trending/topics', '/api/trending/posts', '/api/users/:userId/followers', '/api/users/:userId/follow', '/api/posts/:postId', '/api/upload-cover', '/api/cover', '/api/posts/:postId/like', '/api/posts/:postId/save', '/api/posts/:postId/comments', '/api/profile/design-settings', '/api/profile/design-settings/reset', '/api/posts/comments/:commentId/like', '/api/posts/comments/:commentId', '/api/posts/comments/:commentId/report', '/api/health', '/api/subscribe', '/api/ask', '/api/chat', '/api/converse', '/api/conversations/export', '/auth/magic-link', '/auth/verify-magic-link', '/auth/resend-magic-link', '/auth/github', '/auth/facebook', '/auth/google', '/api/profile/custom-sections', '/api/profile/custom-sections/:sectionId', '/api/admin/jobs/categories', '/api/admin/jobs/categories/:categoryId', '/api/jobs/categories/stats', '/api/posts/:postId/share', '/api/posts/user/:userId', '/api/posts', '/api/upload-logo', '/api/comments/:commentId/privacy', '/api/messages/:messageId', '/api/conversations/:conversationId/read', '/api/conversations/unread/count', '/api/users/:userId/contributions', '/api/users/:userId/contributions/:date', '/api/analytics/also-viewed', '/api/pages/suggestions', '/api/pages/:pageId/follow', '/api/analytics/profile', '/api/analytics/track-view', '/api/ratings', '/api/ratings/:userId', '/api/websocket/config', '/api/websocket/token', '/api/conversations/:conversationId/typing', '/api/conversations/:conversationId/clear', '/api/conversations/:conversationId/pin', '/api/messages/:messageId/forward', '/api/messages/:messageId/reaction', '/sitemap-users.xml', '/sitemap.xml', '/sitemap-static.xml', '/sitemap-products.xml', '/sitemap-stores.xml', '/sitemap-images.xml', '/sitemap-comments.xml', '/sitemap-posts.xml', '/sitemap-jobs.xml', '/sitemap-templates.xml', '/sitemap-pages.xml', '/sitemap-tags.xml', '/api/follow/requests', '/api/follow/requests/:requestId/accept', '/api/follow/requests/:requestId/decline', '/api/profile/section-order', '/api/profile/layout-presets', '/api/profile/layout-presets/:id', '/api/profile/:nickname/ai-status', '/api/profile/:nickname/ask', '/api/ai/conversations', '/api/profile/section-names', '/api/ai/stats', '/robots.txt', '/api/projects/:projectId' ]; app.use((req, res, next) => { // التحقق إذا كان المسار في القائمة أو يبدأ بأي مسار من القائمة (للتعامل مع :id) const isExcluded = excludedPaths.some(excludedPath => { // لو المسار في القائمة فيه :id (زي /api/skills/:skillId) if (excludedPath.includes(':')) { const basePath = excludedPath.split('/:')[0]; return req.path.startsWith(basePath); } // المطابقة التامة أو المسار يبدأ بيه مع / return req.path === excludedPath || req.path.startsWith(excludedPath + '/'); }); if (isExcluded) { return next(); } csrfProtection(req, res, next); }); app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.info(`Request: ${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms`); }); next(); }); app.get('/api/check-session', authenticateToken, async (req, res) => { try { const user = await User.findById(req.user.userId).select('username email profile'); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json({ valid: true, user: { userId: req.user.userId, email: req.user.email, isAdmin: req.user.isAdmin, username: user.username, profile: user.profile } }); } catch (error) { logger.error(`Error checking session: ${error.message}`); Sentry.captureException(error); res.status(500).json({ error: 'Failed to check session' }); } }); const swaggerOptions = { swaggerDefinition: { openapi: '3.0.0', info: { title: 'Portfolio API', version: '1.0.0', description: 'API for Ibrahim Al-Asfar\'s portfolio website' }, servers: [ { url: process.env.BASE_URL, description: 'Production server' }, { url: 'http://localhost:7860', description: 'Local development server' } ], components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } } } }, apis: ['./docs/swagger.yaml'] }; const swaggerDocs = swaggerJsDoc(swaggerOptions); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs)); app.use(passport.initialize()); cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET }); const storage = new CloudinaryStorage({ cloudinary: cloudinary, params: { folder: 'Uploads', allowed_formats: ['jpeg', 'png', 'pdf'], resource_type: 'auto' } }); // إعداد تخزين مؤقت للملفات const memoryStorage = multer.memoryStorage(); const uploadTheme = multer({ storage: memoryStorage, limits: { fileSize: 50 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (file.originalname.endsWith('.zip')) { cb(null, true); } else { cb(new Error('Only ZIP files are allowed'), false); } } }); // ============================================ // POST UPLOAD - رفع ميديا المنشورات // ============================================ const uploadPost = multer({ storage: new CloudinaryStorage({ cloudinary: cloudinary, params: async (req, file) => { let resourceType = 'auto'; let folder = `Posts/${req.user.userId}`; let allowedFormats = []; let transformation = []; // 📸 الصور if (file.mimetype.startsWith('image/')) { resourceType = 'image'; allowedFormats = [ 'jpeg', 'jpg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'tif', 'ico', 'svg', 'avif' ]; folder = `Posts/${req.user.userId}/images`; transformation = [ { quality: 'auto:good' }, { fetch_format: 'auto' } ]; } // 🎬 الفيديوهات else if (file.mimetype.startsWith('video/')) { resourceType = 'video'; allowedFormats = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv']; folder = `Posts/${req.user.userId}/videos`; transformation = [ { quality: 'auto' } ]; } return { folder: folder, allowed_formats: allowedFormats, resource_type: resourceType, public_id: `post_${Date.now()}_${file.originalname.split('.')[0]}`, transformation: transformation.length > 0 ? transformation : undefined }; } }), limits: { fileSize: 50 * 1024 * 1024 // 50MB }, fileFilter: (req, file, cb) => { const imageTypes = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff', 'image/tif', 'image/x-icon', 'image/svg+xml', 'image/avif' ]; const videoTypes = [ 'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska' ]; const allAllowed = [...imageTypes, ...videoTypes]; if (!allAllowed.includes(file.mimetype)) { return cb(new Error( 'Unsupported file type. Supported formats:\n' + '📸 Images: JPEG, PNG, GIF, WEBP, BMP, TIFF, SVG, AVIF\n' + '🎬 Videos: MP4, WEBM, OGG, MOV, AVI, MKV' ), false); } let maxSize = 50 * 1024 * 1024; if (file.mimetype.startsWith('image/')) { maxSize = 10 * 1024 * 1024; // 10MB للصور } else if (file.mimetype.startsWith('video/')) { maxSize = 50 * 1024 * 1024; // 50MB للفيديوهات } if (file.size > maxSize) { const typeName = file.mimetype.startsWith('image/') ? 'images' : 'videos'; return cb(new Error( `File too large. Maximum ${maxSize / 1024 / 1024}MB for ${typeName}` ), false); } cb(null, true); } }); const upload = multer({ storage: new CloudinaryStorage({ cloudinary: cloudinary, params: async (req, file) => ({ folder: `Uploads/${req.user.userId}`, allowed_formats: ['jpeg', 'png', 'pdf'], resource_type: 'auto', public_id: `${Date.now()}_${file.originalname}` }) }), limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']; if (!allowedTypes.includes(file.mimetype)) { return cb(new Error('Only JPEG, PNG, or PDF files are allowed')); } cb(null, true); } }); // ============================================ // Multer configuration for HTML/JS/CSS/ZIP files // ============================================ const uploadHTML = multer({ storage: multer.memoryStorage(), // ✅ تخزين مؤقت في الذاكرة للمعالجة limits: { fileSize: 10 * 1024 * 1024 }, // 10MB fileFilter: (req, file, cb) => { const allowedTypes = [ 'text/html', 'text/css', 'text/javascript', 'application/javascript', 'application/x-javascript', 'application/zip', 'application/x-zip-compressed', 'application/x-html+xml' ]; const allowedExtensions = ['.html', '.htm', '.css', '.js', '.zip']; const ext = '.' + file.originalname.split('.').pop(); if (allowedTypes.includes(file.mimetype) || allowedExtensions.includes(ext)) { cb(null, true); } else { cb(new Error('Only HTML, CSS, JS, or ZIP files are allowed'), false); } } }); const uploadPDF = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (file.mimetype !== 'application/pdf') { return cb(new Error('Only PDF files are allowed')); } cb(null, true); } }); // ============================================ // STORY UPLOAD - رفع ميديا القصص (مطور) // ============================================ const uploadStory = multer({ storage: new CloudinaryStorage({ cloudinary: cloudinary, params: async (req, file) => { // تحديد نوع المورد let resourceType = 'auto'; let folder = `Stories/${req.user.userId}`; let allowedFormats = []; let transformation = []; let eager = []; // 📸 الصور if (file.mimetype.startsWith('image/')) { resourceType = 'image'; allowedFormats = [ 'jpeg', 'jpg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'tif', 'ico', 'svg', 'avif', 'heic', 'heif' ]; folder = `Stories/${req.user.userId}/images`; transformation = [ { quality: 'auto:good' }, { fetch_format: 'auto' } ]; eager = [ { width: 1080, height: 1920, crop: 'limit' }, { width: 540, height: 960, crop: 'limit' } ]; } // 🎬 الفيديوهات else if (file.mimetype.startsWith('video/')) { resourceType = 'video'; allowedFormats = [ 'mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv', 'flv', 'wmv', 'm4v', 'mpg', 'mpeg', '3gp' ]; folder = `Stories/${req.user.userId}/videos`; transformation = [ { quality: 'auto' } ]; eager = [ { format: 'mp4', quality: 'auto' }, { format: 'webm', quality: 'auto' } ]; } // 🎵 الصوتيات else if (file.mimetype.startsWith('audio/')) { resourceType = 'video'; // Cloudinary يتعامل مع الصوت كـ video allowedFormats = ['mp3', 'wav', 'aac', 'ogg', 'm4a', 'flac', 'webm']; folder = `Stories/${req.user.userId}/audio`; transformation = [ { quality: 'auto' }, { audio_codec: 'aac' } ]; eager = [ { format: 'mp3', audio_codec: 'mp3' }, { format: 'm4a', audio_codec: 'aac' } ]; // ✅ إرجاع مباشر للصوتيات return { folder: folder, allowed_formats: allowedFormats, resource_type: resourceType, public_id: `audio_${Date.now()}_${file.originalname.split('.')[0]}`, transformation: transformation, eager: eager, eager_async: true }; } return { folder: folder, allowed_formats: allowedFormats, resource_type: resourceType, public_id: `story_${Date.now()}_${file.originalname.split('.')[0]}`, transformation: transformation.length > 0 ? transformation : undefined, eager: eager.length > 0 ? eager : undefined, eager_async: true }; } }), limits: { fileSize: 50 * 1024 * 1024 // 50MB }, fileFilter: (req, file, cb) => { const imageTypes = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff', 'image/tif', 'image/x-icon', 'image/svg+xml', 'image/avif', 'image/heic', 'image/heif' ]; const videoTypes = [ 'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska', 'video/x-flv', 'video/x-ms-wmv', 'video/mp4v-es', 'video/mpeg', 'video/3gpp', 'video/3gpp2' ]; const audioTypes = [ 'audio/mpeg', 'audio/wav', 'audio/aac', 'audio/ogg', 'audio/mp4', 'audio/flac', 'audio/webm', 'audio/x-m4a' ]; const allAllowed = [...imageTypes, ...videoTypes, ...audioTypes]; // التحقق من النوع if (!allAllowed.includes(file.mimetype)) { return cb(new Error( 'Unsupported file type. Supported formats:\n' + '📸 Images: JPEG, PNG, GIF, WEBP, BMP, TIFF, SVG, AVIF, HEIC\n' + '🎬 Videos: MP4, WEBM, OGG, MOV, AVI, MKV, FLV, WMV, MPEG, 3GP\n' + '🎵 Audio: MP3, WAV, AAC, OGG, M4A, FLAC, WEBM' ), false); } // التحقق من الحجم حسب النوع let maxSize = 50 * 1024 * 1024; // 50MB افتراضي if (file.mimetype.startsWith('image/')) { maxSize = 20 * 1024 * 1024; // 20MB للصور } else if (file.mimetype.startsWith('video/')) { maxSize = 50 * 1024 * 1024; // 50MB للفيديوهات } else if (file.mimetype.startsWith('audio/')) { maxSize = 15 * 1024 * 1024; // 15MB للصوتيات } if (file.size > maxSize) { const typeName = file.mimetype.startsWith('image/') ? 'images' : file.mimetype.startsWith('video/') ? 'videos' : 'audio'; return cb(new Error( `File too large. Maximum ${maxSize / 1024 / 1024}MB for ${typeName}` ), false); } cb(null, true); } }); // ============================================ // MIDDLEWARE - معالجة أخطاء رفع الملفات (مطور) // ============================================ const handleUploadError = (err, req, res, next) => { if (err instanceof multer.MulterError) { const errorMap = { 'LIMIT_FILE_SIZE': 'File too large. Maximum size is 50MB.', 'LIMIT_FILE_COUNT': 'Too many files.', 'LIMIT_FIELD_KEY': 'Field name too long.', 'LIMIT_FIELD_VALUE': 'Field value too long.', 'LIMIT_FIELD_COUNT': 'Too many fields.', 'LIMIT_PART_COUNT': 'Too many parts.', 'LIMIT_UNEXPECTED_FILE': 'Unexpected file field.' }; return res.status(400).json({ error: errorMap[err.code] || `Upload error: ${err.message}`, code: err.code }); } if (err) { return res.status(400).json({ error: err.message || 'File upload failed', code: 'FILE_ERROR' }); } next(); }; // ============================================ // Multer configuration for message attachments (temporary storage) // ============================================ const messageUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 }, fileFilter: (req, file, cb) => { const allowedTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4', 'video/webm', 'video/ogg', 'application/pdf', 'audio/mpeg', 'audio/webm', 'audio/mp4', 'audio/ogg' ]; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('File type not allowed. Allowed: images, videos, PDF, audio'), false); } } }); mongoose.connect(process.env.MONGODB_URI) .then(() => logger.info('Connected to MongoDB')) .catch(err => { logger.error(`MongoDB connection error: ${err.message}`, { stack: err.stack }); Sentry.captureException(err); process.exit(1); }); const MONGODB_URI = process.env.MONGODB_URI; const JWT_SECRET = process.env.JWT_SECRET; const PORT = process.env.PORT || 7860; const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET; const FACEBOOK_CLIENT_ID = process.env.FACEBOOK_CLIENT_ID; const FACEBOOK_CLIENT_SECRET = process.env.FACEBOOK_CLIENT_SECRET; const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID; const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET; const EMAIL_USER = process.env.EMAIL_USER; const EMAIL_PASS = process.env.EMAIL_PASS; const HUGGING_FACE_TOKEN = process.env.HUGGING_FACE_TOKEN; const AI_API_URL = process.env.AI_API_URL; const FRONTEND_URL = process.env.FRONTEND_URL || 'https://mark-elasfar.web.app'; const FRONTEND_URL_XCV = process.env.FRONTEND_URL_XCV || 'https://xccv.vercel.app'; if (!MONGODB_URI || !JWT_SECRET || !GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET || !FACEBOOK_CLIENT_ID || !FACEBOOK_CLIENT_SECRET || !GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET || !EMAIL_USER || !EMAIL_PASS || !HUGGING_FACE_TOKEN || !process.env.BASE_URL || !FRONTEND_URL || !process.env.CLOUDINARY_CLOUD_NAME || !process.env.CLOUDINARY_API_KEY || !process.env.CLOUDINARY_API_SECRET || !process.env.GITHUB_TOKEN || !process.env.SENTRY_DSN) { logger.error('Missing environment variables'); process.exit(1); } webpush.setVapidDetails( 'mailto:marklasfar@gmail.com', process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY ); const WEB_URL = process.env.WEB_URL; const BASE_URL = process.env.BASE_URL; // ✅ استخدام Brevo API بدلاً من nodemailer const BREVO_API_KEY = process.env.BREVO_API_KEY; async function sendEmailWithBrevo(to, subject, htmlContent, textContent) { try { const response = await fetch('https://api.brevo.com/v3/smtp/email', { method: 'POST', headers: { 'Content-Type': 'application/json', 'api-key': BREVO_API_KEY }, body: JSON.stringify({ sender: { name: 'MGZon', email: 'amarlasfar0@gmail.com' }, to: [{ email: to }], subject: subject, htmlContent: htmlContent, textContent: textContent || htmlContent.replace(/<[^>]*>/g, '') }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to send email'); } const data = await response.json(); console.log('✅ Email sent via Brevo:', data.messageId); return data; } catch (error) { console.error('❌ Brevo error:', error.message); throw error; } } // ============================================ // 📧 HELPER FUNCTION FOR SENDING EMAILS // ============================================ async function sendStoreEmailWithBrevo(to, subject, htmlContent, textContent) { try { const response = await fetch('https://api.brevo.com/v3/smtp/email', { method: 'POST', headers: { 'Content-Type': 'application/json', 'api-key': process.env.BREVO_API_KEY }, body: JSON.stringify({ sender: { name: 'MGZon Store', email: process.env.EMAIL_USER || 'no-reply@mgzon.com' }, to: [{ email: to }], subject: subject, htmlContent: htmlContent, textContent: textContent || htmlContent.replace(/<[^>]*>/g, '') }) }); if (!response.ok) { const error = await response.json(); console.error('Brevo error:', error); throw new Error(error.message || 'Failed to send email'); } const data = await response.json(); console.log(`✅ Email sent to ${to}: ${data.messageId}`); return data; } catch (error) { console.error('❌ Failed to send email:', error.message); // لا نرمي الخطأ عشان العملية تكمل return null; } } // ============================================ // Helper: إرسال إشعارات عند الدفع // ============================================ async function sendOrderPaidNotifications(orderId, req) { try { const order = await Order.findById(orderId) .populate('buyerId') .populate('sellerId'); if (!order) return; const storeSettings = await StoreSettings.findOne({ userId: order.sellerId._id }); const currencySymbol = storeSettings?.currencySymbol || '$'; // 📧 إشعار للمشتري await sendStoreEmailWithBrevo( order.buyerId.email, `✅ Payment Confirmed - Order #${order.orderNumber}`, `

Payment Confirmed! 🎉

Your order #${order.orderNumber} has been paid successfully.

Amount: ${currencySymbol}${order.totalAmount}

The seller will process your order shortly.

View Order `, `Payment confirmed for order #${order.orderNumber}` ); // 📧 إشعار للبائع await sendStoreEmailWithBrevo( order.sellerId.email, `💰 Payment Received - Order #${order.orderNumber}`, `

Payment Received! 🎉

Order #${order.orderNumber} has been paid by ${order.buyerId.profile?.nickname || order.buyerId.username}.

Amount: ${currencySymbol}${order.totalAmount}

Items: ${order.items.length} product(s)

Process Order `, `Payment received for order #${order.orderNumber}` ); // 📱 إشعار Socket.IO const io = req.app?.get('io'); if (io) { io.to(`user_${order.sellerId._id}`).emit('payment_received', { orderId: order._id, orderNumber: order.orderNumber, buyerName: order.buyerId.profile?.nickname || order.buyerId.username, amount: order.totalAmount, currencySymbol: currencySymbol }); io.to(`user_${order.buyerId._id}`).emit('payment_confirmed', { orderId: order._id, orderNumber: order.orderNumber, amount: order.totalAmount, currencySymbol: currencySymbol }); } console.log(`✅ Notifications sent for order #${order.orderNumber}`); } catch (error) { console.error('❌ Error sending notifications:', error); } } app.get('/api/test-email-config', async (req, res) => { try { // اختبار الاتصال فقط await transporter.verify(); res.json({ success: true, message: 'SMTP connection successful!' }); } catch (error) { console.error('SMTP Error:', error); res.json({ success: false, error: error.message }); } }); // mongoose.connect(MONGODB_URI) // .then(() => logger.info('Connected to MongoDB')) // .catch(err => logger.error('MongoDB connection error:', err)); // ============================================ // نموذج إعدادات الموقع (Site Settings) // ============================================ const siteSettingsSchema = new mongoose.Schema({ logo: { type: String, default: '/assets/img/logo.svg' }, siteName: { type: String, default: 'MGZon' }, primaryColor: { type: String, default: '#1d4ed8' }, secondaryColor: { type: String, default: '#8b5cf6' }, navbarLinks: [{ label: { type: String, required: true }, href: { type: String, required: true }, order: { type: Number, default: 0 } }], footerText: { type: String, default: '© MGZon. All rights reserved' }, updatedAt: { type: Date, default: Date.now } }); const SiteSettings = mongoose.model('SiteSettings', siteSettingsSchema); const projectSchema = new mongoose.Schema({ title: { type: String, required: true }, description: { type: String, required: true }, image: { type: String }, rating: { type: String }, stars: { type: Number }, links: [{ option: String, value: String, isPrivate: { type: Boolean, default: false } }], userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, isPublic: { type: Boolean, default: true }, // ✅ دعم GitHub import githubData: { type: mongoose.Schema.Types.Mixed, default: null }, importedFrom: { type: String, enum: ['github', 'gitlab', 'manual', null], default: null }, githubId: { type: String, sparse: true, index: true }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); // تحديث updatedAt تلقائياً projectSchema.pre('save', function(next) { this.updatedAt = Date.now(); next(); }); const Project = mongoose.model('Project', projectSchema); const commentSchema = new mongoose.Schema({ projectId: { type: mongoose.Schema.Types.ObjectId, ref: 'Project', required: true }, // ✅ تغيير من Mixed إلى ObjectId projectOwnerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, // ✅ صاحب المشروع userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, rating: { type: Number, required: true, min: 1, max: 5 }, text: { type: String, required: true }, timestamp: { type: Date, default: Date.now }, likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], parentCommentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment', default: null }, replies: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }], isEdited: { type: Boolean, default: false }, editedAt: { type: Date }, reportCount: { type: Number, default: 0 }, hidden: { type: Boolean, default: false }, visibility: { type: String, enum: ['public', 'logged-in', 'only-me'], default: 'public' }, notified: { type: Boolean, default: false }, reports: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, reporterUsername: { type: String }, reason: { type: String }, reportedAt: { type: Date, default: Date.now }, resolved: { type: Boolean, default: false }, resolvedAt: { type: Date }, resolvedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, adminNote: { type: String } }] }); const Comment = mongoose.model('Comment', commentSchema); // ============================================ // Message Report Schema - تقارير الرسائل المخالفة // ============================================ const messageReportSchema = new mongoose.Schema({ messageId: { type: mongoose.Schema.Types.ObjectId, ref: 'Message', required: true }, reporterId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, reporterUsername: { type: String }, reportedUserId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, reason: { type: String, required: true }, messageContent: { type: String }, messageAttachments: [{ type: mongoose.Schema.Types.Mixed }], conversationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Conversation' }, status: { type: String, enum: ['pending', 'resolved', 'dismissed'], default: 'pending' }, resolution: { type: String, enum: ['dismiss', 'warn', 'delete', 'hide', null], default: null }, adminNote: { type: String }, resolvedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, resolvedAt: { type: Date }, reportedAt: { type: Date, default: Date.now } }); messageReportSchema.index({ messageId: 1 }); messageReportSchema.index({ reporterId: 1 }); messageReportSchema.index({ status: 1 }); messageReportSchema.index({ reportedAt: -1 }); const MessageReport = mongoose.model('MessageReport', messageReportSchema); const postSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, content: { type: String, required: true, maxlength: 5000 }, images: [{ url: String, publicId: String, width: Number, height: Number }], video: { url: String, publicId: String, duration: Number }, likes: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, likedAt: { type: Date, default: Date.now } }], comments: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, text: String, createdAt: { type: Date, default: Date.now }, likes: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, likedAt: { type: Date, default: Date.now } }], edited: { type: Boolean, default: false }, editedAt: Date, reports: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, reason: { type: String, default: 'No reason provided' }, reportedAt: { type: Date, default: Date.now } }], reportCount: { type: Number, default: 0 }, hidden: { type: Boolean, default: false }, replies: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, text: String, createdAt: { type: Date, default: Date.now }, likes: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, likedAt: { type: Date, default: Date.now } }], edited: { type: Boolean, default: false }, editedAt: Date, reports: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, reason: { type: String, default: 'No reason provided' }, reportedAt: { type: Date, default: Date.now } }], reportCount: { type: Number, default: 0 }, hidden: { type: Boolean, default: false }, // ✅ ✅ ✅ المفتاح: الردود تحت الردود (Self-Referencing) replies: { type: Array, default: [] } }] }], shares: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, sharedAt: { type: Date, default: Date.now } }], saves: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, savedAt: { type: Date, default: Date.now } }], visibility: { type: String, enum: ['public', 'followers', 'only-me'], default: 'public' }, tags: [String], mentions: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, username: String, position: Number }], sharedFrom: { originalPostId: { type: mongoose.Schema.Types.ObjectId, ref: 'Post' }, originalAuthorId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, originalAuthorName: { type: String }, sharedAt: { type: Date, default: Date.now } }, pinned: { type: Boolean, default: false }, pinnedAt: Date, isEdited: { type: Boolean, default: false }, editedAt: Date, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); // Indexes for performance postSchema.index({ userId: 1, createdAt: -1 }); postSchema.index({ createdAt: -1 }); postSchema.index({ tags: 1 }); postSchema.index({ 'mentions.userId': 1 }); postSchema.pre('save', function(next) { this.updatedAt = Date.now(); next(); }); const Post = mongoose.model('Post', postSchema); const jobSchema = new mongoose.Schema({ // صاحب الوظيفة (اللي منشئها) employerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, title: { type: String, required: true, trim: true }, description: { type: String, required: true }, requirements: { type: String, required: true }, responsibilities: { type: String }, // تفاصيل الوظيفة category: { type: String, enum: ['technology', 'design', 'marketing', 'sales', 'customer-service', 'finance', 'hr', 'education', 'healthcare', 'other'], required: true }, jobType: { type: String, enum: ['full-time', 'part-time', 'contract', 'freelance', 'internship', 'remote'], required: true }, experienceLevel: { type: String, enum: ['entry', 'junior', 'mid', 'senior', 'lead', 'executive'], required: true }, // الموقع location: { type: String, required: true }, isRemote: { type: Boolean, default: false }, // الراتب salaryMin: { type: Number }, salaryMax: { type: Number }, salaryCurrency: { type: String, default: 'USD' }, isSalaryNegotiable: { type: Boolean, default: false }, // مهارات مطلوبة requiredSkills: [{ type: String, trim: true }], // التعليم educationLevel: { type: String, enum: ['high-school', 'associate', 'bachelor', 'master', 'phd', 'not-specified'], default: 'not-specified' }, // عدد الوظائف المتاحة vacancies: { type: Number, default: 1 }, // تاريخ النشر والانتهاء postedAt: { type: Date, default: Date.now }, deadline: { type: Date, required: true }, // حالة الوظيفة status: { type: String, enum: ['active', 'paused', 'closed', 'expired'], default: 'active' }, // عدد المشاهدات والمتقدمين views: { type: Number, default: 0 }, applicationsCount: { type: Number, default: 0 }, // هل الوظيفة مدفوعة (للإعلانات المميزة) isFeatured: { type: Boolean, default: false }, featuredUntil: { type: Date }, // Company info (if employer is a company) companyName: { type: String }, companyLogo: { type: String }, companyWebsite: { type: String }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); // Indexes for better performance jobSchema.index({ title: 'text', description: 'text', requirements: 'text' }); jobSchema.index({ category: 1, jobType: 1, experienceLevel: 1 }); jobSchema.index({ location: 1 }); jobSchema.index({ status: 1, deadline: 1 }); jobSchema.index({ employerId: 1 }); jobSchema.index({ isFeatured: 1, featuredUntil: 1 }); jobSchema.pre('save', function(next) { this.updatedAt = Date.now(); next(); }); const Job = mongoose.model('Job', jobSchema); const jobApplicationSchema = new mongoose.Schema({ jobId: { type: mongoose.Schema.Types.ObjectId, ref: 'Job', required: true }, applicantId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, // بيانات المتقدم fullName: { type: String, required: true }, email: { type: String, required: true }, phone: { type: String }, // الملفات المرفقة resume: { url: String, publicId: String, fileName: String }, coverLetter: { type: String }, portfolio: { type: String }, linkedin: { type: String }, github: { type: String }, // أسئلة إضافية answers: [{ question: String, answer: String }], // حالة التقديم status: { type: String, enum: ['pending', 'reviewed', 'shortlisted', 'interview', 'accepted', 'rejected', 'withdrawn'], default: 'pending' }, // ملاحظات صاحب الوظيفة employerNotes: { type: String }, rating: { type: Number, min: 1, max: 5 }, // مراحل المقابلة interviews: [{ scheduledAt: Date, type: { type: String, enum: ['phone', 'video', 'onsite', 'technical'] }, meetingLink: String, notes: String, status: { type: String, enum: ['scheduled', 'completed', 'cancelled', 'no-show'], default: 'scheduled' } }], appliedAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); // منع التقديم المتكرر لنفس الوظيفة jobApplicationSchema.index({ jobId: 1, applicantId: 1 }, { unique: true }); jobApplicationSchema.index({ jobId: 1, status: 1 }); jobApplicationSchema.index({ applicantId: 1 }); jobApplicationSchema.pre('save', function(next) { this.updatedAt = Date.now(); next(); }); const JobApplication = mongoose.model('JobApplication', jobApplicationSchema); const jobSaveSchema = new mongoose.Schema({ jobId: { type: mongoose.Schema.Types.ObjectId, ref: 'Job', required: true }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, savedAt: { type: Date, default: Date.now } }); jobSaveSchema.index({ jobId: 1, userId: 1 }, { unique: true }); const JobSave = mongoose.model('JobSave', jobSaveSchema); const jobCategorySchema = new mongoose.Schema({ name: { type: String, required: true, unique: true }, nameAr: String, icon: String, color: String, order: { type: Number, default: 0 }, isActive: { type: Boolean, default: true } }); const JobCategory = mongoose.model('JobCategory', jobCategorySchema); const storySchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, media: { url: { type: String, default: null }, publicId: { type: String, default: null }, type: { type: String, enum: ['image', 'video', 'audio', 'none'], default: 'none' } }, filter: { type: String, default: 'none', enum: ['none', 'vintage', 'noir', 'cool', 'warm', 'dramatic', 'dreamy', 'vivid', 'fade', 'sepia', 'grayscale', 'blur', 'grain', 'vignette', 'neon', 'retro', 'cinematic', 'pastel'] }, // ✅ حقل جديد لضبط قوة الفلتر filterIntensity: { type: Number, default: 0.5, min: 0, max: 1 }, text: { type: String, maxlength: 2000, default: '' }, backgroundColor: { type: String, default: '#000000' }, textColor: { type: String, default: '#ffffff' }, allowDownload: { type: Boolean, default: false }, audio: { url: { type: String, default: null }, publicId: { type: String, default: null }, duration: { type: Number, default: 0 }, // مدة التسجيل بالثواني waveform: { type: Array, default: [] } // شكل الموجة للتسجيل }, // ✅ حقل جديد لدعم الفلاتر filterApplied: { type: Boolean, default: false }, textStyle: { fontSize: { type: Number, default: 32 }, // حجم الخط fontFamily: { type: String, default: 'Inter' }, // نوع الخط fontWeight: { type: String, default: '700' }, // سمك الخط textAlign: { type: String, default: 'center' }, // محاذاة النص position: { // موضع النص x: { type: Number, default: 50 }, // % من العرض (50 = منتصف) y: { type: Number, default: 50 } // % من الارتفاع (50 = منتصف) }, rotation: { type: Number, default: 0 }, // دوران النص opacity: { type: Number, default: 1 }, // شفافية النص textShadow: { type: String, default: '0 2px 10px rgba(0,0,0,0.5)' }, background: { type: String, default: 'transparent' }, // خلفية النص padding: { type: String, default: '0' }, borderRadius: { type: String, default: '0' }, maxWidth: { type: Number, default: 90 } // % من عرض القصة }, views: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, viewedAt: { type: Date, default: Date.now } }], reactions: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, type: { type: String, enum: ['❤️', '😂', '😮', '😢', '😡'], default: '❤️' }, createdAt: { type: Date, default: Date.now } }], quickReactions: { likes: { type: Number, default: 0 }, hearts: { type: Number, default: 0 }, laughs: { type: Number, default: 0 }, wows: { type: Number, default: 0 }, sads: { type: Number, default: 0 }, angrys: { type: Number, default: 0 } }, visibility: { type: String, enum: ['public', 'followers', 'only-me'], default: 'followers' }, replies: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, text: { type: String, maxlength: 200 }, createdAt: { type: Date, default: Date.now } }], // ✅ حقل جديد: لمنع تكرار إرسال الإشعارات notificationSent: { type: Boolean, default: false }, createdAt: { type: Date, default: Date.now, index: true }, expiresAt: { type: Date, default: () => new Date(Date.now() + 24 * 60 * 60 * 1000) } }); // فهارس محسّنة storySchema.index({ userId: 1, createdAt: -1 }); storySchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); storySchema.index({ 'audio.url': 1 }); storySchema.index({ filter: 1 }); // فيريتول (Virtual) لحساب عدد المشاهدات storySchema.virtual('viewsCount').get(function() { return this.views?.length || 0; }); // فيريتول لحساب عدد التفاعلات storySchema.virtual('reactionsCount').get(function() { return this.reactions?.length || 0; }); // طريقة للتحقق مما إذا كانت القصة منتهية storySchema.methods.isExpired = function() { return new Date() > this.expiresAt; }; // طريقة للتحقق مما إذا كان المستخدم قد شاهد القصة storySchema.methods.hasViewed = function(userId) { return this.views?.some(v => v.userId.toString() === userId.toString()); }; const Story = mongoose.model('Story', storySchema); const reportSchema = new mongoose.Schema({ reporterId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, targetType: { type: String, enum: ['post', 'comment', 'user', 'profile'], required: true, index: true }, targetId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true }, reason: { type: String, enum: ['spam', 'harassment', 'hate_speech', 'violence', 'nudity', 'misinformation', 'other'], required: true }, details: { type: String, maxLength: 500, default: '' }, status: { type: String, enum: ['pending', 'reviewed', 'resolved', 'dismissed'], default: 'pending', index: true }, adminNotes: { type: String, maxLength: 1000, default: '' }, resolvedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, resolvedAt: { type: Date } }, { timestamps: true }); // Indexes for faster queries reportSchema.index({ targetType: 1, targetId: 1, status: 1 }); reportSchema.index({ reporterId: 1, createdAt: -1 }); // Check if user already reported this content reportSchema.statics.hasUserReported = async function(userId, targetType, targetId) { const existing = await this.findOne({ reporterId: userId, targetType, targetId, status: { $in: ['pending', 'reviewed'] } }); return !!existing; }; // Get report count for a target reportSchema.statics.getReportCount = async function(targetType, targetId) { return await this.countDocuments({ targetType, targetId, status: 'pending' }); }; const Report = mongoose.model('Report', reportSchema); const notificationSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, type: { type: String, enum: [ 'like', 'comment', 'share', 'follow', 'mention','report', 'report_resolved', 'post_approved', 'system', 'message', 'reply', 'admin_report', 'admin_info', 'warning', 'rating', 'application', 'new_order', 'order_status_update', 'product_sold', 'store_follow', 'subscription_expiring', // قبل انتهاء الاشتراك بـ 7,3,1 يوم 'subscription_expired', // بعد ما ينتهي الاشتراك 'subscription_approved', // لما المسؤول يوافق على الاشتراك 'subscription_rejected', // لما المسؤول يرفض الاشتراك 'subscription_pending', // اشتراك جديد في انتظار الموافقة 'subscription_cancelled' // لما المستخدم يلغي الاشتراك ], required: true }, actorId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, actorName: String, actorAvatar: String, targetId: { type: String }, targetType: { type: String }, content: String, read: { type: Boolean, default: false }, createdAt: { type: Date, default: Date.now } }); notificationSchema.index({ userId: 1, createdAt: -1 }); notificationSchema.index({ userId: 1, read: 1 }); notificationSchema.index({ type: 1, createdAt: -1 }); const Notification = mongoose.model('Notification', notificationSchema); const followSchema = new mongoose.Schema({ followerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, followingId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, status: { type: String, enum: ['pending', 'accepted', 'blocked'], default: 'accepted' }, createdAt: { type: Date, default: Date.now } }); // Unique compound index followSchema.index({ followerId: 1, followingId: 1 }, { unique: true }); followSchema.index({ followingId: 1, createdAt: -1 }); const Follow = mongoose.model('Follow', followSchema); // ============================================ // 🛒 DIGITAL STORE MODELS // ============================================ // 1. نموذج المنتج (Product) const productSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, type: { type: String, enum: ['digital', 'project', 'service'], required: true }, title: { type: String, required: true }, description: { type: String, required: true }, price: { type: Number, required: true, min: 0 }, currency: { type: String, default: 'USD' }, // Digital Product Fields (ملفات للتحميل) fileUrl: { type: String }, fileSize: { type: Number }, // Service Fields (خدمات) deliveryTime: { type: String }, // مثلاً "3-5 business days" // Common Fields images: [{ type: String }], tags: [String], isActive: { type: Boolean, default: true }, salesCount: { type: Number, default: 0 }, averageRating: { type: Number, default: 0 }, reviews: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, rating: { type: Number, min: 1, max: 5 }, comment: { type: String, maxlength: 500 }, createdAt: { type: Date, default: Date.now } }], createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); // Indexes for better performance productSchema.index({ userId: 1, type: 1, isActive: 1 }); productSchema.index({ userId: 1, createdAt: -1 }); productSchema.index({ tags: 1 }); const Product = mongoose.model('Product', productSchema); // 2. نموذج الطلب (Order) const orderSchema = new mongoose.Schema({ orderNumber: { type: String, unique: true }, buyerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, sellerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, items: [{ productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' }, productType: { type: String }, title: { type: String }, quantity: { type: Number, default: 1 }, price: { type: Number }, fileUrl: { type: String }, downloadCount: { type: Number, default: 0 } }], trackingNumber: { type: String }, // للشحن cancelledAt: { type: Date }, cancellationReason: { type: String }, downloadableFiles: [{ fileName: String, fileUrl: String, expiresAt: Date, downloadCount: { type: Number, default: 0 } }], subtotal: { type: Number, required: true }, totalAmount: { type: Number, required: true }, currency: { type: String, default: 'USD' }, status: { type: String, enum: ['pending', 'paid', 'processing', 'completed', 'cancelled', 'refunded'], default: 'pending' }, paymentMethod: { type: String }, // paypal, stripe, vodafone, instapay, bank paymentDetails: { type: mongoose.Schema.Types.Mixed }, buyerNotes: { type: String, maxlength: 500 }, sellerNotes: { type: String, maxlength: 500 }, deliveredAt: { type: Date }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); // Generate unique order number before saving orderSchema.pre('save', async function(next) { if (!this.orderNumber) { const date = new Date(); const timestamp = date.getTime().toString().slice(-8); const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0'); this.orderNumber = `ORD-${timestamp}-${random}`; } next(); }); orderSchema.index({ status: 1 }); const Order = mongoose.model('Order', orderSchema); // 3. نموذج إعدادات المتجر (Store Settings) /** * نموذج إعدادات المتجر - يدعم القوالب، الاشتراكات، الإحصائيات المتقدمة * @version 3.0.0 */ const storeSettingsSchema = new mongoose.Schema({ // ============================================ // المعلومات الأساسية // ============================================ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, unique: true, index: true }, enabled: { type: Boolean, default: false }, // ============================================ // معلومات المتجر العامة // ============================================ storeName: { type: String, default: '', trim: true, maxlength: 100 }, storeLogo: { type: String, default: '' }, storeBanner: { type: String, default: '' }, storeDescription: { type: String, default: '', maxlength: 500 }, contactEmail: { type: String, default: '', lowercase: true, trim: true }, // ============================================ // العملة والإعدادات المالية // ============================================ currency: { type: String, default: 'USD', uppercase: true }, currencySymbol: { type: String, default: '$' }, // إعدادات الدفع المتقدمة paymentSettings: { currencyPosition: { type: String, enum: ['left', 'right'], default: 'left' }, decimalPlaces: { type: Number, default: 2, min: 0, max: 4 }, thousandsSeparator: { type: String, default: ',' }, decimalSeparator: { type: String, default: '.' }, enableCOD: { type: Boolean, default: false }, enableInstallments: { type: Boolean, default: false }, installmentProviders: [{ type: String }], minOrderAmount: { type: Number, default: 0 }, freeShippingThreshold: { type: Number, default: 0 }, taxRate: { type: Number, default: 0, min: 0, max: 100 }, shippingCost: { type: Number, default: 0 } }, // طرق الدفع paymentMethods: [{ type: { type: String, enum: ['paypal', 'stripe', 'vodafone', 'instapay', 'bank', 'cod'] }, label: { type: String }, details: { // 📱 للدفع المباشر phone: { type: String }, // Vodafone/InstaPay email: { type: String }, // PayPal // 🏦 للتحويل البنكي bankName: { type: String }, accountName: { type: String }, accountNumber: { type: String }, iban: { type: String }, // 💳 لـ Stripe accountId: { type: String }, webhookSecret: { type: String }, // ⭐ whsec_xxx (مهم جداً!) publishableKey: { type: String }, secretKey: { type: String }, // 💰 لـ PayPal webhookId: { type: String }, clientId: { type: String }, clientSecret: { type: String } }, isActive: { type: Boolean, default: true }, createdAt: { type: Date, default: Date.now } }], // ============================================ // 🎨 إعدادات التصميم (Theme) // ============================================ activeThemeId: { type: mongoose.Schema.Types.ObjectId, ref: 'StoreTheme', default: null }, // الألوان الأساسية primaryColor: { type: String, default: '#3b82f6', match: /^#[0-9A-Fa-f]{6}$/ }, secondaryColor: { type: String, default: '#8b5cf6', match: /^#[0-9A-Fa-f]{6}$/ }, // الألوان الموسعة colorPalette: { primaryHover: { type: String, default: '#2563eb' }, secondaryHover: { type: String, default: '#7c3aed' }, buttonText: { type: String, default: '#ffffff' }, linkColor: { type: String, default: '#3b82f6' }, linkHover: { type: String, default: '#2563eb' }, headingColor: { type: String, default: '#1f2937' }, bodyColor: { type: String, default: '#374151' }, borderColor: { type: String, default: '#e5e7eb' }, successColor: { type: String, default: '#10b981' }, errorColor: { type: String, default: '#ef4444' }, warningColor: { type: String, default: '#f59e0b' } }, // الخطوط fontFamily: { type: String, default: 'Inter, sans-serif' }, typography: { headingFont: { type: String, default: 'Inter, sans-serif' }, baseFontSize: { type: String, default: '16px', match: /^\d+(px|rem)$/ }, enableGoogleFonts: { type: Boolean, default: true } }, // التخطيط borderRadius: { type: Number, default: 16, min: 0, max: 48 }, shadowType: { type: String, enum: ['none', 'sm', 'md', 'lg', 'xl'], default: 'md' }, animationType: { type: String, enum: ['none', 'lift', 'scale', 'glow'], default: 'lift' }, glassEffect: { type: Boolean, default: false }, // الخلفية backgroundType: { type: String, enum: ['default', 'dots', 'grid', 'cross', 'gradient', 'image'], default: 'default' }, backgroundValue: { type: String, default: '' }, // ============================================ // 🖼️ BACKGROUNDS & GALLERY // ============================================ // الخلفيات المخصصة التي رفعها المستخدم customBackgrounds: [{ id: { type: String, required: true }, name: { type: String, default: 'Custom Background' }, url: { type: String, required: true }, category: { type: String, default: 'custom' }, type: { type: String, enum: ['image', 'gif', 'gradient'], default: 'image' }, uploadedAt: { type: Date, default: Date.now }, isActive: { type: Boolean, default: true } }], // سجل الخلفيات التي طبقها المستخدم backgroundHistory: [{ url: { type: String, required: true }, name: { type: String }, appliedAt: { type: Date, default: Date.now }, duration: { type: Number, default: 0 } // المدة التي استخدمت فيها (بالأيام) }], // الخلفية الحالية currentBackground: { url: { type: String, default: '' }, name: { type: String, default: '' }, type: { type: String, enum: ['image', 'gif', 'gradient'], default: 'image' }, appliedAt: { type: Date, default: Date.now } }, // CSS مخصص customCss: { type: String, default: '', maxlength: 100000 }, // إعدادات التخطيط المتقدمة layoutSettings: { // الأساسية headerLayout: { type: String, enum: ['default', 'centered', 'minimal', 'transparent'], default: 'default' }, footerLayout: { type: String, enum: ['default', 'minimal', 'columns'], default: 'default' }, sidebarPosition: { type: String, enum: ['left', 'right', 'none'], default: 'none' }, productsPerRowMobile: { type: Number, default: 2, min: 1, max: 2 }, productsPerRowTablet: { type: Number, default: 3, min: 1, max: 4 }, productsPerRowDesktop: { type: Number, default: 4, min: 1, max: 6 }, enableStickyAddToCart: { type: Boolean, default: true }, enableQuickView: { type: Boolean, default: true }, enableCompareProducts: { type: Boolean, default: false }, enableWishlist: { type: Boolean, default: false }, enableRecentlyViewed: { type: Boolean, default: true }, enableProductReviews: { type: Boolean, default: true }, // قالب التخطيط (محدث) layoutTemplate: { type: String, enum: ['default', 'sidebar-left', 'sidebar-right', 'product-focused', 'minimal', 'magazine', 'compact', 'dark-store'], default: 'default' }, // أنماط البطاقات و Hero cardStyle: { type: String, enum: ['default', 'compact', 'minimal', 'detailed'], default: 'default' }, heroStyle: { type: String, enum: ['default', 'large', 'fullscreen', 'minimal', 'none'], default: 'default' }, // إظهار/إخفاء الأقسام showStats: { type: Boolean, default: true }, showAnalytics: { type: Boolean, default: true }, showHero: { type: Boolean, default: true }, showFeatured: { type: Boolean, default: true }, showCategories: { type: Boolean, default: true }, // الثيم ونمط العرض theme: { type: String, enum: ['light', 'dark', 'auto'], default: 'light' }, productViewType: { type: String, enum: ['grid', 'list', 'masonry'], default: 'grid' }, // إعدادات الـ compact layout compactMode: { enabled: { type: Boolean, default: false }, hidePrices: { type: Boolean, default: false }, hideRatings: { type: Boolean, default: false }, hideDescription: { type: Boolean, default: false }, showQuickAdd: { type: Boolean, default: true } }, // إعدادات الـ magazine layout magazineMode: { showHeroSlider: { type: Boolean, default: true }, heroSlidesCount: { type: Number, default: 3, min: 1, max: 5 }, showFeaturedGrid: { type: Boolean, default: true }, showLatestPosts: { type: Boolean, default: true }, postsCount: { type: Number, default: 3, min: 1, max: 6 } }, // إعدادات الـ dark store darkStoreMode: { neonAccents: { type: Boolean, default: true }, accentColor: { type: String, default: '#00d4ff', match: /^#[0-9A-Fa-f]{6}$/ }, glowEffect: { type: Boolean, default: true }, blurEffect: { type: Boolean, default: true } }, // ترتيب الأقسام المخصص sectionsOrder: [{ type: String }], // إعدادات متقدمة advanced: { containerMaxWidth: { type: String, default: '1400px' }, enableParallax: { type: Boolean, default: false }, enableParticles: { type: Boolean, default: false }, customBackgroundImage: { type: String, default: '' } } }, // ============================================ // سلوك المنتجات // ============================================ productViewType: { type: String, enum: ['modal', 'page'], default: 'modal' }, autoApproveReviews: { type: Boolean, default: false }, // ============================================ // Page Builder // ============================================ pageBuilder: { type: mongoose.Schema.Types.Mixed, default: { sections: [ { id: 'hero', type: 'hero', enabled: true, order: 1, content: { title: 'Welcome to my store', subtitle: '', buttonText: 'Shop Now', buttonLink: '#products' } }, { id: 'featured', type: 'featured', enabled: true, order: 2, title: 'Featured Products', limit: 6 }, { id: 'products', type: 'products', enabled: true, order: 3, title: 'All Products', layout: 'grid' }, { id: 'categories', type: 'categories', enabled: true, order: 4, title: 'Categories' }, { id: 'testimonials', type: 'testimonials', enabled: true, order: 5, title: 'What Customers Say' }, { id: 'faq', type: 'faq', enabled: true, order: 6, title: 'Frequently Asked Questions' }, { id: 'contact', type: 'contact', enabled: true, order: 7, title: 'Contact Me' } ] } }, pagesOrder: { type: mongoose.Schema.Types.Mixed, default: { products: 1, about: 2, contact: 3 } }, // ============================================ // التواصل الاجتماعي // ============================================ socialLinks: { instagram: { type: String, default: '' }, facebook: { type: String, default: '' }, twitter: { type: String, default: '' }, tiktok: { type: String, default: '' }, linkedin: { type: String, default: '' }, youtube: { type: String, default: '' }, whatsapp: { type: String, default: '' }, telegram: { type: String, default: '' } }, // ============================================ // SEO والإحصائيات // ============================================ seoSettings: { metaTitleTemplate: { type: String, default: '{{title}} | {{storeName}}' }, metaDescriptionTemplate: { type: String, default: '' }, productTitleTemplate: { type: String, default: '{{title}} - {{storeName}}' }, enableBreadcrumbs: { type: Boolean, default: true }, sitemapEnabled: { type: Boolean, default: true }, robotsTxt: { type: String, default: '' }, canonicalUrl: { type: String, default: '' } }, socialMediaSettings: { facebookPixel: { type: String, default: '' }, googleAnalytics: { type: String, default: '' }, tiktokPixel: { type: String, default: '' }, twitterPixel: { type: String, default: '' }, linkedInInsight: { type: String, default: '' } }, // ============================================ // العلامة التجارية // ============================================ branding: { favicon: { type: String, default: '' }, watermark: { type: String, default: '' }, customDomain: { type: String, default: '' }, customDomainVerified: { type: Boolean, default: false }, removeBranding: { type: Boolean, default: false } // إزالة علامة MGZon }, // ============================================ // إعدادات الأداء // ============================================ performanceSettings: { enableLazyLoad: { type: Boolean, default: true }, enableImageOptimization: { type: Boolean, default: true }, enablePreload: { type: Boolean, default: true }, cacheTTL: { type: Number, default: 3600 }, // ثواني enableServiceWorker: { type: Boolean, default: false } }, // ============================================ // إعدادات الإشعارات // ============================================ notificationSettings: { newOrderEmail: { type: Boolean, default: true }, newOrderSMS: { type: Boolean, default: false }, lowStockAlert: { type: Boolean, default: true }, lowStockThreshold: { type: Number, default: 5 }, dailySummaryEmail: { type: Boolean, default: false }, weeklyReportEmail: { type: Boolean, default: true }, sendInvoiceEmail: { type: Boolean, default: true } }, // ============================================ // إعدادات الأمان // ============================================ securitySettings: { enableCaptcha: { type: Boolean, default: false }, captchaSiteKey: { type: String, default: '' }, enable2FA: { type: Boolean, default: false }, ipWhitelist: [{ type: String }], ipBlacklist: [{ type: String }], rateLimitPerMinute: { type: Number, default: 60 } }, // ============================================ // الإحصائيات (مدمجة) // ============================================ productsCount: { type: Number, default: 0 }, followersCount: { type: Number, default: 0 }, averageRating: { type: Number, default: 0, min: 0, max: 5 }, extendedStats: { totalViews: { type: Number, default: 0 }, totalUniqueVisitors: { type: Number, default: 0 }, totalOrders: { type: Number, default: 0 }, totalSales: { type: Number, default: 0 }, totalShares: { type: Number, default: 0 }, conversionRate: { type: Number, default: 0 }, averageOrderValue: { type: Number, default: 0 }, topReferrers: [{ source: String, count: Number }], dailyStats: { type: mongoose.Schema.Types.Mixed, default: {} }, monthlyStats: { type: mongoose.Schema.Types.Mixed, default: {} } }, // ============================================ // حدود المنتجات (بناءً على الاشتراك) // ============================================ limits: { maxProducts: { type: Number, default: 0 }, maxDigitalProducts: { type: Number, default: 0 }, maxProjects: { type: Number, default: 0 }, maxServices: { type: Number, default: 0 }, maxPages: { type: Number, default: 0 }, maxStorageMB: { type: Number, default: 100 }, maxBandwidthGB: { type: Number, default: 10 } }, // ============================================ // حالة الاشتراك (مخزنة مؤقتاً) // ============================================ subscriptionStatus: { isActive: { type: Boolean, default: false }, planId: { type: mongoose.Schema.Types.ObjectId, ref: 'SubscriptionPlan' }, planName: { type: String, default: '' }, expiresAt: { type: Date }, cachedAt: { type: Date, default: Date.now } }, // ============================================ // أكواد الخصم العامة للمتجر // ============================================ storeCoupons: [{ code: { type: String, uppercase: true, trim: true }, discount: { type: Number, min: 0, max: 100 }, discountType: { type: String, enum: ['percentage', 'fixed'], default: 'percentage' }, minPurchase: { type: Number, default: 0 }, maxDiscount: { type: Number }, validFrom: { type: Date, default: Date.now }, validUntil: { type: Date }, usageLimit: { type: Number }, usedCount: { type: Number, default: 0 }, isActive: { type: Boolean, default: true } }], // ============================================ // مناطق الشحن // ============================================ shippingZones: [{ name: { type: String, required: true }, countries: [{ type: String }], cities: [{ type: String }], cost: { type: Number, default: 0 }, freeShippingAbove: { type: Number, default: 0 }, estimatedDays: { type: String, default: '3-7 business days' } }], // ============================================ // إعدادات النسخ الاحتياطي // ============================================ backupSettings: { autoBackup: { type: Boolean, default: false }, backupFrequency: { type: String, enum: ['daily', 'weekly', 'monthly'], default: 'weekly' }, lastBackupAt: { type: Date }, backupSize: { type: Number, default: 0 }, backupLocation: { type: String, default: '' } }, // ============================================ // إعدادات التصدير // ============================================ exportSettings: { enableCSVExport: { type: Boolean, default: true }, enableJSONExport: { type: Boolean, default: true }, enablePDFExport: { type: Boolean, default: true }, exportEmailReports: { type: Boolean, default: false }, exportEmail: { type: String, default: '' } }, // ============================================ // إعدادات متقدمة أخرى // ============================================ enableGuestCheckout: { type: Boolean, default: true }, enableCookiesConsent: { type: Boolean, default: true }, enableTermsCheckbox: { type: Boolean, default: true }, termsUrl: { type: String, default: '' }, privacyUrl: { type: String, default: '' }, createdAt: { type: Date, default: Date.now, immutable: true }, updatedAt: { type: Date, default: Date.now } }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // ============================================ // ✅ Indexes محسنة // ============================================ storeSettingsSchema.index({ userId: 1 }, { unique: true }); storeSettingsSchema.index({ enabled: 1 }); storeSettingsSchema.index({ storeName: 1 }); storeSettingsSchema.index({ averageRating: -1 }); storeSettingsSchema.index({ productsCount: -1 }); storeSettingsSchema.index({ followersCount: -1 }); storeSettingsSchema.index({ createdAt: -1 }); storeSettingsSchema.index({ 'subscriptionStatus.isActive': 1 }); storeSettingsSchema.index({ 'subscriptionStatus.expiresAt': 1 }); storeSettingsSchema.index({ 'branding.customDomain': 1 }); storeSettingsSchema.index({ 'extendedStats.totalViews': -1 }); storeSettingsSchema.index({ 'extendedStats.totalSales': -1 }); // ============================================ // ✅ Virtuals // ============================================ storeSettingsSchema.virtual('storeUrl').get(function() { return `${FRONTEND_URL}/shop/${this.userId}`; }); storeSettingsSchema.virtual('hasCustomTheme').get(function() { return !!this.activeThemeId; }); storeSettingsSchema.virtual('isSubscriptionActive').get(function() { if (!this.subscriptionStatus.isActive) return false; if (!this.subscriptionStatus.expiresAt) return true; return new Date() < this.subscriptionStatus.expiresAt; }); // ============================================ // ✅ Pre-save Middleware // ============================================ storeSettingsSchema.pre('save', function(next) { this.updatedAt = Date.now(); // تحديث رمز العملة تلقائياً const currencySymbols = { 'USD': '$', 'EUR': '€', 'GBP': '£', 'EGP': 'ج.م', 'SAR': '﷼', 'AED': 'د.إ', 'CAD': 'C$', 'AUD': 'A$', 'JPY': '¥', 'CNY': '¥', 'INR': '₹', 'BRL': 'R$' }; if (this.currency && currencySymbols[this.currency]) { this.currencySymbol = currencySymbols[this.currency]; } // تنظيف الروابط if (this.socialLinks) { Object.keys(this.socialLinks).forEach(key => { if (this.socialLinks[key]) { this.socialLinks[key] = this.socialLinks[key].trim(); } }); } // تنظيف الأكواد المكررة في storeCoupons if (this.storeCoupons && this.storeCoupons.length > 0) { const uniqueCodes = new Map(); for (const coupon of this.storeCoupons) { if (!uniqueCodes.has(coupon.code)) { uniqueCodes.set(coupon.code, coupon); } } this.storeCoupons = Array.from(uniqueCodes.values()); } next(); }); // ============================================ // ✅ Static Methods // ============================================ /** * جلب أو إنشاء إعدادات المتجر لمستخدم */ storeSettingsSchema.statics.getOrCreate = async function(userId) { let settings = await this.findOne({ userId }); if (!settings) { settings = new this({ userId }); await settings.save(); } return settings; }; /** * تحديث إحصائيات المتجر */ storeSettingsSchema.statics.updateStats = async function(userId, stats) { const updateData = {}; if (stats.productsCount !== undefined) updateData.productsCount = stats.productsCount; if (stats.followersCount !== undefined) updateData.followersCount = stats.followersCount; if (stats.averageRating !== undefined) updateData.averageRating = stats.averageRating; if (stats.totalViews !== undefined) updateData['extendedStats.totalViews'] = stats.totalViews; if (stats.totalSales !== undefined) updateData['extendedStats.totalSales'] = stats.totalSales; if (stats.totalOrders !== undefined) updateData['extendedStats.totalOrders'] = stats.totalOrders; return this.findOneAndUpdate( { userId }, { $inc: updateData, $set: { updatedAt: Date.now() } }, { new: true } ); }; /** * جلب المتاجر الأكثر مبيعاً */ storeSettingsSchema.statics.getTopStores = async function(limit = 10) { return this.find({ enabled: true }) .sort({ 'extendedStats.totalSales': -1 }) .limit(limit) .select('userId storeName storeLogo productsCount followersCount extendedStats.totalSales'); }; /** * جلب المتاجر النشطة (مع اشتراك ساري) */ storeSettingsSchema.statics.getActiveStores = async function(limit = 50) { const now = new Date(); return this.find({ enabled: true, 'subscriptionStatus.isActive': true, $or: [ { 'subscriptionStatus.expiresAt': { $exists: false } }, { 'subscriptionStatus.expiresAt': { $gte: now } } ] }).limit(limit); }; // ============================================ // ✅ Instance Methods // ============================================ /** * تحديث إحصائيات الاشتراك */ storeSettingsSchema.methods.updateSubscriptionStatus = async function(subscription) { this.subscriptionStatus = { isActive: subscription.status === 'active', planId: subscription.planId, planName: subscription.planName, expiresAt: subscription.endDate, cachedAt: new Date() }; // تحديث الحدود بناءً على الخطة if (subscription.plan) { this.limits = { maxProducts: subscription.plan.features?.maxProducts || 0, maxDigitalProducts: subscription.plan.features?.maxDigitalProducts || 0, maxProjects: subscription.plan.features?.maxProjects || 0, maxServices: subscription.plan.features?.maxServices || 0, maxPages: subscription.plan.features?.maxPages || 0, maxStorageMB: subscription.plan.features?.maxStorageMB || 100, maxBandwidthGB: subscription.plan.features?.maxBandwidthGB || 10 }; this.branding.removeBranding = subscription.plan.features?.removeBranding || false; } await this.save(); }; /** * التحقق من وجود منتج في المتجر */ storeSettingsSchema.methods.canAddProduct = async function(type) { if (!this.subscriptionStatus.isActive) return false; const currentCounts = { product: this.productsCount, digital: await mongoose.model('Product').countDocuments({ userId: this.userId, type: 'digital', isActive: true }), project: await mongoose.model('Product').countDocuments({ userId: this.userId, type: 'project', isActive: true }), service: await mongoose.model('Product').countDocuments({ userId: this.userId, type: 'service', isActive: true }) }; const limits = this.limits; if (limits.maxProducts > 0 && currentCounts.product >= limits.maxProducts) { return false; } if (type === 'digital' && limits.maxDigitalProducts > 0 && currentCounts.digital >= limits.maxDigitalProducts) { return false; } if (type === 'project' && limits.maxProjects > 0 && currentCounts.project >= limits.maxProjects) { return false; } if (type === 'service' && limits.maxServices > 0 && currentCounts.service >= limits.maxServices) { return false; } return true; }; /** * تسجيل مشاهدة للمتجر */ storeSettingsSchema.methods.trackView = async function() { this.extendedStats.totalViews += 1; await this.save(); }; /** * تسجيل عملية بيع */ storeSettingsSchema.methods.trackSale = async function(amount) { this.extendedStats.totalSales += amount; this.extendedStats.totalOrders += 1; // تحديث متوسط قيمة الطلب this.extendedStats.averageOrderValue = this.extendedStats.totalSales / this.extendedStats.totalOrders; // تحديث نسبة التحويل (يمكن تحديثها لاحقاً مع عدد الزوار) if (this.extendedStats.totalUniqueVisitors > 0) { this.extendedStats.conversionRate = (this.extendedStats.totalOrders / this.extendedStats.totalUniqueVisitors) * 100; } await this.save(); }; /** * التحقق من صحة كوبون الخصم */ storeSettingsSchema.methods.validateCoupon = async function(code, cartTotal) { const coupon = this.storeCoupons?.find(c => c.code === code.toUpperCase() && c.isActive); if (!coupon) return { valid: false, message: 'Invalid coupon code' }; const now = new Date(); if (coupon.validUntil && now > coupon.validUntil) { return { valid: false, message: 'Coupon has expired' }; } if (coupon.validFrom && now < coupon.validFrom) { return { valid: false, message: 'Coupon not yet active' }; } if (coupon.usageLimit && coupon.usedCount >= coupon.usageLimit) { return { valid: false, message: 'Coupon usage limit reached' }; } if (cartTotal < coupon.minPurchase) { return { valid: false, message: `Minimum purchase of ${this.currencySymbol}${coupon.minPurchase} required` }; } let discount = 0; if (coupon.discountType === 'percentage') { discount = (cartTotal * coupon.discount) / 100; if (coupon.maxDiscount && discount > coupon.maxDiscount) { discount = coupon.maxDiscount; } } else { discount = Math.min(coupon.discount, cartTotal); } return { valid: true, discount: discount, finalTotal: cartTotal - discount, coupon: coupon }; }; const StoreSettings = mongoose.model('StoreSettings', storeSettingsSchema); /** * نموذج قوالب المتجر - يدعم القوالب المخصصة والافتراضية * @version 2.0.0 */ const storeThemeSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, unique: true, index: true }, // معلومات القالب الأساسية name: { type: String, default: 'Default Theme', trim: true, maxlength: 100 }, isActive: { type: Boolean, default: true, index: true }, version: { type: String, default: '1.0.0', match: /^\d+\.\d+\.\d+$/ }, // تصميم القالب (Layout) layout: { type: String, enum: ['full-width', 'boxed', 'fluid', 'dashboard', 'minimal'], default: 'full-width' }, // قوالب HTML مخصصة customTemplates: { header: { type: String, default: '', maxlength: 50000 }, footer: { type: String, default: '', maxlength: 50000 }, sidebar: { type: String, default: '', maxlength: 50000 }, productCard: { type: String, default: '', maxlength: 20000 }, categoryCard: { type: String, default: '', maxlength: 20000 } }, // ملف القالب المرفوع (ZIP) uploadedTheme: { filename: { type: String }, originalName: { type: String }, fileUrl: { type: String }, fileSize: { type: Number }, fileHash: { type: String }, // للتأكد من عدم تكرار الرفع mimeType: { type: String }, uploadedAt: { type: Date, default: Date.now }, extractedFiles: { htmlFiles: [{ name: String, size: Number }], cssFiles: [{ name: String, size: Number }], jsFiles: [{ name: String, size: Number }], imageFiles: [{ name: String, size: Number, url: String }] } }, // إعدادات القالب settings: { containerWidth: { type: String, default: '1280px', match: /^\d+(px|rem|em|%|vw|vh)$/ }, containerPadding: { type: String, default: '20px' }, showBreadcrumb: { type: Boolean, default: true }, stickyHeader: { type: Boolean, default: false }, backToTop: { type: Boolean, default: true }, showSearchBar: { type: Boolean, default: true }, showCategories: { type: Boolean, default: true }, productsPerRow: { type: Number, enum: [2, 3, 4, 6], default: 4 }, productsPerPage: { type: Number, min: 6, max: 48, default: 12 }, enableLazyLoad: { type: Boolean, default: true }, enableAnimations: { type: Boolean, default: true } }, // ألوان القالب (CSS Variables) colors: { primary: { type: String, default: '#3b82f6', match: /^#[0-9A-Fa-f]{6}$/ }, secondary: { type: String, default: '#8b5cf6', match: /^#[0-9A-Fa-f]{6}$/ }, accent: { type: String, default: '#f59e0b', match: /^#[0-9A-Fa-f]{6}$/ }, background: { type: String, default: '#ffffff', match: /^#[0-9A-Fa-f]{6}$/ }, text: { type: String, default: '#1f2937', match: /^#[0-9A-Fa-f]{6}$/ }, textLight: { type: String, default: '#6b7280', match: /^#[0-9A-Fa-f]{6}$/ }, border: { type: String, default: '#e5e7eb', match: /^#[0-9A-Fa-f]{6}$/ }, success: { type: String, default: '#10b981', match: /^#[0-9A-Fa-f]{6}$/ }, error: { type: String, default: '#ef4444', match: /^#[0-9A-Fa-f]{6}$/ }, warning: { type: String, default: '#f59e0b', match: /^#[0-9A-Fa-f]{6}$/ } }, // خطوط القالب typography: { fontFamily: { type: String, default: 'Inter, sans-serif', enum: ['Inter, sans-serif', 'Poppins, sans-serif', 'Roboto, sans-serif', 'Open Sans, sans-serif', 'Playfair Display, serif'] }, headingFont: { type: String, default: 'Inter, sans-serif' }, baseFontSize: { type: String, default: '16px', match: /^\d+(px|rem)$/ }, enableGoogleFonts: { type: Boolean, default: true } }, // CSS/JS المخصص customAssets: { css: { type: String, default: '', maxlength: 100000 }, js: { type: String, default: '', maxlength: 100000 }, headHtml: { type: String, default: '', maxlength: 10000 }, bodyHtml: { type: String, default: '', maxlength: 10000 } }, // تحسين محركات البحث (SEO للقالب نفسه) seo: { metaTitle: { type: String, maxlength: 70 }, metaDescription: { type: String, maxlength: 160 }, metaKeywords: { type: String, maxlength: 200 }, ogImage: { type: String }, twitterCard: { type: String, default: 'summary_large_image' }, canonicalUrl: { type: String }, structuredData: { type: mongoose.Schema.Types.Mixed, default: {} } }, // إحصائيات استخدام القالب stats: { views: { type: Number, default: 0 }, lastUsed: { type: Date }, templateChanges: { type: Number, default: 0 }, lastChangeAt: { type: Date } }, // حالة القالب isDeleted: { type: Boolean, default: false, index: true }, deletedAt: { type: Date }, createdAt: { type: Date, default: Date.now, immutable: true }, updatedAt: { type: Date, default: Date.now } }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // ============================================ // Indexes لتحسين الأداء // ============================================ storeThemeSchema.index({ userId: 1, isActive: 1 }); storeThemeSchema.index({ 'stats.views': -1 }); storeThemeSchema.index({ updatedAt: -1 }); storeThemeSchema.index({ isDeleted: 1 }); // ============================================ // Virtuals // ============================================ storeThemeSchema.virtual('isCustomTheme').get(function() { return !!this.uploadedTheme?.fileUrl; }); storeThemeSchema.virtual('themeUrl').get(function() { const baseUrl = process.env.BASE_URL || 'https://mgzon-server.hf.space'; return `${baseUrl}/api/store/theme/${this.userId}`; }); // ============================================ // Middleware: قبل الحفظ // ============================================ storeThemeSchema.pre('save', function(next) { this.updatedAt = Date.now(); // تنظيف HTML من الأكواد الضارة if (this.customTemplates.header) { this.customTemplates.header = sanitizeHtml(this.customTemplates.header); } if (this.customTemplates.footer) { this.customTemplates.footer = sanitizeHtml(this.customTemplates.footer); } // توليد hash للملف إذا لم يكن موجوداً if (this.uploadedTheme && this.uploadedTheme.fileUrl && !this.uploadedTheme.fileHash) { this.uploadedTheme.fileHash = createHash('md5') .update(this.uploadedTheme.fileUrl + this.userId) .digest('hex'); } next(); }); // ============================================ // Static Methods // ============================================ /** * جلب القالب النشط لمستخدم معين * @param {string} userId - معرف المستخدم * @returns {Promise} - القالب النشط */ storeThemeSchema.statics.getActiveTheme = async function(userId) { let theme = await this.findOne({ userId, isActive: true, isDeleted: false }); if (!theme) { // إنشاء قالب افتراضي theme = new this({ userId }); await theme.save(); } // تحديث إحصائيات الاستخدام theme.stats.views += 1; theme.stats.lastUsed = new Date(); await theme.save(); return theme; }; /** * نسخ قالب من مستخدم إلى آخر * @param {string} sourceUserId - مصدر القالب * @param {string} targetUserId - المستخدم المستهدف * @returns {Promise} - القالب الجديد */ storeThemeSchema.statics.cloneTheme = async function(sourceUserId, targetUserId) { const sourceTheme = await this.findOne({ userId: sourceUserId, isActive: true }); if (!sourceTheme) { throw new Error('Source theme not found'); } const themeData = sourceTheme.toObject(); delete themeData._id; delete themeData.userId; delete themeData.createdAt; delete themeData.updatedAt; delete themeData.stats; const newTheme = new this({ ...themeData, userId: targetUserId, name: `${sourceTheme.name} (Copy)`, stats: { views: 0, templateChanges: 0 } }); await newTheme.save(); return newTheme; }; /** * إعادة تعيين القالب إلى الإعدادات الافتراضية * @param {string} userId - معرف المستخدم * @returns {Promise} - القالب المعاد تعيينه */ storeThemeSchema.statics.resetToDefault = async function(userId) { await this.deleteOne({ userId }); return this.getActiveTheme(userId); }; // ============================================ // Instance Methods // ============================================ /** * تطبيق القالب على HTML المحتوى * @param {string} content - المحتوى الأصلي * @returns {string} - المحتوى بعد تطبيق القالب */ storeThemeSchema.methods.applyToContent = function(content) { let result = content; // تطبيق الهيدر المخصص if (this.customTemplates.header) { result = this.customTemplates.header + result; } // تطبيق الفوتر المخصص if (this.customTemplates.footer) { result = result + this.customTemplates.footer; } // إضافة CSS المخصص if (this.customAssets.css) { const cssTag = ``; result = result.replace('', `${cssTag}`); } // إضافة JS المخصص if (this.customAssets.js) { const jsTag = `

Loading preview...

Live Preview Mode
Updated
`; res.setHeader('Content-Type', 'text/html'); res.send(html); } catch (error) { console.error('Preview page error:', error); res.status(500).send(` Preview Error

Preview Unavailable

${error.message}

`); } }); // ============================================ // GET /api/store/preview - جلب بيانات المعاينة (للمالك فقط) - النسخة الآمنة // ============================================ app.get('/api/store/preview', authenticateToken, async (req, res) => { try { const userId = req.user.userId; // 1️⃣ جلب إعدادات المتجر let storeSettings = await StoreSettings.findOne({ userId }).lean(); if (!storeSettings) { storeSettings = new StoreSettings({ userId }); await storeSettings.save(); storeSettings = storeSettings.toObject(); } // 2️⃣ جلب المنتجات النشطة let products = []; try { products = await Product.find({ userId, isActive: true }) .select('title description price type images tags salesCount averageRating reviews deliveryTime') .sort({ createdAt: -1 }) .limit(50) .lean(); } catch (err) { console.error('Products fetch error:', err); products = []; } // 3️⃣ جلب الأقسام (مع fallback آمن) let sections = []; try { if (storeSettings.pageBuilder && storeSettings.pageBuilder.sections) { if (Array.isArray(storeSettings.pageBuilder.sections)) { sections = storeSettings.pageBuilder.sections; } else if (typeof storeSettings.pageBuilder.sections === 'object') { sections = Object.values(storeSettings.pageBuilder.sections); } } // لو لسة مفيش أقسام، استخدم الأقسام الافتراضية if (!sections || sections.length === 0) { sections = [ { id: 'hero', type: 'hero', enabled: true, order: 1, content: { title: `Welcome to ${storeSettings.storeName || 'My Store'}`, subtitle: '', buttonText: 'Shop Now', buttonLink: '#products' } }, { id: 'products', type: 'products', enabled: true, order: 2, title: 'All Products', layout: 'grid', limit: 12 } ]; } } catch (err) { console.error('Sections error:', err); sections = [ { id: 'products', type: 'products', enabled: true, order: 1, title: 'Products', layout: 'grid', limit: 12, content: {} } ]; } // 4️⃣ جلب صفحات المتجر let customPages = []; try { customPages = await Page.find({ storeId: userId, isEnabled: true }) .sort({ order: 1 }) .select('slug title type order') .lean(); } catch (err) { customPages = []; } const defaultPages = [ { slug: 'products', title: 'Products', type: 'default', isEnabled: true, order: 1 }, { slug: 'about', title: 'About Us', type: 'default', isEnabled: true, order: 2 }, { slug: 'contact', title: 'Contact', type: 'default', isEnabled: true, order: 3 } ]; const pages = [...defaultPages, ...customPages]; // 5️⃣ جلب بيانات المستخدم let user = null; try { user = await User.findById(userId).select('username profile.nickname profile.avatar email').lean(); } catch (err) { user = { username: 'user', email: '', profile: {} }; } // 6️⃣ تحويل المنتجات لصيغة آمنة const safeProducts = (products || []).map(p => ({ _id: p._id, title: p.title || 'Product', description: p.description || '', price: p.price || 0, type: p.type || 'digital', images: (p.images && Array.isArray(p.images)) ? p.images : [], tags: (p.tags && Array.isArray(p.tags)) ? p.tags : [], salesCount: p.salesCount || 0, averageRating: p.averageRating || 0, reviewsCount: (p.reviews && p.reviews.length) || 0, deliveryTime: p.deliveryTime || null })); // 7️⃣ تحويل الأقسام لصيغة آمنة const safeSections = sections.map(section => ({ id: section.id || section._id || `section_${Date.now()}_${Math.random()}`, type: section.type || 'html', enabled: section.enabled !== false, order: typeof section.order === 'number' ? section.order : 0, title: section.title || null, limit: section.limit || null, layout: section.layout || null, content: section.content || {} })); // 8️⃣ إرسال الرد res.json({ success: true, store: { id: userId, username: user?.username || 'user', nickname: user?.profile?.nickname || null, avatar: user?.profile?.avatar || null, storeName: storeSettings.storeName || user?.profile?.nickname || user?.username || 'My Store', storeLogo: storeSettings.storeLogo || null, storeBanner: storeSettings.storeBanner || null, storeDescription: storeSettings.storeDescription || '', contactEmail: storeSettings.contactEmail || user?.email || '', currency: storeSettings.currency || 'USD', currencySymbol: storeSettings.currencySymbol || '$', enabled: storeSettings.enabled !== false, productViewType: storeSettings.productViewType || 'modal' }, settings: { primaryColor: storeSettings.primaryColor || '#3b82f6', secondaryColor: storeSettings.secondaryColor || '#8b5cf6', fontFamily: storeSettings.fontFamily || 'Inter, sans-serif', borderRadius: storeSettings.borderRadius || 16, shadowType: storeSettings.shadowType || 'md', animationType: storeSettings.animationType || 'lift', glassEffect: storeSettings.glassEffect || false, backgroundType: storeSettings.backgroundType || 'default', backgroundValue: storeSettings.backgroundValue || '', customCss: storeSettings.customCss || '' }, pages: pages.map(page => ({ _id: page._id, slug: page.slug, title: page.title, type: page.type, isEnabled: page.isEnabled !== false, order: page.order || 0 })), products: safeProducts, sections: safeSections }); } catch (error) { console.error('Preview API error:', error); // ✅ في حالة أي خطأ، ارجع بيانات افتراضية res.status(200).json({ success: true, store: { id: req.user.userId, username: 'user', nickname: null, avatar: null, storeName: 'My Store', storeLogo: null, storeBanner: null, storeDescription: '', contactEmail: '', currency: 'USD', currencySymbol: '$', enabled: true, productViewType: 'modal' }, settings: { primaryColor: '#3b82f6', secondaryColor: '#8b5cf6', fontFamily: 'Inter, sans-serif', borderRadius: 16, shadowType: 'md', animationType: 'lift', glassEffect: false, backgroundType: 'default', backgroundValue: '', customCss: '' }, pages: [ { slug: 'products', title: 'Products', type: 'default', isEnabled: true, order: 1 }, { slug: 'about', title: 'About Us', type: 'default', isEnabled: true, order: 2 }, { slug: 'contact', title: 'Contact', type: 'default', isEnabled: true, order: 3 } ], products: [], sections: [ { id: 'products', type: 'products', enabled: true, order: 1, title: 'Products', layout: 'grid', limit: 12, content: {} } ] }); } }); // GET /api/store/preview-data - Modified to support nickname app.get('/api/store/preview-data', authenticateToken, async (req, res) => { try { const userId = req.user.userId; // جلب إعدادات المتجر let storeSettings = await StoreSettings.findOne({ userId }); if (!storeSettings) { storeSettings = new StoreSettings({ userId }); await storeSettings.save(); } // جلب المنتجات النشطة (كل المنتجات، مش محدودة) const products = await Product.find({ userId, isActive: true }).sort({ createdAt: -1 }); // جلب الأقسام مع ترتيبها let sections = storeSettings.pageBuilder?.sections || []; if (sections.length === 0) { sections = getDefaultSections(); } // جلب إعدادات التصميم const designSettings = storeSettings.designSettings || {}; // جلب بيانات المستخدم const user = await User.findById(userId).select('profile.nickname username'); res.json({ success: true, settings: { storeName: storeSettings.storeName || user?.profile?.nickname || user?.username || 'My Store', storeDescription: storeSettings.storeDescription || '', storeLogo: storeSettings.storeLogo || '', storeBanner: storeSettings.storeBanner || '', primaryColor: designSettings.primaryColor || storeSettings.primaryColor || '#3b82f6', secondaryColor: designSettings.secondaryColor || storeSettings.secondaryColor || '#8b5cf6', fontFamily: designSettings.fontFamily || storeSettings.fontFamily || 'Inter, sans-serif', borderRadius: designSettings.borderRadius || storeSettings.borderRadius || 16, shadowType: designSettings.shadowType || storeSettings.shadowType || 'md', animationType: designSettings.animationType || storeSettings.animationType || 'lift', glassEffect: designSettings.glassEffect || storeSettings.glassEffect || false, backgroundType: designSettings.backgroundType || storeSettings.backgroundType || 'default', backgroundValue: designSettings.backgroundValue || storeSettings.backgroundValue || '', zoomLevel: designSettings.zoomLevel || 100, customCss: designSettings.customCss || '', currency: storeSettings.currency || 'USD', currencySymbol: storeSettings.currencySymbol || '$', storeEnabled: storeSettings.enabled || false, nickname: user?.profile?.nickname || user?.username }, products: products.map(p => ({ _id: p._id, title: p.title, description: p.description, price: p.price, type: p.type, images: p.images || [], isActive: p.isActive, salesCount: p.salesCount || 0 })), sections: sections.map(s => ({ ...s, enabled: s.enabled !== false })) }); } catch (error) { console.error('Preview data error:', error); res.status(500).json({ error: 'Failed to load preview data' }); } }); function getDefaultSections() { return [ { id: 'hero', type: 'hero', enabled: true, order: 1, content: { title: 'Welcome to my store', subtitle: '', buttonText: 'Shop Now', buttonLink: '#products' } }, { id: 'featured', type: 'featured', enabled: true, order: 2, title: 'Featured Products', limit: 6 }, { id: 'products', type: 'products', enabled: true, order: 3, title: 'All Products', layout: 'grid', limit: 12 }, { id: 'categories', type: 'categories', enabled: true, order: 4, title: 'Categories' }, { id: 'testimonials', type: 'testimonials', enabled: false, order: 5, title: 'What Customers Say', limit: 6, content: { items: [ { text: "Amazing products! Highly recommend this store.", author: "Sarah Johnson", role: "Verified Buyer", rating: 5 }, { text: "Great quality and fast delivery. Will shop again!", author: "Michael Chen", role: "Repeat Customer", rating: 5 }, { text: "The support team is very helpful. 10/10 experience.", author: "Emily Davis", role: "Happy Customer", rating: 5 } ] } }, { id: 'faq', type: 'faq', enabled: false, order: 6, title: 'Frequently Asked Questions', content: { items: [ { question: "How do I download my purchase?", answer: "After payment confirmation, you'll receive a download link via email and in your order history." }, { question: "What is your refund policy?", answer: "We offer a 30-day money-back guarantee on all digital products." }, { question: "How long does delivery take?", answer: "For digital products, delivery is instant. For services, delivery time varies." } ] } }, { id: 'contact', type: 'contact', enabled: false, order: 7, title: 'Contact Me', content: { email: '', phone: '' } } ]; } // POST /api/store/preview/update - تحديث بيانات المعاينة (لإرسالها للـ iframe) app.post('/api/store/preview/update', authenticateToken, async (req, res) => { try { const { settings, sections } = req.body; // ✅ بناء كائن التحديث بشكل صحيح مع الحقول الجديدة const updateData = {}; // تحديث إعدادات المتجر الأساسية if (settings) { updateData.storeName = settings.storeName || ''; updateData.storeDescription = settings.storeDescription || ''; updateData.storeLogo = settings.storeLogo || ''; updateData.storeBanner = settings.storeBanner || ''; updateData.currency = settings.currency || 'USD'; // رمز العملة const currencySymbols = { 'USD': '$', 'EUR': '€', 'GBP': '£', 'EGP': 'ج.م', 'SAR': '﷼', 'AED': 'د.إ' }; updateData.currencySymbol = currencySymbols[settings.currency] || '$'; // 🎨 إعدادات التصميم (الحقول الجديدة) if (settings.primaryColor !== undefined) updateData.primaryColor = settings.primaryColor; if (settings.secondaryColor !== undefined) updateData.secondaryColor = settings.secondaryColor; if (settings.fontFamily !== undefined) updateData.fontFamily = settings.fontFamily; if (settings.borderRadius !== undefined) updateData.borderRadius = settings.borderRadius; if (settings.shadowType !== undefined) updateData.shadowType = settings.shadowType; if (settings.animationType !== undefined) updateData.animationType = settings.animationType; if (settings.glassEffect !== undefined) updateData.glassEffect = settings.glassEffect; if (settings.backgroundType !== undefined) updateData.backgroundType = settings.backgroundType; if (settings.backgroundValue !== undefined) updateData.backgroundValue = settings.backgroundValue; if (settings.customCss !== undefined) updateData.customCss = settings.customCss; // zoomLevel - إذا كنت تريد إضافته للموديل if (settings.zoomLevel !== undefined) updateData.zoomLevel = settings.zoomLevel; // طرق الدفع إذا وجدت if (settings.paymentMethods !== undefined) updateData.paymentMethods = settings.paymentMethods; // روابط التواصل الاجتماعي if (settings.socialLinks !== undefined) updateData.socialLinks = settings.socialLinks; } // تحديث الأقسام if (sections !== undefined) { updateData['pageBuilder.sections'] = sections; } updateData.updatedAt = Date.now(); // ✅ تنفيذ التحديث const updatedSettings = await StoreSettings.findOneAndUpdate( { userId: req.user.userId }, updateData, { upsert: true, new: true } ); console.log('✅ Preview settings updated for user:', req.user.userId); console.log('📦 Updated fields:', Object.keys(updateData)); res.json({ success: true, settings: updatedSettings, message: 'Preview settings saved successfully' }); } catch (error) { console.error('Preview update error:', error); res.status(500).json({ error: 'Failed to update preview: ' + error.message }); } }); // ============================================ // 🎯 STORE PREVIEW ENDPOINTS - FIX // ============================================ // GET /api/store/preview/products - جلب جميع منتجات المتجر للمعاينة app.get('/api/store/preview/products', authenticateToken, async (req, res) => { try { const products = await Product.find({ userId: req.user.userId, isActive: true }).sort({ createdAt: -1 }).limit(50); res.json({ success: true, products: products.map(p => ({ _id: p._id, title: p.title, description: p.description, price: p.price, type: p.type, images: p.images || [], isActive: p.isActive, salesCount: p.salesCount || 0 })) }); } catch (error) { console.error('Error fetching preview products:', error); res.status(500).json({ error: 'Failed to fetch products' }); } }); // GET /api/store/preview/settings - جلب إعدادات المتجر للمعاينة app.get('/api/store/preview/settings', authenticateToken, async (req, res) => { try { let storeSettings = await StoreSettings.findOne({ userId: req.user.userId }); if (!storeSettings) { storeSettings = new StoreSettings({ userId: req.user.userId }); await storeSettings.save(); } const designSettings = storeSettings.designSettings || {}; res.json({ success: true, settings: { storeName: storeSettings.storeName || '', storeDescription: storeSettings.storeDescription || '', storeLogo: storeSettings.storeLogo || '', storeBanner: storeSettings.storeBanner || '', primaryColor: designSettings.primaryColor || storeSettings.primaryColor || '#3b82f6', secondaryColor: designSettings.secondaryColor || storeSettings.secondaryColor || '#8b5cf6', fontFamily: designSettings.fontFamily || storeSettings.fontFamily || 'Inter, sans-serif', borderRadius: designSettings.borderRadius || storeSettings.borderRadius || 16, shadowType: designSettings.shadowType || storeSettings.shadowType || 'md', animationType: designSettings.animationType || storeSettings.animationType || 'lift', glassEffect: designSettings.glassEffect || storeSettings.glassEffect || false, backgroundType: designSettings.backgroundType || storeSettings.backgroundType || 'default', backgroundValue: designSettings.backgroundValue || storeSettings.backgroundValue || '', zoomLevel: designSettings.zoomLevel || 100, customCss: designSettings.customCss || '', currency: storeSettings.currency || 'USD', currencySymbol: storeSettings.currencySymbol || '$', storeEnabled: storeSettings.enabled || false } }); } catch (error) { console.error('Error fetching preview settings:', error); res.status(500).json({ error: 'Failed to load settings' }); } }); // GET /api/store/preview/sections - جلب أقسام المتجر للمعاينة app.get('/api/store/preview/sections', authenticateToken, async (req, res) => { try { let storeSettings = await StoreSettings.findOne({ userId: req.user.userId }); if (!storeSettings) { storeSettings = new StoreSettings({ userId: req.user.userId }); await storeSettings.save(); } let sections = storeSettings.pageBuilder?.sections || []; if (sections.length === 0) { sections = [ { id: 'hero', type: 'hero', enabled: true, order: 1, content: { title: 'Welcome to my store', subtitle: '', buttonText: 'Shop Now', buttonLink: '#products' } }, { id: 'featured', type: 'featured', enabled: true, order: 2, title: 'Featured Products', limit: 6 }, { id: 'products', type: 'products', enabled: true, order: 3, title: 'All Products', layout: 'grid' }, { id: 'categories', type: 'categories', enabled: true, order: 4, title: 'Categories' } ]; } res.json({ success: true, sections }); } catch (error) { console.error('Error fetching preview sections:', error); res.status(500).json({ error: 'Failed to load sections' }); } }); // ============================================ // 🎯 PRODUCT PREVIEW PAGE // ============================================ // ============================================ // GET /api/store/preview/pages - جلب صفحات المتجر للمعاينة (للمالك فقط) // ============================================ app.get('/api/store/preview/pages', authenticateToken, async (req, res) => { try { const userId = req.user.userId; // الصفحات الافتراضية const defaultPages = [ { slug: 'products', title: 'Products', type: 'default', isEnabled: true, order: 1 }, { slug: 'about', title: 'About Us', type: 'default', isEnabled: true, order: 2 }, { slug: 'contact', title: 'Contact', type: 'default', isEnabled: true, order: 3 } ]; // الصفحات المخصصة من قاعدة البيانات const customPages = await Page.find({ storeId: userId, isEnabled: true }) .sort({ order: 1 }) .select('slug title type order'); res.json({ success: true, pages: [...defaultPages, ...customPages] }); } catch (error) { console.error('Error fetching preview pages:', error); res.status(500).json({ error: 'Failed to fetch pages' }); } }); // ============================================ // GET /api/store/preview/follow-status - حالة متابعة المتجر للمعاينة (للمالك فقط) // ============================================ app.get('/api/store/preview/follow-status', authenticateToken, async (req, res) => { try { const userId = req.user.userId; const followersCount = await StoreFollow.countDocuments({ storeId: userId }); res.json({ success: true, followersCount, isFollowing: false // لا يمكن للمالك متابعة متجره بنفسه }); } catch (error) { console.error('Error fetching preview follow status:', error); res.json({ success: true, followersCount: 0, isFollowing: false }); } }); // ============================================ // POST /api/store/reset-template - إزالة القالب المطبق والعودة للوضع الافتراضي // ============================================ app.post('/api/store/reset-template', authenticateToken, async (req, res) => { try { const userId = req.user.userId; // إزالة appliedTemplate من StoreSettings await StoreSettings.findOneAndUpdate( { userId }, { $unset: { appliedTemplate: 1 }, $set: { updatedAt: Date.now() } } ); // إعادة تعيين القالب النشط إلى الافتراضي await StoreTheme.findOneAndUpdate( { userId }, { $set: { isActive: false, updatedAt: Date.now() } } ); // إنشاء قالب افتراضي جديد const defaultTheme = new StoreTheme({ userId }); await defaultTheme.save(); console.log(`✅ Template reset for user ${userId}`); res.json({ success: true, message: 'Template reset successfully. Store restored to default layout.' }); } catch (error) { console.error('Error resetting template:', error); res.status(500).json({ error: 'Failed to reset template' }); } }); // ============================================ // GET /api/store/preview/page/:slug - جلب صفحة معاينة (للمالك فقط) // ============================================ app.get('/api/store/preview/page/:slug', authenticateToken, async (req, res) => { try { const userId = req.user.userId; const { slug } = req.params; // جلب إعدادات المتجر const storeSettings = await StoreSettings.findOne({ userId }); if (!storeSettings) { return res.status(404).json({ error: 'Store not found' }); } // جلب بيانات المستخدم const user = await User.findById(userId).select('username profile.nickname profile.avatar email'); // الصفحات الافتراضية if (slug === 'about') { return res.json({ success: true, page: { slug: 'about', title: 'About Us', content: storeSettings?.storeDescription || 'Welcome to our store! We offer the best products and services.', type: 'default', seo: { title: `About ${storeSettings?.storeName || user.profile?.nickname || user.username} | MGZon Store`, description: storeSettings?.storeDescription?.substring(0, 160) || 'Learn more about our store and what we offer.' } } }); } if (slug === 'contact') { return res.json({ success: true, page: { slug: 'contact', title: 'Contact Us', content: `
${storeSettings?.socialLinks?.instagram ? ` ` : ''} ${storeSettings?.socialLinks?.facebook ? `

Facebook

Facebook Page
` : ''} ${storeSettings?.socialLinks?.twitter ? ` ` : ''}
`, type: 'default', seo: { title: `Contact ${storeSettings?.storeName || user.profile?.nickname || user.username} | MGZon Store`, description: `Get in touch with ${storeSettings?.storeName || user.profile?.nickname || user.username}. Send us a message and we'll get back to you soon.` } } }); } // ✅ الصفحات المخصصة (اللي المستخدم يضيفها) const page = await Page.findOne({ storeId: userId, slug, isEnabled: true }); if (!page) { return res.status(404).json({ error: 'Page not found' }); } res.json({ success: true, page: { slug: page.slug, title: page.title, content: page.content || '

No content available.

', type: page.type, seo: page.seo || {} } }); } catch (error) { console.error('Error fetching preview page:', error); res.status(500).json({ error: 'Failed to fetch page' }); } }); // GET /shop/preview/product/:productId - صفحة معاينة المنتج app.get('/shop/preview/product/:productId', authenticateToken, async (req, res) => { try { const productId = req.params.productId; // جلب المنتج من قاعدة البيانات const product = await Product.findOne({ _id: productId, userId: req.user.userId }); if (!product) { return res.status(404).send(` Product Not Found

Product Not Found

The product you're looking for doesn't exist.

`); } // جلب إعدادات المتجر let storeSettings = await StoreSettings.findOne({ userId: req.user.userId }); if (!storeSettings) { storeSettings = new StoreSettings({ userId: req.user.userId }); await storeSettings.save(); } const designSettings = storeSettings.designSettings || {}; const html = ` Preview: ${escapeHtml(product.title)} - MGZon Store
${escapeHtml(product.title)}
${product.images && product.images.length > 1 ? `
${product.images.map((img, idx) => `
`).join('')}
` : ''}
${product.type === 'digital' ? 'Digital Product' : product.type === 'project' ? 'Project' : 'Service'}

${escapeHtml(product.title)}

${Array(5).fill().map((_, i) => `` ).join('')}
(${product.reviews?.length || 0} reviews)
${product.price} ${storeSettings.currencySymbol || '$'} ${product.oldPrice ? ` ${product.oldPrice} ${storeSettings.currencySymbol || '$'} Save ${Math.round(((product.oldPrice - product.price) / product.oldPrice) * 100)}% ` : ''}
${product.type === 'digital' ? `

Instant delivery after payment

` : product.type === 'service' && product.deliveryTime ? `

Delivery: ${escapeHtml(product.deliveryTime)}

` : ''}

Description

${escapeHtml(product.description)}
${product.tags && product.tags.length > 0 ? `

Tags

${product.tags.map(tag => ` #${escapeHtml(tag)} `).join('')}
` : ''} ${product.type === 'service' ? `
Available
` : ''}

Sold by ${escapeHtml(storeSettings.storeName || 'Store Owner')}

100% secure payment | Money-back guarantee

Product Preview Mode
`; res.setHeader('Content-Type', 'text/html'); res.send(html); } catch (error) { console.error('Product preview error:', error); res.status(500).send(` Preview Error

Preview Error

${escapeHtml(error.message)}

`); } }); // GET /api/store/preview/product/:productId - جلب منتج واحد للمعاينة app.get('/api/store/preview/product/:productId', authenticateToken, async (req, res) => { try { const product = await Product.findOne({ _id: req.params.productId, userId: req.user.userId }); if (!product) { return res.status(404).json({ error: 'Product not found' }); } res.json({ success: true, product: { _id: product._id, title: product.title, description: product.description, price: product.price, type: product.type, images: product.images || [], tags: product.tags || [], deliveryTime: product.deliveryTime, fileUrl: product.fileUrl, averageRating: product.averageRating || 0, reviewsCount: product.reviews?.length || 0, salesCount: product.salesCount || 0, isActive: product.isActive } }); } catch (error) { console.error('Error fetching product preview:', error); res.status(500).json({ error: 'Failed to load product' }); } }); // ============================================ // 🏪 PUBLIC STORE ENDPOINTS (للمستخدمين العاديين) // ============================================ // ========== APIs مخصصة لصاحب المتجر (مع توكن) ========== // GET /api/store/my/pages - جلب صفحات المتجر (للمالك) مع كامل البيانات app.get('/api/store/my/pages', authenticateToken, async (req, res) => { try { const userId = req.user.userId; const customPages = await Page.find({ storeId: userId }) .sort({ order: 1 }); const defaultPages = [ { slug: 'products', title: 'Products', type: 'default', isDefault: true, isEnabled: true, order: 1, canEdit: false, canDelete: false, canToggle: false, visibility: 'public', content: '', css: '', js: '' }, { slug: 'about', title: 'About Us', type: 'default', isDefault: true, isEnabled: true, order: 2, canEdit: true, canDelete: false, canToggle: true, visibility: 'public', content: '', css: '', js: '' }, { slug: 'contact', title: 'Contact', type: 'default', isDefault: true, isEnabled: true, order: 3, canEdit: true, canDelete: false, canToggle: true, visibility: 'public', content: '', css: '', js: '' } ]; let allPages = [...defaultPages]; for (const customPage of customPages) { const existingIndex = allPages.findIndex(p => p.slug === customPage.slug); const hasShortcodes = customPage.content && customPage.content.includes('['); if (existingIndex !== -1) { allPages[existingIndex] = { ...allPages[existingIndex], _id: customPage._id, content: customPage.content || '', css: customPage.css || '', js: customPage.js || '', seo: customPage.seo || {}, uploadedFile: customPage.uploadedFile, canEdit: true, canDelete: false, canToggle: true, hasShortcodes: hasShortcodes }; } else { allPages.push({ _id: customPage._id, slug: customPage.slug, title: customPage.title, content: customPage.content || '', css: customPage.css || '', js: customPage.js || '', type: customPage.type, isEnabled: customPage.isEnabled, order: customPage.order, seo: customPage.seo || {}, uploadedFile: customPage.uploadedFile, canEdit: true, canDelete: true, canToggle: true, hasShortcodes: hasShortcodes }); } } allPages.sort((a, b) => (a.order || 0) - (b.order || 0)); const storeSettings = await StoreSettings.findOne({ userId }); const user = await User.findById(userId).select('profile.nickname username'); const nickname = storeSettings?.storeName || user?.profile?.nickname || user?.username; // ✅ جلب الـ Shortcodes المتاحة للمستخدم const availableShortcodes = await StoreShortcode.find({ userId: userId, isActive: true }).select('name displayName description parameters'); const builtinShortcodes = StoreShortcode.getBuiltinShortcodes(); res.json({ success: true, pages: allPages, storeNickname: nickname || 'store', shortcodes: { available: availableShortcodes.map(sc => ({ name: sc.name, displayName: sc.displayName, description: sc.description, example: `[${sc.name}${sc.parameters?.length ? ' limit="6"' : ''}]`, parameters: sc.parameters })), builtin: builtinShortcodes.map(sc => ({ name: sc.name, displayName: sc.displayName, description: sc.description, example: `[${sc.name}${sc.parameters?.length ? ' limit="6"' : ''}]` })) } }); } catch (error) { console.error('Error fetching my pages:', error); res.status(500).json({ error: 'Failed to fetch pages' }); } }); // POST /api/store/my/pages - إنشاء صفحة جديدة app.post('/api/store/my/pages', authenticateToken, async (req, res) => { try { const { slug, title, content, seo, order } = req.body; // التحقق من وجود slug if (!slug || !title) { return res.status(400).json({ error: 'Slug and title are required' }); } // التحقق من عدم وجود slug مكرر const existingPage = await Page.findOne({ storeId: req.user.userId, slug }); if (existingPage) { return res.status(400).json({ error: 'Page with this slug already exists' }); } const page = new Page({ storeId: req.user.userId, slug: slug.toLowerCase().replace(/[^a-z0-9-]/g, '-'), title, content: content || '', type: 'custom', isEnabled: true, order: order || 0, seo: seo || {} }); await page.save(); res.status(201).json({ success: true, page }); } catch (error) { console.error('Error creating page:', error); res.status(500).json({ error: 'Failed to create page' }); } }); // ============================================ // POST /api/store/my/pages/preview - معاينة صفحة (كاملة ومتطابقة مع المتجر) // ============================================ app.post('/api/store/my/pages/preview', authenticateToken, async (req, res) => { try { const { title, content, css, js } = req.body; // ✅ جلب إعدادات المتجر للمستخدم الحالي const storeSettings = await StoreSettings.findOne({ userId: req.user.userId }); const user = await User.findById(req.user.userId); const nickname = user?.profile?.nickname || user?.username || 'store'; const storeName = storeSettings?.storeName || nickname; const storeLogo = storeSettings?.storeLogo || ''; const storeBanner = storeSettings?.storeBanner || ''; // ✅ إعدادات التصميم const primaryColor = storeSettings?.primaryColor || '#3b82f6'; const secondaryColor = storeSettings?.secondaryColor || '#8b5cf6'; const fontFamily = storeSettings?.fontFamily || 'Inter, sans-serif'; const borderRadius = storeSettings?.borderRadius || 16; const shadowType = storeSettings?.shadowType || 'md'; const animationType = storeSettings?.animationType || 'lift'; const glassEffect = storeSettings?.glassEffect || false; const backgroundType = storeSettings?.backgroundType || 'default'; const backgroundValue = storeSettings?.backgroundValue || ''; const customCss = storeSettings?.customCss || ''; // ✅ تنظيف المحتوى من الأكواد الضارة const cleanContent = content?.replace(/)<[^<]*)*<\/script>/gi, '') || '

No content yet.

'; const cleanCss = css?.replace(/expression\(/gi, '').replace(/behavior\s*:/gi, '') || ''; const cleanJs = js?.replace(/document\.write/gi, '').replace(/eval\(/gi, '') || ''; // ✅ بناء خلفية الصفحة let backgroundStyle = ''; if (backgroundType === 'gradient' && backgroundValue) { backgroundStyle = `background: ${backgroundValue}; background-attachment: fixed;`; } else if (backgroundType === 'image' && backgroundValue) { backgroundStyle = `background: url(${backgroundValue}) center/cover fixed;`; } else if (backgroundType === 'dots') { backgroundStyle = `background-image: radial-gradient(circle, #cbd5e1 1px, transparent 1px); background-size: 24px 24px;`; } else if (backgroundType === 'grid') { backgroundStyle = `background-image: linear-gradient(to right, #e5e7eb 1px, transparent 1px), linear-gradient(to bottom, #e5e7eb 1px, transparent 1px); background-size: 24px 24px;`; } else if (backgroundType === 'cross') { backgroundStyle = `background-image: linear-gradient(45deg, #cbd5e1 1px, transparent 1px), linear-gradient(-45deg, #cbd5e1 1px, transparent 1px); background-size: 24px 24px;`; } else { backgroundStyle = 'background: #f9fafb;'; } // ✅ تأثير الظل const shadowMap = { none: 'none', sm: '0 1px 2px 0 rgba(0,0,0,0.05)', md: '0 4px 6px -1px rgba(0,0,0,0.1)', lg: '0 10px 15px -3px rgba(0,0,0,0.1)', xl: '0 20px 25px -5px rgba(0,0,0,0.1)' }; const shadowValue = shadowMap[shadowType] || shadowMap.md; // ✅ تأثير التحويم const hoverClass = animationType === 'lift' ? 'hover-lift' : animationType === 'scale' ? 'hover-scale' : animationType === 'glow' ? 'hover-glow' : ''; const glassClass = glassEffect ? 'glass-effect' : ''; const html = ` ${escapeHtml(title || 'Preview')} - ${escapeHtml(storeName)}
${storeBanner ? `
Store Banner
` : '
'}
${cleanContent}

© ${new Date().getFullYear()} ${escapeHtml(storeName)}. All rights reserved.

Powered by MGZon

`; res.setHeader('Content-Type', 'text/html'); res.send(html); } catch (error) { console.error('Preview generation error:', error); res.status(500).json({ error: 'Failed to generate preview: ' + error.message }); } }); // ============================================ // 4. Page Statistics // ============================================ app.get('/api/store/my/pages/stats', authenticateToken, async (req, res) => { try { const pages = await Page.find({ storeId: req.user.userId }) .select('title slug type stats content') .sort({ 'stats.views': -1 }); const totalViews = pages.reduce((sum, p) => sum + (p.stats?.views || 0), 0); // ✅ حساب عدد الصفحات التي تحتوي على Shortcodes let pagesWithShortcodes = 0; let totalShortcodesUsed = 0; for (const page of pages) { if (page.content && page.content.includes('[')) { pagesWithShortcodes++; // حساب عدد الـ Shortcodes في الصفحة const matches = page.content.match(/\[(\w+)(?:\s+[^\]]*)?\]/g); if (matches) { totalShortcodesUsed += matches.length; } } } // ✅ جلب أكثر الـ Shortcodes استخداماً const shortcodeStats = await StoreShortcode.aggregate([ { $match: { userId: req.user.userId } }, { $project: { name: 1, 'stats.usageCount': 1 } }, { $sort: { 'stats.usageCount': -1 } }, { $limit: 5 } ]); res.json({ success: true, stats: { totalPages: pages.length, totalViews: totalViews, pagesWithShortcodes: pagesWithShortcodes, totalShortcodesUsed: totalShortcodesUsed, mostViewed: pages.slice(0, 5).map(p => ({ _id: p._id, title: p.title, slug: p.slug, type: p.type, views: p.stats?.views || 0, hasShortcodes: !!(p.content && p.content.includes('[')) })), pages: pages.map(p => ({ _id: p._id, title: p.title, slug: p.slug, type: p.type, views: p.stats?.views || 0, uniqueViews: p.stats?.uniqueViews || 0, hasShortcodes: !!(p.content && p.content.includes('[')) })), topShortcodes: shortcodeStats } }); } catch (error) { console.error('Error fetching page stats:', error); res.status(500).json({ error: 'Failed to fetch stats' }); } }); // ============================================ // POST /api/store/my/pages/upload - رفع ملف HTML (محدث بالكامل) // يدعم: استخراج SEO، CSS، JS، Shortcodes، ومعالجة الصور // ============================================ app.post('/api/store/my/pages/upload', authenticateToken, uploadHTML.single('file'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } const file = req.file; let fileContent = file.buffer.toString('utf8'); // ✅ التحقق من حجم الملف (حد أقصى 5MB للـ HTML) if (file.size > 5 * 1024 * 1024) { return res.status(400).json({ error: 'File size must be less than 5MB' }); } // ============================================ // ✅ 1. استخراج title (حد أقصى 100 حرف) // ============================================ let title = file.originalname.replace(/\.(html|htm)$/i, ''); const titleMatch = fileContent.match(/(.*?)<\/title>/i); if (titleMatch) { title = titleMatch[1].trim(); } if (title.length > 100) { title = title.substring(0, 97) + '...'; } // ============================================ // ✅ 2. استخراج meta description // ============================================ let description = ''; const descMatch = fileContent.match(/<meta\s+name=["']description["']\s+content=["'](.*?)["']/i); if (descMatch) { description = descMatch[1].trim(); if (description.length > 160) { description = description.substring(0, 157) + '...'; } } // ============================================ // ✅ 3. استخراج meta keywords // ============================================ let keywords = ''; const keywordsMatch = fileContent.match(/<meta\s+name=["']keywords["']\s+content=["'](.*?)["']/i); if (keywordsMatch) { keywords = keywordsMatch[1].trim(); if (keywords.length > 200) { keywords = keywords.substring(0, 197) + '...'; } } // ============================================ // ✅ 4. استخراج Open Graph meta tags // ============================================ let ogImage = ''; let ogTitle = ''; let ogDescription = ''; const ogImageMatch = fileContent.match(/<meta\s+property=["']og:image["']\s+content=["'](.*?)["']/i); if (ogImageMatch) ogImage = ogImageMatch[1].trim(); const ogTitleMatch = fileContent.match(/<meta\s+property=["']og:title["']\s+content=["'](.*?)["']/i); if (ogTitleMatch) ogTitle = ogTitleMatch[1].trim(); const ogDescMatch = fileContent.match(/<meta\s+property=["']og:description["']\s+content=["'](.*?)["']/i); if (ogDescMatch) ogDescription = ogDescMatch[1].trim(); // ============================================ // ✅ 5. استخراج CSS // ============================================ let extractedCss = ''; // من <style> tags const styleMatches = fileContent.match(/<style[^>]*>([\s\S]*?)<\/style>/gi); if (styleMatches) { extractedCss = styleMatches.map(s => s.replace(/<\/?style[^>]*>/gi, '')).join('\n'); } // من <link rel="stylesheet"> const linkMatches = fileContent.match(/<link[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>/gi); if (linkMatches) { for (const link of linkMatches) { const hrefMatch = link.match(/href=["']([^"']+)["']/i); if (hrefMatch && !hrefMatch[1].startsWith('http')) { // روابط داخلية فقط، نتجاهل الـ CDN extractedCss += `/* External: ${hrefMatch[1]} */\n`; } } } // ============================================ // ✅ 6. استخراج JavaScript // ============================================ let extractedJs = ''; const scriptMatches = fileContent.match(/<script[^>]*>([\s\S]*?)<\/script>/gi); if (scriptMatches) { for (const script of scriptMatches) { // تجاهل script src (روابط خارجية) if (!script.includes('src=')) { let jsContent = script.replace(/<\/?script[^>]*>/gi, ''); // ✅ إزالة الكود الخطير jsContent = jsContent.replace(/document\.write/gi, ''); jsContent = jsContent.replace(/eval\(/gi, ''); jsContent = jsContent.replace(/Function\(/gi, ''); extractedJs += jsContent + '\n'; } } } // ============================================ // ✅ 7. استخراج HTML content (من body) // ============================================ let extractedHtml = ''; const bodyMatch = fileContent.match(/<body[^>]*>([\s\S]*?)<\/body>/i); if (bodyMatch) { extractedHtml = bodyMatch[1]; } else { const htmlMatch = fileContent.match(/<html[^>]*>([\s\S]*?)<\/html>/i); if (htmlMatch) { let fullHtml = htmlMatch[1]; extractedHtml = fullHtml.replace(/<head[^>]*>[\s\S]*?<\/head>/i, ''); } else { extractedHtml = fileContent; } } // ✅ تنقية المحتوى من الـ XSS والأكواد الضارة extractedHtml = extractedHtml .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') .replace(/<iframe\b[^>]*>/gi, '') .replace(/<object\b[^>]*>/gi, '') .replace(/<embed\b[^>]*>/gi, '') .replace(/\s(on\w+)=("[^"]*"|'[^']*'|[^\s>]+)/gi, '') .replace(/javascript:/gi, '') .replace(/vbscript:/gi, ''); // ✅ كشف وجود Shortcodes في المحتوى const hasShortcodes = /\[(\w+)(?:\s+[^\]]*)?\]/.test(extractedHtml); const shortcodesFound = []; const shortcodeMatches = extractedHtml.match(/\[(\w+)(?:\s+[^\]]*)?\]/g); if (shortcodeMatches) { const uniqueShortcodes = [...new Set(shortcodeMatches.map(m => { const match = m.match(/\[(\w+)/); return match ? match[1] : null; }).filter(Boolean))]; shortcodesFound.push(...uniqueShortcodes); } // ============================================ // ✅ 8. استخراج slug من اسم الملف // ============================================ let slug = file.originalname .replace(/\.(html|htm)$/i, '') .toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); if (!slug) slug = 'page'; if (slug.length > 50) slug = slug.substring(0, 50); // ✅ التأكد من عدم وجود slug مكرر let finalSlug = slug; let counter = 1; while (await Page.findOne({ storeId: req.user.userId, slug: finalSlug })) { finalSlug = `${slug}-${counter}`; counter++; } // ============================================ // ✅ 9. معالجة الصور في المحتوى (تحويل الروابط النسبية إلى مطلقة) // ============================================ // نترك الصور كما هي لأنها ستُعرض من نفس المجال // ============================================ // ✅ 10. حفظ الصفحة في قاعدة البيانات // ============================================ const page = new Page({ storeId: req.user.userId, slug: finalSlug, title: title, content: extractedHtml, css: extractedCss, js: extractedJs, type: 'uploaded', isEnabled: true, order: await Page.countDocuments({ storeId: req.user.userId }), uploadedFile: { filename: finalSlug, originalName: file.originalname, fileSize: file.size, mimeType: file.mimetype, uploadedAt: new Date() }, seo: { title: title.length > 70 ? title.substring(0, 67) + '...' : title, description: description, keywords: keywords, ogImage: ogImage, noindex: false, nofollow: false } }); await page.save(); // ✅ تهيئة Shortcodes للمستخدم إذا لم تكن موجودة if (hasShortcodes) { await ShortcodeService.initializeUserShortcodes(req.user.userId); } console.log(`✅ Page uploaded: ${finalSlug} by user ${req.user.userId}`); if (hasShortcodes) { console.log(`📝 Shortcodes detected: ${shortcodesFound.join(', ')}`); } // ============================================ // ✅ 11. إرسال الرد مع معلومات إضافية // ============================================ res.status(201).json({ success: true, page: { _id: page._id, slug: page.slug, title: page.title, type: page.type, hasShortcodes: hasShortcodes, shortcodesFound: shortcodesFound, contentLength: extractedHtml.length, cssLength: extractedCss.length, jsLength: extractedJs.length }, message: `${file.originalname} uploaded successfully!${hasShortcodes ? ' Shortcodes detected and will be processed dynamically.' : ''}`, tips: hasShortcodes ? [ '📝 Shortcodes detected! They will be processed when the page is viewed.', '💡 You can use shortcodes like [products limit="6"] to display products dynamically.', '🔧 Edit the page to see all available shortcodes.' ] : [ '💡 You can add shortcodes like [products limit="6"] to display products dynamically.', '🔧 Go to the page editor to add interactive elements.' ] }); } catch (error) { console.error('Error uploading page:', error); // ✅ معالجة أخطاء التحقق من الصحة if (error.name === 'ValidationError') { const errors = Object.values(error.errors).map(e => e.message); return res.status(400).json({ error: 'Validation failed', details: errors, message: errors[0] }); } // ✅ معالجة أخطاء MongoDB if (error.code === 11000) { return res.status(409).json({ error: 'Duplicate page slug', message: 'A page with this URL already exists. Please rename your file.' }); } res.status(500).json({ error: 'Failed to upload page', message: error.message, details: process.env.NODE_ENV === 'development' ? error.stack : undefined }); } }); // POST /api/store/my/pages/upload-zip - رفع ملف ZIP ذكي (Optimized) app.post( '/api/store/my/pages/upload-zip', authenticateToken, uploadHTML.single('file'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } const file = req.file; if (!file.originalname.endsWith('.zip')) { return res.status(400).json({ error: 'Only ZIP files are allowed' }); } const zip = new AdmZip(file.buffer); const zipEntries = zip.getEntries(); const createdPages = []; const extractedFiles = { htmlFiles: [], cssFiles: [], jsFiles: [], imageFiles: [], otherFiles: [] }; // ============================================ // 1️⃣ تصنيف الملفات // ============================================ for (const entry of zipEntries) { if (entry.isDirectory) continue; const fileName = entry.entryName; const ext = fileName.split('.').pop().toLowerCase(); const content = entry.getData(); if (ext === 'html' || ext === 'htm') { extractedFiles.htmlFiles.push({ name: fileName, content: content.toString('utf8'), size: content.length }); } else if (ext === 'css') { extractedFiles.cssFiles.push({ name: fileName, content: content.toString('utf8'), size: content.length }); } else if (ext === 'js') { extractedFiles.jsFiles.push({ name: fileName, content: content.toString('utf8'), size: content.length }); } else if (['jpg','jpeg','png','gif','svg','webp'].includes(ext)) { extractedFiles.imageFiles.push({ name: fileName, content, ext }); } } // ============================================ // 🔥 تجهيز slugs مرة واحدة (مهم جداً للأداء) // ============================================ const existingPages = await Page.find( { storeId: req.user.userId }, { slug: 1 } ); const existingSlugs = new Set(existingPages.map(p => p.slug)); const generateUniqueSlug = (base) => { let slug = base; let counter = 1; while (existingSlugs.has(slug)) { slug = `${base}-${counter++}`; } existingSlugs.add(slug); return slug; }; // ============================================ // ⚡ رفع الصور بالتوازي (أهم تحسين) // ============================================ const uploadedImages = await Promise.all( extractedFiles.imageFiles.map(async (img) => { try { const b64 = img.content.toString('base64'); const dataURI = `data:image/${img.ext};base64,${b64}`; const result = await cloudinary.uploader.upload(dataURI, { folder: `StorePages/${req.user.userId}/images`, public_id: img.name.replace(/[^a-z0-9.-]/gi, '-'), resource_type: 'image' }); return { name: img.name, url: result.secure_url }; } catch (err) { console.error('Image upload failed:', err.message); return null; } }) ); const validImages = uploadedImages.filter(Boolean); // ============================================ // 2️⃣ سيناريو ملف واحد // ============================================ if ( extractedFiles.htmlFiles.length === 1 && (extractedFiles.cssFiles.length || extractedFiles.jsFiles.length) ) { const htmlFile = extractedFiles.htmlFiles[0]; const css = extractedFiles.cssFiles.map(c => c.content).join('\n'); const js = extractedFiles.jsFiles.map(j => j.content).join('\n'); const title = (htmlFile.content.match(/<title>(.*?)<\/title>/i)?.[1]) || htmlFile.name.replace(/\.html$/i, ''); const body = htmlFile.content.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] || htmlFile.content; const slug = generateUniqueSlug( htmlFile.name.replace(/\.html$/i, '').toLowerCase().replace(/[^a-z0-9-]/g, '-') ); const page = new Page({ storeId: req.user.userId, slug, title: title.substring(0, 100), content: body, css, js, type: 'uploaded', isEnabled: true, order: existingPages.length }); await page.save(); createdPages.push({ slug: page.slug, title: page.title }); } // ============================================ // 3️⃣ عدة صفحات HTML // ============================================ else if (extractedFiles.htmlFiles.length > 1) { for (const htmlFile of extractedFiles.htmlFiles) { const base = htmlFile.name.replace(/\.html$/i, ''); const css = extractedFiles.cssFiles .filter(c => c.name.includes(base)) .map(c => c.content) .join('\n'); const js = extractedFiles.jsFiles .filter(j => j.name.includes(base)) .map(j => j.content) .join('\n'); const title = (htmlFile.content.match(/<title>(.*?)<\/title>/i)?.[1]) || base; const body = htmlFile.content.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] || htmlFile.content; const slug = generateUniqueSlug( base.toLowerCase().replace(/[^a-z0-9-]/g, '-') ); const page = new Page({ storeId: req.user.userId, slug, title: title.substring(0, 100), content: body, css, js, type: 'uploaded', isEnabled: true, order: existingPages.length }); await page.save(); createdPages.push({ slug: page.slug, title: page.title }); } } // ============================================ // 4️⃣ CSS / JS فقط // ============================================ else { for (const css of extractedFiles.cssFiles) { const base = css.name.replace(/\.css$/i, ''); const slug = generateUniqueSlug(base.toLowerCase()); const page = new Page({ storeId: req.user.userId, slug, title: base, content: `<pre>${css.content.slice(0, 1000)}</pre>`, css: css.content, js: '', type: 'uploaded', isEnabled: true, order: existingPages.length }); await page.save(); createdPages.push({ slug: page.slug, title: page.title }); } for (const js of extractedFiles.jsFiles) { const base = js.name.replace(/\.js$/i, ''); const slug = generateUniqueSlug(base.toLowerCase()); const page = new Page({ storeId: req.user.userId, slug, title: base, content: `<pre>${js.content.slice(0, 1000)}</pre>`, css: '', js: js.content, type: 'uploaded', isEnabled: true, order: existingPages.length }); await page.save(); createdPages.push({ slug: page.slug, title: page.title }); } } // ============================================ // 5️⃣ response // ============================================ return res.status(201).json({ success: true, pages: createdPages, summary: { totalPages: createdPages.length, htmlFiles: extractedFiles.htmlFiles.length, cssFiles: extractedFiles.cssFiles.length, jsFiles: extractedFiles.jsFiles.length, imagesUploaded: validImages.length } }); } catch (error) { console.error('ZIP upload error:', error); return res.status(500).json({ error: 'Failed to process ZIP', details: error.message }); } } ); // PUT /api/store/my/pages/reorder - تحديث ترتيب الصفحات (Drag & Drop) app.put('/api/store/my/pages/reorder', authenticateToken, async (req, res) => { try { const { pages } = req.body; // [{ id: 'pageId', slug: 'slug', order: 0 }] if (!pages || !Array.isArray(pages)) { return res.status(400).json({ error: 'Invalid pages data' }); } // ✅ تحديث ترتيب الصفحات المخصصة في قاعدة البيانات for (const page of pages) { if (page.id && page.id !== 'products' && page.id !== 'about' && page.id !== 'contact') { // صفحة مخصصة await Page.findOneAndUpdate( { _id: page.id, storeId: req.user.userId }, { order: page.order, updatedAt: Date.now() } ); } else if (page.id === 'about' || page.id === 'contact') { // صفحة افتراضية - نخزن ترتيبها في StoreSettings const existingPage = await Page.findOne({ storeId: req.user.userId, slug: page.id }); if (existingPage) { await Page.findOneAndUpdate( { _id: existingPage._id }, { order: page.order } ); } else { // إنشاء سجل للصفحة الافتراضية await Page.create({ storeId: req.user.userId, slug: page.id, title: page.id === 'about' ? 'About Us' : 'Contact Us', type: 'custom', isDefault: true, order: page.order, isEnabled: true }); } } } // ✅ تخزين ترتيب صفحة products في StoreSettings const productsOrder = pages.find(p => p.id === 'products'); if (productsOrder) { await StoreSettings.findOneAndUpdate( { userId: req.user.userId }, { 'pagesOrder.products': productsOrder.order } ); } res.json({ success: true, message: 'Pages reordered successfully' }); } catch (error) { console.error('Error reordering pages:', error); res.status(500).json({ error: 'Failed to reorder pages' }); } }); // PUT /api/store/my/pages/:pageId - تحديث صفحة (مع دعم CSS/JS) app.put('/api/store/my/pages/:pageId', authenticateToken, async (req, res) => { try { const { title, content, css, js, isEnabled, order, seo, visibility, settings, slug, processShortcodes } = req.body; const existingPage = await Page.findOne({ _id: req.params.pageId, storeId: req.user.userId }); if (!existingPage) { if (req.params.pageId === 'about' || req.params.pageId === 'contact') { const newPage = new Page({ storeId: req.user.userId, slug: req.params.pageId, title: title || (req.params.pageId === 'about' ? 'About Us' : 'Contact Us'), content: content || '', css: css || '', js: js || '', type: 'custom', isDefault: true, isEnabled: isEnabled !== undefined ? isEnabled : true, order: order || 0, seo: seo || {}, visibility: visibility || 'public', settings: settings || {} }); await newPage.save(); // ✅ تهيئة Shortcodes للمستخدم await ShortcodeService.initializeUserShortcodes(req.user.userId); return res.json({ success: true, page: newPage }); } return res.status(404).json({ error: 'Page not found' }); } // ✅ إذا تم طلب معالجة Shortcodes في المحتوى قبل الحفظ let processedContent = content !== undefined ? content : existingPage.content; if (processShortcodes === 'true' && processedContent && processedContent.includes('[')) { const userToken = req.headers.authorization?.split(' ')[1]; processedContent = await ShortcodeService.parseShortcodes( processedContent, req.user.userId, { userToken } ); console.log(`📝 Shortcodes processed in page ${existingPage.slug} before saving`); } const updateData = { title: title || existingPage.title, content: processedContent, css: css !== undefined ? css : existingPage.css, js: js !== undefined ? js : existingPage.js, isEnabled: isEnabled !== undefined ? isEnabled : existingPage.isEnabled, order: order !== undefined ? order : existingPage.order, seo: seo || existingPage.seo, visibility: visibility || existingPage.visibility, settings: settings || existingPage.settings, updatedAt: Date.now() }; if (!existingPage.isDefault && slug) { updateData.slug = slug.toLowerCase().replace(/[^a-z0-9-]/g, '-'); } const page = await Page.findOneAndUpdate( { _id: req.params.pageId, storeId: req.user.userId }, updateData, { new: true } ); res.json({ success: true, page, shortcodesProcessed: processShortcodes === 'true' }); } catch (error) { console.error('Error updating page:', error); res.status(500).json({ error: 'Failed to update page' }); } }); // GET /api/store/my/pages/:pageId - جلب صفحة محددة (موجود وشغال) app.get('/api/store/my/pages/:pageId', authenticateToken, async (req, res) => { try { const page = await Page.findOne({ _id: req.params.pageId, storeId: req.user.userId }); if (!page) { return res.status(404).json({ error: 'Page not found' }); } res.json({ success: true, page: { _id: page._id, slug: page.slug, title: page.title, content: page.content || '', css: page.css || '', js: page.js || '', type: page.type, isEnabled: page.isEnabled, order: page.order, seo: page.seo || {}, uploadedFile: page.uploadedFile } }); } catch (error) { console.error('Error fetching page:', error); res.status(500).json({ error: 'Failed to fetch page' }); } }); // DELETE /api/store/my/pages/:pageId - حذف صفحة app.delete('/api/store/my/pages/:pageId', authenticateToken, async (req, res) => { try { const page = await Page.findOne({ _id: req.params.pageId, storeId: req.user.userId }); if (!page) { return res.status(404).json({ error: 'Page not found' }); } // حذف ملف Cloudinary إذا وجد if (page.type === 'uploaded' && page.uploadedFile?.fileUrl) { try { const publicId = page.uploadedFile.filename; await cloudinary.uploader.destroy(`StorePages/${req.user.userId}/${publicId}`, { resource_type: 'raw' }); } catch (cloudError) { console.error('Cloudinary deletion error:', cloudError); } } await Page.findByIdAndDelete(req.params.pageId); res.json({ success: true }); } catch (error) { console.error('Error deleting page:', error); res.status(500).json({ error: 'Failed to delete page' }); } }); // POST /api/store/my/pages/:pageId/duplicate - نسخ صفحة app.post('/api/store/my/pages/:pageId/duplicate', authenticateToken, async (req, res) => { try { const originalPage = await Page.findOne({ _id: req.params.pageId, storeId: req.user.userId }); if (!originalPage) { return res.status(404).json({ error: 'Page not found' }); } const newSlug = `${originalPage.slug}-copy`; const newTitle = `${originalPage.title} (Copy)`; // التأكد من عدم وجود slug مكرر let finalSlug = newSlug; let counter = 1; while (await Page.findOne({ storeId: req.user.userId, slug: finalSlug })) { counter++; finalSlug = `${newSlug}-${counter}`; } const duplicatedPage = new Page({ storeId: req.user.userId, slug: finalSlug, title: newTitle, content: originalPage.content, css: originalPage.css, js: originalPage.js, type: 'custom', isEnabled: false, // نسخة غير مفعلة افتراضياً order: await Page.countDocuments({ storeId: req.user.userId }), seo: originalPage.seo, visibility: originalPage.visibility, settings: originalPage.settings }); await duplicatedPage.save(); res.status(201).json({ success: true, page: duplicatedPage }); } catch (error) { console.error('Error duplicating page:', error); res.status(500).json({ error: 'Failed to duplicate page' }); } }); // GET /api/store/my/pages/:pageId/export - تصدير صفحة كملف HTML app.get('/api/store/my/pages/:pageId/export', authenticateToken, async (req, res) => { try { const page = await Page.findOne({ _id: req.params.pageId, storeId: req.user.userId }); if (!page) { return res.status(404).json({ error: 'Page not found' }); } const user = await User.findById(req.user.userId); const storeSettings = await StoreSettings.findOne({ userId: req.user.userId }); const html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${escapeHtml(page.title)} - ${escapeHtml(storeSettings?.storeName || user?.username)}
${page.content || '

No content available.

'}
`; res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Disposition', `attachment; filename="${page.slug}.html"`); res.send(html); } catch (error) { console.error('Error exporting page:', error); res.status(500).json({ error: 'Failed to export page' }); } }); // GET /api/store/orders/:orderId - جلب طلب محدد app.get('/api/store/orders/:orderId', authenticateToken, async (req, res) => { try { const order = await Order.findById(req.params.orderId) .populate('buyerId', 'username email profile.nickname') .populate('sellerId', 'username profile.nickname'); if (!order) { return res.status(404).json({ error: 'Order not found' }); } // التحقق من أن المستخدم هو المشتري أو البائع if (order.buyerId._id.toString() !== req.user.userId && order.sellerId._id.toString() !== req.user.userId) { return res.status(403).json({ error: 'Unauthorized' }); } // جلب إعدادات المتجر للـ currency symbol const storeSettings = await StoreSettings.findOne({ userId: order.sellerId }); res.json({ ...order.toObject(), currencySymbol: storeSettings?.currencySymbol || '$' }); } catch (error) { console.error('Error fetching order:', error); res.status(500).json({ error: 'Failed to fetch order' }); } }); // PUT /api/store/orders/:orderId/status - تحديث حالة الطلب app.put('/api/store/orders/:orderId/status', authenticateToken, async (req, res) => { try { const { status, sellerNotes } = req.body; const order = await Order.findOneAndUpdate( { _id: req.params.orderId, sellerId: req.user.userId }, { status, sellerNotes: sellerNotes || '', ...(status === 'completed' ? { deliveredAt: Date.now() } : {}), updatedAt: Date.now() }, { new: true } ); if (!order) { return res.status(404).json({ error: 'Order not found' }); } // إذا تم إكمال الطلب، أضف للإيرادات if (status === 'completed') { await Earnings.findOneAndUpdate( { userId: req.user.userId }, { $inc: { totalSales: 1, totalEarnings: order.totalAmount, lifetimeEarnings: order.totalAmount }, $push: { transactions: { orderId: order._id, amount: order.totalAmount, type: 'sale', status: 'completed', createdAt: Date.now() } } }, { upsert: true } ); // تحديث إحصائيات الشهر const currentMonth = new Date().toISOString().slice(0, 7); await Earnings.findOneAndUpdate( { userId: req.user.userId }, { $inc: { 'monthlyStats.$[elem].sales': 1, 'monthlyStats.$[elem].earnings': order.totalAmount } }, { arrayFilters: [{ 'elem.month': currentMonth }], upsert: true } ); } res.json({ success: true, order }); } catch (error) { console.error('Error updating order status:', error); res.status(500).json({ error: 'Failed to update order status' }); } }); // POST /api/store/orders/:orderId/download - تحميل ملفات المنتج (للمنتجات الرقمية) app.post('/api/store/orders/:orderId/download/:productId', authenticateToken, async (req, res) => { try { const order = await Order.findById(req.params.orderId); if (!order) { return res.status(404).json({ error: 'Order not found' }); } // التحقق من أن المستخدم هو المشتري والطلب مكتمل if (order.buyerId.toString() !== req.user.userId || order.status !== 'completed') { return res.status(403).json({ error: 'Unauthorized or order not completed' }); } const product = order.items.find(i => i.productId.toString() === req.params.productId); if (!product || !product.fileUrl) { return res.status(404).json({ error: 'File not found' }); } // تحديث عدد مرات التحميل await Order.findByIdAndUpdate(req.params.orderId, { $inc: { 'items.$[item].downloadCount': 1 } }, { arrayFilters: [{ 'item.productId': req.params.productId }] }); // إعادة رابط التحميل (أو إعادة توجيه) res.json({ downloadUrl: product.fileUrl }); } catch (error) { console.error('Error downloading file:', error); res.status(500).json({ error: 'Failed to download file' }); } }); // ============================================ // POST /api/store/orders/:orderId/upload - رفع ملف لمنتج في الطلب // ============================================ app.post('/api/store/orders/:orderId/upload', authenticateToken, upload.single('file'), async (req, res) => { try { const { orderId } = req.params; const { productId } = req.body; // 1. التحقق من وجود الطلب وأن البائع هو صاحبه const order = await Order.findOne({ _id: orderId, sellerId: req.user.userId }); if (!order) { return res.status(404).json({ error: 'Order not found or unauthorized' }); } // 2. التحقق من وجود ملف مرفوع if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } // 3. التحقق من وجود المنتج في الطلب const item = order.items.find(i => i.productId.toString() === productId); if (!item) { return res.status(404).json({ error: 'Product not found in this order' }); } // 4. التحقق من صيغة الملف (نفس الـ upload العادي) const allowedTypes = [ 'image/jpeg', 'image/png', 'image/jpg', 'image/webp', 'application/pdf', 'application/zip', 'text/plain', 'text/csv', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ]; // التحقق من الامتداد كبديل const ext = '.' + req.file.originalname.split('.').pop().toLowerCase(); const allowedExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.pdf', '.zip', '.txt', '.csv', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']; if (!allowedTypes.includes(req.file.mimetype) && !allowedExtensions.includes(ext)) { return res.status(400).json({ error: 'File type not allowed. Allowed: images, PDF, ZIP, DOC, XLS, PPT, TXT, CSV' }); } // 5. التحقق من الحجم (حد أقصى 20MB للملفات الكبيرة) if (req.file.size > 20 * 1024 * 1024) { return res.status(400).json({ error: 'File size must be less than 20MB' }); } // 6. رفع الملف إلى Cloudinary let fileUrl = req.file.path; // في حالة استخدام Cloudinary storage مباشرة // إذا كان الملف مخزن في الذاكرة (memoryStorage)، نرفعه يدوياً if (!fileUrl && req.file.buffer) { try { // تحديد المجلد الفرعي حسب نوع الملف let folder = `Orders/${req.user.userId}/${orderId}/files`; // رفع الملف إلى Cloudinary const result = await new Promise((resolve, reject) => { const uploadStream = cloudinary.uploader.upload_stream( { folder: folder, resource_type: 'auto', public_id: `${Date.now()}_${req.file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_')}`, allowed_formats: ['jpg', 'jpeg', 'png', 'webp', 'pdf', 'zip', 'txt', 'csv', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'] }, (error, result) => { if (error) reject(error); else resolve(result); } ); uploadStream.end(req.file.buffer); }); fileUrl = result.secure_url; } catch (cloudinaryError) { console.error('Cloudinary upload error:', cloudinaryError); return res.status(500).json({ error: 'Failed to upload file to cloud storage' }); } } // 7. التحقق من وجود URL بعد الرفع if (!fileUrl) { return res.status(500).json({ error: 'Upload failed - no URL returned' }); } // 8. تحديث المنتج في الطلب item.fileUrl = fileUrl; item.fileName = req.file.originalname; item.fileSize = req.file.size; item.fileType = req.file.mimetype; item.uploadedAt = new Date(); await order.save(); // 9. تحديث إحصائيات المنتج الأصلي (اختياري) try { await Product.findByIdAndUpdate(productId, { $set: { fileUrl: fileUrl } }); } catch (e) { console.log('Could not update product fileUrl:', e.message); } // 10. إرسال إشعار للمشتري (اختياري) try { const buyer = await User.findById(order.buyerId); if (buyer) { await sendStoreEmailWithBrevo( buyer.email, `📥 File uploaded for order #${order.orderNumber}`, `

File Uploaded! 📥

Your order #${order.orderNumber} now has a file available for:

${item.title}

File: ${req.file.originalname}

View Order `, `File uploaded for order #${order.orderNumber}` ); } } catch (emailError) { console.log('Failed to send email notification:', emailError.message); } // 11. إرسال إشعار Socket.IO للمشتري const io = req.app.get('io'); if (io) { io.to(`user_${order.buyerId}`).emit('file_uploaded', { orderId: order._id, orderNumber: order.orderNumber, productTitle: item.title, fileName: req.file.originalname }); } res.json({ success: true, message: 'File uploaded successfully!', fileUrl: fileUrl, fileName: req.file.originalname, fileSize: req.file.size }); } catch (error) { console.error('Error uploading file:', error); if (error instanceof multer.MulterError) { return res.status(400).json({ error: `Upload error: ${error.message}` }); } res.status(500).json({ error: 'Failed to upload file: ' + error.message }); } }); // ============================================ // PUT /api/store/orders/:orderId/file-url - إضافة رابط تحميل يدوي // ============================================ app.put('/api/store/orders/:orderId/file-url', authenticateToken, async (req, res) => { try { const { orderId } = req.params; const { productId, fileUrl, fileName } = req.body; // 1. التحقق من وجود الطلب وأن البائع هو صاحبه const order = await Order.findOne({ _id: orderId, sellerId: req.user.userId }); if (!order) { return res.status(404).json({ error: 'Order not found or unauthorized' }); } // 2. التحقق من وجود المنتج في الطلب const item = order.items.find(i => i.productId.toString() === productId); if (!item) { return res.status(404).json({ error: 'Product not found in this order' }); } // 3. التحقق من صحة الرابط if (!fileUrl || !fileUrl.startsWith('http://') && !fileUrl.startsWith('https://')) { return res.status(400).json({ error: 'Invalid file URL. Must start with http:// or https://' }); } // 4. تحديث المنتج في الطلب item.fileUrl = fileUrl; item.fileName = fileName || item.title + '.file'; item.fileType = 'link'; item.uploadedAt = new Date(); await order.save(); // 5. إرسال إشعار للمشتري try { const buyer = await User.findById(order.buyerId); if (buyer) { await sendStoreEmailWithBrevo( buyer.email, `📥 Download link added for order #${order.orderNumber}`, `

Download Link Added! 📥

Your order #${order.orderNumber} now has a download link for:

${item.title}

Click the button below to download:

📥 Download Now

View Order `, `Download link added for order #${order.orderNumber}` ); } } catch (emailError) { console.log('Failed to send email notification:', emailError.message); } // 6. إرسال إشعار Socket.IO للمشتري const io = req.app.get('io'); if (io) { io.to(`user_${order.buyerId}`).emit('file_url_added', { orderId: order._id, orderNumber: order.orderNumber, productTitle: item.title, fileUrl: fileUrl }); } res.json({ success: true, message: 'File URL added successfully!', fileUrl: fileUrl }); } catch (error) { console.error('Error adding file URL:', error); res.status(500).json({ error: 'Failed to add file URL: ' + error.message }); } }); // POST /api/store/orders/:orderId/cancel - إلغاء طلب app.post('/api/store/orders/:orderId/cancel', authenticateToken, async (req, res) => { try { const order = await Order.findOne({ _id: req.params.orderId, buyerId: req.user.userId }); if (!order) return res.status(404).json({ error: 'Order not found' }); if (order.status !== 'pending') { return res.status(400).json({ error: 'Cannot cancel order in current status' }); } order.status = 'cancelled'; order.cancelledAt = new Date(); order.cancellationReason = req.body.reason || 'Cancelled by user'; await order.save(); res.json({ success: true }); } catch (error) { res.status(500).json({ error: 'Failed to cancel order' }); } }); // PUT /api/store/products/:productId - تحديث منتج app.put('/api/store/products/:productId', authenticateToken, async (req, res) => { try { const { title, description, price, isActive, deliveryTime, tags, images } = req.body; // ============================================ // ✅ جلب المنتج الحالي أولاً للتحقق من النوع // ============================================ const existingProduct = await Product.findOne({ _id: req.params.productId, userId: req.user.userId }); if (!existingProduct) { return res.status(404).json({ error: 'Product not found' }); } // ============================================ // ✅ التحقق من الاشتراك والحدود (نفس الـ POST) // ============================================ // 1. التحقق من وجود اشتراك نشط const activeSubscription = await UserSubscription.findOne({ userId: req.user.userId, status: 'active' }).populate('planId'); if (!activeSubscription) { return res.status(403).json({ success: false, error: 'You need an active subscription to manage products. Please subscribe first.', redirect: '/subscription.html', code: 'NO_SUBSCRIPTION' }); } // 2. التحقق من أن الخطة تدعم إنشاء منتجات const plan = activeSubscription.planId; if (!plan || !plan.features || !plan.features.storeEnabled) { return res.status(403).json({ success: false, error: 'Your current plan does not allow managing products. Please upgrade your plan.', redirect: '/subscription.html', code: 'STORE_NOT_IN_PLAN' }); } // 3. التحقق من حدود المنتجات الكلية (لحالة تغيير isActive من false إلى true) const oldType = existingProduct.type; const newType = req.body.type || existingProduct.type; // إذا كان المنتج غير نشط وسوف يصبح نشطاً، تحقق من الحدود if (!existingProduct.isActive && isActive === true) { const currentActiveCount = await Product.countDocuments({ userId: req.user.userId, isActive: true }); const maxProducts = plan.features.maxProducts || 0; if (maxProducts > 0 && currentActiveCount >= maxProducts) { return res.status(403).json({ success: false, error: `Your plan allows up to ${maxProducts} active products. You have reached the limit. Please upgrade to add more products.`, redirect: '/subscription.html', code: 'PRODUCT_LIMIT_REACHED', currentLimit: maxProducts, currentCount: currentActiveCount }); } } // 4. إذا تغير نوع المنتج، تحقق من حدود النوع الجديد if (oldType !== newType) { if (newType === 'digital') { const maxDigital = plan.features.maxDigitalProducts || 0; const currentDigital = await Product.countDocuments({ userId: req.user.userId, type: 'digital', isActive: true }); if (maxDigital > 0 && currentDigital >= maxDigital) { return res.status(403).json({ error: `Your plan allows up to ${maxDigital} digital products. Please upgrade to add more.`, redirect: '/subscription.html' }); } } else if (newType === 'project') { const maxProjects = plan.features.maxProjects || 0; const currentProjects = await Product.countDocuments({ userId: req.user.userId, type: 'project', isActive: true }); if (maxProjects > 0 && currentProjects >= maxProjects) { return res.status(403).json({ error: `Your plan allows up to ${maxProjects} projects. Please upgrade to add more.`, redirect: '/subscription.html' }); } } else if (newType === 'service') { const maxServices = plan.features.maxServices || 0; const currentServices = await Product.countDocuments({ userId: req.user.userId, type: 'service', isActive: true }); if (maxServices > 0 && currentServices >= maxServices) { return res.status(403).json({ error: `Your plan allows up to ${maxServices} services. Please upgrade to add more.`, redirect: '/subscription.html' }); } } } // 5. التحقق من صلاحية الاشتراك (لم تنتهي) const now = new Date(); if (activeSubscription.endDate && new Date(activeSubscription.endDate) < now) { activeSubscription.status = 'expired'; await activeSubscription.save(); return res.status(403).json({ error: 'Your subscription has expired. Please renew to manage products.', redirect: '/subscription.html', code: 'SUBSCRIPTION_EXPIRED' }); } // ============================================ // ✅ تحديث المنتج (الكود الموجود) // ============================================ const product = await Product.findOneAndUpdate( { _id: req.params.productId, userId: req.user.userId }, { title, description, price: parseFloat(price), isActive, deliveryTime, tags: tags ? (Array.isArray(tags) ? tags : JSON.parse(tags)) : [], images: images ? (Array.isArray(images) ? images : JSON.parse(images)) : [], updatedAt: Date.now() }, { new: true } ); if (!product) { return res.status(404).json({ error: 'Product not found' }); } res.json({ success: true, product }); } catch (error) { console.error('Error updating product:', error); res.status(500).json({ error: 'Failed to update product' }); } }); // DELETE /api/store/products/:productId - حذف منتج مع تحديث إحصائيات المتجر app.delete('/api/store/products/:productId', authenticateToken, async (req, res) => { try { // جلب المنتج قبل الحذف عشان نعرف userId بتاعه const product = await Product.findOne({ _id: req.params.productId, userId: req.user.userId }); if (!product) { return res.status(404).json({ error: 'Product not found' }); } // ✅ حذف المنتج await Product.findOneAndDelete({ _id: req.params.productId, userId: req.user.userId }); // ✅ تحديث عدد المنتجات في المتجر (تخفيض 1) await StoreSettings.findOneAndUpdate( { userId: req.user.userId }, { $inc: { productsCount: -1 }, updatedAt: Date.now() } ); // ✅ إعادة حساب متوسط التقييم للمتجر (لأن المنتج المحذوف كان عنده تقييمات) const allProducts = await Product.find({ userId: req.user.userId, isActive: true, 'reviews.0': { $exists: true } }); let totalRating = 0; let totalReviews = 0; for (const p of allProducts) { for (const review of p.reviews) { totalRating += review.rating; totalReviews++; } } const storeAvgRating = totalReviews > 0 ? totalRating / totalReviews : 0; await StoreSettings.findOneAndUpdate( { userId: req.user.userId }, { averageRating: storeAvgRating, updatedAt: Date.now() } ); console.log(`✅ Product deleted: ${product.title}, store updated - productsCount: -1, new averageRating: ${storeAvgRating.toFixed(2)}`); res.json({ success: true, message: 'Product deleted successfully' }); } catch (error) { console.error('Error deleting product:', error); res.status(500).json({ error: 'Failed to delete product' }); } }); // POST /api/store/products/:productId/review - إضافة تقييم مع تحديث إحصائيات المتجر app.post('/api/store/products/:productId/review', authenticateToken, async (req, res) => { try { const { rating, comment } = req.body; const productId = req.params.productId; // ✅ التحقق من صحة البيانات const ratingNum = parseInt(rating); if (isNaN(ratingNum) || ratingNum < 1 || ratingNum > 5) { return res.status(400).json({ error: 'Rating must be between 1 and 5' }); } if (comment && comment.length > 500) { return res.status(400).json({ error: 'Comment cannot exceed 500 characters' }); } // ✅ التحقق من أن المستخدم اشترى المنتج بالفعل const hasPurchased = await Order.findOne({ buyerId: req.user.userId, 'items.productId': productId, status: 'completed' }); if (!hasPurchased) { return res.status(403).json({ error: 'You can only review products you have purchased' }); } // ✅ جلب المنتج const product = await Product.findById(productId); if (!product) { return res.status(404).json({ error: 'Product not found' }); } // ✅ التحقق من عدم وجود تقييم سابق const existingReview = product.reviews.find(r => r.userId.toString() === req.user.userId); if (existingReview) { return res.status(400).json({ error: 'You have already reviewed this product' }); } // ✅ إضافة التقييم product.reviews.push({ userId: req.user.userId, rating: ratingNum, comment: comment || '', createdAt: new Date() }); // ✅ تحديث متوسط تقييم المنتج const totalRatings = product.reviews.length; const sumRatings = product.reviews.reduce((sum, r) => sum + r.rating, 0); product.averageRating = sumRatings / totalRatings; await product.save(); // ✅ إعادة حساب متوسط تقييم المتجر بالكامل (لجميع منتجات المتجر) const allProducts = await Product.find({ userId: product.userId, isActive: true, 'reviews.0': { $exists: true } }); let totalStoreRating = 0; let totalStoreReviews = 0; for (const p of allProducts) { for (const review of p.reviews) { totalStoreRating += review.rating; totalStoreReviews++; } } const storeAvgRating = totalStoreReviews > 0 ? totalStoreRating / totalStoreReviews : 0; await StoreSettings.findOneAndUpdate( { userId: product.userId }, { averageRating: storeAvgRating, updatedAt: Date.now() } ); console.log(`✅ Review added for product ${product.title} - Rating: ${ratingNum}, Store average: ${storeAvgRating.toFixed(2)}`); // ✅ إشعار لصاحب المنتج await sendStoreNotification( product.userId, 'product_review', `${req.user.username} rated your product "${product.title}" ${ratingNum} stars`, `/shop/${req.user.username}/product/${productId}`, req.user.userId ); res.json({ success: true, product, message: 'Review added successfully', storeAverageRating: storeAvgRating }); } catch (error) { console.error('Error adding review:', error); res.status(500).json({ error: 'Failed to add review' }); } }); // GET /api/store/products/:productId/reviews - جلب تقييمات المنتج app.get('/api/store/products/:productId/reviews', async (req, res) => { try { const product = await Product.findById(req.params.productId) .select('reviews averageRating totalReviews') .populate('reviews.userId', 'username profile.nickname profile.avatar'); if (!product) { return res.status(404).json({ error: 'Product not found' }); } res.json({ success: true, averageRating: product.averageRating, totalReviews: product.reviews.length, reviews: product.reviews.sort((a, b) => b.createdAt - a.createdAt) }); } catch (error) { console.error('Error fetching reviews:', error); res.status(500).json({ error: 'Failed to fetch reviews' }); } }); // ============================================ // STORE FOLLOW SYSTEM // ============================================ // POST /api/store/:storeId/follow - متابعة/إلغاء متابعة متجر مع تحديث الإحصائيات app.post('/api/store/:storeId/follow', authenticateToken, async (req, res) => { try { const storeId = req.params.storeId; // التحقق من وجود المتجر const store = await User.findById(storeId); if (!store || !store.profile.storeEnabled) { return res.status(404).json({ error: 'Store not found' }); } // منع المتابعة الذاتية if (storeId === req.user.userId) { return res.status(400).json({ error: 'You cannot follow your own store' }); } const existingFollow = await StoreFollow.findOne({ userId: req.user.userId, storeId }); let isFollowing; let followersCount; if (existingFollow) { // ✅ إلغاء المتابعة await existingFollow.deleteOne(); isFollowing = false; // ✅ تحديث عدد المتابعين في المتجر (تخفيض 1) await StoreSettings.findOneAndUpdate( { userId: storeId }, { $inc: { followersCount: -1 }, updatedAt: Date.now() } ); console.log(`✅ User ${req.user.username} unfollowed store ${store.profile?.nickname || store.username}`); } else { // ✅ متابعة await StoreFollow.create({ userId: req.user.userId, storeId }); isFollowing = true; // ✅ تحديث عدد المتابعين في المتجر (زيادة 1) await StoreSettings.findOneAndUpdate( { userId: storeId }, { $inc: { followersCount: 1 }, updatedAt: Date.now() } ); // ✅ إشعار لصاحب المتجر await sendStoreNotification( storeId, 'store_follow', `${req.user.username} started following your store`, `/shop/${store.profile?.nickname || store.username}`, req.user.userId ); console.log(`✅ User ${req.user.username} followed store ${store.profile?.nickname || store.username}`); } // ✅ جلب عدد المتابعين الجديد followersCount = await StoreFollow.countDocuments({ storeId }); res.json({ success: true, isFollowing, followersCount, message: isFollowing ? 'Store followed successfully' : 'Store unfollowed successfully' }); } catch (error) { console.error('Error toggling store follow:', error); res.status(500).json({ error: 'Failed to toggle store follow' }); } }); // GET /api/store/:storeId/followers - جلب متابعي المتجر app.get('/api/store/:storeId/followers', async (req, res) => { try { const followers = await StoreFollow.find({ storeId: req.params.storeId }) .populate('userId', 'username profile.nickname profile.avatar') .limit(20); res.json({ success: true, followers }); } catch (error) { console.error('Error fetching followers:', error); res.status(500).json({ error: 'Failed to fetch followers' }); } }); // GET /api/store/:storeId/follow-status - جلب حالة متابعة المتجر app.get('/api/store/:storeId/follow-status', authenticateToken, async (req, res) => { try { const storeId = req.params.storeId; const isFollowing = await StoreFollow.exists({ userId: req.user.userId, storeId }); const followersCount = await StoreFollow.countDocuments({ storeId }); res.json({ success: true, isFollowing: !!isFollowing, followersCount }); } catch (error) { res.status(500).json({ error: 'Failed to fetch follow status' }); } }); // ============================================ // GET /api/store/:username/full-template - جلب رابط القالب الكامل للمتجر // ============================================ app.get('/api/store/:username/full-template', async (req, res) => { try { const { username } = req.params; // 1. البحث عن المستخدم const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) { return res.status(404).json({ error: 'Store not found' }); } // 2. جلب إعدادات المتجر const storeSettings = await StoreSettings.findOne({ userId: user._id }); // 3. التحقق من وجود قالب كامل مطبق const appliedTemplate = storeSettings?.appliedTemplate; const templateUrl = appliedTemplate?.templateUrl; if (!templateUrl) { // لا يوجد قالب كامل مطبق، استخدم HTML الافتراضي (الموجود حالياً) return res.json({ hasFullTemplate: false, message: 'No full template applied. Using default layout.' }); } // 4. إرجاد رابط القالب الكامل console.log(`✅ Returning full template URL for ${username}: ${templateUrl}`); res.json({ hasFullTemplate: true, templateUrl: templateUrl, templateInfo: { name: appliedTemplate.name, slug: appliedTemplate.slug, appliedAt: appliedTemplate.appliedAt } }); } catch (error) { console.error('Error fetching full template:', error); res.status(500).json({ error: 'Failed to fetch full template' }); } }); // GET /api/store/:username/products - جلب منتجات متجر معين (للزوار) - UPDATED with filters app.get('/api/store/:username/products', async (req, res) => { try { const { tags, exclude, limit = 50, viewed, parseShortcodes = 'true' } = req.query; const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${req.params.username}$`, $options: 'i' } }, { username: { $regex: `^${req.params.username}$`, $options: 'i' } } ] }); if (!user) { return res.status(404).json({ error: 'Store not found' }); } const storeSettings = await StoreSettings.findOne({ userId: user._id }); if (!storeSettings || !storeSettings.enabled) { return res.status(403).json({ error: 'Store is not active' }); } let query = { userId: user._id, isActive: true }; if (tags && tags.length > 0) { const tagsArray = Array.isArray(tags) ? tags : [tags]; query.tags = { $in: tagsArray }; } if (exclude && exclude !== '') { query._id = { $ne: exclude }; } if (viewed && viewed.length > 0) { const viewedArray = Array.isArray(viewed) ? viewed : [viewed]; query._id = { $nin: viewedArray }; if (exclude && exclude !== '') { query._id = { $nin: [...viewedArray, exclude] }; } } const products = await Product.find(query) .select('title description price type images tags salesCount averageRating reviews deliveryTime') .sort({ createdAt: -1 }) .limit(parseInt(limit) || 50); // ✅ جلب الـ Shortcodes المتاحة للمتجر (لمعالجة وصف المنتجات) const availableShortcodes = await StoreShortcode.find({ userId: user._id, isActive: true }).select('name'); const shortcodeNames = availableShortcodes.map(s => s.name); const formattedProducts = await Promise.all(products.map(async (p) => { let avgRating = p.averageRating || 0; if (p.reviews && p.reviews.length > 0 && !avgRating) { const sum = p.reviews.reduce((acc, rev) => acc + rev.rating, 0); avgRating = sum / p.reviews.length; } let description = p.description || ''; // ✅ معالجة Shortcodes في وصف المنتج إذا وجدت if (parseShortcodes === 'true' && description && description.includes('[')) { const userToken = req.headers.authorization?.split(' ')[1]; description = await ShortcodeService.parseShortcodes( description, user._id, { userToken } ); } return { _id: p._id, title: p.title, description: description, price: p.price, type: p.type, images: p.images || [], tags: p.tags || [], salesCount: p.salesCount || 0, averageRating: avgRating, reviewsCount: p.reviews?.length || 0, deliveryTime: p.deliveryTime || null, isActive: p.isActive, createdAt: p.createdAt, updatedAt: p.updatedAt }; })); res.json({ success: true, products: formattedProducts, storeSettings: { storeName: storeSettings.storeName || user.profile?.nickname || user.username, storeLogo: storeSettings.storeLogo, storeBanner: storeSettings.storeBanner, storeDescription: storeSettings.storeDescription, currency: storeSettings.currency, currencySymbol: storeSettings.currencySymbol, primaryColor: storeSettings.primaryColor, secondaryColor: storeSettings.secondaryColor }, // ✅ إضافة الـ Shortcodes المتاحة للاستخدام في وصف المنتجات availableShortcodes: shortcodeNames }); } catch (error) { console.error('Error fetching store products:', error); res.status(500).json({ error: 'Failed to fetch products' }); } }); // ============================================ // GET /api/store/:username/analytics - جلب تحليلات المتجر (للمالك فقط) // ============================================ app.get('/api/store/:username/analytics', authenticateToken, async (req, res) => { try { const { username } = req.params; // البحث عن المستخدم const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) { return res.status(404).json({ error: 'Store not found' }); } // التحقق من أن المستخدم هو صاحب المتجر if (user._id.toString() !== req.user.userId) { return res.status(403).json({ error: 'Unauthorized' }); } // ============================================ // 📊 1. جلب التحليلات من ProfileAnalytics // ============================================ let analytics = await ProfileAnalytics.findOne({ userId: user._id }); if (!analytics) { analytics = { totalViews: 0, uniqueViews: 0, profileViews: [], dailyStats: {}, monthlyStats: {}, referralSources: {} }; } // ============================================ // 📊 2. إحصائيات الطلبات والإيرادات // ============================================ const ordersStats = await Order.aggregate([ { $match: { sellerId: user._id } }, { $group: { _id: null, totalOrders: { $sum: 1 }, completedOrders: { $sum: { $cond: [{ $eq: ['$status', 'completed'] }, 1, 0] } }, totalRevenue: { $sum: { $cond: [{ $eq: ['$status', 'completed'] }, '$totalAmount', 0] } }, pendingOrders: { $sum: { $cond: [{ $eq: ['$status', 'pending'] }, 1, 0] } } }} ]); // ============================================ // 📊 3. إحصائيات المنتجات // ============================================ const productsStats = await Product.aggregate([ { $match: { userId: user._id } }, { $group: { _id: '$type', count: { $sum: 1 }, totalSales: { $sum: '$salesCount' } }} ]); // ============================================ // 📊 4. إحصائيات المتابعين // ============================================ const followersCount = await StoreFollow.countDocuments({ storeId: user._id }); // ============================================ // 📊 5. المنتجات الأكثر مبيعاً // ============================================ const topProducts = await Product.find({ userId: user._id, isActive: true, salesCount: { $gt: 0 } }) .sort({ salesCount: -1 }) .limit(5) .select('title salesCount price images'); // ============================================ // 📊 6. آخر 7 أيام من الإحصائيات // ============================================ const last7Days = []; for (let i = 6; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split('T')[0]; last7Days.push({ date: dateStr, views: analytics.dailyStats?.[dateStr]?.views || 0, uniqueViews: analytics.dailyStats?.[dateStr]?.uniqueViews || 0, orders: analytics.dailyStats?.[dateStr]?.orders || 0, revenue: analytics.dailyStats?.[dateStr]?.revenue || 0 }); } // ============================================ // 📊 7. آخر 6 أشهر من الإحصائيات // ============================================ const last6Months = []; for (let i = 5; i >= 0; i--) { const date = new Date(); date.setMonth(date.getMonth() - i); const monthStr = date.toISOString().slice(0, 7); last6Months.push({ month: monthStr, views: analytics.monthlyStats?.[monthStr]?.views || 0, uniqueViews: analytics.monthlyStats?.[monthStr]?.uniqueViews || 0, orders: analytics.monthlyStats?.[monthStr]?.orders || 0, revenue: analytics.monthlyStats?.[monthStr]?.revenue || 0 }); } // ============================================ // 📊 8. مصادر الزوار // ============================================ const referralSources = analytics.referralSources || { direct: 0, google: 0, facebook: 0, twitter: 0, linkedin: 0, instagram: 0, github: 0, mgzon: 0, search: 0, other: 0 }; // ============================================ // ✅ إرسال الرد // ============================================ res.json({ success: true, analytics: { // إحصائيات عامة totalViews: analytics.totalViews || 0, uniqueViews: analytics.uniqueViews || 0, followersCount: followersCount, // إحصائيات الطلبات orders: { total: ordersStats[0]?.totalOrders || 0, completed: ordersStats[0]?.completedOrders || 0, pending: ordersStats[0]?.pendingOrders || 0, totalRevenue: ordersStats[0]?.totalRevenue || 0 }, // إحصائيات المنتجات products: { total: await Product.countDocuments({ userId: user._id, isActive: true }), digital: productsStats.find(p => p._id === 'digital')?.count || 0, projects: productsStats.find(p => p._id === 'project')?.count || 0, services: productsStats.find(p => p._id === 'service')?.count || 0, totalSales: productsStats.reduce((sum, p) => sum + p.totalSales, 0) }, // المنتجات الأكثر مبيعاً topProducts: topProducts.map(p => ({ _id: p._id, title: p.title, salesCount: p.salesCount, price: p.price, image: p.images?.[0] || null })), // الإحصائيات الزمنية last7Days: last7Days, last6Months: last6Months, // مصادر الزوار referralSources: referralSources, // آخر المشاهدات recentViews: analytics.profileViews?.slice(-10).reverse().map(view => ({ viewerName: view.viewerName || 'Guest', deviceType: view.deviceType || 'unknown', browser: view.browser || 'unknown', timestamp: view.timestamp, referer: view.referer || 'direct' })) || [] } }); } catch (error) { console.error('Error fetching store analytics:', error); res.status(500).json({ error: 'Failed to fetch analytics' }); } }); // ============================================ // 🗂️ STORE PAGES API ENDPOINTS // ============================================ // ============================================ // ✅ GET /api/store/:username/:slug - جلب صفحة (مع معالجة Shortcodes) // ============================================ app.get('/api/store/:username/:slug', async (req, res) => { try { const { username, slug } = req.params; if (slug === 'follow-status' || slug === 'products' || slug === 'analytics' || slug === 'page') { return res.status(404).json({ error: 'Not found' }); } const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) { return res.status(404).json({ error: 'Store not found' }); } const page = await Page.findOne({ storeId: user._id, slug: slug, isEnabled: true }); if (!page) { return res.status(404).json({ error: 'Page not found' }); } const storeSettings = await StoreSettings.findOne({ userId: user._id }); const theme = await StoreTheme.findOne({ userId: user._id }); // ✅ معالجة الـ Shortcodes في المحتوى const userToken = req.headers.authorization?.split(' ')[1]; const parsedContent = await ShortcodeService.parseShortcodes( page.content || '', user._id, { userToken } ); res.json({ success: true, page: { _id: page._id, slug: page.slug, title: page.title, content: parsedContent, // ✅ استخدام المحتوى المعالج css: page.css || '', js: page.js || '', seo: page.seo || {} }, storeSettings: { storeName: storeSettings?.storeName || user.username, primaryColor: theme?.colors?.primary || storeSettings?.primaryColor || '#3b82f6', secondaryColor: theme?.colors?.secondary || storeSettings?.secondaryColor || '#8b5cf6', currencySymbol: storeSettings?.currencySymbol || '$' } }); } catch (error) { console.error('Error serving page:', error); res.status(500).json({ error: 'Failed to load page' }); } }); // ============================================ // GET /api/store/:username/page/:slug - جلب صفحة معينة مع CSS/JS و SEO // ============================================ app.get('/api/store/:username/page/:slug', async (req, res) => { try { const { username, slug } = req.params; // ============================================ // 1) Find User // ============================================ const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) { return res.status(404).json({ error: 'Store not found' }); } // ============================================ // 2) Store Settings + Theme // ============================================ const storeSettings = await StoreSettings.findOne({ userId: user._id }); if (!storeSettings || !storeSettings.enabled) { return res.status(403).json({ error: 'Store is not active' }); } const theme = await StoreTheme.findOne({ userId: user._id }); // ============================================ // 3) Find Page // ============================================ let page = await Page.findOne({ storeId: user._id, slug, isEnabled: true }); // ============================================ // 4) Default Pages // ============================================ if (!page) { if (slug === 'about') { return res.json({ success: true, page: { slug: 'about', title: 'About Us', content: storeSettings?.storeDescription || 'Welcome to our store! We offer the best products and services.', css: storeSettings?.customCss || '', js: '', type: 'default', isDefault: true, canEdit: true, isOwner: req.user?.userId === user._id.toString(), seo: { title: `About ${storeSettings?.storeName || user.profile?.nickname || user.username} | MGZon Store`, description: storeSettings?.storeDescription?.substring(0, 160) || 'Learn more about our store.', noindex: false, nofollow: false }, visibility: 'public', settings: { showInNav: true, showInFooter: false, openInNewTab: false } } }); } if (slug === 'contact') { return res.json({ success: true, page: { slug: 'contact', title: 'Contact Us', content: generateContactContent(storeSettings, user), css: '', js: '', type: 'default', isDefault: true, canEdit: true, isOwner: req.user?.userId === user._id.toString(), seo: { title: `Contact ${storeSettings?.storeName || user.profile?.nickname || user.username} | MGZon Store`, description: `Get in touch with ${storeSettings?.storeName || user.profile?.nickname || user.username}.`, noindex: false, nofollow: false }, visibility: 'public', settings: { showInNav: true, showInFooter: false, openInNewTab: false } } }); } return res.status(404).json({ error: 'Page not found' }); } // ============================================ // 5) Permissions // ============================================ const isOwner = req.user?.userId === user._id.toString(); const isLoggedIn = !!req.user?.userId; if (page.visibility === 'only-owner' && !isOwner) { return res.status(403).json({ error: 'This page is private' }); } if (page.visibility === 'logged-in' && !isLoggedIn) { return res.status(401).json({ error: 'Please login to view this page' }); } // ============================================ // 6) Shortcodes Processing (NEW FEATURE) // ============================================ const userToken = req.headers.authorization?.split(' ')[1]; let processedContent = page.content || ''; try { processedContent = await ShortcodeService.parseShortcodes( processedContent, user._id, { userToken } ); } catch (err) { console.error('Shortcode error:', err); } // ============================================ // 7) XSS Protection (safe sanitize) // ============================================ const sanitizedContent = processedContent .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/\s(on\w+)=("[^"]*"|'[^']*'|[^\s>]+)/gi, ''); const sanitizedCss = page.css ?.replace(/expression\(/gi, '') ?.replace(/behavior\s*:/gi, '') ?.replace(/javascript\s*:/gi, '') || ''; const sanitizedJs = page.js ?.replace(/document\.write/gi, '') ?.replace(/eval\(/gi, '') || ''; // ============================================ // 8) Views increment (non-blocking) // ============================================ const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; page.incrementViews(ip, req.user?.userId) .catch(err => console.error('Error incrementing views:', err)); // ============================================ // 9) SEO // ============================================ const seoTitle = page.seo?.title || page.title; const seoDescription = page.seo?.description || page.content?.replace(/<[^>]*>/g, '').substring(0, 160) || ''; // ============================================ // 10) Response // ============================================ res.json({ success: true, page: { _id: page._id, slug: page.slug, title: page.title, content: sanitizedContent, css: sanitizedCss, js: sanitizedJs, type: page.type, isDefault: page.isDefault || false, isEnabled: page.isEnabled, order: page.order, canEdit: !page.isDefault || (page.isDefault && slug !== 'products'), isOwner, seo: { title: seoTitle, description: seoDescription, keywords: page.seo?.keywords || '', ogImage: page.seo?.ogImage || null, noindex: page.seo?.noindex || false, nofollow: page.seo?.nofollow || false }, visibility: page.visibility, settings: { showInNav: page.settings?.showInNav !== false, showInFooter: page.settings?.showInFooter || false, openInNewTab: page.settings?.openInNewTab || false, requireAuth: page.settings?.requireAuth || false }, stats: { views: page.stats?.views || 0, uniqueViews: page.stats?.uniqueViews || 0, lastViewedAt: page.stats?.lastViewedAt }, uploadedFile: page.uploadedFile ? { filename: page.uploadedFile.filename, originalName: page.uploadedFile.originalName, fileSize: page.uploadedFile.fileSize, uploadedAt: page.uploadedFile.uploadedAt } : null, createdAt: page.createdAt, updatedAt: page.updatedAt }, // ============================================ // 11) Extra frontend data (THEME + SETTINGS) // ============================================ storeSettings: { storeName: storeSettings?.storeName || user.username, primaryColor: theme?.colors?.primary || storeSettings?.primaryColor || '#3b82f6', secondaryColor: theme?.colors?.secondary || storeSettings?.secondaryColor || '#8b5cf6', currencySymbol: storeSettings?.currencySymbol || '$' } }); } catch (error) { console.error('Error fetching page:', error); res.status(500).json({ error: 'Failed to fetch page' }); } }); // GET /api/store/:username/follow-status - حالة متابعة المتجر (عام) app.get('/api/store/:username/follow-status', async (req, res) => { try { const { username } = req.params; const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) { return res.status(404).json({ error: 'Store not found' }); } const followersCount = await StoreFollow.countDocuments({ storeId: user._id }); // إذا كان المستخدم مسجل دخول، نجيب حالة المتابعة let isFollowing = false; if (req.user && req.user.userId) { isFollowing = await StoreFollow.exists({ userId: req.user.userId, storeId: user._id }); } res.json({ success: true, followersCount, isFollowing: !!isFollowing }); } catch (error) { console.error('Error fetching follow status:', error); res.status(500).json({ error: 'Failed to fetch follow status' }); } }); // ============================================ // GET /api/store/:username/pages - جلب صفحات المتجر (محدث مع دعم القالب المطبق) // ============================================ app.get('/api/store/:username/pages', async (req, res) => { try { const { username } = req.params; // البحث عن المستخدم بالـ nickname أو username const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) { return res.status(404).json({ error: 'Store not found' }); } const storeSettings = await StoreSettings.findOne({ userId: user._id }); // ✅ جلب القالب المطبق const appliedTemplate = storeSettings?.appliedTemplate || null; // جلب الـ Shortcodes المتاحة للمتجر const availableShortcodes = await StoreShortcode.find({ userId: user._id, isActive: true }).select('name displayName type parameters settings'); // الصفحات الافتراضية const defaultPages = [ { slug: 'products', title: 'Products', type: 'default', isEnabled: true, order: 1, stats: { views: 0 } }, { slug: 'about', title: 'About Us', type: 'default', isEnabled: true, order: 2, stats: { views: 0 } }, { slug: 'contact', title: 'Contact', type: 'default', isEnabled: true, order: 3, stats: { views: 0 } } ]; // الصفحات المخصصة من قاعدة البيانات const customPages = await Page.find({ storeId: user._id, isEnabled: true }) .sort({ order: 1 }) .select('slug title type order stats content'); // معالجة Shortcodes داخل المحتوى لكل صفحة const processedPages = await Promise.all( [...defaultPages, ...customPages].map(async (p) => { let processedContent = p.content || ''; try { processedContent = await ShortcodeService.parseShortcodes( processedContent, user._id ); } catch (err) { console.error(`Shortcode error for page ${p.slug}:`, err); } return { _id: p._id || null, slug: p.slug, title: p.title, type: p.type, order: p.order, views: p.stats?.views || 0, content: processedContent }; }) ); res.json({ success: true, pages: processedPages, storeSettings: { storeName: storeSettings?.storeName || user.profile?.nickname || user.username, storeLogo: storeSettings?.storeLogo, contactEmail: storeSettings?.contactEmail || user.email, // ✅ إضافة القالب المطبق appliedTemplate: appliedTemplate ? { slug: appliedTemplate.slug, name: appliedTemplate.name, appliedAt: appliedTemplate.appliedAt, hasFullTemplate: appliedTemplate.hasFullTemplate || false } : null }, // Shortcodes المتاحة للمتجر availableShortcodes: availableShortcodes.map(sc => ({ name: sc.name, displayName: sc.displayName, example: `[${sc.name}${sc.parameters?.length ? ' limit="6"' : ''}]`, parameters: sc.parameters })), // Shortcodes المدمجة في النظام builtinShortcodes: StoreShortcode.getBuiltinShortcodes().map(sc => ({ name: sc.name, displayName: sc.displayName, description: sc.description, example: `[${sc.name}${sc.parameters?.length ? ' limit="6"' : ''}]` })) }); } catch (error) { console.error('Error fetching store pages:', error); res.status(500).json({ error: 'Failed to fetch pages' }); } }); // ============================================ // GET /api/store/:username - جلب معلومات متجر عام (محدث مع دعم القالب المطبق) // ============================================ app.get('/api/store/:username', async (req, res) => { try { const { username } = req.params; // البحث عن المستخدم const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) { return res.status(404).json({ error: 'Store not found' }); } // جلب إعدادات المتجر const storeSettings = await StoreSettings.findOne({ userId: user._id }); if (!storeSettings || !storeSettings.enabled) { return res.status(403).json({ error: 'Store is not active' }); } // ✅ جلب القالب (Theme) const theme = await StoreTheme.findOne({ userId: user._id }); // ✅ جلب القالب المطبق const appliedTemplate = storeSettings.appliedTemplate || null; // ============================================ // ✅ جلب صفحات المتجر مع معالجة Shortcodes // ============================================ const customPages = await Page.find({ storeId: user._id, isEnabled: true }) .sort({ order: 1 }) .select('slug title type order content css js seo'); const customPagesMap = new Map(); for (const page of customPages) { customPagesMap.set(page.slug, page); } const defaultPagesData = [ { slug: 'products', title: 'Products', type: 'default', isEnabled: true, order: storeSettings.pagesOrder?.products || 1, canEdit: false, isCustomized: false }, { slug: 'about', title: 'About Us', type: 'default', isEnabled: true, order: storeSettings.pagesOrder?.about || 2, canEdit: true, isCustomized: false }, { slug: 'contact', title: 'Contact', type: 'default', isEnabled: true, order: storeSettings.pagesOrder?.contact || 3, canEdit: true, isCustomized: false } ]; // ✅ معالجة Shortcodes في محتوى الصفحات المخصصة const userToken = req.headers.authorization?.split(' ')[1]; const pages = []; for (const defaultPage of defaultPagesData) { const customizedPage = customPagesMap.get(defaultPage.slug); if (customizedPage) { // ✅ معالجة Shortcodes في محتوى الصفحة المخصصة let parsedContent = customizedPage.content; if (parsedContent && parsedContent.includes('[')) { parsedContent = await ShortcodeService.parseShortcodes( parsedContent, user._id, { userToken } ); } pages.push({ _id: customizedPage._id, slug: customizedPage.slug, title: customizedPage.title || defaultPage.title, type: customizedPage.type, isEnabled: customizedPage.isEnabled, order: customizedPage.order, content: parsedContent, css: customizedPage.css, js: customizedPage.js, seo: customizedPage.seo, canEdit: true, isCustomized: true }); } else { pages.push({ slug: defaultPage.slug, title: defaultPage.title, type: defaultPage.type, isEnabled: defaultPage.isEnabled, order: defaultPage.order, content: null, canEdit: defaultPage.canEdit, isCustomized: false }); } } for (const customPage of customPages) { if (!defaultPagesData.some(p => p.slug === customPage.slug)) { let parsedContent = customPage.content; if (parsedContent && parsedContent.includes('[')) { parsedContent = await ShortcodeService.parseShortcodes( parsedContent, user._id, { userToken } ); } pages.push({ _id: customPage._id, slug: customPage.slug, title: customPage.title, type: customPage.type, isEnabled: customPage.isEnabled, order: customPage.order, content: parsedContent, css: customPage.css, js: customPage.js, seo: customPage.seo, canEdit: true, isCustomized: true }); } } pages.sort((a, b) => (a.order || 0) - (b.order || 0)); // ============================================ // ✅ جلب المنتجات النشطة // ============================================ const products = await Product.find({ userId: user._id, isActive: true }) .select('title description price type images tags salesCount averageRating reviews deliveryTime') .sort({ createdAt: -1 }) .limit(50); const rawSections = storeSettings.pageBuilder?.sections || []; // ترتيب الأقسام حسب order const sections = rawSections .map(section => ({ id: section.id, type: section.type, enabled: section.enabled !== false, order: section.order || 0, title: section.title || null, limit: section.limit || null, layout: section.layout || null, content: section.content || {} })) .sort((a, b) => (a.order || 0) - (b.order || 0)); const totalSales = await Order.aggregate([ { $match: { sellerId: user._id, status: 'completed' } }, { $group: { _id: null, total: { $sum: '$totalAmount' } } } ]); const productsCount = await Product.countDocuments({ userId: user._id, isActive: true }); const followersCount = await StoreFollow.countDocuments({ storeId: user._id }); // ✅ جلب الـ Shortcodes المتاحة للمتجر const availableShortcodes = await StoreShortcode.find({ userId: user._id, isActive: true }).select('name displayName type'); // ============================================ // ✅ إرسال الرد مع دعم appliedTemplate // ============================================ res.json({ success: true, store: { id: user._id, username: user.username, nickname: user.profile?.nickname, avatar: user.profile?.avatar, storeName: storeSettings.storeName || user.profile?.nickname || user.username, storeLogo: storeSettings.storeLogo || null, storeBanner: storeSettings.storeBanner || null, storeDescription: storeSettings.storeDescription || '', contactEmail: storeSettings.contactEmail || '', productViewType: storeSettings.productViewType || 'modal', currency: storeSettings.currency || 'USD', currencySymbol: storeSettings.currencySymbol || '$', enabled: storeSettings.enabled, productsCount: productsCount, followersCount: followersCount, totalSales: totalSales[0]?.total || 0 }, settings: { primaryColor: theme?.colors?.primary || storeSettings.primaryColor || '#3b82f6', secondaryColor: theme?.colors?.secondary || storeSettings.secondaryColor || '#8b5cf6', fontFamily: theme?.typography?.fontFamily || storeSettings.fontFamily || 'Inter, sans-serif', borderRadius: storeSettings.borderRadius || 16, shadowType: storeSettings.shadowType || 'md', animationType: storeSettings.animationType || 'lift', glassEffect: storeSettings.glassEffect || false, backgroundType: storeSettings.backgroundType || 'default', backgroundValue: storeSettings.backgroundValue || '', currentBackground: storeSettings.currentBackground || { url: storeSettings.backgroundValue || '', type: storeSettings.backgroundType || 'default' }, layoutTemplate: storeSettings.layoutSettings?.layoutTemplate || 'default', customCss: storeSettings.customCss || '', // ✅ إضافة إعدادات القالب theme: theme ? { layout: theme.layout, hasCustomHeader: !!theme.customTemplates?.header, hasCustomFooter: !!theme.customTemplates?.footer, hasUploadedTheme: !!theme.uploadedTheme?.fileUrl } : null, // ✅ إضافة القالب المطبق appliedTemplate: appliedTemplate ? { slug: appliedTemplate.slug, name: appliedTemplate.name, appliedAt: appliedTemplate.appliedAt, hasFullTemplate: appliedTemplate.hasFullTemplate || false, path: appliedTemplate.path } : null }, pages: pages.map(page => ({ _id: page._id, slug: page.slug, title: page.title, type: page.type, isEnabled: page.isEnabled, order: page.order, canEdit: page.canEdit, isCustomized: page.isCustomized, hasContent: !!page.content, hasCss: !!page.css, hasJs: !!page.js })), products: products.map(p => { let avgRating = p.averageRating || 0; if (p.reviews && p.reviews.length > 0 && !avgRating) { const sum = p.reviews.reduce((acc, rev) => acc + rev.rating, 0); avgRating = sum / p.reviews.length; } return { _id: p._id, title: p.title, description: p.description, price: p.price, type: p.type, images: p.images || [], tags: p.tags || [], salesCount: p.salesCount || 0, averageRating: avgRating, reviewsCount: p.reviews?.length || 0, deliveryTime: p.deliveryTime || null }; }), sections: sections.map(section => ({ id: section.id, type: section.type, enabled: section.enabled !== false, order: section.order || 0, title: section.title || null, limit: section.limit || null, layout: section.layout || null, content: section.content || {} })), // ✅ إضافة الـ Shortcodes المتاحة availableShortcodes: availableShortcodes.map(sc => ({ name: sc.name, displayName: sc.displayName, example: `[${sc.name}]` })) }); } catch (error) { console.error('Error fetching public store:', error); res.status(500).json({ error: 'Failed to load store' }); } }); // ============================================ // دالة مساعدة لتوليد محتوى صفحة الاتصال // ============================================ function generateContactContent(storeSettings, user) { const socialLinks = storeSettings?.socialLinks || {}; return `
${socialLinks.instagram ? ` ` : ''} ${socialLinks.facebook ? ` ` : ''} ${socialLinks.twitter ? ` ` : ''}

Send us a message

`; } // ========== 6. دالة مساعدة لتحديث صلاحيات المستخدم ========== async function updateUserPermissions(userId, plan) { const user = await User.findById(userId); if (!user) return; if (!plan) { user.profile.storeEnabled = false; user.profile.maxProducts = 0; user.profile.maxDigitalProducts = 0; user.profile.maxProjects = 0; user.profile.maxServices = 0; user.profile.pageBuilderEnabled = false; user.profile.customDomain = false; user.profile.analyticsEnabled = false; user.profile.prioritySupport = false; user.profile.removeBranding = false; user.profile.teamMembers = 1; user.profile.apiAccess = false; user.profile.customCss = false; user.profile.advancedAnalytics = false; user.profile.exportData = false; } else { user.profile.storeEnabled = plan.features?.storeEnabled || false; user.profile.maxProducts = plan.features?.maxProducts || 0; user.profile.maxDigitalProducts = plan.features?.maxDigitalProducts || 0; user.profile.maxProjects = plan.features?.maxProjects || 0; user.profile.maxServices = plan.features?.maxServices || 0; user.profile.pageBuilderEnabled = plan.features?.pageBuilderEnabled || false; user.profile.customDomain = plan.features?.customDomain || false; user.profile.analyticsEnabled = plan.features?.analyticsEnabled || false; user.profile.prioritySupport = plan.features?.prioritySupport || false; user.profile.removeBranding = plan.features?.removeBranding || false; user.profile.teamMembers = plan.features?.teamMembers || 1; user.profile.apiAccess = plan.features?.apiAccess || false; user.profile.customCss = plan.features?.customCss || false; user.profile.advancedAnalytics = plan.features?.advancedAnalytics || false; user.profile.exportData = plan.features?.exportData || false; } await user.save(); } // ========== 7. سيرفر مهام لتحديث صلاحية الاشتراكات ========== // يمكن تشغيل هذا الـ cron job يومياً const checkExpiredSubscriptions = async () => { const now = new Date(); // العثور على الاشتراكات المنتهية const expired = await UserSubscription.find({ status: 'active', endDate: { $lt: now } }); for (const sub of expired) { sub.status = 'expired'; sub.updatedAt = now; await sub.save(); // تعطيل صلاحيات المستخدم await updateUserPermissions(sub.userId, null); // إشعار المستخدم const user = await User.findById(sub.userId); if (user) { const notification = new Notification({ userId: sub.userId, type: 'subscription_expired', content: 'Your subscription has expired. Please renew to continue accessing premium features.', targetId: `/subscription`, targetType: 'subscription', read: false }); await notification.save(); } } console.log(`Expired ${expired.length} subscriptions`); }; // تشغيل المهمة يومياً (يمكن إضافتها في مكان آخر) setInterval(checkExpiredSubscriptions, 24 * 60 * 60 * 1000); // ============================================ // 🖼️ BACKGROUNDS HELPER FUNCTIONS // ============================================ /** * جلب الخلفيات المتاحة للمستخدم * @param {string} userId - معرف المستخدم * @returns {Array} - قائمة الخلفيات */ async function getAvailableBackgrounds(userId) { // الخلفيات العامة const publicBackgrounds = [ { id: 'bg_purple_abstract', name: 'Purple Abstract', url: 'https://img.magnific.com/premium-photo/blue-sphere-consisting-points-modern-wireframe-elements-technology-grid-sphere-3d-rendering_658411-190.jpg?semt=ais_hybrid&w=740&q=80', category: 'abstract', type: 'image' }, { id: 'bg_glowing_orb', name: 'Glowing Orb', url: 'https://img.magnific.com/premium-photo/glowing-orb-dark-space_303714-9996.jpg?semt=ais_hybrid&w=740&q=80', category: 'abstract', type: 'image' }, { id: 'bg_cosmic_sphere', name: 'Cosmic Sphere', url: 'https://64.media.tumblr.com/cbebea0e22c5e7fb6df960803e9a6bcd/059f55258bab86dd-7d/s2048x3072/02c114e6cfa03fdc04a670fdcbeca9eedff33ba8.pnj', category: 'space', type: 'image' }, { id: 'bg_galaxy_anim', name: 'Galaxy Animation', url: 'https://64.media.tumblr.com/93a523c16ff3501b1fb8b5e69f21165d/tumblr_pad8tumDpI1w6uqh8o1_640.gifv', category: 'space', type: 'gif' }, { id: 'bg_star_field', name: 'Star Field', url: 'https://64.media.tumblr.com/5f5de73c7435cfc6a2455f015325e66a/tumblr_pkt9yhi8oQ1w6uqh8o1_1280.gifv', category: 'space', type: 'gif' }, { id: 'bg_cosmic_dance', name: 'Cosmic Dance', url: 'https://64.media.tumblr.com/70a160a091f547c28dc7f8a834eb24ae/tumblr_pudp7wpAYQ1w6uqh8o1_640.gifv', category: 'space', type: 'gif' }, { id: 'bg_starry_night', name: 'Starry Night', url: 'https://64.media.tumblr.com/da0cf61d7513891c4f8e5d62d07ed1a0/tumblr_mxdx1pDDxh1sso6sco1_500.gif', category: 'space', type: 'gif' }, { id: 'bg_gradient_anim', name: 'Gradient Animation', url: 'https://media.deloitte.com/is/image/deloitte/1920x880_wie-veraendern-sich-die-deutschen-exportstrukturen_promo:720-x-480?defaultImage=default-nonprofile-thumbnail&defaultImageMode=1', category: 'gradient', type: 'image' }, { id: 'bg_color_wave', name: 'Color Wave', url: 'https://i1.sndcdn.com/artworks-000059427896-ueko8g-t500x500.jpg', category: 'gradient', type: 'image' }, { id: 'bg_blue_gradient', name: 'Blue Gradient', url: 'https://media.deloitte.com/is/image/deloitte/ai-in-banking-header-1920-880-1:1200-x-675?$Responsive$&fmt=webp&fit=stretch,1&dpr=off', category: 'gradient', type: 'image' }, { id: 'bg_tech_grid', name: 'Tech Grid', url: 'https://media.deloitte.com/is/image/deloitte/us-variance-in-board-ceo-succession-1920x880:1200-x-675?$Responsive$&fmt=webp&fit=stretch,1&dpr=off', category: 'tech', type: 'image' }, { id: 'bg_digital_network', name: 'Digital Network', url: 'https://miro.medium.com/v2/resize:fit:1400/1*l7Da76q9ZeRbIM1cjiY2Mw.png', category: 'tech', type: 'image' }, { id: 'bg_tech_banner', name: 'Tech Banner', url: 'https://media.deloitte.com/is/image/deloitte/in-industry-banner-gcc-4-1920-x-880:1200-x-627', category: 'tech', type: 'image' }, { id: 'bg_nature_anim', name: 'Nature Animation', url: 'https://www.thisiscolossal.com/wp-content/uploads/2018/04/agif2opt.gif', category: 'nature', type: 'gif' }, { id: 'bg_nature_scene', name: 'Nature Scene', url: 'https://static.wixstatic.com/media/aec810_7a5c7c17a2b5449883ff60fedcc6864e~mv2.gif', category: 'nature', type: 'gif' }, { id: 'bg_nature_flow', name: 'Nature Flow', url: 'https://www.thisiscolossal.com/wp-content/uploads/2018/04/agif5opt.gif', category: 'nature', type: 'gif' } ]; // جلب الخلفيات المخصصة للمستخدم let customBackgrounds = []; if (userId) { const settings = await StoreSettings.findOne({ userId }); if (settings?.customBackgrounds) { customBackgrounds = settings.customBackgrounds; } } return [...publicBackgrounds, ...customBackgrounds]; } /** * تطبيق خلفية على متجر المستخدم */ async function applyBackgroundToStore(userId, background) { const settings = await StoreSettings.findOneAndUpdate( { userId }, { $set: { backgroundType: background.type || 'image', backgroundValue: background.url, 'currentBackground.url': background.url, 'currentBackground.name': background.name || 'Custom Background', 'currentBackground.type': background.type || 'image', 'currentBackground.appliedAt': new Date(), updatedAt: Date.now() }, $push: { backgroundHistory: { url: background.url, name: background.name || 'Custom Background', appliedAt: new Date() } } }, { upsert: true, new: true } ); // أيضاً تحديث theme إذا كان موجود await StoreTheme.findOneAndUpdate( { userId }, { $set: { 'colors.background': background.url, updatedAt: Date.now() } }, { upsert: true } ); return settings; } // ============================================ // CRON JOBS FOR SUBSCRIPTIONS // ============================================ // تشغيل عند بدء السيرفر async function initSubscriptionCronJobs() { console.log('🔄 Initializing subscription cron jobs...'); await checkExpiredSubscriptions(); await sendExpirationReminders(); // تشغيل كل 6 ساعات setInterval(async () => { console.log('🔄 Running subscription maintenance...'); await checkExpiredSubscriptions(); await sendExpirationReminders(); await updateSubscriptionMetrics(); }, 6 * 60 * 60 * 1000); } // إرسال تذكيرات قبل انتهاء الاشتراك async function sendExpirationReminders() { const now = new Date(); const reminderDays = [7, 3, 1]; // قبل 7, 3, 1 يوم for (const days of reminderDays) { const targetDate = new Date(); targetDate.setDate(targetDate.getDate() + days); const startOfDay = new Date(targetDate.setHours(0, 0, 0, 0)); const endOfDay = new Date(targetDate.setHours(23, 59, 59, 999)); const expiringSubscriptions = await UserSubscription.find({ status: 'active', endDate: { $gte: startOfDay, $lte: endOfDay } }).populate('planId').populate('userId'); for (const sub of expiringSubscriptions) { // تجنب إرسال أكثر من تذكير لنفس المستخدم const existingNotification = await Notification.findOne({ userId: sub.userId._id, type: 'subscription_expiring', createdAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } }); if (!existingNotification) { const notification = new Notification({ userId: sub.userId._id, type: 'subscription_expiring', content: `Your subscription to "${sub.planId.name}" will expire in ${days} day${days !== 1 ? 's' : ''}. Renew now to continue enjoying premium features.`, targetId: `/subscription`, targetType: 'subscription', read: false }); await notification.save(); // إرسال إيميل const user = sub.userId; const emailHtml = `

Subscription Expiring Soon

Dear ${user.username},

Your subscription to "${sub.planId.name}" will expire in ${days} day${days !== 1 ? 's' : ''} (${new Date(sub.endDate).toLocaleDateString()}).

Renew now to continue enjoying premium features.

Renew Now → `; await sendStoreEmailWithBrevo(user.email, `Subscription Expiring Soon - ${sub.planId.name}`, emailHtml, ''); } } } } // تحديث إحصائيات الاشتراكات async function updateSubscriptionMetrics() { const today = new Date().toISOString().slice(0, 7); // تسجيل إحصائيات يومية const activeCount = await UserSubscription.countDocuments({ status: 'active' }); const totalRevenue = await SubscriptionEarnings.aggregate([ { $match: { status: 'completed' } }, { $group: { _id: null, total: { $sum: '$amount' } } } ]); console.log(`📊 Subscription Metrics: Active: ${activeCount}, Revenue: ${totalRevenue[0]?.total || 0}`); } // تشغيل cron jobs عند بدء السيرفر initSubscriptionCronJobs(); const fileLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20, message: 'Too many file operations, please try again later.', validate: { trustProxy: false } }); app.use('/api/files', fileLimiter); app.set('view engine', 'ejs'); app.set('views', './views'); app.use(express.static('public')); // Privacy Policy Page app.get('/privacy', (req, res) => { res.render('privacy'); }); // Terms of Service Page app.get('/terms', (req, res) => { res.render('terms'); }); // ============================================ // Sitemap Generator - Full Version // ============================================ // المسار اللي هتحفظ فيه الـ Sitemaps (لو عايز تخدمها كملفات static) const SITEMAP_DIR = path.join(__dirname, 'public'); // التأكد من وجود المجلد if (!fs.existsSync(SITEMAP_DIR)) { fs.mkdirSync(SITEMAP_DIR, { recursive: true }); } // ============================================ // دالة مساعدة لتوليد Sitemap للمتاجر // ============================================ async function generateStoresSitemap() { const today = new Date().toISOString().split('T')[0]; // جلب المتاجر النشطة const stores = await StoreSettings.find({ enabled: true }) .populate('userId', 'profile.nickname username') .sort({ updatedAt: -1 }); let xml = '\n'; xml += '\n'; for (const store of stores) { const nickname = store.userId?.profile?.nickname || store.userId?.username; if (nickname) { xml += ` \n`; xml += ` ${FRONTEND_URL}/shop/${nickname}\n`; xml += ` ${store.updatedAt ? store.updatedAt.toISOString().split('T')[0] : today}\n`; xml += ` daily\n`; xml += ` 0.8\n`; xml += ` \n`; } } xml += ''; // حفظ كملف const filePath = path.join(SITEMAP_DIR, 'sitemap-stores.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-stores.xml (${stores.length} stores)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للمنتجات // ============================================ async function generateProductsSitemap() { const today = new Date().toISOString().split('T')[0]; // جلب المنتجات النشطة (حد أقصى 5000) const products = await Product.find({ isActive: true }) .populate('userId', 'profile.nickname username') .sort({ updatedAt: -1 }) .limit(5000); let xml = '\n'; xml += '\n'; for (const product of products) { const nickname = product.userId?.profile?.nickname || product.userId?.username; if (nickname) { xml += ` \n`; xml += ` ${FRONTEND_URL}/product.html?id=${product._id}&owner=${encodeURIComponent(nickname)}\n`; xml += ` ${product.updatedAt ? product.updatedAt.toISOString().split('T')[0] : today}\n`; xml += ` weekly\n`; xml += ` 0.6\n`; xml += ` \n`; } } xml += ''; // حفظ كملف const filePath = path.join(SITEMAP_DIR, 'sitemap-products.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-products.xml (${products.length} products)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للمستخدمين // ============================================ async function generateUsersSitemap() { const today = new Date().toISOString().split('T')[0]; // جلب المستخدمين العموميين const users = await User.find({ 'profile.isPublic': true }) .select('profile.nickname profile.updatedAt'); let xml = '\n'; xml += '\n'; for (const user of users) { if (user.profile?.nickname) { xml += ` \n`; xml += ` ${FRONTEND_URL}/profile/${user.profile.nickname}\n`; xml += ` ${user.profile.updatedAt ? user.profile.updatedAt.toISOString().split('T')[0] : today}\n`; xml += ` weekly\n`; xml += ` 0.7\n`; xml += ` \n`; } } xml += ''; // حفظ كملف const filePath = path.join(SITEMAP_DIR, 'sitemap-users.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-users.xml (${users.length} users)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للصفحات الثابتة // ============================================ function generateStaticSitemap() { const today = new Date().toISOString().split('T')[0]; // قائمة بكل الصفحات الثابتة من الـ firebase.json const staticPages = [ { loc: '/', changefreq: 'daily', priority: 1.0 }, { loc: '/root.html', changefreq: 'daily', priority: 1.0 }, { loc: '/discover.html', changefreq: 'daily', priority: 1.0 }, { loc: '/feed.html', changefreq: 'daily', priority: 0.9 }, { loc: '/videos.html', changefreq: 'daily', priority: 0.9 }, { loc: '/video.html', changefreq: 'daily', priority: 0.9 }, { loc: '/achievement.html', changefreq: 'daily', priority: 0.9 }, { loc: '/template-store.html', changefreq: 'daily', priority: 0.9 }, { loc: '/explore.html', changefreq: 'daily', priority: 0.9 }, { loc: '/profile.html', changefreq: 'daily', priority: 0.9 }, { loc: '/shop.html', changefreq: 'daily', priority: 0.9 }, { loc: '/jobs.html', changefreq: 'daily', priority: 0.9 }, { loc: '/benchmark.html', changefreq: 'daily', priority: 0.9 }, { loc: '/network.html', changefreq: 'daily', priority: 0.8 }, { loc: '/messages.html', changefreq: 'daily', priority: 0.7 }, { loc: '/product.html', changefreq: 'daily', priority: 0.9 }, { loc: '/cv.html', changefreq: 'weekly', priority: 0.9 }, { loc: '/cv-data.json', changefreq: 'monthly', priority: 0.5 }, { loc: '/article/index.html', changefreq: 'monthly', priority: 0.9 }, { loc: '/checkout.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/order-confirmation.html', changefreq: 'weekly', priority: 0.4 }, { loc: '/my-orders.html', changefreq: 'daily', priority: 0.6 }, { loc: '/settings.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/backgrounds.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/saved.html', changefreq: 'daily', priority: 0.5 }, { loc: '/subscription.html', changefreq: 'daily', priority: 0.8 }, { loc: '/subscription-details.html', changefreq: 'weekly', priority: 0.6 }, { loc: '/pricing.html', changefreq: 'weekly', priority: 0.6 }, { loc: '/project.html', changefreq: 'daily', priority: 0.7 }, { loc: '/project-dashboard-live.html', changefreq: 'daily', priority: 0.6 }, { loc: '/post-job.html', changefreq: 'daily', priority: 0.6 }, { loc: '/my-jobs.html', changefreq: 'daily', priority: 0.6 }, { loc: '/job-applications.html', changefreq: 'daily', priority: 0.6 }, { loc: '/application-details.html', changefreq: 'daily', priority: 0.5 }, { loc: '/edit-job.html', changefreq: 'weekly', priority: 0.4 }, { loc: '/job-details.html', changefreq: 'daily', priority: 0.7 }, { loc: '/job-application.html', changefreq: 'daily', priority: 0.6 }, { loc: '/apply-job.html', changefreq: 'daily', priority: 0.6 }, { loc: '/post.html', changefreq: 'daily', priority: 0.7 }, { loc: '/login.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/register.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/search.html', changefreq: 'daily', priority: 0.7 }, { loc: '/about.html', changefreq: 'monthly', priority: 0.5 }, { loc: '/features.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/success-stories.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/career-advice.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/salary-guide.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/find-candidates.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/recruiting-solutions.html', changefreq: 'weekly', priority: 0.5 }, { loc: '/help.html', changefreq: 'weekly', priority: 0.4 }, { loc: '/privacy.html', changefreq: 'monthly', priority: 0.3 }, { loc: '/terms.html', changefreq: 'monthly', priority: 0.3 }, { loc: '/data-deletion.html', changefreq: 'monthly', priority: 0.2 }, { loc: '/admin/', changefreq: 'daily', priority: 0.4 }, { loc: '/auth/callback.html', changefreq: 'yearly', priority: 0.1 }, { loc: '/article/', changefreq: 'daily', priority: 0.6 } ]; let xml = '\n'; xml += '\n'; for (const page of staticPages) { xml += ` \n`; xml += ` ${FRONTEND_URL}${page.loc}\n`; xml += ` ${today}\n`; xml += ` ${page.changefreq}\n`; xml += ` ${page.priority}\n`; xml += ` \n`; } xml += ''; // حفظ كملف const filePath = path.join(SITEMAP_DIR, 'sitemap-static.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-static.xml (${staticPages.length} pages)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للمنشورات (Posts) - النسخة النهائية // ============================================ async function generatePostsSitemap() { const today = new Date().toISOString().split('T')[0]; // جلب المنشورات العامة (public و followers) - آخر 10,000 منشور const posts = await Post.find({ visibility: { $in: ['public', 'followers'] } }) .populate('userId', 'username profile.nickname') .select('_id content images createdAt updatedAt userId') .sort({ updatedAt: -1 }) .limit(10000) .lean(); // ✅ أضف lean() عشان تقليل الذاكرة let xml = '\n'; xml += ']*>/g, '') .replace(/&/g, 'and') .replace(/\s+/g, ' ') .trim(); } let newsTitle = plainContent.substring(0, 100); if (newsTitle.length === 100) newsTitle += '...'; const postUrl = `${FRONTEND_URL}/post.html?id=${post._id}`; xml += ` \n`; xml += ` ${escapeXml(postUrl)}\n`; xml += ` ${lastmod}\n`; xml += ` weekly\n`; xml += ` 0.7\n`; // ✅ إضافة علامات News Sitemap للمنشورات الحديثة (آخر 48 ساعة) const twoDaysAgo = new Date(); twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); if (post.createdAt && new Date(post.createdAt) > twoDaysAgo && newsTitle) { xml += ` \n`; xml += ` \n`; xml += ` MGZon Social Feed\n`; xml += ` en\n`; xml += ` \n`; xml += ` ${publishedISO}\n`; xml += ` ${escapeXml(newsTitle)}\n`; xml += ` \n`; } // ✅ إضافة الصور الموجودة في المنشور (مع validation أقوى) if (post.images && Array.isArray(post.images) && post.images.length > 0) { for (const img of post.images.slice(0, 10)) { let imageUrl = null; if (typeof img === 'string') imageUrl = img; else if (img && typeof img === 'object' && img.url) imageUrl = img.url; if (imageUrl && typeof imageUrl === 'string' && imageUrl.startsWith('http')) { xml += ` \n`; xml += ` ${escapeXml(imageUrl)}\n`; xml += ` \n`; } } } xml += ` \n`; } xml += ''; const filePath = path.join(SITEMAP_DIR, 'sitemap-posts.xml'); fs.writeFileSync(filePath, xml, 'utf8'); const imageCount = (xml.match(//g) || []).length; console.log(`✅ Saved: sitemap-posts.xml (${posts.length} posts, ${imageCount} images)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للوظائف (Jobs) - النسخة النهائية // ============================================ async function generateJobsSitemap() { const today = new Date().toISOString().split('T')[0]; // جلب الوظائف النشطة فقط (لم تنته صلاحيتها) const jobs = await Job.find({ status: 'active', deadline: { $gt: new Date() } }) .populate('employerId', 'username profile.nickname') .select('_id title createdAt updatedAt location jobType') .sort({ updatedAt: -1 }) .limit(5000) .lean(); let xml = '\n'; // ✅ فقط الـ namespaces اللي محتاجها فعلاً xml += '\n'; for (const job of jobs) { const lastmod = job.updatedAt ? job.updatedAt.toISOString().split('T')[0] : today; const jobUrl = `${FRONTEND_URL}/job-details.html?id=${job._id}`; xml += ` \n`; xml += ` ${escapeXml(jobUrl)}\n`; xml += ` ${lastmod}\n`; xml += ` daily\n`; xml += ` 0.7\n`; xml += ` \n`; } xml += ''; const filePath = path.join(SITEMAP_DIR, 'sitemap-jobs.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-jobs.xml (${jobs.length} jobs)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للقوالب (Templates) - النسخة النهائية // ============================================ async function generateTemplatesSitemap() { const today = new Date().toISOString().split('T')[0]; const templates = await TemplateStore.find({ isActive: true }) .select('_id name previewImage updatedAt') .sort({ downloadsCount: -1 }) .limit(2000) .lean(); let xml = '\n'; xml += '\n`; xml += ` ${escapeXml(templateUrl)}\n`; xml += ` ${lastmod}\n`; xml += ` weekly\n`; xml += ` 0.6\n`; if (template.previewImage) { xml += ` \n`; xml += ` ${escapeXml(template.previewImage)}\n`; xml += ` \n`; } xml += ` \n`; } xml += ''; const filePath = path.join(SITEMAP_DIR, 'sitemap-templates.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-templates.xml (${templates.length} templates)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للصفحات المخصصة - النسخة النهائية // ============================================ async function generatePagesSitemap() { const today = new Date().toISOString().split('T')[0]; const pages = await Page.find({ isEnabled: true, visibility: 'public' }) .populate('storeId', 'profile.nickname username') .select('_id slug updatedAt storeId') .sort({ updatedAt: -1 }) .lean(); let xml = '\n'; xml += '\n'; for (const page of pages) { const lastmod = page.updatedAt ? page.updatedAt.toISOString().split('T')[0] : today; const storeNickname = page.storeId?.profile?.nickname || page.storeId?.username || 'store'; const pageUrl = `${FRONTEND_URL}/shop/${encodeURIComponent(storeNickname)}/page/${page.slug}`; xml += ` \n`; xml += ` ${escapeXml(pageUrl)}\n`; xml += ` ${lastmod}\n`; xml += ` weekly\n`; xml += ` 0.5\n`; xml += ` \n`; } xml += ''; const filePath = path.join(SITEMAP_DIR, 'sitemap-pages.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-pages.xml (${pages.length} custom pages)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للوسوم - النسخة النهائية // ============================================ async function generateTagsSitemap() { const today = new Date().toISOString().split('T')[0]; // جلب الوسوم الأكثر استخداماً من المنشورات const tags = await Post.aggregate([ { $match: { visibility: { $in: ['public', 'followers'] } } }, { $unwind: { path: '$tags', preserveNullAndEmptyArrays: false } }, { $group: { _id: '$tags', count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 500 } ]); let xml = '\n'; xml += '\n'; for (const tag of tags) { const tagName = encodeURIComponent(tag._id); const tagUrl = `${FRONTEND_URL}/search.html?q=${tagName}&type=posts`; xml += ` \n`; xml += ` ${escapeXml(tagUrl)}\n`; xml += ` ${today}\n`; xml += ` weekly\n`; xml += ` 0.4\n`; xml += ` \n`; } xml += ''; const filePath = path.join(SITEMAP_DIR, 'sitemap-tags.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-tags.xml (${tags.length} tags)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للصور - النسخة النهائية // ============================================ async function generateImageSitemap() { const today = new Date().toISOString().split('T')[0]; let xml = '\n'; xml += '\n ${escapeXml(pageUrl)}\n ${product.updatedAt ? product.updatedAt.toISOString().split('T')[0] : today}\n`; for (const imageUrl of product.images.slice(0, 10)) { if (imageUrl && typeof imageUrl === 'string' && imageUrl.startsWith('http')) { hasImages = true; totalImages++; urlBlock += ` \n ${escapeXml(imageUrl)}\n \n`; } } if (hasImages) { urlBlock += ` \n`; xml += urlBlock; totalPages++; } } // 2️⃣ صور المنشورات const posts = await Post.find({ 'images.0': { $exists: true }, visibility: { $in: ['public', 'followers'] } }).select('_id images updatedAt').limit(5000).lean(); for (const post of posts) { if (!post.images || post.images.length === 0) continue; const pageUrl = `${FRONTEND_URL}/post.html?id=${post._id}`; let hasImages = false; let urlBlock = ` \n ${escapeXml(pageUrl)}\n ${post.updatedAt ? post.updatedAt.toISOString().split('T')[0] : today}\n`; for (const img of post.images.slice(0, 10)) { let imageUrl = null; if (typeof img === 'string') imageUrl = img; else if (img && typeof img === 'object' && img.url) imageUrl = img.url; if (imageUrl && typeof imageUrl === 'string' && imageUrl.startsWith('http')) { hasImages = true; totalImages++; urlBlock += ` \n ${escapeXml(imageUrl)}\n \n`; } } if (hasImages) { urlBlock += ` \n`; xml += urlBlock; totalPages++; } } xml += ''; const filePath = path.join(SITEMAP_DIR, 'sitemap-images.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-images.xml (${totalImages} images, ${totalPages} pages)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap للتعليقات - النسخة النهائية (اختياري) // ============================================ async function generateCommentsSitemapLight() { const today = new Date().toISOString().split('T')[0]; // ✅ جلب بس التعليقات اللي عليها تفاعل (likes أو replies) const comments = await Comment.find({ hidden: false, visibility: 'public', $or: [ { likes: { $exists: true, $not: { $size: 0 } } }, { replies: { $exists: true, $not: { $size: 0 } } } ] }) .select('_id projectId timestamp') .sort({ likesCount: -1 }) .limit(2000) // ✅ حد أقل عشان السرعة .lean(); let xml = '\n'; xml += '\n'; for (const comment of comments) { if (!comment.projectId) continue; const lastmod = comment.timestamp ? comment.timestamp.toISOString().split('T')[0] : today; const commentUrl = `${FRONTEND_URL}/project.html?id=${comment.projectId}&comment=${comment._id}`; xml += ` \n`; xml += ` ${escapeXml(commentUrl)}\n`; xml += ` ${lastmod}\n`; xml += ` monthly\n`; xml += ` 0.3\n`; xml += ` \n`; } xml += ''; const filePath = path.join(SITEMAP_DIR, 'sitemap-comments.xml'); fs.writeFileSync(filePath, xml, 'utf8'); console.log(`✅ Saved: sitemap-comments.xml (${comments.length} comments)`); return xml; } // ============================================ // دالة مساعدة لتوليد Sitemap الرئيسي (index) // ============================================ function generateMainSitemapIndex() { const today = new Date().toISOString().split('T')[0]; const sitemap = ` ${FRONTEND_URL}/sitemap-static.xml ${today} ${FRONTEND_URL}/sitemap-stores.xml ${today} ${FRONTEND_URL}/sitemap-products.xml ${today} ${FRONTEND_URL}/sitemap-users.xml ${today} ${FRONTEND_URL}/sitemap-posts.xml ${today} ${FRONTEND_URL}/sitemap-comments.xml ${today} ${FRONTEND_URL}/sitemap-jobs.xml ${today} ${FRONTEND_URL}/sitemap-templates.xml ${today} ${FRONTEND_URL}/sitemap-pages.xml ${today} ${FRONTEND_URL}/sitemap-images.xml ${today} ${FRONTEND_URL}/sitemap-tags.xml ${today} ${FRONTEND_URL}/annotations.xml ${today} ${FRONTEND_URL}/context.xml ${today} `; const filePath = path.join(SITEMAP_DIR, 'sitemap.xml'); fs.writeFileSync(filePath, sitemap, 'utf8'); console.log(`✅ Saved: sitemap.xml (index with ${sitemap.match(//g)?.length || 0} sub-sitemaps)`); return sitemap; } // ============================================ // دالة مساعدة لتوليد robots.txt // ============================================ function generateRobotsTxt() { const robots = `User-agent: * Allow: / Disallow: /api/ Disallow: /admin/ Disallow: /settings Disallow: /login Disallow: /register Disallow: /logout Disallow: /reset-password Disallow: /auth/ # Allow search engines to find important content Allow: /shop/ Allow: /product.html Allow: /post.html Allow: /job-details.html Allow: /profile/ Allow: /template-store.html Allow: /ibrahim_al_asfar.ged # Crawl delay (be nice to search engines) Crawl-delay: 1 # Sitemaps Sitemap: ${FRONTEND_URL}/sitemap.xml Sitemap: ${FRONTEND_URL}/sitemap-static.xml Sitemap: ${FRONTEND_URL}/sitemap-stores.xml Sitemap: ${FRONTEND_URL}/sitemap-products.xml Sitemap: ${FRONTEND_URL}/sitemap-users.xml Sitemap: ${FRONTEND_URL}/sitemap-posts.xml Sitemap: ${FRONTEND_URL}/sitemap-jobs.xml Sitemap: ${FRONTEND_URL}/sitemap-templates.xml Sitemap: ${FRONTEND_URL}/sitemap-pages.xml Sitemap: ${FRONTEND_URL}/sitemap-images.xml Sitemap: ${FRONTEND_URL}/sitemap-comments.xml Sitemap: ${FRONTEND_URL}/sitemap-tags.xml`; const filePath = path.join(SITEMAP_DIR, 'robots.txt'); fs.writeFileSync(filePath, robots, 'utf8'); console.log(`✅ Saved: robots.txt`); return robots; } // ============================================ // الدالة الرئيسية لتوليد كل الـ Sitemaps // ============================================ async function regenerateAllSitemaps() { const startTime = Date.now(); console.log('🔄 Starting sitemap regeneration...'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); try { // توليد كل الـ Sitemaps بالتوازي (أسرع) const results = await Promise.all([ generateStaticSitemap(), generateStoresSitemap(), generateProductsSitemap(), generateUsersSitemap(), generatePostsSitemap(), generateJobsSitemap(), generateTemplatesSitemap(), generatePagesSitemap(), generateTagsSitemap(), generateImageSitemap(), generateCommentsSitemapLight(), generateMainSitemapIndex(), Promise.resolve(generateRobotsTxt()) ]); const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(`✅ All sitemaps regenerated successfully in ${elapsed}s!`); console.log(` - Posts: ${results[4]?.match(//g)?.length || 0} URLs`); console.log(` - Jobs: ${results[5]?.match(//g)?.length || 0} URLs`); console.log(` - Templates: ${results[6]?.match(//g)?.length || 0} URLs`); console.log(` - Pages: ${results[7]?.match(//g)?.length || 0} URLs`); console.log(` - Tags: ${results[8]?.match(//g)?.length || 0} URLs`); // تسجيل في الـ logs const logEntry = { timestamp: new Date().toISOString(), elapsed: `${elapsed}s`, success: true, counts: { posts: results[4]?.match(//g)?.length || 0, jobs: results[5]?.match(//g)?.length || 0, templates: results[6]?.match(//g)?.length || 0 } }; const logPath = path.join(SITEMAP_DIR, 'sitemap-logs.json'); let logs = []; if (fs.existsSync(logPath)) { logs = JSON.parse(fs.readFileSync(logPath, 'utf8')); } logs.unshift(logEntry); if (logs.length > 100) logs.pop(); // احتفظ بآخر 100 تسجيل فقط fs.writeFileSync(logPath, JSON.stringify(logs, null, 2)); return true; } catch (error) { console.error('❌ Error regenerating sitemaps:', error); // تسجيل الخطأ const errorLogPath = path.join(SITEMAP_DIR, 'sitemap-error.log'); const errorEntry = `[${new Date().toISOString()}] ${error.message}\n${error.stack}\n\n`; fs.appendFileSync(errorLogPath, errorEntry); return false; } } // ============================================ // تشغيل التوليد التلقائي كل 24 ساعة // ============================================ // تشغيل لأول مرة عند بدء السيرفر setTimeout(() => { regenerateAllSitemaps(); }, 5000); // بعد 5 ثواني من بدء السيرفر // جدولة التشغيل كل 24 ساعة const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; setInterval(() => { console.log('⏰ Running scheduled sitemap regeneration...'); regenerateAllSitemaps(); }, TWENTY_FOUR_HOURS); // ============================================ // APIs لخدمة الـ Sitemaps كملفات static (أسرع من dynamic) // ============================================ // خدمة sitemap.xml من الملف الـ static app.get('/sitemap.xml', (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { // لو الملف مش موجود، يولده على طول generateMainSitemapIndex().then(xml => { res.header('Content-Type', 'application/xml'); res.send(xml); }); } }); // خدمة sitemap-static.xml app.get('/sitemap-static.xml', (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-static.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { generateStaticSitemap().then(xml => { res.header('Content-Type', 'application/xml'); res.send(xml); }); } }); // خدمة sitemap-stores.xml app.get('/sitemap-stores.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-stores.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { const xml = await generateStoresSitemap(); res.header('Content-Type', 'application/xml'); res.send(xml); } }); // خدمة sitemap-products.xml app.get('/sitemap-products.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-products.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { const xml = await generateProductsSitemap(); res.header('Content-Type', 'application/xml'); res.send(xml); } }); // خدمة sitemap-users.xml app.get('/sitemap-users.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-users.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { const xml = await generateUsersSitemap(); res.header('Content-Type', 'application/xml'); res.send(xml); } }); app.get('/sitemap-images.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-images.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { const xml = await generateImageSitemap(); res.header('Content-Type', 'application/xml'); res.send(xml); } }); app.get('/sitemap-comments.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-comments.xml'); if (fs.existsSync(filePath)) { res.setHeader('Content-Type', 'application/xml'); return res.sendFile(filePath); } try { const xml = await generateCommentsSitemapLight(); res.setHeader('Content-Type', 'application/xml'); res.send(xml); } catch (error) { res.status(500).send('Error generating sitemap'); } }); // خدمة sitemap-posts.xml app.get('/sitemap-posts.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-posts.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { const xml = await generatePostsSitemap(); res.header('Content-Type', 'application/xml'); res.send(xml); } }); // خدمة sitemap-jobs.xml app.get('/sitemap-jobs.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-jobs.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { const xml = await generateJobsSitemap(); res.header('Content-Type', 'application/xml'); res.send(xml); } }); // خدمة sitemap-templates.xml app.get('/sitemap-templates.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-templates.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { const xml = await generateTemplatesSitemap(); res.header('Content-Type', 'application/xml'); res.send(xml); } }); // خدمة sitemap-pages.xml app.get('/sitemap-pages.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-pages.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { const xml = await generatePagesSitemap(); res.header('Content-Type', 'application/xml'); res.send(xml); } }); // خدمة sitemap-tags.xml app.get('/sitemap-tags.xml', async (req, res) => { const filePath = path.join(SITEMAP_DIR, 'sitemap-tags.xml'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'application/xml'); res.sendFile(filePath); } else { const xml = await generateTagsSitemap(); res.header('Content-Type', 'application/xml'); res.send(xml); } }); // خدمة robots.txt app.get('/robots.txt', (req, res) => { const filePath = path.join(SITEMAP_DIR, 'robots.txt'); if (fs.existsSync(filePath)) { res.header('Content-Type', 'text/plain'); res.sendFile(filePath); } else { const robots = generateRobotsTxt(); res.header('Content-Type', 'text/plain'); res.send(robots); } }); // ============================================ // API لإعادة توليد الـ Sitemaps يدوياً (Admin فقط) // ============================================ app.post('/api/admin/regenerate-sitemaps', authenticateToken, async (req, res) => { try { // التحقق من صلاحيات الأدمن const admin = await User.findById(req.user.userId); if (!admin || !admin.isAdmin) { return res.status(403).json({ error: 'Admin access required' }); } const success = await regenerateAllSitemaps(); if (success) { res.json({ success: true, message: 'Sitemaps regenerated successfully', timestamp: new Date().toISOString() }); } else { res.status(500).json({ error: 'Failed to regenerate sitemaps' }); } } catch (error) { console.error('Error manually regenerating sitemaps:', error); res.status(500).json({ error: 'Failed to regenerate sitemaps' }); } }); // Serve static verification files app.get('/google620570ce87abd87a.html', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'google620570ce87abd87a.html')); }); app.get('/BingSiteAuth.xml', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'BingSiteAuth.xml')); }); app.get('/yandex_b820fb59d7fe880e.html', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'yandex_b820fb59d7fe880e.html')); }); module.exports = { sendEmailWithBrevo, sendStoreEmailWithBrevo, Page, SubscriptionPlan, PlatformPaymentMethod, UserSubscription, SubscriptionSettings, SubscriptionEarnings, Earnings, Coupon, PlatformBank, StoreFollow, Cart, LayoutPreset, User, Product, Order, Review, AIConversation, Skill, Conversation, Message, StoreTheme, StoreSettings, Follow, Notification, SiteSettings, Project, Comment, MessageReport, Report, Post, Job, JobApplication, JobSave, JobCategory, Story, StoreFollow, Rating, ProfileAnalytics, AlsoViewed, PageSuggestion }; // Serve profile image and logos app.use('/images', express.static('public/images')); app.get('/', (req, res) => { res.render('index'); }); // ============================================ // 🚀 GRAPHQL SETUP // ============================================ const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@as-integrations/express4'); const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default'); const typeDefs = require('./graphql/schema'); const resolvers = require('./graphql/resolvers'); const context = require('./graphql/context'); // إنشاء GraphQL Server const graphqlServer = new ApolloServer({ typeDefs, resolvers, introspection: true, // ← خليها true عشان تظهر الواجهة plugins: [ ApolloServerPluginLandingPageLocalDefault({ embed: true }) // ← السطر الجديد ], formatError: (formattedError) => { console.error('GraphQL Error:', formattedError); return formattedError; } }); // بدء GraphQL Server (انتظار async) const startGraphQL = async () => { await graphqlServer.start(); console.log('✅ GraphQL Server started'); // تطبيق GraphQL Middleware app.use( '/graphql', express.json(), expressMiddleware(graphqlServer, { context: async ({ req }) => context({ req }) }) ); console.log('🚀 GraphQL Server running on /graphql'); }; // تشغيل GraphQL startGraphQL().catch(err => { console.error('❌ Failed to start GraphQL Server:', err); }); const server = app.listen(PORT, () => { logger.info(`🚀 Server running on port ${PORT}`); logger.info(`📡 Timeout set to: ${server.timeout / 1000} seconds`); }); // Increase timeouts for large file uploads (themes, etc.) server.timeout = 120000; // 120 seconds - reading timeout server.keepAliveTimeout = 120000; // 120 seconds - keep-alive server.headersTimeout = 120000; // 120 seconds - headers timeout // Graceful shutdown process.on('SIGTERM', () => { logger.info('SIGTERM signal received: closing HTTP server'); server.close(() => { logger.info('HTTP server closed'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT signal received: closing HTTP server'); server.close(() => { logger.info('HTTP server closed'); process.exit(0); }); });