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 = ` `; // تحويل 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}`, `
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}`, `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