MGZON commited on
Commit
143e20b
·
verified ·
1 Parent(s): 0408221

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +87 -176
server.js CHANGED
@@ -448,125 +448,15 @@ const userSchema = new mongoose.Schema({
448
  otpExpires: Date,
449
  refreshTokens: [{ token: String, createdAt: { type: Date, default: Date.now } }],
450
  notifications: [{ type: String }],
451
- profile: {
452
- nickname: { type: String, sparse: true },
453
- avatar: String,
454
- status: { type: String, default: 'Available', enum: ['Available', 'Busy', 'Open to Work'] },
455
- jobTitle: String,
456
- pdfFormat: { type: String, enum: ['jspdf', 'canva', 'template1', 'template2'], default: 'jspdf' },
457
- bio: String,
458
- phone: { type: String, default: '' },
459
- socialLinks: {
460
- linkedin: { type: String, default: '' },
461
- behance: { type: String, default: '' },
462
- github: { type: String, default: '' },
463
- whatsapp: { type: String, default: '' }
464
- },
465
- education: [{ institution: String, degree: String, year: String }],
466
- experience: [{ company: String, role: String, duration: String }],
467
- certificates: [{ name: String, issuer: String, year: String }],
468
- skills: [{ name: String, percentage: Number }],
469
- projects: [
470
- {
471
- isPrivate: { type: Boolean, default: false },
472
- title: String,
473
- description: String,
474
- image: String,
475
- rating: String,
476
- stars: { type: Number, min: 0, max: 5 },
477
- isPublic: { type: Boolean, default: true },
478
- links: [{ option: String, value: String }]
479
- }
480
- ],
481
- githubRepos: [
482
- {
483
- id: String,
484
- name: String,
485
- description: String,
486
- url: String,
487
- image: String
488
- }
489
- ],
490
-
491
- theme: {
492
- id: { type: String, default: 'default' },
493
- primaryColor: { type: String, default: '#3b82f6' },
494
- secondaryColor: { type: String, default: '#8b5cf6' },
495
- fontFamily: { type: String, default: 'Inter' },
496
- borderRadius: { type: String, default: '0.5rem' },
497
- },
498
-
499
- // ✅ إضافة إعدادات التخطيط (Layout)
500
- layout: {
501
- type: { type: String, enum: ['grid', 'list', 'masonry'], default: 'grid' },
502
- columns: { type: Number, default: 3 },
503
- showProjectImages: { type: Boolean, default: true },
504
- showProjectDescriptions: { type: Boolean, default: true },
505
- showProjectRatings: { type: Boolean, default: true },
506
- showProjectLinks: { type: Boolean, default: true },
507
- },
508
-
509
- // ✅ إضافة إعدادات الهيدر
510
- header: {
511
- showAvatar: { type: Boolean, default: true },
512
- showJobTitle: { type: Boolean, default: true },
513
- showBio: { type: Boolean, default: true },
514
- showContactInfo: { type: Boolean, default: true },
515
- showSocialLinks: { type: Boolean, default: true },
516
- layout: { type: String, enum: ['centered', 'left-aligned'], default: 'centered' },
517
- },
518
-
519
- // ✅ إضافة إعدادات الفوتر
520
- footer: {
521
- showCopyright: { type: Boolean, default: true },
522
- customText: { type: String, default: '' },
523
- },
524
-
525
- // ✅ إضافة إعدادات SEO
526
- seo: {
527
- title: { type: String, default: '' },
528
- description: { type: String, default: '' },
529
- keywords: { type: String, default: '' },
530
- ogImage: { type: String, default: '' },
531
- ogTitle: { type: String, default: '' },
532
- ogDescription: { type: String, default: '' },
533
- twitterCard: { type: String, enum: ['summary', 'summary_large_image', 'app', 'player'], default: 'summary_large_image' },
534
- twitterSite: { type: String, default: '' },
535
- canonicalUrl: { type: String, default: '' },
536
- noindex: { type: Boolean, default: false },
537
- nofollow: { type: Boolean, default: false },
538
- },
539
-
540
- // ✅ إضافة إعدادات Schema.org
541
- schema: {
542
- type: { type: String, enum: ['Person', 'Organization', 'ProfessionalService', 'LocalBusiness'], default: 'Person' },
543
- name: { type: String, default: '' },
544
- description: { type: String, default: '' },
545
- image: { type: String, default: '' },
546
- sameAs: [{ type: String }],
547
- jobTitle: { type: String, default: '' },
548
- worksFor: { type: String, default: '' },
549
- alumniOf: [{ type: String }],
550
- knowsAbout: [{ type: String }],
551
- },
552
- // canvaAccessToken: String,
553
- // canvaRefreshToken: String,
554
-
555
-
556
-
557
- customFields: [{ name: String, value: String }],
558
- interests: [String],
559
- isPublic: { type: Boolean, default: true },
560
- avatarDisplayType: { type: String, enum: ['svg', 'normal'], default: 'normal' },
561
- svgColor: { type: String, default: '#000000' },
562
- portfolioName: { type: String, default: 'Portfolio' },
563
- pushNotifications: { type: Boolean, default: false }
564
  }
565
  },
566
  {
567
  minimize: false,
568
- toJSON: { getters: false, virtuals: false },
569
- toObject: { getters: false, virtuals: false }
570
 
571
  });
572
 
@@ -1512,15 +1402,31 @@ async function authenticateToken(req, res, next) {
1512
 
1513
 
1514
  app.get('/api/verify-token', authenticateToken, async (req, res) => {
1515
- const user = await User.findById(req.user.userId);
1516
- if (!user) return res.status(404).json({ error: 'User not found' });
1517
- res.json({
1518
- valid: true,
1519
- userId: req.user.userId,
1520
- isAdmin: req.user.isAdmin,
1521
- username: user.username,
1522
- profile: user.profile
1523
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1524
  });
1525
 
1526
  app.post('/api/logout', authenticateToken, async (req, res) => {
@@ -2341,7 +2247,7 @@ app.delete('/api/skills/:skillId', authenticateToken, isAdmin, async (req, res)
2341
  // تحديث endpoint /api/profile/me
2342
  app.get('/api/profile/me', authenticateToken, async (req, res) => {
2343
  try {
2344
- // ✅ استخدم lean() لتجنب مشاكل Mongoose getters
2345
  const user = await User.findById(req.user.userId)
2346
  .select('username profile')
2347
  .lean();
@@ -2350,39 +2256,34 @@ app.get('/api/profile/me', authenticateToken, async (req, res) => {
2350
  return res.status(404).json({ error: 'المستخدم غير موجود' });
2351
  }
2352
 
2353
- // ✅ تأكد من وجود profile
2354
- if (!user.profile) {
2355
- user.profile = {};
2356
- }
2357
-
2358
- // ✅ القيم الافتراضية للـ profile
2359
- const profile = {
2360
- portfolioName: user.profile.portfolioName || 'Portfolio',
2361
- nickname: user.profile.nickname || '',
2362
- jobTitle: user.profile.jobTitle || '',
2363
- bio: user.profile.bio || '',
2364
- phone: user.profile.phone || '',
2365
- isPublic: user.profile.isPublic ?? true,
2366
- status: user.profile.status || 'Available',
2367
- socialLinks: user.profile.socialLinks || { linkedin: '', behance: '', github: '', whatsapp: '' },
2368
- avatar: user.profile.avatar || '',
2369
- avatarDisplayType: user.profile.avatarDisplayType || 'normal',
2370
- svgColor: user.profile.svgColor || '#000000',
2371
- pdfFormat: user.profile.pdfFormat || 'jspdf',
2372
- education: user.profile.education || [],
2373
- experience: user.profile.experience || [],
2374
- certificates: user.profile.certificates || [],
2375
- skills: user.profile.skills || [],
2376
- projects: user.profile.projects || [],
2377
- interests: user.profile.interests || [],
2378
- theme: user.profile.theme || {
2379
  id: 'default',
2380
  primaryColor: '#3b82f6',
2381
  secondaryColor: '#8b5cf6',
2382
  fontFamily: 'Inter',
2383
  borderRadius: '0.5rem',
2384
  },
2385
- layout: user.profile.layout || {
2386
  type: 'grid',
2387
  columns: 3,
2388
  showProjectImages: true,
@@ -2390,7 +2291,7 @@ app.get('/api/profile/me', authenticateToken, async (req, res) => {
2390
  showProjectRatings: true,
2391
  showProjectLinks: true,
2392
  },
2393
- header: user.profile.header || {
2394
  showAvatar: true,
2395
  showJobTitle: true,
2396
  showBio: true,
@@ -2398,11 +2299,11 @@ app.get('/api/profile/me', authenticateToken, async (req, res) => {
2398
  showSocialLinks: true,
2399
  layout: 'centered',
2400
  },
2401
- footer: user.profile.footer || {
2402
  showCopyright: true,
2403
  customText: '',
2404
  },
2405
- seo: user.profile.seo || {
2406
  title: '',
2407
  description: '',
2408
  keywords: '',
@@ -2415,7 +2316,7 @@ app.get('/api/profile/me', authenticateToken, async (req, res) => {
2415
  noindex: false,
2416
  nofollow: false,
2417
  },
2418
- schema: user.profile.schema || {
2419
  type: 'Person',
2420
  name: '',
2421
  description: '',
@@ -2426,15 +2327,19 @@ app.get('/api/profile/me', authenticateToken, async (req, res) => {
2426
  alumniOf: [],
2427
  knowsAbout: [],
2428
  },
2429
- pushNotifications: user.profile.pushNotifications || false
2430
  };
2431
 
 
 
 
2432
  res.json({
2433
  username: user.username,
2434
  profile: profile
2435
  });
 
2436
  } catch (error) {
2437
- logger.error(`Error fetching profile: ${error.message}`);
2438
  res.status(500).json({ error: 'خطأ في استرجاع الملف الشخصي' });
2439
  }
2440
  });
@@ -2477,21 +2382,28 @@ app.get('/api/profile/:nickname', async (req, res) => {
2477
  try {
2478
  const decodedNickname = decodeURIComponent(req.params.nickname);
2479
 
2480
- // 1. جلب المستخدم مع البروفايل
2481
  const user = await User.findOne({
2482
  $or: [
2483
  { 'profile.nickname': { $regex: `^${decodedNickname}$`, $options: 'i' } },
2484
  { username: { $regex: `^${decodedNickname}$`, $options: 'i' } },
2485
  ],
2486
- }).select('username profile notifications');
 
 
2487
 
2488
  if (!user) {
2489
  logger.warn(`Profile not found for nickname: ${decodedNickname}`);
2490
  return res.status(404).json({ error: `Profile not found for ${decodedNickname}` });
2491
  }
2492
 
2493
- // 2. التحقق من الخصوصية
2494
- const isOwner = req.user && req.user.userId === user._id.toString();
 
 
 
 
 
2495
 
2496
  if (!user.profile.isPublic && !isOwner) {
2497
  logger.warn(`Unauthorized access attempt to private profile: ${decodedNickname}`);
@@ -2501,7 +2413,7 @@ app.get('/api/profile/:nickname', async (req, res) => {
2501
  // 3. جلب المشاريع من Project collection
2502
  const projectsQuery = { userId: user._id };
2503
  if (!isOwner) {
2504
- projectsQuery.isPublic = true; // غير المالك يشوف العامة فقط
2505
  }
2506
 
2507
  const projects = await Project.find(projectsQuery)
@@ -2509,11 +2421,11 @@ app.get('/api/profile/:nickname', async (req, res) => {
2509
  .sort({ createdAt: -1 })
2510
  .lean();
2511
 
2512
- // 4. تجهيز الرد مع كل البيانات بما في ذلك المظهر و SEO
2513
  const response = {
2514
  username: user.username,
2515
  profile: {
2516
- // المعلومات الأساسية (موجودة)
2517
  nickname: user.profile.nickname || user.username,
2518
  portfolioName: user.profile.portfolioName || 'Portfolio',
2519
  avatar: user.profile.avatar || '/assets/img/default-avatar.png',
@@ -2544,7 +2456,7 @@ app.get('/api/profile/:nickname', async (req, res) => {
2544
  skills: user.profile.skills || [],
2545
  interests: user.profile.interests || [],
2546
 
2547
- // إعدادات المظهر (الجديدة)
2548
  theme: user.profile.theme || {
2549
  id: 'default',
2550
  primaryColor: '#3b82f6',
@@ -2553,7 +2465,7 @@ app.get('/api/profile/:nickname', async (req, res) => {
2553
  borderRadius: '0.5rem',
2554
  },
2555
 
2556
- // إعدادات التخطيط (الجديدة)
2557
  layout: user.profile.layout || {
2558
  type: 'grid',
2559
  columns: 3,
@@ -2563,7 +2475,7 @@ app.get('/api/profile/:nickname', async (req, res) => {
2563
  showProjectLinks: true,
2564
  },
2565
 
2566
- // إعدادات الهيدر (الجديدة)
2567
  header: user.profile.header || {
2568
  showAvatar: true,
2569
  showJobTitle: true,
@@ -2573,13 +2485,13 @@ app.get('/api/profile/:nickname', async (req, res) => {
2573
  layout: 'centered',
2574
  },
2575
 
2576
- // إعدادات الفوتر (الجديدة)
2577
  footer: user.profile.footer || {
2578
  showCopyright: true,
2579
  customText: '',
2580
  },
2581
 
2582
- // إعدادات SEO (الجديدة)
2583
  seo: user.profile.seo || {
2584
  title: user.profile.portfolioName || 'My Portfolio',
2585
  description: user.profile.bio || '',
@@ -2594,7 +2506,7 @@ app.get('/api/profile/:nickname', async (req, res) => {
2594
  nofollow: false,
2595
  },
2596
 
2597
- // إعدادات Schema (الجديدة)
2598
  schema: user.profile.schema || {
2599
  type: 'Person',
2600
  name: user.profile.nickname || user.username,
@@ -2609,7 +2521,7 @@ app.get('/api/profile/:nickname', async (req, res) => {
2609
  },
2610
  };
2611
 
2612
- // 5. تسجيل المشاهدة (اختياري)
2613
  if (!isOwner) {
2614
  try {
2615
  // Google Analytics
@@ -2628,10 +2540,10 @@ app.get('/api/profile/:nickname', async (req, res) => {
2628
  }, { timeout: 5000 });
2629
  }
2630
 
2631
- // إشعار push لصاحب الملف الشخصي
2632
- if (user.notifications?.length > 0) {
2633
  const subscription = user.notifications[0];
2634
- if (subscription.endpoint && subscription.keys?.p256dh && subscription.keys?.auth) {
2635
  const payload = JSON.stringify({
2636
  title: '👀 Profile Viewed',
2637
  body: `Your profile was viewed by ${req.user?.username || 'someone'}`,
@@ -2640,7 +2552,6 @@ app.get('/api/profile/:nickname', async (req, res) => {
2640
  }
2641
  }
2642
  } catch (analyticsError) {
2643
- // لا نوقف التنفيذ إذا فشلت التحليلات
2644
  logger.error(`Analytics error: ${analyticsError.message}`);
2645
  }
2646
  }
 
448
  otpExpires: Date,
449
  refreshTokens: [{ token: String, createdAt: { type: Date, default: Date.now } }],
450
  notifications: [{ type: String }],
451
+ profile: {
452
+ type: mongoose.Schema.Types.Mixed,
453
+ default: {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  }
455
  },
456
  {
457
  minimize: false,
458
+ // toJSON: { getters: false, virtuals: false },
459
+ // toObject: { getters: false, virtuals: false }
460
 
461
  });
462
 
 
1402
 
1403
 
1404
  app.get('/api/verify-token', authenticateToken, async (req, res) => {
1405
+ try {
1406
+ // استخدم lean() وحدد الحقول اللي عايزها بس
1407
+ const user = await User.findById(req.user.userId)
1408
+ .select('username profile.nickname profile.portfolioName isAdmin')
1409
+ .lean() // ⭐ هذا هو الحل السحري
1410
+ .exec();
1411
+
1412
+ if (!user) {
1413
+ return res.status(404).json({ error: 'User not found' });
1414
+ }
1415
+
1416
+ res.json({
1417
+ valid: true,
1418
+ userId: req.user.userId,
1419
+ isAdmin: req.user.isAdmin || user.isAdmin,
1420
+ username: user.username,
1421
+ profile: {
1422
+ nickname: user.profile?.nickname,
1423
+ portfolioName: user.profile?.portfolioName || 'Portfolio'
1424
+ }
1425
+ });
1426
+ } catch (error) {
1427
+ logger.error(`Error in verify-token: ${error.message}`);
1428
+ res.status(500).json({ error: 'Internal server error' });
1429
+ }
1430
  });
1431
 
1432
  app.post('/api/logout', authenticateToken, async (req, res) => {
 
2247
  // تحديث endpoint /api/profile/me
2248
  app.get('/api/profile/me', authenticateToken, async (req, res) => {
2249
  try {
2250
+ // ✅ استخدم lean()
2251
  const user = await User.findById(req.user.userId)
2252
  .select('username profile')
2253
  .lean();
 
2256
  return res.status(404).json({ error: 'المستخدم غير موجود' });
2257
  }
2258
 
2259
+ // ✅ القيم الافتراضية للـ profile (نسخة نظيفة)
2260
+ const defaultProfile = {
2261
+ portfolioName: 'Portfolio',
2262
+ nickname: '',
2263
+ jobTitle: '',
2264
+ bio: '',
2265
+ phone: '',
2266
+ isPublic: true,
2267
+ status: 'Available',
2268
+ socialLinks: { linkedin: '', behance: '', github: '', whatsapp: '' },
2269
+ avatar: '',
2270
+ avatarDisplayType: 'normal',
2271
+ svgColor: '#000000',
2272
+ pdfFormat: 'jspdf',
2273
+ education: [],
2274
+ experience: [],
2275
+ certificates: [],
2276
+ skills: [],
2277
+ projects: [],
2278
+ interests: [],
2279
+ theme: {
 
 
 
 
 
2280
  id: 'default',
2281
  primaryColor: '#3b82f6',
2282
  secondaryColor: '#8b5cf6',
2283
  fontFamily: 'Inter',
2284
  borderRadius: '0.5rem',
2285
  },
2286
+ layout: {
2287
  type: 'grid',
2288
  columns: 3,
2289
  showProjectImages: true,
 
2291
  showProjectRatings: true,
2292
  showProjectLinks: true,
2293
  },
2294
+ header: {
2295
  showAvatar: true,
2296
  showJobTitle: true,
2297
  showBio: true,
 
2299
  showSocialLinks: true,
2300
  layout: 'centered',
2301
  },
2302
+ footer: {
2303
  showCopyright: true,
2304
  customText: '',
2305
  },
2306
+ seo: {
2307
  title: '',
2308
  description: '',
2309
  keywords: '',
 
2316
  noindex: false,
2317
  nofollow: false,
2318
  },
2319
+ schema: {
2320
  type: 'Person',
2321
  name: '',
2322
  description: '',
 
2327
  alumniOf: [],
2328
  knowsAbout: [],
2329
  },
2330
+ pushNotifications: false
2331
  };
2332
 
2333
+ // ✅ دمج default مع اللي موجود
2334
+ const profile = { ...defaultProfile, ...(user.profile || {}) };
2335
+
2336
  res.json({
2337
  username: user.username,
2338
  profile: profile
2339
  });
2340
+
2341
  } catch (error) {
2342
+ logger.error(`Error fetching profile/me: ${error.message}`);
2343
  res.status(500).json({ error: 'خطأ في استرجاع الملف الشخصي' });
2344
  }
2345
  });
 
2382
  try {
2383
  const decodedNickname = decodeURIComponent(req.params.nickname);
2384
 
2385
+ // 1. جلب المستخدم مع البروفايل - ✅ أضفنا .lean()
2386
  const user = await User.findOne({
2387
  $or: [
2388
  { 'profile.nickname': { $regex: `^${decodedNickname}$`, $options: 'i' } },
2389
  { username: { $regex: `^${decodedNickname}$`, $options: 'i' } },
2390
  ],
2391
+ })
2392
+ .select('username profile notifications')
2393
+ .lean(); // ⭐ هذا هو المطلوب
2394
 
2395
  if (!user) {
2396
  logger.warn(`Profile not found for nickname: ${decodedNickname}`);
2397
  return res.status(404).json({ error: `Profile not found for ${decodedNickname}` });
2398
  }
2399
 
2400
+ // 2. التحقق من الخصوصية - ✅ user._id بقى string
2401
+ const isOwner = req.user && req.user.userId === user._id; // مش محتاج toString()
2402
+
2403
+ // ✅ التحقق من وجود profile
2404
+ if (!user.profile) {
2405
+ user.profile = {};
2406
+ }
2407
 
2408
  if (!user.profile.isPublic && !isOwner) {
2409
  logger.warn(`Unauthorized access attempt to private profile: ${decodedNickname}`);
 
2413
  // 3. جلب المشاريع من Project collection
2414
  const projectsQuery = { userId: user._id };
2415
  if (!isOwner) {
2416
+ projectsQuery.isPublic = true;
2417
  }
2418
 
2419
  const projects = await Project.find(projectsQuery)
 
2421
  .sort({ createdAt: -1 })
2422
  .lean();
2423
 
2424
+ // 4. تجهيز الرد مع default values
2425
  const response = {
2426
  username: user.username,
2427
  profile: {
2428
+ // المعلومات الأساسية
2429
  nickname: user.profile.nickname || user.username,
2430
  portfolioName: user.profile.portfolioName || 'Portfolio',
2431
  avatar: user.profile.avatar || '/assets/img/default-avatar.png',
 
2456
  skills: user.profile.skills || [],
2457
  interests: user.profile.interests || [],
2458
 
2459
+ // إعدادات المظهر
2460
  theme: user.profile.theme || {
2461
  id: 'default',
2462
  primaryColor: '#3b82f6',
 
2465
  borderRadius: '0.5rem',
2466
  },
2467
 
2468
+ // إعدادات التخطيط
2469
  layout: user.profile.layout || {
2470
  type: 'grid',
2471
  columns: 3,
 
2475
  showProjectLinks: true,
2476
  },
2477
 
2478
+ // إعدادات الهيدر
2479
  header: user.profile.header || {
2480
  showAvatar: true,
2481
  showJobTitle: true,
 
2485
  layout: 'centered',
2486
  },
2487
 
2488
+ // إعدادات الفوتر
2489
  footer: user.profile.footer || {
2490
  showCopyright: true,
2491
  customText: '',
2492
  },
2493
 
2494
+ // إعدادات SEO
2495
  seo: user.profile.seo || {
2496
  title: user.profile.portfolioName || 'My Portfolio',
2497
  description: user.profile.bio || '',
 
2506
  nofollow: false,
2507
  },
2508
 
2509
+ // إعدادات Schema
2510
  schema: user.profile.schema || {
2511
  type: 'Person',
2512
  name: user.profile.nickname || user.username,
 
2521
  },
2522
  };
2523
 
2524
+ // 5. تسجيل المشاهدة (اختياري) - ✅ تأكد من وجود user.notifications
2525
  if (!isOwner) {
2526
  try {
2527
  // Google Analytics
 
2540
  }, { timeout: 5000 });
2541
  }
2542
 
2543
+ // إشعار push لصاحب الملف الشخصي - ✅ تأكد من وجود notifications
2544
+ if (user.notifications && user.notifications.length > 0) {
2545
  const subscription = user.notifications[0];
2546
+ if (subscription && subscription.endpoint && subscription.keys?.p256dh && subscription.keys?.auth) {
2547
  const payload = JSON.stringify({
2548
  title: '👀 Profile Viewed',
2549
  body: `Your profile was viewed by ${req.user?.username || 'someone'}`,
 
2552
  }
2553
  }
2554
  } catch (analyticsError) {
 
2555
  logger.error(`Analytics error: ${analyticsError.message}`);
2556
  }
2557
  }