ibrahimlasfar commited on
Commit
172f873
·
1 Parent(s): 1497dfb

Add Node.js portfolio API with Docker

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules/
2
+ .env
3
+ *.log
4
+ .DS_Store
5
+ logs/
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile لـ Node.js
2
+ FROM node:18-alpine
3
+
4
+ # Create app directory
5
+ WORKDIR /app
6
+
7
+ # Copy package files
8
+ COPY package*.json ./
9
+
10
+ # Install dependencies
11
+ RUN npm ci --only=production
12
+
13
+ # Copy source code
14
+ COPY . .
15
+
16
+ # Create necessary directories
17
+ RUN mkdir -p public/images views docs logs
18
+
19
+ # Expose port
20
+ EXPOSE 7860
21
+
22
+ # Health check
23
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
24
+ CMD node -e "require('http').get('http://localhost:7860/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
25
+
26
+ # Start server
27
+ CMD ["node", "server.js"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ibrahim Al-Asfar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,12 +1,37 @@
1
  ---
2
- title: Server
3
- emoji: 📊
4
- colorFrom: yellow
5
  colorTo: blue
6
  sdk: docker
 
7
  pinned: false
8
  license: apache-2.0
9
- short_description: portfolio-api
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Portfolio API
3
+ emoji: 🚀
4
+ colorFrom: purple
5
  colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  license: apache-2.0
 
10
  ---
11
 
12
+ # Ibrahim Al-Asfar Portfolio API
13
+
14
+ Full-stack web developer portfolio backend API with authentication, file uploads, and AI features.
15
+
16
+ ## API Endpoints
17
+
18
+ - `GET /` - Homepage
19
+ - `GET /api/health` - System health check
20
+ - `GET /api/projects` - Get all public projects
21
+ - `GET /api/skills` - Get all skills
22
+ - `POST /api/register` - Register new user
23
+ - `POST /api/login` - User login
24
+ - `GET /api/profile/:nickname` - Get user profile
25
+
26
+ ## Documentation
27
+
28
+ Interactive API documentation available at `/api-docs`
29
+
30
+ ## Technology Stack
31
+
32
+ - Node.js & Express
33
+ - MongoDB
34
+ - JWT Authentication
35
+ - Cloudinary for media
36
+ - Google/Facebook/GitHub OAuth
37
+ - Google Gemini AI
docs/swagger.yaml ADDED
@@ -0,0 +1,1510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Portfolio API
4
+ version: 1.0.0
5
+ description: API for Ibrahim Al-Asfar's portfolio website, supporting user authentication, project management, profile handling, and more.
6
+ servers:
7
+ - url: https://portfolio-backend-beta-two.vercel.app
8
+ description: Production server
9
+ - url: http://localhost:3000
10
+ description: Local development server
11
+ components:
12
+ securitySchemes:
13
+ bearerAuth:
14
+ type: http
15
+ scheme: bearer
16
+ bearerFormat: JWT
17
+ schemas:
18
+ Project:
19
+ type: object
20
+ properties:
21
+ _id:
22
+ type: string
23
+ title:
24
+ type: string
25
+ description:
26
+ type: string
27
+ image:
28
+ type: string
29
+ rating:
30
+ type: string
31
+ stars:
32
+ type: number
33
+ links:
34
+ type: array
35
+ items:
36
+ type: object
37
+ properties:
38
+ option:
39
+ type: string
40
+ value:
41
+ type: string
42
+ isPrivate:
43
+ type: boolean
44
+ Comment:
45
+ type: object
46
+ properties:
47
+ _id:
48
+ type: string
49
+ projectId:
50
+ type: string
51
+ userId:
52
+ type: object
53
+ properties:
54
+ username:
55
+ type: string
56
+ email:
57
+ type: string
58
+ rating:
59
+ type: number
60
+ text:
61
+ type: string
62
+ timestamp:
63
+ type: string
64
+ format: date-time
65
+ replies:
66
+ type: array
67
+ items:
68
+ type: object
69
+ properties:
70
+ text:
71
+ type: string
72
+ timestamp:
73
+ type: string
74
+ format: date-time
75
+ Skill:
76
+ type: object
77
+ properties:
78
+ _id:
79
+ type: string
80
+ name:
81
+ type: string
82
+ icon:
83
+ type: string
84
+ percentage:
85
+ type: number
86
+ UserProfile:
87
+ type: object
88
+ properties:
89
+ username:
90
+ type: string
91
+ profile:
92
+ type: object
93
+ properties:
94
+ nickname:
95
+ type: string
96
+ portfolioName:
97
+ type: string
98
+ avatar:
99
+ type: string
100
+ avatarDisplayType:
101
+ type: string
102
+ enum: ['svg', 'normal']
103
+ svgColor:
104
+ type: string
105
+ jobTitle:
106
+ type: string
107
+ bio:
108
+ type: string
109
+ phone:
110
+ type: string
111
+ socialLinks:
112
+ type: object
113
+ properties:
114
+ linkedin:
115
+ type: string
116
+ behance:
117
+ type: string
118
+ github:
119
+ type: string
120
+ whatsapp:
121
+ type: string
122
+ education:
123
+ type: array
124
+ items:
125
+ type: object
126
+ properties:
127
+ institution:
128
+ type: string
129
+ degree:
130
+ type: string
131
+ year:
132
+ type: string
133
+ experience:
134
+ type: array
135
+ items:
136
+ type: object
137
+ properties:
138
+ company:
139
+ type: string
140
+ role:
141
+ type: string
142
+ duration:
143
+ type: string
144
+ certificates:
145
+ type: array
146
+ items:
147
+ type: object
148
+ properties:
149
+ name:
150
+ type: string
151
+ issuer:
152
+ type: string
153
+ year:
154
+ type: string
155
+ skills:
156
+ type: array
157
+ items:
158
+ type: object
159
+ properties:
160
+ name:
161
+ type: string
162
+ percentage:
163
+ type: number
164
+ projects:
165
+ type: array
166
+ items:
167
+ type: object
168
+ properties:
169
+ title:
170
+ type: string
171
+ description:
172
+ type: string
173
+ image:
174
+ type: string
175
+ links:
176
+ type: array
177
+ items:
178
+ type: object
179
+ properties:
180
+ option:
181
+ type: string
182
+ value:
183
+ type: string
184
+ interests:
185
+ type: array
186
+ items:
187
+ type: string
188
+ customFields:
189
+ type: array
190
+ items:
191
+ type: object
192
+ properties:
193
+ key:
194
+ type: string
195
+ value:
196
+ type: string
197
+ Notification:
198
+ type: object
199
+ properties:
200
+ message:
201
+ type: string
202
+ paths:
203
+ /api/login:
204
+ post:
205
+ summary: Log in a user
206
+ description: Authenticates a user with email and password. Returns tokens for admin users or sends OTP for non-admin users.
207
+ requestBody:
208
+ required: true
209
+ content:
210
+ application/json:
211
+ schema:
212
+ type: object
213
+ properties:
214
+ email:
215
+ type: string
216
+ example: user@example.com
217
+ password:
218
+ type: string
219
+ example: password123
220
+ required:
221
+ - email
222
+ - password
223
+ responses:
224
+ '200':
225
+ description: OTP sent or tokens returned
226
+ content:
227
+ application/json:
228
+ schema:
229
+ oneOf:
230
+ - type: object
231
+ properties:
232
+ message:
233
+ type: string
234
+ example: OTP sent to your email
235
+ - type: object
236
+ properties:
237
+ token:
238
+ type: string
239
+ refreshToken:
240
+ type: string
241
+ '400':
242
+ description: Invalid input
243
+ content:
244
+ application/json:
245
+ schema:
246
+ type: object
247
+ properties:
248
+ errors:
249
+ type: array
250
+ items:
251
+ type: object
252
+ properties:
253
+ msg:
254
+ type: string
255
+ param:
256
+ type: string
257
+ location:
258
+ type: string
259
+ '401':
260
+ description: Invalid credentials
261
+ '500':
262
+ description: Server error
263
+ /api/login/verify-otp:
264
+ post:
265
+ summary: Verify OTP for login
266
+ description: Verifies the OTP sent to the user's email to complete login.
267
+ requestBody:
268
+ required: true
269
+ content:
270
+ application/json:
271
+ schema:
272
+ type: object
273
+ properties:
274
+ email:
275
+ type: string
276
+ example: user@example.com
277
+ otp:
278
+ type: string
279
+ example: "123456"
280
+ required:
281
+ - email
282
+ - otp
283
+ responses:
284
+ '200':
285
+ description: Login successful, returns tokens
286
+ content:
287
+ application/json:
288
+ schema:
289
+ type: object
290
+ properties:
291
+ token:
292
+ type: string
293
+ refreshToken:
294
+ type: string
295
+ '400':
296
+ description: Invalid or expired OTP
297
+ '500':
298
+ description: Server error
299
+ /api/register:
300
+ post:
301
+ summary: Register a new user
302
+ description: Creates a new user account with username, email, and password.
303
+ requestBody:
304
+ required: true
305
+ content:
306
+ application/json:
307
+ schema:
308
+ type: object
309
+ properties:
310
+ username:
311
+ type: string
312
+ example: testuser
313
+ email:
314
+ type: string
315
+ example: user@example.com
316
+ password:
317
+ type: string
318
+ example: password123
319
+ required:
320
+ - email
321
+ - password
322
+ responses:
323
+ '201':
324
+ description: User registered successfully
325
+ content:
326
+ application/json:
327
+ schema:
328
+ type: object
329
+ properties:
330
+ token:
331
+ type: string
332
+ refreshToken:
333
+ type: string
334
+ '400':
335
+ description: Invalid input or email already exists
336
+ content:
337
+ application/json:
338
+ schema:
339
+ type: object
340
+ properties:
341
+ errors:
342
+ type: array
343
+ items:
344
+ type: object
345
+ properties:
346
+ msg:
347
+ type: string
348
+ param:
349
+ type: string
350
+ location:
351
+ type: string
352
+ '500':
353
+ description: Server error
354
+ /api/verify-token:
355
+ get:
356
+ summary: Verify JWT token
357
+ description: Verifies the provided JWT token and returns user information.
358
+ security:
359
+ - bearerAuth: []
360
+ responses:
361
+ '200':
362
+ description: Token is valid
363
+ content:
364
+ application/json:
365
+ schema:
366
+ type: object
367
+ properties:
368
+ valid:
369
+ type: boolean
370
+ userId:
371
+ type: string
372
+ isAdmin:
373
+ type: boolean
374
+ username:
375
+ type: string
376
+ profile:
377
+ $ref: '#/components/schemas/UserProfile'
378
+ '401':
379
+ description: Token is required
380
+ '403':
381
+ description: Invalid token
382
+ '404':
383
+ description: User not found
384
+ /api/logout:
385
+ post:
386
+ summary: Log out a user
387
+ description: Invalidates the refresh token and logs out the user.
388
+ security:
389
+ - bearerAuth: []
390
+ requestBody:
391
+ required: true
392
+ content:
393
+ application/json:
394
+ schema:
395
+ type: object
396
+ properties:
397
+ refreshToken:
398
+ type: string
399
+ required:
400
+ - refreshToken
401
+ responses:
402
+ '200':
403
+ description: Logged out successfully
404
+ content:
405
+ application/json:
406
+ schema:
407
+ type: object
408
+ properties:
409
+ message:
410
+ type: string
411
+ example: Logged out successfully
412
+ '404':
413
+ description: User not found
414
+ '500':
415
+ description: Server error
416
+ /api/upload:
417
+ post:
418
+ summary: Upload a file
419
+ description: Uploads a file (JPEG, PNG, or PDF) to Cloudinary (authenticated users only).
420
+ security:
421
+ - bearerAuth: []
422
+ requestBody:
423
+ required: true
424
+ content:
425
+ multipart/form-data:
426
+ schema:
427
+ type: object
428
+ properties:
429
+ file:
430
+ type: string
431
+ format: binary
432
+ required:
433
+ - file
434
+ responses:
435
+ '200':
436
+ description: File uploaded successfully
437
+ content:
438
+ application/json:
439
+ schema:
440
+ type: object
441
+ properties:
442
+ message:
443
+ type: string
444
+ example: "File uploaded successfully: <fileUrl>"
445
+ '400':
446
+ description: No file uploaded or invalid file type
447
+ '401':
448
+ description: Authentication required
449
+ /api/projects:
450
+ get:
451
+ summary: Get all projects
452
+ description: Retrieves a list of all projects.
453
+ responses:
454
+ '200':
455
+ description: List of projects
456
+ content:
457
+ application/json:
458
+ schema:
459
+ type: array
460
+ items:
461
+ $ref: '#/components/schemas/Project'
462
+ '500':
463
+ description: Server error
464
+ post:
465
+ summary: Create a new project
466
+ description: Creates a new project (admin only).
467
+ security:
468
+ - bearerAuth: []
469
+ requestBody:
470
+ required: true
471
+ content:
472
+ application/json:
473
+ schema:
474
+ type: object
475
+ properties:
476
+ title:
477
+ type: string
478
+ example: Project Title
479
+ description:
480
+ type: string
481
+ example: Project description
482
+ image:
483
+ type: string
484
+ example: https://example.com/image.jpg
485
+ rating:
486
+ type: string
487
+ example: High
488
+ stars:
489
+ type: number
490
+ example: 5
491
+ links:
492
+ type: array
493
+ items:
494
+ type: object
495
+ properties:
496
+ option:
497
+ type: string
498
+ value:
499
+ type: string
500
+ isPrivate:
501
+ type: boolean
502
+ required:
503
+ - title
504
+ - description
505
+ - image
506
+ - rating
507
+ - stars
508
+ - links
509
+ responses:
510
+ '201':
511
+ description: Project created
512
+ content:
513
+ application/json:
514
+ schema:
515
+ $ref: '#/components/schemas/Project'
516
+ '400':
517
+ description: Invalid input
518
+ content:
519
+ application/json:
520
+ schema:
521
+ type: object
522
+ properties:
523
+ errors:
524
+ type: array
525
+ items:
526
+ type: object
527
+ properties:
528
+ msg:
529
+ type: string
530
+ param:
531
+ type: string
532
+ location:
533
+ type: string
534
+ '403':
535
+ description: Admin access required
536
+ '500':
537
+ description: Server error
538
+ /api/projects/{projectId}:
539
+ put:
540
+ summary: Update a project
541
+ description: Updates an existing project (admin only).
542
+ security:
543
+ - bearerAuth: []
544
+ parameters:
545
+ - in: path
546
+ name: projectId
547
+ required: true
548
+ schema:
549
+ type: string
550
+ description: The ID of the project to update
551
+ requestBody:
552
+ required: true
553
+ content:
554
+ application/json:
555
+ schema:
556
+ type: object
557
+ properties:
558
+ title:
559
+ type: string
560
+ example: Updated Project Title
561
+ description:
562
+ type: string
563
+ example: Updated project description
564
+ image:
565
+ type: string
566
+ example: https://example.com/updated-image.jpg
567
+ rating:
568
+ type: string
569
+ example: High
570
+ stars:
571
+ type: number
572
+ example: 4
573
+ links:
574
+ type: array
575
+ items:
576
+ type: object
577
+ properties:
578
+ option:
579
+ type: string
580
+ value:
581
+ type: string
582
+ isPrivate:
583
+ type: boolean
584
+ required:
585
+ - title
586
+ - description
587
+ - image
588
+ - rating
589
+ - stars
590
+ - links
591
+ responses:
592
+ '200':
593
+ description: Project updated
594
+ content:
595
+ application/json:
596
+ schema:
597
+ $ref: '#/components/schemas/Project'
598
+ '400':
599
+ description: Invalid input
600
+ content:
601
+ application/json:
602
+ schema:
603
+ type: object
604
+ properties:
605
+ errors:
606
+ type: array
607
+ items:
608
+ type: object
609
+ properties:
610
+ msg:
611
+ type: string
612
+ param:
613
+ type: string
614
+ location:
615
+ type: string
616
+ '403':
617
+ description: Admin access required
618
+ '404':
619
+ description: Project not found
620
+ '500':
621
+ description: Server error
622
+ delete:
623
+ summary: Delete a project
624
+ description: Deletes a project and its associated comments (admin only).
625
+ security:
626
+ - bearerAuth: []
627
+ parameters:
628
+ - in: path
629
+ name: projectId
630
+ required: true
631
+ schema:
632
+ type: string
633
+ description: The ID of the project to delete
634
+ responses:
635
+ '204':
636
+ description: Project deleted
637
+ '403':
638
+ description: Admin access required
639
+ '500':
640
+ description: Server error
641
+ /api/comments/{projectId}:
642
+ get:
643
+ summary: Get comments for a project
644
+ description: Retrieves all comments for a specific project.
645
+ parameters:
646
+ - in: path
647
+ name: projectId
648
+ required: true
649
+ schema:
650
+ type: string
651
+ description: The ID of the project
652
+ responses:
653
+ '200':
654
+ description: List of comments
655
+ content:
656
+ application/json:
657
+ schema:
658
+ type: array
659
+ items:
660
+ $ref: '#/components/schemas/Comment'
661
+ '500':
662
+ description: Server error
663
+ /api/comments:
664
+ get:
665
+ summary: Get all comments
666
+ description: Retrieves all comments with user and project details (admin only).
667
+ security:
668
+ - bearerAuth: []
669
+ responses:
670
+ '200':
671
+ description: List of comments
672
+ content:
673
+ application/json:
674
+ schema:
675
+ type: array
676
+ items:
677
+ type: object
678
+ properties:
679
+ _id:
680
+ type: string
681
+ projectId:
682
+ type: string
683
+ projectTitle:
684
+ type: string
685
+ userId:
686
+ type: object
687
+ properties:
688
+ username:
689
+ type: string
690
+ email:
691
+ type: string
692
+ rating:
693
+ type: number
694
+ text:
695
+ type: string
696
+ timestamp:
697
+ type: string
698
+ format: date-time
699
+ replies:
700
+ type: array
701
+ items:
702
+ type: object
703
+ properties:
704
+ text:
705
+ type: string
706
+ timestamp:
707
+ type: string
708
+ format: date-time
709
+ '403':
710
+ description: Admin access required
711
+ '500':
712
+ description: Server error
713
+ post:
714
+ summary: Add a comment
715
+ description: Adds a new comment to a project (authenticated users only).
716
+ security:
717
+ - bearerAuth: []
718
+ requestBody:
719
+ required: true
720
+ content:
721
+ application/json:
722
+ schema:
723
+ type: object
724
+ properties:
725
+ projectId:
726
+ type: string
727
+ example: 507f1f77bcf86cd799439011
728
+ rating:
729
+ type: number
730
+ example: 5
731
+ text:
732
+ type: string
733
+ example: Great project!
734
+ required:
735
+ - projectId
736
+ - rating
737
+ - text
738
+ responses:
739
+ '201':
740
+ description: Comment added
741
+ content:
742
+ application/json:
743
+ schema:
744
+ $ref: '#/components/schemas/Comment'
745
+ '400':
746
+ description: Invalid input
747
+ content:
748
+ application/json:
749
+ schema:
750
+ type: object
751
+ properties:
752
+ errors:
753
+ type: array
754
+ items:
755
+ type: object
756
+ properties:
757
+ msg:
758
+ type: string
759
+ param:
760
+ type: string
761
+ location:
762
+ type: string
763
+ '401':
764
+ description: Authentication required
765
+ '500':
766
+ description: Server error
767
+ /api/comments/{commentId}/reply:
768
+ post:
769
+ summary: Reply to a comment
770
+ description: Adds a reply to an existing comment (admin only).
771
+ security:
772
+ - bearerAuth: []
773
+ parameters:
774
+ - in: path
775
+ name: commentId
776
+ required: true
777
+ schema:
778
+ type: string
779
+ description: The ID of the comment to reply to
780
+ requestBody:
781
+ required: true
782
+ content:
783
+ application/json:
784
+ schema:
785
+ type: object
786
+ properties:
787
+ text:
788
+ type: string
789
+ example: Thanks for your feedback!
790
+ required:
791
+ - text
792
+ responses:
793
+ '200':
794
+ description: Reply added
795
+ content:
796
+ application/json:
797
+ schema:
798
+ $ref: '#/components/schemas/Comment'
799
+ '400':
800
+ description: Invalid input
801
+ content:
802
+ application/json:
803
+ schema:
804
+ type: object
805
+ properties:
806
+ errors:
807
+ type: array
808
+ items:
809
+ type: object
810
+ properties:
811
+ msg:
812
+ type: string
813
+ param:
814
+ type: string
815
+ location:
816
+ type: string
817
+ '403':
818
+ description: Admin access required
819
+ '404':
820
+ description: Comment not found
821
+ '500':
822
+ description: Server error
823
+ /api/comments/{commentId}:
824
+ delete:
825
+ summary: Delete a comment
826
+ description: Deletes a comment (admin only).
827
+ security:
828
+ - bearerAuth: []
829
+ parameters:
830
+ - in: path
831
+ name: commentId
832
+ required: true
833
+ schema:
834
+ type: string
835
+ description: The ID of the comment to delete
836
+ responses:
837
+ '204':
838
+ description: Comment deleted
839
+ '403':
840
+ description: Admin access required
841
+ '500':
842
+ description: Server error
843
+ /api/skills:
844
+ get:
845
+ summary: Get all skills
846
+ description: Retrieves a list of all skills.
847
+ responses:
848
+ '200':
849
+ description: List of skills
850
+ content:
851
+ application/json:
852
+ schema:
853
+ type: array
854
+ items:
855
+ $ref: '#/components/schemas/Skill'
856
+ '500':
857
+ description: Server error
858
+ post:
859
+ summary: Create a new skill
860
+ description: Creates a new skill (admin only).
861
+ security:
862
+ - bearerAuth: []
863
+ requestBody:
864
+ required: true
865
+ content:
866
+ application/json:
867
+ schema:
868
+ type: object
869
+ properties:
870
+ name:
871
+ type: string
872
+ example: JavaScript
873
+ icon:
874
+ type: string
875
+ example: https://example.com/icon.png
876
+ percentage:
877
+ type: number
878
+ example: 90
879
+ required:
880
+ - name
881
+ - icon
882
+ - percentage
883
+ responses:
884
+ '201':
885
+ description: Skill created
886
+ content:
887
+ application/json:
888
+ schema:
889
+ $ref: '#/components/schemas/Skill'
890
+ '400':
891
+ description: Invalid input
892
+ content:
893
+ application/json:
894
+ schema:
895
+ type: object
896
+ properties:
897
+ errors:
898
+ type: array
899
+ items:
900
+ type: object
901
+ properties:
902
+ msg:
903
+ type: string
904
+ param:
905
+ type: string
906
+ location:
907
+ type: string
908
+ '403':
909
+ description: Admin access required
910
+ '500':
911
+ description: Server error
912
+ /api/skills/{skillId}:
913
+ put:
914
+ summary: Update a skill
915
+ description: Updates an existing skill (admin only).
916
+ security:
917
+ - bearerAuth: []
918
+ parameters:
919
+ - in: path
920
+ name: skillId
921
+ required: true
922
+ schema:
923
+ type: string
924
+ description: The ID of the skill to update
925
+ requestBody:
926
+ required: true
927
+ content:
928
+ application/json:
929
+ schema:
930
+ type: object
931
+ properties:
932
+ name:
933
+ type: string
934
+ example: Updated JavaScript
935
+ icon:
936
+ type: string
937
+ example: https://example.com/updated-icon.png
938
+ percentage:
939
+ type: number
940
+ example: 95
941
+ required:
942
+ - name
943
+ - icon
944
+ - percentage
945
+ responses:
946
+ '200':
947
+ description: Skill updated
948
+ content:
949
+ application/json:
950
+ schema:
951
+ $ref: '#/components/schemas/Skill'
952
+ '400':
953
+ description: Invalid input
954
+ content:
955
+ application/json:
956
+ schema:
957
+ type: object
958
+ properties:
959
+ errors:
960
+ type: array
961
+ items:
962
+ type: object
963
+ properties:
964
+ msg:
965
+ type: string
966
+ param:
967
+ type: string
968
+ location:
969
+ type: string
970
+ '403':
971
+ description: Admin access required
972
+ '404':
973
+ description: Skill not found
974
+ '500':
975
+ description: Server error
976
+ delete:
977
+ summary: Delete a skill
978
+ description: Deletes a skill (admin only).
979
+ security:
980
+ - bearerAuth: []
981
+ parameters:
982
+ - in: path
983
+ name: skillId
984
+ required: true
985
+ schema:
986
+ type: string
987
+ description: The ID of the skill to delete
988
+ responses:
989
+ '204':
990
+ description: Skill deleted
991
+ '403':
992
+ description: Admin access required
993
+ '500':
994
+ description: Server error
995
+ /api/profile/{nickname}:
996
+ get:
997
+ summary: Get a user's profile
998
+ description: Retrieves a user's profile by nickname or username.
999
+ parameters:
1000
+ - in: path
1001
+ name: nickname
1002
+ required: true
1003
+ schema:
1004
+ type: string
1005
+ description: The nickname or username of the user
1006
+ responses:
1007
+ '200':
1008
+ description: User profile
1009
+ content:
1010
+ application/json:
1011
+ schema:
1012
+ $ref: '#/components/schemas/UserProfile'
1013
+ '404':
1014
+ description: User not found
1015
+ /api/profile:
1016
+ put:
1017
+ summary: Update user profile
1018
+ description: Updates the authenticated user's profile, including optional file uploads for avatar and project images.
1019
+ security:
1020
+ - bearerAuth: []
1021
+ requestBody:
1022
+ required: true
1023
+ content:
1024
+ multipart/form-data:
1025
+ schema:
1026
+ type: object
1027
+ properties:
1028
+ nickname:
1029
+ type: string
1030
+ example: johndoe
1031
+ jobTitle:
1032
+ type: string
1033
+ example: Full-Stack Developer
1034
+ bio:
1035
+ type: string
1036
+ example: Experienced developer with a passion for web technologies
1037
+ phone:
1038
+ type: string
1039
+ example: +1234567890
1040
+ socialLinks:
1041
+ type: string
1042
+ example: '{"linkedin":"https://linkedin.com/in/johndoe","github":"https://github.com/johndoe"}'
1043
+ education:
1044
+ type: string
1045
+ example: '[{"institution":"University","degree":"BSc","year":"2020"}]'
1046
+ experience:
1047
+ type: string
1048
+ example: '[{"company":"Tech Corp","role":"Developer","duration":"2020-2022"}]'
1049
+ certificates:
1050
+ type: string
1051
+ example: '[{"name":"AWS Certified","issuer":"Amazon","year":"2021"}]'
1052
+ skills:
1053
+ type: string
1054
+ example: '[{"name":"JavaScript","percentage":90}]'
1055
+ projects:
1056
+ type: string
1057
+ example: '[{"title":"Project X","description":"A web app","image":"https://example.com/image.jpg"}]'
1058
+ interests:
1059
+ type: string
1060
+ example: '["coding","gaming"]'
1061
+ isPublic:
1062
+ type: boolean
1063
+ example: true
1064
+ avatarDisplayType:
1065
+ type: string
1066
+ enum: ['svg', 'normal']
1067
+ example: normal
1068
+ svgColor:
1069
+ type: string
1070
+ example: '#000000'
1071
+ avatar:
1072
+ type: string
1073
+ format: binary
1074
+ projectImages:
1075
+ type: array
1076
+ items:
1077
+ type: string
1078
+ format: binary
1079
+ responses:
1080
+ '200':
1081
+ description: Profile updated
1082
+ content:
1083
+ application/json:
1084
+ schema:
1085
+ type: object
1086
+ properties:
1087
+ success:
1088
+ type: boolean
1089
+ message:
1090
+ type: string
1091
+ example: Profile updated successfully
1092
+ profile:
1093
+ $ref: '#/components/schemas/UserProfile'
1094
+ hasTransparency:
1095
+ type: boolean
1096
+ '400':
1097
+ description: Invalid input
1098
+ content:
1099
+ application/json:
1100
+ schema:
1101
+ type: object
1102
+ properties:
1103
+ errors:
1104
+ type: array
1105
+ items:
1106
+ type: object
1107
+ properties:
1108
+ msg:
1109
+ type: string
1110
+ param:
1111
+ type: string
1112
+ location:
1113
+ type: string
1114
+ '401':
1115
+ description: Authentication required
1116
+ '404':
1117
+ description: User not found
1118
+ '500':
1119
+ description: Server error
1120
+ /api/profile/pdf/{nickname}:
1121
+ get:
1122
+ summary: Generate a PDF resume
1123
+ description: Generates a PDF resume for a user by nickname, using jsPDF with autoTable for professional formatting. Only accessible for public profiles or authenticated users.
1124
+ parameters:
1125
+ - in: path
1126
+ name: nickname
1127
+ required: true
1128
+ schema:
1129
+ type: string
1130
+ description: The nickname or username of the user
1131
+ responses:
1132
+ '200':
1133
+ description: PDF resume generated
1134
+ content:
1135
+ application/pdf:
1136
+ schema:
1137
+ type: string
1138
+ format: binary
1139
+ '403':
1140
+ description: Profile is private
1141
+ '404':
1142
+ description: User not found
1143
+ '500':
1144
+ description: Server error
1145
+ /api/notifications:
1146
+ get:
1147
+ summary: Get user notifications
1148
+ description: Retrieves all notifications for the authenticated user (admin only).
1149
+ security:
1150
+ - bearerAuth: []
1151
+ responses:
1152
+ '200':
1153
+ description: List of notifications
1154
+ content:
1155
+ application/json:
1156
+ schema:
1157
+ type: array
1158
+ items:
1159
+ $ref: '#/components/schemas/Notification'
1160
+ '403':
1161
+ description: Admin access required
1162
+ '500':
1163
+ description: Server error
1164
+ /api/user-interactions:
1165
+ get:
1166
+ summary: Get user interactions
1167
+ description: Retrieves all comments made by the authenticated user.
1168
+ security:
1169
+ - bearerAuth: []
1170
+ responses:
1171
+ '200':
1172
+ description: List of user comments
1173
+ content:
1174
+ application/json:
1175
+ schema:
1176
+ type: array
1177
+ items:
1178
+ $ref: '#/components/schemas/Comment'
1179
+ '401':
1180
+ description: Authentication required
1181
+ '500':
1182
+ description: Server error
1183
+ /api/users:
1184
+ get:
1185
+ summary: Get all users
1186
+ description: Retrieves a list of all users (admin only).
1187
+ security:
1188
+ - bearerAuth: []
1189
+ responses:
1190
+ '200':
1191
+ description: List of users
1192
+ content:
1193
+ application/json:
1194
+ schema:
1195
+ type: array
1196
+ items:
1197
+ type: object
1198
+ properties:
1199
+ username:
1200
+ type: string
1201
+ email:
1202
+ type: string
1203
+ profile:
1204
+ $ref: '#/components/schemas/UserProfile'
1205
+ '403':
1206
+ description: Admin access required
1207
+ '500':
1208
+ description: Server error
1209
+ /api/users/{userId}:
1210
+ delete:
1211
+ summary: Delete a user
1212
+ description: Deletes a user and their comments (admin only).
1213
+ security:
1214
+ - bearerAuth: []
1215
+ parameters:
1216
+ - in: path
1217
+ name: userId
1218
+ required: true
1219
+ schema:
1220
+ type: string
1221
+ description: The ID of the user to delete
1222
+ responses:
1223
+ '204':
1224
+ description: User deleted
1225
+ '403':
1226
+ description: Admin access required
1227
+ '500':
1228
+ description: Server error
1229
+ /api/forgot-password:
1230
+ post:
1231
+ summary: Request password reset
1232
+ description: Sends a password reset OTP to the user's email.
1233
+ requestBody:
1234
+ required: true
1235
+ content:
1236
+ application/json:
1237
+ schema:
1238
+ type: object
1239
+ properties:
1240
+ email:
1241
+ type: string
1242
+ example: user@example.com
1243
+ required:
1244
+ - email
1245
+ responses:
1246
+ '200':
1247
+ description: Reset code sent
1248
+ content:
1249
+ application/json:
1250
+ schema:
1251
+ type: object
1252
+ properties:
1253
+ message:
1254
+ type: string
1255
+ example: Reset code sent to your email
1256
+ '400':
1257
+ description: Email required
1258
+ '404':
1259
+ description: User not found
1260
+ '500':
1261
+ description: Server error
1262
+ /api/reset-password:
1263
+ post:
1264
+ summary: Reset password
1265
+ description: Resets the user's password using the OTP.
1266
+ requestBody:
1267
+ required: true
1268
+ content:
1269
+ application/json:
1270
+ schema:
1271
+ type: object
1272
+ properties:
1273
+ email:
1274
+ type: string
1275
+ example: user@example.com
1276
+ otp:
1277
+ type: string
1278
+ example: "123456"
1279
+ newPassword:
1280
+ type: string
1281
+ example: newpassword123
1282
+ required:
1283
+ - email
1284
+ - otp
1285
+ - newPassword
1286
+ responses:
1287
+ '200':
1288
+ description: Password reset successfully
1289
+ content:
1290
+ application/json:
1291
+ schema:
1292
+ type: object
1293
+ properties:
1294
+ message:
1295
+ type: string
1296
+ example: Password reset successfully
1297
+ '400':
1298
+ description: Invalid input or OTP
1299
+ content:
1300
+ application/json:
1301
+ schema:
1302
+ type: object
1303
+ properties:
1304
+ errors:
1305
+ type: array
1306
+ items:
1307
+ type: object
1308
+ properties:
1309
+ msg:
1310
+ type: string
1311
+ param:
1312
+ type: string
1313
+ location:
1314
+ type: string
1315
+ '500':
1316
+ description: Server error
1317
+ /api/health:
1318
+ get:
1319
+ summary: Check server health
1320
+ description: Checks the health of the server and MongoDB connection.
1321
+ responses:
1322
+ '200':
1323
+ description: Server is healthy
1324
+ content:
1325
+ application/json:
1326
+ schema:
1327
+ type: object
1328
+ properties:
1329
+ status:
1330
+ type: string
1331
+ example: ok
1332
+ mongodb:
1333
+ type: string
1334
+ example: connected
1335
+ timestamp:
1336
+ type: string
1337
+ format: date-time
1338
+ '500':
1339
+ description: Server error
1340
+ /api/users/search:
1341
+ get:
1342
+ summary: Search users
1343
+ description: Searches for public users by nickname or username.
1344
+ parameters:
1345
+ - in: query
1346
+ name: query
1347
+ required: true
1348
+ schema:
1349
+ type: string
1350
+ description: Search query for nickname or username
1351
+ responses:
1352
+ '200':
1353
+ description: List of matching users
1354
+ content:
1355
+ application/json:
1356
+ schema:
1357
+ type: array
1358
+ items:
1359
+ type: object
1360
+ properties:
1361
+ username:
1362
+ type: string
1363
+ nickname:
1364
+ type: string
1365
+ avatar:
1366
+ type: string
1367
+ profileUrl:
1368
+ type: string
1369
+ portfolioName:
1370
+ type: string
1371
+ '500':
1372
+ description: Server error
1373
+ /api/conversations/export:
1374
+ get:
1375
+ summary: Export conversations
1376
+ description: Exports all conversations as a CSV file (admin only).
1377
+ security:
1378
+ - bearerAuth: []
1379
+ responses:
1380
+ '200':
1381
+ description: Conversations exported as CSV
1382
+ content:
1383
+ text/csv:
1384
+ schema:
1385
+ type: string
1386
+ format: binary
1387
+ '403':
1388
+ description: Admin access required
1389
+ '500':
1390
+ description: Server error
1391
+ /api/github-projects:
1392
+ get:
1393
+ summary: Get GitHub projects
1394
+ description: Retrieves the GitHub repositories for the user Mark-Lasfar.
1395
+ responses:
1396
+ '200':
1397
+ description: List of GitHub repositories
1398
+ content:
1399
+ application/json:
1400
+ schema:
1401
+ type: array
1402
+ items:
1403
+ type: object
1404
+ properties:
1405
+ id:
1406
+ type: number
1407
+ name:
1408
+ type: string
1409
+ description:
1410
+ type: string
1411
+ html_url:
1412
+ type: string
1413
+ created_at:
1414
+ type: string
1415
+ format: date-time
1416
+ '500':
1417
+ description: Server error
1418
+ /auth/google:
1419
+ get:
1420
+ summary: Initiate Google OAuth login
1421
+ description: Redirects to Google for OAuth authentication.
1422
+ responses:
1423
+ '302':
1424
+ description: Redirect to Google authentication
1425
+ /auth/facebook:
1426
+ get:
1427
+ summary: Initiate Facebook OAuth login
1428
+ description: Redirects to Facebook for OAuth authentication.
1429
+ responses:
1430
+ '302':
1431
+ description: Redirect to Facebook authentication
1432
+ /api/revoke-token:
1433
+ post:
1434
+ summary: Revoke a specific refresh token
1435
+ tags: [Authentication]
1436
+ security:
1437
+ - bearerAuth: []
1438
+ requestBody:
1439
+ required: true
1440
+ content:
1441
+ application/json:
1442
+ schema:
1443
+ type: object
1444
+ properties:
1445
+ refreshToken:
1446
+ type: string
1447
+ description: The refresh token to revoke
1448
+ required:
1449
+ - refreshToken
1450
+ responses:
1451
+ 200:
1452
+ description: Refresh token revoked successfully
1453
+ content:
1454
+ application/json:
1455
+ schema:
1456
+ type: object
1457
+ properties:
1458
+ success:
1459
+ type: boolean
1460
+ message:
1461
+ type: string
1462
+ 400:
1463
+ description: Invalid request
1464
+ 401:
1465
+ description: Unauthorized
1466
+ 500:
1467
+ description: Server error
1468
+ /auth/github:
1469
+ get:
1470
+ summary: Initiate GitHub OAuth login
1471
+ description: Redirects to GitHub for OAuth authentication.
1472
+ responses:
1473
+ '302':
1474
+ description: Redirect to GitHub authentication
1475
+ /auth/google/callback:
1476
+ get:
1477
+ summary: Google OAuth callback
1478
+ description: Handles the callback from Google OAuth and redirects with tokens.
1479
+ responses:
1480
+ '302':
1481
+ description: Redirect to client with tokens or error
1482
+ /auth/facebook/callback:
1483
+ get:
1484
+ summary: Facebook OAuth callback
1485
+ description: Handles the callback from Facebook OAuth and redirects with tokens.
1486
+ responses:
1487
+ '302':
1488
+ description: Redirect to client with tokens or error
1489
+ /auth/github/callback:
1490
+ get:
1491
+ summary: GitHub OAuth callback
1492
+ description: Handles the callback from GitHub OAuth and redirects with tokens.
1493
+ responses:
1494
+ '302':
1495
+ description: Redirect to client with tokens or error
1496
+ /:
1497
+ get:
1498
+ summary: Welcome message
1499
+ description: Returns a welcome message for the API.
1500
+ responses:
1501
+ '200':
1502
+ description: Welcome message
1503
+ content:
1504
+ application/json:
1505
+ schema:
1506
+ type: object
1507
+ properties:
1508
+ message:
1509
+ type: string
1510
+ example: Welcome to Ibrahim Al-Asfar's Portfolio Backend API
images/logo.png ADDED
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@mark-lasfar/portfolio-backend",
3
+ "version": "1.0.1",
4
+ "description": "Backend for Ibrahim Al-Asfar's portfolio website, supporting authentication, project management, and AI-powered chat.",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js",
8
+ "build": "echo \"No build step needed for backend\"",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "keywords": [
12
+ "portfolio",
13
+ "backend",
14
+ "express",
15
+ "mongodb",
16
+ "cloudinary",
17
+ "authentication",
18
+ "oauth",
19
+ "ai"
20
+ ],
21
+ "author": "Ibrahim Al-Asfar <admin@elasfar.com>",
22
+ "license": "MIT",
23
+ "homepage": "https://github.com/Mark-Lasfar/portfolio-backend#readme",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/Mark-Lasfar/portfolio-backend.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/Mark-Lasfar/portfolio-backend/issues"
30
+ },
31
+ "dependencies": {
32
+ "@canva/app-ui-kit": "^4.10.0",
33
+ "@sentry/node": "^7.120.4",
34
+ "@sentry/profiling-node": "^10.0.0",
35
+ "@sentry/tracing": "^7.120.4",
36
+ "axios": "^1.14.0",
37
+ "bcryptjs": "^2.4.3",
38
+ "cloudinary": "^1.41.3",
39
+ "compression": "^1.8.1",
40
+ "cookie-parser": "^1.4.7",
41
+ "cors": "^2.8.6",
42
+ "csurf": "^1.11.0",
43
+ "docx": "^9.6.1",
44
+ "dotenv": "^16.6.1",
45
+ "ejs": "^5.0.1",
46
+ "express": "^4.22.1",
47
+ "express-rate-limit": "^8.3.2",
48
+ "express-timeout-handler": "^2.2.2",
49
+ "express-validator": "^7.3.1",
50
+ "googleapis": "^154.1.0",
51
+ "helmet": "^8.1.0",
52
+ "jimp": "^1.6.0",
53
+ "jsonwebtoken": "^9.0.3",
54
+ "jspdf": "^3.0.4",
55
+ "jspdf-autotable": "^5.0.7",
56
+ "lru-cache": "^11.2.7",
57
+ "mongoose": "^8.23.0",
58
+ "morgan": "^1.10.1",
59
+ "multer": "^1.4.5-lts.1",
60
+ "multer-storage-cloudinary": "^4.0.0",
61
+ "node-cron": "^4.2.1",
62
+ "nodemailer": "^6.10.1",
63
+ "passport": "^0.7.0",
64
+ "passport-facebook": "^3.0.0",
65
+ "passport-github2": "^0.1.12",
66
+ "passport-google-oauth20": "^2.0.0",
67
+ "passport-mgzon": "^1.0.2",
68
+ "passport-oauth2": "^1.8.0",
69
+ "sharp": "^0.32.6",
70
+ "swagger-jsdoc": "^6.2.8",
71
+ "swagger-ui-express": "^5.0.1",
72
+ "validator": "^13.12.0",
73
+ "web-push": "^3.6.7",
74
+ "winston": "^3.19.0"
75
+ }
76
+ }
public/icons/logo.png ADDED
public/images/logo.png ADDED
server.js ADDED
The diff for this file is too large to render. See raw diff
 
translations.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "en-US": {
3
+ "app_name": "Asafr",
4
+ "short_description": "Generate professional resumes from user profiles",
5
+ "tagline": "Create stunning resumes with ease",
6
+ "description": "Asafr helps users create professional PDF resumes from their portfolio profiles, supporting Arabic and English."
7
+ }}
vercel.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 2,
3
+ "builds": [
4
+ {
5
+ "src": "server.js",
6
+ "use": "@vercel/node",
7
+ "config": {
8
+ "includeFiles": [
9
+ "views/**",
10
+ "public/**",
11
+ "docs/**"
12
+ ]
13
+ }
14
+ }
15
+ ],
16
+ "routes": [
17
+ {
18
+ "src": "/api/(.*)",
19
+ "dest": "server.js"
20
+ },
21
+ {
22
+ "src": "/auth/(.*)",
23
+ "dest": "server.js"
24
+ },
25
+ {
26
+ "src": "/api-docs/(.*)",
27
+ "dest": "server.js"
28
+ },
29
+ {
30
+ "src": "/",
31
+ "dest": "server.js"
32
+ },
33
+ {
34
+ "src": "/(.*\\.(ejs|html|css|js|png|jpg|jpeg|gif|svg|ico|json|yaml|yml))$",
35
+ "dest": "/public/$1"
36
+ },
37
+ {
38
+ "src": "/(.*)",
39
+ "dest": "server.js"
40
+ }
41
+ ],
42
+ "env": {
43
+ "NODE_ENV": "production"
44
+ },
45
+ "functions": {
46
+ "server.js": {
47
+ "memory": 1024,
48
+ "maxDuration": 10
49
+ }
50
+ }
51
+ }
views/images/logo.png ADDED
views/index.ejs ADDED
@@ -0,0 +1,646 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="description" content="Ibrahim Al-Asfar Portfolio API - Full-stack web developer portfolio backend API">
8
+ <meta name="keywords" content="portfolio, API, Node.js, Express, MongoDB, developer">
9
+ <meta name="author" content="Ibrahim Al-Asfar">
10
+ <meta property="og:title" content="Ibrahim Al-Asfar Portfolio API">
11
+ <meta property="og:description" content="Full-stack web developer portfolio API">
12
+ <meta property="og:type" content="website">
13
+ <title> Portfolio API</title>
14
+
15
+ <!-- Tailwind CSS -->
16
+ <script src="https://cdn.tailwindcss.com"></script>
17
+ <!-- Font Awesome -->
18
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
19
+
20
+
21
+ <!-- Custom Styles -->
22
+ <style>
23
+ @keyframes fadeInUp {
24
+ from {
25
+ opacity: 0;
26
+ transform: translateY(30px);
27
+ }
28
+
29
+ to {
30
+ opacity: 1;
31
+ transform: translateY(0);
32
+ }
33
+ }
34
+
35
+ .animate-fadeInUp {
36
+ animation: fadeInUp 0.6s ease-out;
37
+ }
38
+
39
+ .gradient-bg {
40
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
41
+ }
42
+
43
+ .card-hover {
44
+ transition: all 0.3s ease;
45
+ }
46
+
47
+ .card-hover:hover {
48
+ transform: translateY(-5px);
49
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
50
+ }
51
+
52
+ /* Custom scrollbar */
53
+ ::-webkit-scrollbar {
54
+ width: 8px;
55
+ }
56
+
57
+ ::-webkit-scrollbar-track {
58
+ background: #f1f1f1;
59
+ }
60
+
61
+ ::-webkit-scrollbar-thumb {
62
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
63
+ border-radius: 4px;
64
+ }
65
+
66
+ ::-webkit-scrollbar-thumb:hover {
67
+ background: #5a67d8;
68
+ }
69
+
70
+ /* Logo styling */
71
+ .logo-img {
72
+ width: 32px;
73
+ height: 32px;
74
+ object-fit: contain;
75
+ border-radius: 8px;
76
+ }
77
+
78
+ /* Fallback styles if Tailwind doesn't load */
79
+ .bg-gradient-to-br {
80
+ background: linear-gradient(to bottom right, #f9fafb, #f3f4f6);
81
+ }
82
+
83
+ .shadow-lg {
84
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
85
+ }
86
+
87
+ .rounded-lg {
88
+ border-radius: 0.5rem;
89
+ }
90
+
91
+ .p-2 {
92
+ padding: 0.5rem;
93
+ }
94
+
95
+ .p-3 {
96
+ padding: 0.75rem;
97
+ }
98
+
99
+ .p-5 {
100
+ padding: 1.25rem;
101
+ }
102
+
103
+ .p-6 {
104
+ padding: 1.5rem;
105
+ }
106
+
107
+ .py-8 {
108
+ padding-top: 2rem;
109
+ padding-bottom: 2rem;
110
+ }
111
+
112
+ .py-16 {
113
+ padding-top: 4rem;
114
+ padding-bottom: 4rem;
115
+ }
116
+
117
+ .py-20 {
118
+ padding-top: 5rem;
119
+ padding-bottom: 5rem;
120
+ }
121
+
122
+ .px-4 {
123
+ padding-left: 1rem;
124
+ padding-right: 1rem;
125
+ }
126
+
127
+ .text-center {
128
+ text-align: center;
129
+ }
130
+
131
+ .text-white {
132
+ color: white;
133
+ }
134
+
135
+ .text-gray-600 {
136
+ color: #4b5563;
137
+ }
138
+
139
+ .text-gray-800 {
140
+ color: #1f2937;
141
+ }
142
+
143
+ .text-xl {
144
+ font-size: 1.25rem;
145
+ }
146
+
147
+ .text-2xl {
148
+ font-size: 1.5rem;
149
+ }
150
+
151
+ .text-3xl {
152
+ font-size: 1.875rem;
153
+ }
154
+
155
+ .text-5xl {
156
+ font-size: 3rem;
157
+ }
158
+
159
+ .font-bold {
160
+ font-weight: 700;
161
+ }
162
+
163
+ .font-semibold {
164
+ font-weight: 600;
165
+ }
166
+
167
+ .bg-white {
168
+ background-color: white;
169
+ }
170
+
171
+ .bg-gray-800 {
172
+ background-color: #1f2937;
173
+ }
174
+
175
+ .bg-green-500 {
176
+ background-color: #10b981;
177
+ }
178
+
179
+ .mb-2 {
180
+ margin-bottom: 0.5rem;
181
+ }
182
+
183
+ .mb-4 {
184
+ margin-bottom: 1rem;
185
+ }
186
+
187
+ .mb-6 {
188
+ margin-bottom: 1.5rem;
189
+ }
190
+
191
+ .mb-8 {
192
+ margin-bottom: 2rem;
193
+ }
194
+
195
+ .mb-12 {
196
+ margin-bottom: 3rem;
197
+ }
198
+
199
+ .mt-2 {
200
+ margin-top: 0.5rem;
201
+ }
202
+
203
+ .mr-2 {
204
+ margin-right: 0.5rem;
205
+ }
206
+
207
+ .flex {
208
+ display: flex;
209
+ }
210
+
211
+ .inline-flex {
212
+ display: inline-flex;
213
+ }
214
+
215
+ .grid {
216
+ display: grid;
217
+ }
218
+
219
+ .items-center {
220
+ align-items: center;
221
+ }
222
+
223
+ .justify-center {
224
+ justify-content: center;
225
+ }
226
+
227
+ .justify-between {
228
+ justify-content: space-between;
229
+ }
230
+
231
+ .gap-4 {
232
+ gap: 1rem;
233
+ }
234
+
235
+ .gap-6 {
236
+ gap: 1.5rem;
237
+ }
238
+
239
+ .gap-8 {
240
+ gap: 2rem;
241
+ }
242
+
243
+ .max-w-2xl {
244
+ max-width: 42rem;
245
+ }
246
+
247
+ .max-w-7xl {
248
+ max-width: 80rem;
249
+ }
250
+
251
+ .mx-auto {
252
+ margin-left: auto;
253
+ margin-right: auto;
254
+ }
255
+
256
+ .w-12 {
257
+ width: 3rem;
258
+ }
259
+
260
+ .h-12 {
261
+ height: 3rem;
262
+ }
263
+
264
+ .w-3 {
265
+ width: 0.75rem;
266
+ }
267
+
268
+ .h-3 {
269
+ height: 0.75rem;
270
+ }
271
+
272
+ .rounded-full {
273
+ border-radius: 9999px;
274
+ }
275
+
276
+ .rounded-xl {
277
+ border-radius: 0.75rem;
278
+ }
279
+
280
+ .animate-pulse {
281
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
282
+ }
283
+
284
+ @keyframes pulse {
285
+
286
+ 0%,
287
+ 100% {
288
+ opacity: 1;
289
+ }
290
+
291
+ 50% {
292
+ opacity: .5;
293
+ }
294
+ }
295
+
296
+ .sticky {
297
+ position: sticky;
298
+ }
299
+
300
+ .top-0 {
301
+ top: 0;
302
+ }
303
+
304
+ .z-50 {
305
+ z-index: 50;
306
+ }
307
+
308
+ .absolute {
309
+ position: absolute;
310
+ }
311
+
312
+ .relative {
313
+ position: relative;
314
+ }
315
+
316
+ .inset-0 {
317
+ top: 0;
318
+ right: 0;
319
+ bottom: 0;
320
+ left: 0;
321
+ }
322
+
323
+ .overflow-hidden {
324
+ overflow: hidden;
325
+ }
326
+
327
+ .bg-opacity-10 {
328
+ opacity: 0.1;
329
+ }
330
+
331
+ .transition {
332
+ transition-property: all;
333
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
334
+ transition-duration: 150ms;
335
+ }
336
+
337
+ .hover\:shadow-lg:hover {
338
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
339
+ }
340
+
341
+ .hover\:bg-gray-900:hover {
342
+ background-color: #111827;
343
+ }
344
+
345
+ .hover\:text-purple-600:hover {
346
+ color: #7c3aed;
347
+ }
348
+
349
+ @media (min-width: 640px) {
350
+ .sm\:px-6 {
351
+ padding-left: 1.5rem;
352
+ padding-right: 1.5rem;
353
+ }
354
+ }
355
+
356
+ @media (min-width: 768px) {
357
+ .md\:grid-cols-2 {
358
+ grid-template-columns: repeat(2, minmax(0, 1fr));
359
+ }
360
+
361
+ .md\:grid-cols-3 {
362
+ grid-template-columns: repeat(3, minmax(0, 1fr));
363
+ }
364
+
365
+ .md\:text-6xl {
366
+ font-size: 3.75rem;
367
+ }
368
+ }
369
+
370
+ @media (min-width: 1024px) {
371
+ .lg\:grid-cols-3 {
372
+ grid-template-columns: repeat(3, minmax(0, 1fr));
373
+ }
374
+
375
+ .lg\:px-8 {
376
+ padding-left: 2rem;
377
+ padding-right: 2rem;
378
+ }
379
+ }
380
+ </style>
381
+ </head>
382
+
383
+ <body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen">
384
+
385
+ <!-- Navigation -->
386
+ <nav class="bg-white shadow-lg sticky top-0 z-50">
387
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
388
+ <div class="flex justify-between items-center h-16">
389
+ <div class="flex items-center space-x-3">
390
+ <!-- Logo Image -->
391
+ <img src="images/logo.png" alt="Logo" class="logo-img"
392
+ onerror="this.onerror=null; this.parentElement.innerHTML='<div class=\'gradient-bg rounded-lg p-2\'><i class=\'fas fa-code text-white text-xl\'></i></div>'">
393
+ <h1
394
+ class="text-xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">
395
+ Portfolio API
396
+ </h1>
397
+ </div>
398
+ <div class="flex items-center space-x-4">
399
+ <a href="/api-docs" target="_blank" class="text-gray-600 hover:text-purple-600 transition"
400
+ title="API Documentation">
401
+ <i class="fas fa-book text-lg"></i>
402
+ </a>
403
+ <a href="https://github.com/Mark-Lasfar" target="_blank"
404
+ class="text-gray-600 hover:text-purple-600 transition" title="GitHub">
405
+ <i class="fab fa-github text-lg"></i>
406
+ </a>
407
+ <a href="https://linkedin.com/in/ibrahim-elasfar" target="_blank"
408
+ class="text-gray-600 hover:text-purple-600 transition" title="LinkedIn">
409
+ <i class="fab fa-linkedin text-lg"></i>
410
+ </a>
411
+ </div>
412
+ </div>
413
+ </div>
414
+ </nav>
415
+
416
+ <!-- Hero Section -->
417
+ <div class="relative overflow-hidden">
418
+ <div class="absolute inset-0 bg-gradient-to-r from-purple-600 to-blue-600 opacity-10"></div>
419
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
420
+ <div class="text-center animate-fadeInUp">
421
+ <div class="inline-flex items-center justify-center p-2 bg-green-100 rounded-full mb-4">
422
+ <i class="fas fa-check-circle text-green-600 mr-2"></i>
423
+ <span class="text-green-600 text-sm font-medium">API is running</span>
424
+ </div>
425
+ <h1
426
+ class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-purple-600 via-blue-600 to-purple-600 bg-clip-text text-transparent mb-6">
427
+ Welcome to My Portfolio API
428
+ </h1>
429
+ <p class="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
430
+ Full-stack web developer passionate about building modern web applications with cutting-edge
431
+ technologies.
432
+ </p>
433
+ <div class="flex flex-wrap justify-center gap-4">
434
+ <a href="/api-docs"
435
+ class="gradient-bg text-white px-8 py-3 rounded-lg font-semibold hover:shadow-lg transition-all inline-flex items-center">
436
+ <i class="fas fa-rocket mr-2"></i>
437
+ Explore API
438
+ </a>
439
+ <a href="https://github.com/Mark-Lasfar" target="_blank"
440
+ class="bg-gray-800 text-white px-8 py-3 rounded-lg font-semibold hover:bg-gray-900 transition-all inline-flex items-center">
441
+ <i class="fab fa-github mr-2"></i>
442
+ GitHub
443
+ </a>
444
+ </div>
445
+ </div>
446
+ </div>
447
+ </div>
448
+
449
+ <!-- Stats Section with Real-time Data -->
450
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
451
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
452
+ <div class="bg-white rounded-xl shadow-lg p-6 card-hover animate-fadeInUp" style="animation-delay: 0.1s">
453
+ <div class="gradient-bg w-12 h-12 rounded-lg flex items-center justify-center mb-4">
454
+ <i class="fas fa-database text-white text-xl"></i>
455
+ </div>
456
+ <h3 class="text-2xl font-bold text-gray-800 mb-2">Database Status</h3>
457
+ <div class="flex items-center">
458
+ <div class="w-3 h-3 bg-green-500 rounded-full animate-pulse mr-2"></div>
459
+ <p class="text-gray-600" id="db-status">MongoDB Connected</p>
460
+ </div>
461
+ </div>
462
+
463
+ <div class="bg-white rounded-xl shadow-lg p-6 card-hover animate-fadeInUp" style="animation-delay: 0.2s">
464
+ <div class="gradient-bg w-12 h-12 rounded-lg flex items-center justify-center mb-4">
465
+ <i class="fas fa-cloud-upload-alt text-white text-xl"></i>
466
+ </div>
467
+ <h3 class="text-2xl font-bold text-gray-800 mb-2">Cloudinary</h3>
468
+ <p class="text-gray-600" id="cloudinary-status">Media Storage Active</p>
469
+ </div>
470
+
471
+ <div class="bg-white rounded-xl shadow-lg p-6 card-hover animate-fadeInUp" style="animation-delay: 0.3s">
472
+ <div class="gradient-bg w-12 h-12 rounded-lg flex items-center justify-center mb-4">
473
+ <i class="fas fa-chart-line text-white text-xl"></i>
474
+ </div>
475
+ <h3 class="text-2xl font-bold text-gray-800 mb-2">Analytics</h3>
476
+ <p class="text-gray-600" id="sentry-status">Sentry Monitoring Active</p>
477
+ </div>
478
+ </div>
479
+ </div>
480
+
481
+ <!-- API Endpoints Section -->
482
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
483
+ <h2 class="text-3xl font-bold text-center text-gray-800 mb-12">API Endpoints</h2>
484
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
485
+ <div class="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition">
486
+ <div class="flex items-center justify-between mb-3">
487
+ <span class="bg-green-100 text-green-600 text-xs font-semibold px-2 py-1 rounded">GET</span>
488
+ <i class="fas fa-users text-gray-400"></i>
489
+ </div>
490
+ <code class="text-sm text-gray-700">/api/projects</code>
491
+ <p class="text-gray-500 text-sm mt-2">Get all public projects</p>
492
+ </div>
493
+
494
+ <div class="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition">
495
+ <div class="flex items-center justify-between mb-3">
496
+ <span class="bg-green-100 text-green-600 text-xs font-semibold px-2 py-1 rounded">GET</span>
497
+ <i class="fas fa-chart-simple text-gray-400"></i>
498
+ </div>
499
+ <code class="text-sm text-gray-700">/api/skills</code>
500
+ <p class="text-gray-500 text-sm mt-2">Get all skills</p>
501
+ </div>
502
+
503
+ <div class="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition">
504
+ <div class="flex items-center justify-between mb-3">
505
+ <span class="bg-green-100 text-green-600 text-xs font-semibold px-2 py-1 rounded">GET</span>
506
+ <i class="fas fa-heart text-gray-400"></i>
507
+ </div>
508
+ <code class="text-sm text-gray-700">/api/health</code>
509
+ <p class="text-gray-500 text-sm mt-2">Check system health</p>
510
+ </div>
511
+
512
+ <div class="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition">
513
+ <div class="flex items-center justify-between mb-3">
514
+ <span class="bg-blue-100 text-blue-600 text-xs font-semibold px-2 py-1 rounded">POST</span>
515
+ <i class="fas fa-user-plus text-gray-400"></i>
516
+ </div>
517
+ <code class="text-sm text-gray-700">/api/register</code>
518
+ <p class="text-gray-500 text-sm mt-2">Create new account</p>
519
+ </div>
520
+
521
+ <div class="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition">
522
+ <div class="flex items-center justify-between mb-3">
523
+ <span class="bg-blue-100 text-blue-600 text-xs font-semibold px-2 py-1 rounded">POST</span>
524
+ <i class="fas fa-sign-in-alt text-gray-400"></i>
525
+ </div>
526
+ <code class="text-sm text-gray-700">/api/login</code>
527
+ <p class="text-gray-500 text-sm mt-2">Authenticate user</p>
528
+ </div>
529
+
530
+ <div class="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition">
531
+ <div class="flex items-center justify-between mb-3">
532
+ <span class="bg-purple-100 text-purple-600 text-xs font-semibold px-2 py-1 rounded">AUTH</span>
533
+ <i class="fab fa-google text-gray-400"></i>
534
+ </div>
535
+ <code class="text-sm text-gray-700">/auth/google</code>
536
+ <p class="text-gray-500 text-sm mt-2">Google OAuth</p>
537
+ </div>
538
+ </div>
539
+ </div>
540
+
541
+ <!-- Tech Stack Section -->
542
+ <div class="bg-white py-16">
543
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
544
+ <h2 class="text-3xl font-bold text-center text-gray-800 mb-12">Technology Stack</h2>
545
+ <div class="flex flex-wrap justify-center gap-8">
546
+ <div class="text-center">
547
+ <i class="fab fa-node-js text-5xl text-green-600"></i>
548
+ <p class="mt-2 text-gray-600">Node.js</p>
549
+ </div>
550
+ <div class="text-center">
551
+ <i class="fab fa-react text-5xl text-blue-500"></i>
552
+ <p class="mt-2 text-gray-600">React</p>
553
+ </div>
554
+ <div class="text-center">
555
+ <i class="fas fa-database text-5xl text-green-700"></i>
556
+ <p class="mt-2 text-gray-600">MongoDB</p>
557
+ </div>
558
+ <div class="text-center">
559
+ <i class="fas fa-project-diagram text-5xl text-pink-500"></i>
560
+ <p class="mt-2 text-gray-600">GraphQL</p>
561
+ </div>
562
+
563
+ <div class="text-center">
564
+ <i class="fab fa-docker text-5xl text-blue-600"></i>
565
+ <p class="mt-2 text-gray-600">Docker</p>
566
+ </div>
567
+ <div class="text-center">
568
+ <i class="fab fa-aws text-5xl text-orange-500"></i>
569
+ <p class="mt-2 text-gray-600">AWS</p>
570
+ </div>
571
+ </div>
572
+ </div>
573
+ </div>
574
+
575
+ <!-- Footer -->
576
+ <footer class="bg-gray-800 text-white py-8">
577
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
578
+ <p class="text-gray-400">&copy; 2026 Ibrahim Al-Asfar. All rights reserved.</p>
579
+ <p class="text-gray-500 text-sm mt-2">Built with ❤️ using Node.js, Express, MongoDB & TailwindCSS</p>
580
+ <p class="text-gray-600 text-xs mt-4">
581
+ <i class="fas fa-code mr-1"></i> API Version 1.0.0
582
+ </p>
583
+ </div>
584
+ </footer>
585
+
586
+ <script>
587
+ // Fetch real-time data
588
+ async function fetchStats() {
589
+ try {
590
+ const response = await fetch('/api/health');
591
+ const data = await response.json();
592
+ console.log('System Health:', data);
593
+
594
+ // Update statuses based on real data
595
+ if (data.mongodb === 'connected') {
596
+ document.getElementById('db-status').innerHTML = 'MongoDB Connected ✅';
597
+ document.getElementById('db-status').className = 'text-green-600';
598
+ }
599
+ if (data.cloudinary === 'configured') {
600
+ document.getElementById('cloudinary-status').innerHTML = 'Cloudinary Active ✅';
601
+ document.getElementById('cloudinary-status').className = 'text-green-600';
602
+ }
603
+ if (data.sentry === 'configured') {
604
+ document.getElementById('sentry-status').innerHTML = 'Sentry Active ✅';
605
+ document.getElementById('sentry-status').className = 'text-green-600';
606
+ }
607
+ } catch (error) {
608
+ console.error('Error fetching stats:', error);
609
+ document.getElementById('db-status').innerHTML = 'Connection Error ❌';
610
+ document.getElementById('db-status').className = 'text-red-600';
611
+ }
612
+ }
613
+ fetchStats();
614
+
615
+ // Refresh stats every 30 seconds
616
+ setInterval(fetchStats, 30000);
617
+
618
+ // Dark mode handling
619
+ if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
620
+ document.documentElement.classList.add('dark');
621
+ } else {
622
+ document.documentElement.classList.remove('dark');
623
+ }
624
+
625
+ // Add smooth scroll behavior
626
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
627
+ anchor.addEventListener('click', function (e) {
628
+ e.preventDefault();
629
+ document.querySelector(this.getAttribute('href')).scrollIntoView({
630
+ behavior: 'smooth'
631
+ });
632
+ });
633
+ });
634
+
635
+ // Add page load animation
636
+ window.addEventListener('load', () => {
637
+ document.body.style.opacity = '0';
638
+ document.body.style.transition = 'opacity 0.5s ease';
639
+ setTimeout(() => {
640
+ document.body.style.opacity = '1';
641
+ }, 100);
642
+ });
643
+ </script>
644
+ </body>
645
+
646
+ </html>