MGZON commited on
Commit
337fa45
·
verified ·
1 Parent(s): 7d56b2d

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +494 -19
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 => item.name && typeof item.percentage === 'number' && item.percentage >= 0 && item.percentage <= 100);
 
 
 
 
 
 
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
- const parsedSkills = parseJSON(skills, user.profile.skills);
 
 
 
 
 
 
 
 
2957
  let parsedProjects = parseJSON(projects, user.profile.projects);
2958
  const parsedInterests = parseJSON(interests, user.profile.interests);
2959
- const githubProjectIds = req.body.githubProjectIds || '[]'; // قيمة افتراضية آمنة
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2960
  const parsedGithubProjectIds = parseJSON(githubProjectIds, []);
2961
 
2962
- // Handle avatar image with transparency check
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
- // Update user profile
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: parsedSkills,
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) => {