Update server.js
Browse files
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 |
-
|
| 452 |
-
|
| 453 |
-
|
| 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 |
-
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
-
|
| 1521 |
-
|
| 1522 |
-
|
| 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()
|
| 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 |
-
// ✅
|
| 2354 |
-
|
| 2355 |
-
|
| 2356 |
-
|
| 2357 |
-
|
| 2358 |
-
|
| 2359 |
-
|
| 2360 |
-
|
| 2361 |
-
|
| 2362 |
-
|
| 2363 |
-
|
| 2364 |
-
|
| 2365 |
-
|
| 2366 |
-
|
| 2367 |
-
|
| 2368 |
-
|
| 2369 |
-
|
| 2370 |
-
|
| 2371 |
-
|
| 2372 |
-
|
| 2373 |
-
|
| 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:
|
| 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:
|
| 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:
|
| 2402 |
showCopyright: true,
|
| 2403 |
customText: '',
|
| 2404 |
},
|
| 2405 |
-
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:
|
| 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:
|
| 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 |
-
})
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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. تجهيز الرد مع
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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
|
| 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 |
}
|