Update server.js
Browse files
server.js
CHANGED
|
@@ -17,6 +17,7 @@ const { jsPDF } = require('jspdf');
|
|
| 17 |
const Jimp = require('jimp');
|
| 18 |
const fs = require('fs');
|
| 19 |
const fetch = global.fetch || require('node-fetch');
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
const path = require('path');
|
|
@@ -152,7 +153,10 @@ const excludedPaths = [
|
|
| 152 |
'/api/profile',
|
| 153 |
'/api/refresh-token',
|
| 154 |
'/api/logout',
|
| 155 |
-
'/api/verify-token'
|
|
|
|
|
|
|
|
|
|
| 156 |
];
|
| 157 |
|
| 158 |
app.use((req, res, next) => {
|
|
@@ -2328,7 +2332,15 @@ app.get('/api/profile/me', authenticateToken, async (req, res) => {
|
|
| 2328 |
alumniOf: [],
|
| 2329 |
knowsAbout: [],
|
| 2330 |
},
|
| 2331 |
-
pushNotifications: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2332 |
};
|
| 2333 |
|
| 2334 |
// ✅ دمج default مع اللي موجود
|
|
@@ -2449,6 +2461,13 @@ app.get('/api/profile/:nickname', async (req, res) => {
|
|
| 2449 |
github: '',
|
| 2450 |
whatsapp: ''
|
| 2451 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2452 |
|
| 2453 |
// الأقسام الأخرى
|
| 2454 |
education: user.profile.education || [],
|
|
@@ -2812,7 +2831,13 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 2812 |
body('skills').optional().custom(value => {
|
| 2813 |
try {
|
| 2814 |
const parsed = JSON.parse(value);
|
| 2815 |
-
return Array.isArray(parsed) && parsed.every(item =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2816 |
} catch {
|
| 2817 |
return false;
|
| 2818 |
}
|
|
@@ -2835,6 +2860,8 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 2835 |
}).withMessage('Invalid interests format'),
|
| 2836 |
body('isPublic').optional().isBoolean().withMessage('isPublic must be a boolean'),
|
| 2837 |
body('avatarDisplayType').optional().isIn(['svg', 'normal']).withMessage('Invalid avatar display type'),
|
|
|
|
|
|
|
| 2838 |
body('svgColor').optional().matches(/^#[0-9A-Fa-f]{6}$/).withMessage('Invalid SVG color format'),
|
| 2839 |
body('githubProjectIds').optional().custom(value => {
|
| 2840 |
try {
|
|
@@ -2898,6 +2925,20 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 2898 |
return false;
|
| 2899 |
}
|
| 2900 |
}).withMessage('Invalid schema format'),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2901 |
], async (req, res) => {
|
| 2902 |
const errors = validationResult(req);
|
| 2903 |
if (!errors.isEmpty()) {
|
|
@@ -2908,8 +2949,7 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 2908 |
const {
|
| 2909 |
nickname, jobTitle, bio, phone, socialLinks, education, experience,
|
| 2910 |
certificates, skills, projects, interests, isPublic, avatarDisplayType,
|
| 2911 |
-
svgColor, status, portfolioName, pdfFormat, theme, layout, header, footer, seo, schema
|
| 2912 |
-
|
| 2913 |
} = req.body;
|
| 2914 |
|
| 2915 |
const user = await User.findById(req.user.userId);
|
|
@@ -2928,9 +2968,7 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 2928 |
const parseJSON = (str, defaultValue) => {
|
| 2929 |
try {
|
| 2930 |
if (!str) return defaultValue;
|
| 2931 |
-
// Parse the JSON string
|
| 2932 |
const parsed = JSON.parse(str);
|
| 2933 |
-
// Merge with default values
|
| 2934 |
return { ...defaultValue, ...parsed };
|
| 2935 |
} catch (error) {
|
| 2936 |
logger.error(`Invalid JSON: ${error.message}`);
|
|
@@ -2939,6 +2977,7 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 2939 |
}
|
| 2940 |
};
|
| 2941 |
|
|
|
|
| 2942 |
if (theme) user.profile.theme = parseJSON(theme, user.profile.theme);
|
| 2943 |
if (layout) user.profile.layout = parseJSON(layout, user.profile.layout);
|
| 2944 |
if (header) user.profile.header = parseJSON(header, user.profile.header);
|
|
@@ -2946,20 +2985,50 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 2946 |
if (seo) user.profile.seo = parseJSON(seo, user.profile.seo);
|
| 2947 |
if (schema) user.profile.schema = parseJSON(schema, user.profile.schema);
|
| 2948 |
|
| 2949 |
-
|
| 2950 |
-
|
| 2951 |
// Parse input fields
|
| 2952 |
const parsedSocialLinks = parseJSON(socialLinks, user.profile.socialLinks);
|
| 2953 |
const parsedEducation = parseJSON(education, user.profile.education);
|
| 2954 |
const parsedExperience = parseJSON(experience, user.profile.experience);
|
| 2955 |
const parsedCertificates = parseJSON(certificates, user.profile.certificates);
|
| 2956 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2957 |
let parsedProjects = parseJSON(projects, user.profile.projects);
|
| 2958 |
const parsedInterests = parseJSON(interests, user.profile.interests);
|
| 2959 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2960 |
const parsedGithubProjectIds = parseJSON(githubProjectIds, []);
|
| 2961 |
|
| 2962 |
-
// Handle avatar image
|
| 2963 |
let hasTransparency = false;
|
| 2964 |
if (req.files && req.files.avatar) {
|
| 2965 |
try {
|
|
@@ -3023,7 +3092,7 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 3023 |
}
|
| 3024 |
}
|
| 3025 |
|
| 3026 |
-
//
|
| 3027 |
user.profile = {
|
| 3028 |
nickname: nickname || user.profile.nickname,
|
| 3029 |
avatar: user.profile.avatar || undefined,
|
|
@@ -3034,8 +3103,9 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 3034 |
education: parsedEducation,
|
| 3035 |
experience: parsedExperience,
|
| 3036 |
certificates: parsedCertificates,
|
| 3037 |
-
skills:
|
| 3038 |
projects: parsedProjects,
|
|
|
|
| 3039 |
interests: parsedInterests,
|
| 3040 |
isPublic: isPublic !== undefined ? isPublic === 'true' : user.profile.isPublic,
|
| 3041 |
avatarDisplayType: avatarDisplayType || user.profile.avatarDisplayType,
|
|
@@ -3044,6 +3114,7 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 3044 |
portfolioName: portfolioName || user.profile.portfolioName || 'Portfolio',
|
| 3045 |
status: status || user.profile.status || 'Available',
|
| 3046 |
pdfFormat: pdfFormat || user.profile.pdfFormat || 'jspdf',
|
|
|
|
| 3047 |
theme: theme ? parseJSON(theme, user.profile.theme) : user.profile.theme || {
|
| 3048 |
id: 'default',
|
| 3049 |
primaryColor: '#3b82f6',
|
|
@@ -3051,7 +3122,6 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 3051 |
fontFamily: 'Inter',
|
| 3052 |
borderRadius: '0.5rem',
|
| 3053 |
},
|
| 3054 |
-
|
| 3055 |
layout: layout ? parseJSON(layout, user.profile.layout) : user.profile.layout || {
|
| 3056 |
type: 'grid',
|
| 3057 |
columns: 3,
|
|
@@ -3060,7 +3130,6 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 3060 |
showProjectRatings: true,
|
| 3061 |
showProjectLinks: true,
|
| 3062 |
},
|
| 3063 |
-
|
| 3064 |
header: header ? parseJSON(header, user.profile.header) : user.profile.header || {
|
| 3065 |
showAvatar: true,
|
| 3066 |
showJobTitle: true,
|
|
@@ -3069,12 +3138,10 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 3069 |
showSocialLinks: true,
|
| 3070 |
layout: 'centered',
|
| 3071 |
},
|
| 3072 |
-
|
| 3073 |
footer: footer ? parseJSON(footer, user.profile.footer) : user.profile.footer || {
|
| 3074 |
showCopyright: true,
|
| 3075 |
customText: '',
|
| 3076 |
},
|
| 3077 |
-
|
| 3078 |
seo: seo ? parseJSON(seo, user.profile.seo) : user.profile.seo || {
|
| 3079 |
title: portfolioName || 'My Portfolio',
|
| 3080 |
description: bio || '',
|
|
@@ -3088,7 +3155,6 @@ app.put('/api/profile', authenticateToken, upload.fields([
|
|
| 3088 |
noindex: false,
|
| 3089 |
nofollow: false,
|
| 3090 |
},
|
| 3091 |
-
|
| 3092 |
schema: schema ? parseJSON(schema, user.profile.schema) : user.profile.schema || {
|
| 3093 |
type: 'Person',
|
| 3094 |
name: nickname || user.username,
|
|
@@ -3273,6 +3339,52 @@ app.delete('/api/users/:userId', authenticateToken, isAdmin, async (req, res) =>
|
|
| 3273 |
}
|
| 3274 |
});
|
| 3275 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3276 |
app.get('/api/profile/pdf/:nickname', authenticateToken, async (req, res) => {
|
| 3277 |
try {
|
| 3278 |
const decodedNickname = decodeURIComponent(req.params.nickname);
|
|
@@ -4216,7 +4328,370 @@ app.get('/api/github-projects', async (req, res) => {
|
|
| 4216 |
});
|
| 4217 |
|
| 4218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4220 |
app.post('/api/revoke-token', authenticateToken, [
|
| 4221 |
body('refreshToken').notEmpty().withMessage('Refresh token is required')
|
| 4222 |
], async (req, res) => {
|
|
|
|
| 17 |
const Jimp = require('jimp');
|
| 18 |
const fs = require('fs');
|
| 19 |
const fetch = global.fetch || require('node-fetch');
|
| 20 |
+
const pdfParse = require('pdf-parse');
|
| 21 |
|
| 22 |
|
| 23 |
const path = require('path');
|
|
|
|
| 153 |
'/api/profile',
|
| 154 |
'/api/refresh-token',
|
| 155 |
'/api/logout',
|
| 156 |
+
'/api/verify-token',
|
| 157 |
+
'/api/parse-resume',
|
| 158 |
+
'api/profile/resume/:nickname',
|
| 159 |
+
'api/profile/pdf/:nickname'
|
| 160 |
];
|
| 161 |
|
| 162 |
app.use((req, res, next) => {
|
|
|
|
| 2332 |
alumniOf: [],
|
| 2333 |
knowsAbout: [],
|
| 2334 |
},
|
| 2335 |
+
pushNotifications: false,
|
| 2336 |
+
audio: {
|
| 2337 |
+
url: '',
|
| 2338 |
+
title: '',
|
| 2339 |
+
delay: 0,
|
| 2340 |
+
loop: true,
|
| 2341 |
+
volume: 50
|
| 2342 |
+
}
|
| 2343 |
+
|
| 2344 |
};
|
| 2345 |
|
| 2346 |
// ✅ دمج default مع اللي موجود
|
|
|
|
| 2461 |
github: '',
|
| 2462 |
whatsapp: ''
|
| 2463 |
},
|
| 2464 |
+
audio: user.profile.audio || {
|
| 2465 |
+
url: '',
|
| 2466 |
+
title: '',
|
| 2467 |
+
delay: 0,
|
| 2468 |
+
loop: true,
|
| 2469 |
+
volume: 50
|
| 2470 |
+
},
|
| 2471 |
|
| 2472 |
// الأقسام الأخرى
|
| 2473 |
education: user.profile.education || [],
|
|
|
|
| 2831 |
body('skills').optional().custom(value => {
|
| 2832 |
try {
|
| 2833 |
const parsed = JSON.parse(value);
|
| 2834 |
+
return Array.isArray(parsed) && parsed.every(item =>
|
| 2835 |
+
item.name &&
|
| 2836 |
+
typeof item.percentage === 'number' &&
|
| 2837 |
+
item.percentage >= 0 &&
|
| 2838 |
+
item.percentage <= 100 &&
|
| 2839 |
+
(item.icon === undefined || typeof item.icon === 'string')
|
| 2840 |
+
);
|
| 2841 |
} catch {
|
| 2842 |
return false;
|
| 2843 |
}
|
|
|
|
| 2860 |
}).withMessage('Invalid interests format'),
|
| 2861 |
body('isPublic').optional().isBoolean().withMessage('isPublic must be a boolean'),
|
| 2862 |
body('avatarDisplayType').optional().isIn(['svg', 'normal']).withMessage('Invalid avatar display type'),
|
| 2863 |
+
body('resumeUrl').optional().isURL().withMessage('Invalid resume URL'),
|
| 2864 |
+
|
| 2865 |
body('svgColor').optional().matches(/^#[0-9A-Fa-f]{6}$/).withMessage('Invalid SVG color format'),
|
| 2866 |
body('githubProjectIds').optional().custom(value => {
|
| 2867 |
try {
|
|
|
|
| 2925 |
return false;
|
| 2926 |
}
|
| 2927 |
}).withMessage('Invalid schema format'),
|
| 2928 |
+
|
| 2929 |
+
body('audio').optional().custom(value => {
|
| 2930 |
+
try {
|
| 2931 |
+
const parsed = JSON.parse(value);
|
| 2932 |
+
return parsed &&
|
| 2933 |
+
(parsed.url === undefined || typeof parsed.url === 'string') &&
|
| 2934 |
+
(parsed.title === undefined || typeof parsed.title === 'string') &&
|
| 2935 |
+
(parsed.delay === undefined || (typeof parsed.delay === 'number' && parsed.delay >= 0 && parsed.delay <= 10)) &&
|
| 2936 |
+
(parsed.loop === undefined || typeof parsed.loop === 'boolean') &&
|
| 2937 |
+
(parsed.volume === undefined || (typeof parsed.volume === 'number' && parsed.volume >= 0 && parsed.volume <= 100));
|
| 2938 |
+
} catch {
|
| 2939 |
+
return false;
|
| 2940 |
+
}
|
| 2941 |
+
}).withMessage('Invalid audio format'),
|
| 2942 |
], async (req, res) => {
|
| 2943 |
const errors = validationResult(req);
|
| 2944 |
if (!errors.isEmpty()) {
|
|
|
|
| 2949 |
const {
|
| 2950 |
nickname, jobTitle, bio, phone, socialLinks, education, experience,
|
| 2951 |
certificates, skills, projects, interests, isPublic, avatarDisplayType,
|
| 2952 |
+
svgColor, status, portfolioName, pdfFormat, theme, layout, header, footer, seo, schema, resumeUrl
|
|
|
|
| 2953 |
} = req.body;
|
| 2954 |
|
| 2955 |
const user = await User.findById(req.user.userId);
|
|
|
|
| 2968 |
const parseJSON = (str, defaultValue) => {
|
| 2969 |
try {
|
| 2970 |
if (!str) return defaultValue;
|
|
|
|
| 2971 |
const parsed = JSON.parse(str);
|
|
|
|
| 2972 |
return { ...defaultValue, ...parsed };
|
| 2973 |
} catch (error) {
|
| 2974 |
logger.error(`Invalid JSON: ${error.message}`);
|
|
|
|
| 2977 |
}
|
| 2978 |
};
|
| 2979 |
|
| 2980 |
+
// تحديث إعدادات المظهر
|
| 2981 |
if (theme) user.profile.theme = parseJSON(theme, user.profile.theme);
|
| 2982 |
if (layout) user.profile.layout = parseJSON(layout, user.profile.layout);
|
| 2983 |
if (header) user.profile.header = parseJSON(header, user.profile.header);
|
|
|
|
| 2985 |
if (seo) user.profile.seo = parseJSON(seo, user.profile.seo);
|
| 2986 |
if (schema) user.profile.schema = parseJSON(schema, user.profile.schema);
|
| 2987 |
|
|
|
|
|
|
|
| 2988 |
// Parse input fields
|
| 2989 |
const parsedSocialLinks = parseJSON(socialLinks, user.profile.socialLinks);
|
| 2990 |
const parsedEducation = parseJSON(education, user.profile.education);
|
| 2991 |
const parsedExperience = parseJSON(experience, user.profile.experience);
|
| 2992 |
const parsedCertificates = parseJSON(certificates, user.profile.certificates);
|
| 2993 |
+
|
| 2994 |
+
// ✅ معالجة المهارات مع الأيقونات
|
| 2995 |
+
const parsedSkills = parseJSON(skills, []);
|
| 2996 |
+
const skillsWithIcons = parsedSkills.map(skill => ({
|
| 2997 |
+
name: skill.name || '',
|
| 2998 |
+
percentage: skill.percentage || 0,
|
| 2999 |
+
icon: skill.icon || ''
|
| 3000 |
+
}));
|
| 3001 |
+
|
| 3002 |
let parsedProjects = parseJSON(projects, user.profile.projects);
|
| 3003 |
const parsedInterests = parseJSON(interests, user.profile.interests);
|
| 3004 |
+
|
| 3005 |
+
// ✅ معالجة إعدادات الصوت
|
| 3006 |
+
let audioSettings = {
|
| 3007 |
+
url: '',
|
| 3008 |
+
title: '',
|
| 3009 |
+
delay: 0,
|
| 3010 |
+
loop: true,
|
| 3011 |
+
volume: 50
|
| 3012 |
+
};
|
| 3013 |
+
if (req.body.audio) {
|
| 3014 |
+
try {
|
| 3015 |
+
const audioData = JSON.parse(req.body.audio);
|
| 3016 |
+
audioSettings = {
|
| 3017 |
+
url: audioData.url || '',
|
| 3018 |
+
title: audioData.title || '',
|
| 3019 |
+
delay: audioData.delay || 0,
|
| 3020 |
+
loop: audioData.loop !== undefined ? audioData.loop : true,
|
| 3021 |
+
volume: audioData.volume || 50
|
| 3022 |
+
};
|
| 3023 |
+
} catch (e) {
|
| 3024 |
+
logger.error('Error parsing audio data:', e);
|
| 3025 |
+
}
|
| 3026 |
+
}
|
| 3027 |
+
|
| 3028 |
+
const githubProjectIds = req.body.githubProjectIds || '[]';
|
| 3029 |
const parsedGithubProjectIds = parseJSON(githubProjectIds, []);
|
| 3030 |
|
| 3031 |
+
// Handle avatar image
|
| 3032 |
let hasTransparency = false;
|
| 3033 |
if (req.files && req.files.avatar) {
|
| 3034 |
try {
|
|
|
|
| 3092 |
}
|
| 3093 |
}
|
| 3094 |
|
| 3095 |
+
// ✅ تحديث كامل بيانات المستخدم
|
| 3096 |
user.profile = {
|
| 3097 |
nickname: nickname || user.profile.nickname,
|
| 3098 |
avatar: user.profile.avatar || undefined,
|
|
|
|
| 3103 |
education: parsedEducation,
|
| 3104 |
experience: parsedExperience,
|
| 3105 |
certificates: parsedCertificates,
|
| 3106 |
+
skills: skillsWithIcons,
|
| 3107 |
projects: parsedProjects,
|
| 3108 |
+
resumeUrl: resumeUrl || user.profile.resumeUrl || '',
|
| 3109 |
interests: parsedInterests,
|
| 3110 |
isPublic: isPublic !== undefined ? isPublic === 'true' : user.profile.isPublic,
|
| 3111 |
avatarDisplayType: avatarDisplayType || user.profile.avatarDisplayType,
|
|
|
|
| 3114 |
portfolioName: portfolioName || user.profile.portfolioName || 'Portfolio',
|
| 3115 |
status: status || user.profile.status || 'Available',
|
| 3116 |
pdfFormat: pdfFormat || user.profile.pdfFormat || 'jspdf',
|
| 3117 |
+
audio: audioSettings,
|
| 3118 |
theme: theme ? parseJSON(theme, user.profile.theme) : user.profile.theme || {
|
| 3119 |
id: 'default',
|
| 3120 |
primaryColor: '#3b82f6',
|
|
|
|
| 3122 |
fontFamily: 'Inter',
|
| 3123 |
borderRadius: '0.5rem',
|
| 3124 |
},
|
|
|
|
| 3125 |
layout: layout ? parseJSON(layout, user.profile.layout) : user.profile.layout || {
|
| 3126 |
type: 'grid',
|
| 3127 |
columns: 3,
|
|
|
|
| 3130 |
showProjectRatings: true,
|
| 3131 |
showProjectLinks: true,
|
| 3132 |
},
|
|
|
|
| 3133 |
header: header ? parseJSON(header, user.profile.header) : user.profile.header || {
|
| 3134 |
showAvatar: true,
|
| 3135 |
showJobTitle: true,
|
|
|
|
| 3138 |
showSocialLinks: true,
|
| 3139 |
layout: 'centered',
|
| 3140 |
},
|
|
|
|
| 3141 |
footer: footer ? parseJSON(footer, user.profile.footer) : user.profile.footer || {
|
| 3142 |
showCopyright: true,
|
| 3143 |
customText: '',
|
| 3144 |
},
|
|
|
|
| 3145 |
seo: seo ? parseJSON(seo, user.profile.seo) : user.profile.seo || {
|
| 3146 |
title: portfolioName || 'My Portfolio',
|
| 3147 |
description: bio || '',
|
|
|
|
| 3155 |
noindex: false,
|
| 3156 |
nofollow: false,
|
| 3157 |
},
|
|
|
|
| 3158 |
schema: schema ? parseJSON(schema, user.profile.schema) : user.profile.schema || {
|
| 3159 |
type: 'Person',
|
| 3160 |
name: nickname || user.username,
|
|
|
|
| 3339 |
}
|
| 3340 |
});
|
| 3341 |
|
| 3342 |
+
|
| 3343 |
+
// ============================================
|
| 3344 |
+
// GET /api/profile/resume/:nickname - جلب السيرة الذاتية (رابط أو PDF)
|
| 3345 |
+
// ============================================
|
| 3346 |
+
app.get('/api/profile/resume/:nickname', authenticateToken, async (req, res) => {
|
| 3347 |
+
try {
|
| 3348 |
+
const decodedNickname = decodeURIComponent(req.params.nickname);
|
| 3349 |
+
const user = await User.findOne({
|
| 3350 |
+
$or: [
|
| 3351 |
+
{ 'profile.nickname': decodedNickname },
|
| 3352 |
+
{ username: decodedNickname },
|
| 3353 |
+
],
|
| 3354 |
+
});
|
| 3355 |
+
|
| 3356 |
+
if (!user) {
|
| 3357 |
+
return res.status(404).json({ error: 'Profile not found' });
|
| 3358 |
+
}
|
| 3359 |
+
|
| 3360 |
+
// التحقق من الخصوصية
|
| 3361 |
+
const isOwner = req.user && req.user.userId === user._id.toString();
|
| 3362 |
+
if (!user.profile.isPublic && !isOwner) {
|
| 3363 |
+
return res.status(403).json({ error: 'Profile is private' });
|
| 3364 |
+
}
|
| 3365 |
+
|
| 3366 |
+
// ✅ إذا كان فيه رابط خارجي، ارجع الرابط
|
| 3367 |
+
if (user.profile.resumeUrl && user.profile.resumeUrl !== '') {
|
| 3368 |
+
return res.json({
|
| 3369 |
+
type: 'url',
|
| 3370 |
+
url: user.profile.resumeUrl,
|
| 3371 |
+
message: 'External resume URL found'
|
| 3372 |
+
});
|
| 3373 |
+
}
|
| 3374 |
+
|
| 3375 |
+
// ✅ إذا مفيش رابط، ارجع إشارة لتوليد PDF
|
| 3376 |
+
return res.json({
|
| 3377 |
+
type: 'generate',
|
| 3378 |
+
nickname: decodedNickname,
|
| 3379 |
+
message: 'Generate PDF from profile data'
|
| 3380 |
+
});
|
| 3381 |
+
|
| 3382 |
+
} catch (error) {
|
| 3383 |
+
logger.error(`Error fetching resume: ${error.message}`);
|
| 3384 |
+
res.status(500).json({ error: 'Failed to fetch resume' });
|
| 3385 |
+
}
|
| 3386 |
+
});
|
| 3387 |
+
|
| 3388 |
app.get('/api/profile/pdf/:nickname', authenticateToken, async (req, res) => {
|
| 3389 |
try {
|
| 3390 |
const decodedNickname = decodeURIComponent(req.params.nickname);
|
|
|
|
| 4328 |
});
|
| 4329 |
|
| 4330 |
|
| 4331 |
+
// ---------------------------
|
| 4332 |
+
// ============================================
|
| 4333 |
+
// قائمة المهارات الشاملة
|
| 4334 |
+
// ============================================
|
| 4335 |
+
|
| 4336 |
+
const SKILLS_DATABASE = {
|
| 4337 |
+
// 🌐 Frontend
|
| 4338 |
+
'html': 'HTML5', 'html5': 'HTML5', 'css': 'CSS3', 'css3': 'CSS3',
|
| 4339 |
+
'sass': 'Sass', 'scss': 'SCSS', 'less': 'Less', 'tailwind': 'Tailwind CSS',
|
| 4340 |
+
'bootstrap': 'Bootstrap', 'javascript': 'JavaScript', 'js': 'JavaScript',
|
| 4341 |
+
'ecmascript': 'JavaScript', 'es6': 'JavaScript (ES6)', 'typescript': 'TypeScript',
|
| 4342 |
+
'ts': 'TypeScript', 'react': 'React.js', 'reactjs': 'React.js', 'next': 'Next.js',
|
| 4343 |
+
'nextjs': 'Next.js', 'vue': 'Vue.js', 'vuejs': 'Vue.js', 'nuxt': 'Nuxt.js',
|
| 4344 |
+
'angular': 'Angular', 'angularjs': 'AngularJS', 'svelte': 'Svelte',
|
| 4345 |
+
'jquery': 'jQuery', 'webpack': 'Webpack', 'vite': 'Vite', 'babel': 'Babel',
|
| 4346 |
+
|
| 4347 |
+
// 📱 Mobile
|
| 4348 |
+
'react native': 'React Native', 'reactnative': 'React Native', 'flutter': 'Flutter',
|
| 4349 |
+
'dart': 'Dart', 'swift': 'Swift', 'ios': 'iOS Development', 'kotlin': 'Kotlin',
|
| 4350 |
+
'android': 'Android Development', 'java': 'Java', 'xamarin': 'Xamarin',
|
| 4351 |
+
'ionic': 'Ionic', 'capacitor': 'Capacitor', 'cordova': 'Cordova',
|
| 4352 |
+
|
| 4353 |
+
// ⚙️ Backend
|
| 4354 |
+
'node': 'Node.js', 'nodejs': 'Node.js', 'express': 'Express.js',
|
| 4355 |
+
'nestjs': 'NestJS', 'python': 'Python', 'django': 'Django', 'flask': 'Flask',
|
| 4356 |
+
'fastapi': 'FastAPI', 'php': 'PHP', 'laravel': 'Laravel', 'symfony': 'Symfony',
|
| 4357 |
+
'codeigniter': 'CodeIgniter', 'c#': 'C#', 'csharp': 'C#', 'dotnet': '.NET',
|
| 4358 |
+
'asp.net': 'ASP.NET', 'go': 'Go', 'golang': 'Go', 'rust': 'Rust',
|
| 4359 |
+
'ruby': 'Ruby', 'rails': 'Ruby on Rails', 'spring': 'Spring Boot', 'springboot': 'Spring Boot',
|
| 4360 |
+
|
| 4361 |
+
// 🗄️ Databases
|
| 4362 |
+
'mongodb': 'MongoDB', 'mongo': 'MongoDB', 'mysql': 'MySQL',
|
| 4363 |
+
'postgresql': 'PostgreSQL', 'postgres': 'PostgreSQL', 'sqlite': 'SQLite',
|
| 4364 |
+
'oracle': 'Oracle Database', 'mssql': 'MS SQL Server', 'firebase': 'Firebase',
|
| 4365 |
+
'redis': 'Redis', 'elasticsearch': 'Elasticsearch', 'dynamodb': 'DynamoDB',
|
| 4366 |
+
|
| 4367 |
+
// ☁️ Cloud & DevOps
|
| 4368 |
+
'aws': 'AWS', 'amazon web services': 'AWS', 'azure': 'Microsoft Azure',
|
| 4369 |
+
'gcp': 'Google Cloud', 'docker': 'Docker', 'kubernetes': 'Kubernetes', 'k8s': 'Kubernetes',
|
| 4370 |
+
'jenkins': 'Jenkins', 'terraform': 'Terraform', 'ansible': 'Ansible',
|
| 4371 |
+
|
| 4372 |
+
// 🛠️ Tools
|
| 4373 |
+
'git': 'Git', 'github': 'GitHub', 'gitlab': 'GitLab', 'bitbucket': 'Bitbucket',
|
| 4374 |
+
'jira': 'Jira', 'trello': 'Trello', 'postman': 'Postman', 'figma': 'Figma',
|
| 4375 |
+
'photoshop': 'Photoshop', 'illustrator': 'Illustrator',
|
| 4376 |
+
|
| 4377 |
+
// 📊 Data Science
|
| 4378 |
+
'machine learning': 'Machine Learning', 'ml': 'Machine Learning',
|
| 4379 |
+
'deep learning': 'Deep Learning', 'ai': 'Artificial Intelligence',
|
| 4380 |
+
'tensorflow': 'TensorFlow', 'pytorch': 'PyTorch', 'pandas': 'Pandas',
|
| 4381 |
+
'numpy': 'NumPy', 'data analysis': 'Data Analysis',
|
| 4382 |
+
|
| 4383 |
+
// 🎨 Soft Skills
|
| 4384 |
+
'leadership': 'Leadership', 'project management': 'Project Management',
|
| 4385 |
+
'agile': 'Agile', 'scrum': 'Scrum', 'communication': 'Communication',
|
| 4386 |
+
'problem solving': 'Problem Solving', 'teamwork': 'Teamwork',
|
| 4387 |
+
'time management': 'Time Management',
|
| 4388 |
+
|
| 4389 |
+
// 🎯 Additional
|
| 4390 |
+
'rest api': 'REST API', 'graphql': 'GraphQL', 'microservices': 'Microservices',
|
| 4391 |
+
'serverless': 'Serverless', 'seo': 'SEO', 'testing': 'Software Testing',
|
| 4392 |
+
'jest': 'Jest', 'cypress': 'Cypress',
|
| 4393 |
+
|
| 4394 |
+
// 🌍 Languages
|
| 4395 |
+
'arabic': 'Arabic', 'english': 'English', 'french': 'French', 'german': 'German', 'spanish': 'Spanish',
|
| 4396 |
+
};
|
| 4397 |
+
|
| 4398 |
+
// ============================================
|
| 4399 |
+
// دوال استخراج البيانات الأساسية
|
| 4400 |
+
// ============================================
|
| 4401 |
+
|
| 4402 |
+
function extractEmail(text) {
|
| 4403 |
+
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
| 4404 |
+
const emails = text.match(emailRegex);
|
| 4405 |
+
return emails && emails.length > 0 ? emails[0] : '';
|
| 4406 |
+
}
|
| 4407 |
+
|
| 4408 |
+
function extractPhone(text) {
|
| 4409 |
+
const phoneRegex = /(\+20|0)[1-9][0-9]{8,9}/g;
|
| 4410 |
+
const phones = text.match(phoneRegex);
|
| 4411 |
+
return phones && phones.length > 0 ? phones[0] : '';
|
| 4412 |
+
}
|
| 4413 |
+
|
| 4414 |
+
function extractName(text) {
|
| 4415 |
+
const lines = text.split('\n');
|
| 4416 |
+
for (const line of lines) {
|
| 4417 |
+
const trimmed = line.trim();
|
| 4418 |
+
if (trimmed.length > 3 && trimmed.length < 50 && !trimmed.includes('@') && !trimmed.match(/\d/)) {
|
| 4419 |
+
return trimmed;
|
| 4420 |
+
}
|
| 4421 |
+
}
|
| 4422 |
+
return '';
|
| 4423 |
+
}
|
| 4424 |
+
|
| 4425 |
+
function extractBio(text) {
|
| 4426 |
+
const lines = text.split('\n');
|
| 4427 |
+
for (const line of lines) {
|
| 4428 |
+
const trimmed = line.trim();
|
| 4429 |
+
if (trimmed.length > 30 && trimmed.length < 200 && !trimmed.includes('@') && !trimmed.match(/\d{10,}/)) {
|
| 4430 |
+
return trimmed;
|
| 4431 |
+
}
|
| 4432 |
+
}
|
| 4433 |
+
return text.length > 0 ? text.substring(0, 300).replace(/\n/g, ' ') : '';
|
| 4434 |
+
}
|
| 4435 |
+
|
| 4436 |
+
// ============================================
|
| 4437 |
+
// استخراج المسمى الوظيفي (نسخة واحدة موحدة)
|
| 4438 |
+
// ============================================
|
| 4439 |
+
function extractJobTitle(text) {
|
| 4440 |
+
const lines = text.split('\n');
|
| 4441 |
+
// قائمة موسعة بالمسميات الوظيفية
|
| 4442 |
+
const jobTitlesList = [
|
| 4443 |
+
// English
|
| 4444 |
+
'developer', 'engineer', 'architect', 'designer', 'analyst', 'manager',
|
| 4445 |
+
'consultant', 'specialist', 'administrator', 'coordinator', 'director',
|
| 4446 |
+
'lead', 'senior', 'junior', 'full stack', 'frontend', 'front-end',
|
| 4447 |
+
'backend', 'back-end', 'devops', 'data scientist', 'data engineer',
|
| 4448 |
+
'product manager', 'project manager', 'software engineer', 'web developer',
|
| 4449 |
+
'mobile developer', 'ios developer', 'android developer', 'qa engineer',
|
| 4450 |
+
'system administrator', 'database administrator', 'security analyst',
|
| 4451 |
+
'network engineer', 'cloud architect', 'technical lead', 'tech lead',
|
| 4452 |
+
'cto', 'ceo', 'founder', 'co-founder', 'freelancer',
|
| 4453 |
+
// Arabic
|
| 4454 |
+
'مطور', 'مهندس', 'محلل', 'مدير', 'مصمم', 'خبير', 'قائد', 'مبرمج',
|
| 4455 |
+
'مهندس برمجيات', 'مطور ويب', 'مطور تطبيقات', 'مهندس نظم', 'مسؤول قواعد بيانات'
|
| 4456 |
+
];
|
| 4457 |
+
|
| 4458 |
+
for (const line of lines) {
|
| 4459 |
+
const lowerLine = line.toLowerCase();
|
| 4460 |
+
for (const title of jobTitlesList) {
|
| 4461 |
+
if (lowerLine.includes(title.toLowerCase())) {
|
| 4462 |
+
// تجنب الأسطر الطويلة جداً أو التي تحتوي على ايميل
|
| 4463 |
+
const trimmed = line.trim();
|
| 4464 |
+
if (trimmed.length < 100 && !trimmed.includes('@')) {
|
| 4465 |
+
return trimmed;
|
| 4466 |
+
}
|
| 4467 |
+
}
|
| 4468 |
+
}
|
| 4469 |
+
}
|
| 4470 |
+
return '';
|
| 4471 |
+
}
|
| 4472 |
+
|
| 4473 |
+
// ============================================
|
| 4474 |
+
// استخراج الاهتمامات
|
| 4475 |
+
// ============================================
|
| 4476 |
+
function extractInterests(text) {
|
| 4477 |
+
const interests = [];
|
| 4478 |
+
const interestKeywords = [
|
| 4479 |
+
'reading', 'coding', 'design', 'photography', 'travel', 'music',
|
| 4480 |
+
'sports', 'chess', 'painting', 'drawing', 'writing', 'blogging',
|
| 4481 |
+
'gaming', 'hiking', 'swimming', 'yoga', 'meditation', 'cooking',
|
| 4482 |
+
'dancing', 'singing', 'volunteering', 'teaching', 'learning',
|
| 4483 |
+
'قراءة', 'تصميم', 'سفر', 'موسيقى', 'رياضة', 'شطرنج', 'رسم', 'كتابة',
|
| 4484 |
+
'طبخ', 'رقص', 'تطوع', 'تعليم', 'تكنولوجيا', 'برمجة'
|
| 4485 |
+
];
|
| 4486 |
+
const lowerText = text.toLowerCase();
|
| 4487 |
+
|
| 4488 |
+
for (const interest of interestKeywords) {
|
| 4489 |
+
if (lowerText.includes(interest)) {
|
| 4490 |
+
interests.push(interest);
|
| 4491 |
+
}
|
| 4492 |
+
}
|
| 4493 |
+
return [...new Set(interests)].slice(0, 10);
|
| 4494 |
+
}
|
| 4495 |
+
|
| 4496 |
+
// ============================================
|
| 4497 |
+
// دالة الأيقونات
|
| 4498 |
+
// ============================================
|
| 4499 |
+
function getIconForSkill(keyword) {
|
| 4500 |
+
const iconMap = {
|
| 4501 |
+
'react': 'bx bxl-react', 'angular': 'bx bxl-angular', 'vue': 'bx bxl-vuejs',
|
| 4502 |
+
'node': 'bx bxl-nodejs', 'python': 'bx bxl-python', 'java': 'bx bxl-java',
|
| 4503 |
+
'javascript': 'bx bxl-javascript', 'typescript': 'bx bxl-typescript', 'php': 'bx bxl-php',
|
| 4504 |
+
'laravel': 'bx bxl-laravel', 'django': 'bx bxl-django', 'flutter': 'bx bxl-flutter',
|
| 4505 |
+
'swift': 'bx bxl-swift', 'kotlin': 'bx bxl-kotlin', 'go': 'bx bxl-go-lang',
|
| 4506 |
+
'rust': 'bx bxl-rust', 'mongodb': 'bx bxl-mongodb', 'mysql': 'bx bxl-mysql',
|
| 4507 |
+
'postgresql': 'bx bxl-postgresql', 'firebase': 'bx bxl-firebase', 'aws': 'bx bxl-aws',
|
| 4508 |
+
'docker': 'bx bxl-docker', 'kubernetes': 'bx bxl-kubernetes', 'git': 'bx bxl-git',
|
| 4509 |
+
'github': 'bx bxl-github', 'gitlab': 'bx bxl-gitlab', 'figma': 'bx bxl-figma',
|
| 4510 |
+
'html': 'bx bxl-html5', 'css': 'bx bxl-css3', 'sass': 'bx bxl-sass',
|
| 4511 |
+
'bootstrap': 'bx bxl-bootstrap', 'tailwind': 'bx bxl-tailwind-css', 'jquery': 'bx bxl-jquery',
|
| 4512 |
+
};
|
| 4513 |
+
for (const [key, icon] of Object.entries(iconMap)) {
|
| 4514 |
+
if (keyword.toLowerCase().includes(key)) return icon;
|
| 4515 |
+
}
|
| 4516 |
+
return 'bx bx-code-alt';
|
| 4517 |
+
}
|
| 4518 |
+
|
| 4519 |
+
// ============================================
|
| 4520 |
+
// استخراج المهارات (النسخة النهائية)
|
| 4521 |
+
// ============================================
|
| 4522 |
+
function extractSkills(text) {
|
| 4523 |
+
const foundSkills = new Map();
|
| 4524 |
+
const lowerText = text.toLowerCase();
|
| 4525 |
+
|
| 4526 |
+
for (const [keyword, skillName] of Object.entries(SKILLS_DATABASE)) {
|
| 4527 |
+
const regex = new RegExp(`\\b${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
| 4528 |
+
if (regex.test(lowerText)) {
|
| 4529 |
+
if (!foundSkills.has(skillName)) {
|
| 4530 |
+
const count = (lowerText.match(new RegExp(keyword, 'gi')) || []).length;
|
| 4531 |
+
let percentage = Math.min(95, 60 + (count * 5));
|
| 4532 |
+
|
| 4533 |
+
// تحسين النسبة بناءً على السياق
|
| 4534 |
+
if (lowerText.includes(`advanced ${keyword}`) || lowerText.includes(`expert ${keyword}`)) {
|
| 4535 |
+
percentage = Math.min(98, percentage + 10);
|
| 4536 |
+
} else if (lowerText.includes(`beginner ${keyword}`) || lowerText.includes(`basic ${keyword}`)) {
|
| 4537 |
+
percentage = Math.max(40, percentage - 20);
|
| 4538 |
+
}
|
| 4539 |
+
|
| 4540 |
+
const icon = getIconForSkill(keyword);
|
| 4541 |
+
foundSkills.set(skillName, { name: skillName, percentage, icon });
|
| 4542 |
+
}
|
| 4543 |
+
}
|
| 4544 |
+
}
|
| 4545 |
+
|
| 4546 |
+
return Array.from(foundSkills.values())
|
| 4547 |
+
.sort((a, b) => b.percentage - a.percentage)
|
| 4548 |
+
.slice(0, 20);
|
| 4549 |
+
}
|
| 4550 |
+
|
| 4551 |
+
// ============================================
|
| 4552 |
+
// استخراج التعليم
|
| 4553 |
+
// ============================================
|
| 4554 |
+
function extractEducation(text) {
|
| 4555 |
+
const education = [];
|
| 4556 |
+
const lines = text.split('\n');
|
| 4557 |
+
const eduKeywords = ['university', 'college', 'institute', 'faculty', 'school',
|
| 4558 |
+
'bachelor', 'master', 'diploma', 'degree', 'phd', 'doctorate',
|
| 4559 |
+
'جامعة', 'كلية', 'معهد', 'بكالوريوس', 'ماجستير', 'دكتوراه'];
|
| 4560 |
+
|
| 4561 |
+
for (let i = 0; i < lines.length; i++) {
|
| 4562 |
+
const line = lines[i].toLowerCase();
|
| 4563 |
+
if (eduKeywords.some(keyword => line.includes(keyword))) {
|
| 4564 |
+
const institution = lines[i].trim();
|
| 4565 |
+
let degree = '';
|
| 4566 |
+
let year = '';
|
| 4567 |
+
|
| 4568 |
+
const yearMatch = lines[i].match(/\b(19|20)\d{2}\b/);
|
| 4569 |
+
if (yearMatch) year = yearMatch[0];
|
| 4570 |
+
|
| 4571 |
+
if (i + 1 < lines.length && lines[i + 1].length < 100) {
|
| 4572 |
+
degree = lines[i + 1].trim();
|
| 4573 |
+
}
|
| 4574 |
+
|
| 4575 |
+
if (institution && institution.length > 2) {
|
| 4576 |
+
education.push({ institution, degree, year });
|
| 4577 |
+
if (education.length >= 4) break;
|
| 4578 |
+
}
|
| 4579 |
+
}
|
| 4580 |
+
}
|
| 4581 |
+
return education;
|
| 4582 |
+
}
|
| 4583 |
+
|
| 4584 |
+
// ============================================
|
| 4585 |
+
// استخراج الخبرات
|
| 4586 |
+
// ============================================
|
| 4587 |
+
function extractExperience(text) {
|
| 4588 |
+
const experience = [];
|
| 4589 |
+
const lines = text.split('\n');
|
| 4590 |
+
const expKeywords = ['experience', 'work', 'employment', 'job', 'company',
|
| 4591 |
+
'position', 'role', 'career', 'خبرة', 'عمل', 'شركة', 'وظيفة'];
|
| 4592 |
+
|
| 4593 |
+
for (let i = 0; i < lines.length; i++) {
|
| 4594 |
+
const line = lines[i].toLowerCase();
|
| 4595 |
+
if (expKeywords.some(keyword => line.includes(keyword))) {
|
| 4596 |
+
const company = lines[i].trim();
|
| 4597 |
+
let role = '';
|
| 4598 |
+
let duration = '';
|
| 4599 |
+
|
| 4600 |
+
const durationMatch = lines[i].match(/\b(19|20)\d{2}\s*[-–—]\s*(present|now|current|(19|20)\d{2}|\d{4})\b/i);
|
| 4601 |
+
if (durationMatch) duration = durationMatch[0];
|
| 4602 |
+
|
| 4603 |
+
if (i + 1 < lines.length && lines[i + 1].length < 100) {
|
| 4604 |
+
role = lines[i + 1].trim();
|
| 4605 |
+
}
|
| 4606 |
+
|
| 4607 |
+
if (company && company.length > 2 && !expKeywords.some(k => company.toLowerCase().includes(k))) {
|
| 4608 |
+
experience.push({ company, role, duration });
|
| 4609 |
+
if (experience.length >= 6) break;
|
| 4610 |
+
}
|
| 4611 |
+
}
|
| 4612 |
+
}
|
| 4613 |
+
return experience;
|
| 4614 |
+
}
|
| 4615 |
+
|
| 4616 |
+
// ============================================
|
| 4617 |
+
// استخراج الشهادات
|
| 4618 |
+
// ============================================
|
| 4619 |
+
function extractCertificates(text) {
|
| 4620 |
+
const certificates = [];
|
| 4621 |
+
const lines = text.split('\n');
|
| 4622 |
+
const certKeywords = ['certificate', 'certification', 'course', 'training',
|
| 4623 |
+
'diploma', 'credential', 'شهادة', 'دورة', 'تدريب'];
|
| 4624 |
+
|
| 4625 |
+
for (let i = 0; i < lines.length; i++) {
|
| 4626 |
+
const line = lines[i].toLowerCase();
|
| 4627 |
+
if (certKeywords.some(keyword => line.includes(keyword))) {
|
| 4628 |
+
const name = lines[i].trim();
|
| 4629 |
+
let issuer = '';
|
| 4630 |
+
let year = '';
|
| 4631 |
+
|
| 4632 |
+
const yearMatch = lines[i].match(/\b(19|20)\d{2}\b/);
|
| 4633 |
+
if (yearMatch) year = yearMatch[0];
|
| 4634 |
+
|
| 4635 |
+
if (i + 1 < lines.length && lines[i + 1].length < 80) {
|
| 4636 |
+
issuer = lines[i + 1].trim();
|
| 4637 |
+
}
|
| 4638 |
+
|
| 4639 |
+
if (name && name.length > 3 && !certKeywords.some(k => name.toLowerCase().includes(k))) {
|
| 4640 |
+
certificates.push({ name, issuer, year });
|
| 4641 |
+
if (certificates.length >= 6) break;
|
| 4642 |
+
}
|
| 4643 |
+
}
|
| 4644 |
+
}
|
| 4645 |
+
return certificates;
|
| 4646 |
+
}
|
| 4647 |
|
| 4648 |
+
// ============================================
|
| 4649 |
+
// POST /api/parse-resume - الـ endpoint الرئيسي
|
| 4650 |
+
// ============================================
|
| 4651 |
+
app.post('/api/parse-resume', authenticateToken, upload.single('resume'), async (req, res) => {
|
| 4652 |
+
try {
|
| 4653 |
+
if (!req.file) {
|
| 4654 |
+
return res.status(400).json({ error: 'No file uploaded' });
|
| 4655 |
+
}
|
| 4656 |
+
|
| 4657 |
+
if (req.file.mimetype !== 'application/pdf') {
|
| 4658 |
+
return res.status(400).json({ error: 'Only PDF files are allowed' });
|
| 4659 |
+
}
|
| 4660 |
+
|
| 4661 |
+
const pdfData = await pdfParse(req.file.buffer);
|
| 4662 |
+
const text = pdfData.text;
|
| 4663 |
+
|
| 4664 |
+
if (!text || text.length < 50) {
|
| 4665 |
+
return res.status(400).json({ error: 'Could not extract text from PDF. Make sure it contains readable text.' });
|
| 4666 |
+
}
|
| 4667 |
+
|
| 4668 |
+
const extractedData = {
|
| 4669 |
+
fullName: extractName(text),
|
| 4670 |
+
email: extractEmail(text),
|
| 4671 |
+
phone: extractPhone(text),
|
| 4672 |
+
bio: extractBio(text),
|
| 4673 |
+
jobTitle: extractJobTitle(text),
|
| 4674 |
+
skills: extractSkills(text),
|
| 4675 |
+
education: extractEducation(text),
|
| 4676 |
+
experience: extractExperience(text),
|
| 4677 |
+
certificates: extractCertificates(text),
|
| 4678 |
+
interests: extractInterests(text)
|
| 4679 |
+
};
|
| 4680 |
+
|
| 4681 |
+
logger.info(`Resume parsed successfully for user ${req.user.userId}`);
|
| 4682 |
+
|
| 4683 |
+
res.json({
|
| 4684 |
+
success: true,
|
| 4685 |
+
data: extractedData,
|
| 4686 |
+
message: 'Resume parsed successfully! Review and save the extracted data.'
|
| 4687 |
+
});
|
| 4688 |
+
|
| 4689 |
+
} catch (error) {
|
| 4690 |
+
logger.error(`Error parsing resume: ${error.message}`);
|
| 4691 |
+
res.status(500).json({ error: 'Failed to parse resume: ' + error.message });
|
| 4692 |
+
}
|
| 4693 |
+
});
|
| 4694 |
+
// -----------------------------
|
| 4695 |
app.post('/api/revoke-token', authenticateToken, [
|
| 4696 |
body('refreshToken').notEmpty().withMessage('Refresh token is required')
|
| 4697 |
], async (req, res) => {
|