├── .dockerignore
├── .gitignore
├── Blog.postman_collection.json
├── Dockerfile
├── README.md
├── config
└── test.ts
├── docker-compose.yml
├── package-lock.json
├── package.json
├── src
├── app.ts
├── controller
│ ├── blog.controller.ts
│ ├── session.controller.ts
│ └── user.controller.ts
├── interface
│ ├── ErrorResponse.ts
│ ├── RequestValidators.ts
│ └── jwtPayload.ts
├── middleware
│ ├── errorHandler.ts
│ ├── isAuth.ts
│ ├── notFound.ts
│ └── validateResource.ts
├── model
│ ├── blog.models.ts
│ └── user.models.ts
├── routes
│ ├── blog.routes.ts
│ ├── index.ts
│ ├── session.routes.ts
│ └── user.routes.ts
├── schema
│ ├── blog.schema.ts
│ └── user.schema.ts
├── server.ts
├── service
│ ├── blog.service.ts
│ └── user.service.ts
└── utils
│ ├── connectDB.ts
│ ├── jwt.utils.ts
│ └── logger.ts
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /config/default.ts
--------------------------------------------------------------------------------
/Blog.postman_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "_postman_id": "04602781-d7f5-42da-a350-86089b380fd4",
4 | "name": "Blog",
5 | "description": "\n\n\n\n# Affable\n
\n\nAffable manages your Blog through REST API and you can also see others' Blogs . \n\n\n## Demo\n\nInsert gif or link to demo\n\n## Requirements\n
\n \n
\n \n
\n
\n\n\n## Environment Variables\n\nTo run this project, you will need to add the following environment variables to your **config/default.json** file\n\n`PORT`\n\n`HOST`\n\n`MONGO_URI`\n\n`saltWorkFactor`\n\n`jwtSecret`\n\nyou can also take **config/test.json** as reference \n## Installation\n\nInstall my-project with npm\n\n```bash\n npm install\n npm run dev #if your are a developer \n npm run start\n```\n \n## API Reference\n\n### Authentication\n\n#### Create User\n```http\n POST /api/v2/user/\n```\n| Request Body | Type | Description |\n| :-------- | :------- | :------------------------- |\n| `userName` | `string` | **Required** .user's username |\n| `email` | `string` | **Required** .user's email |\n| `password` | `string` | **Required** .user's password |\n| `passwordConfirmation` | `string` | **Required** |\n\n#### Login \n```http\n POST /api/v2/session/login\n```\n| Request Body | Type | Description |\n| :-------- | :------- | :------------------------- |\n| `email` | `string` | **Required** .email of exist user |\n| `passwrod` | `string` | **Required** .password of that email |\n\n#### Find User\n```http\n GET /api/v2/user?\n```\n| Request Query | Type | Description |\n| :-------- | :------- | :------------------------- |\n| `userName` | `string` | username of selected user |\n| `email` | `string` | email of selected user |\n| `id` | `Int` | id of selected user |\n\n#### Logout\n```http\n GET /api/v2/session/logout\n```\n### CRUD Blog\n#### Create Blog\n```http\n POST /api/v2/blog/\n```\n| Constraints | Type | Description |\n| :-------- | :------- | :------------------------- |\n| `isAuthenticated` | `middleware`| **Required** you must be logged in to create a post |\n\n| Request Body | Type | Description |\n| :-------- | :------- | :------------------------- |\n| `title` | `string` | **Required** .main title of blog |\n| `description` | `string` | describe your blog |\n| `tags` | `string` | tag your post to specific topic [\"programming\", \"health\", \"sports\"] |\n\n#### Find Blog\n```http\n GET /api/v2/blog?\n```\n| Request Query | Type | Description |\n| :-------- | :------- | :------------------------- |\n| `id` | `string` | get blog with it's id |\n| `author` | `string` | get blogs for specific author |\n\n#### Delete Blog\n```http\n DELETE /api/v2/blog/${id}\n```\n| Constraints | Type | Description |\n| :-------- | :------- | :------------------------- |\n| `isAuthenticated` | `middleware`| **Required** you must be logged in to create a blog |\n| `isAuthorized` | `middleware`| **Required** you must be that owner of the blog |\n\n| Request Parameters | Type | Description |\n| :-------- | :------- | :------------------------- |\n| `id` | `string` | **Required** .id of deleted blog|\n\n\n\n\n\n## Contributing\n\nContributions are always welcome!\n\n\n\n## Authors\n\n- [@AhmedEid](https://github.com/ahmedeid6842/)\n\n\n## Lessons Learned\n\n- How to transition from javascript to typescript and reap the benefits of typescript.\n- How to use Zod for validation.\n- How to migrate MongDB and typescript.\n- There is always something new to learn.",
6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
7 | },
8 | "item": [
9 | {
10 | "name": "create new user",
11 | "request": {
12 | "method": "POST",
13 | "header": [],
14 | "body": {
15 | "mode": "raw",
16 | "raw": "{\r\n \"userName\":\"ahmed eid\",\r\n \"email\": \"ahmedeid6842@gmail.com\",\r\n \"password\":\"Aa12345@\",\r\n \"passwordConfirmation\":\"Aa12345@\"\r\n}",
17 | "options": {
18 | "raw": {
19 | "language": "json"
20 | }
21 | }
22 | },
23 | "url": {
24 | "raw": "localhost:3000/api/v2/user",
25 | "host": [
26 | "localhost"
27 | ],
28 | "port": "3000",
29 | "path": [
30 | "api",
31 | "v2",
32 | "user"
33 | ]
34 | }
35 | },
36 | "response": [
37 | {
38 | "name": "create new user",
39 | "originalRequest": {
40 | "method": "POST",
41 | "header": [],
42 | "body": {
43 | "mode": "raw",
44 | "raw": "{\r\n \"userName\":\"ahmed eid\",\r\n \"email\": \"ahmedeid6842@gmail.com\",\r\n \"password\":\"Aa12345@\",\r\n \"passwordConfirmation\":\"Aa12345@\"\r\n}",
45 | "options": {
46 | "raw": {
47 | "language": "json"
48 | }
49 | }
50 | },
51 | "url": {
52 | "raw": "localhost:3000/api/v2/user",
53 | "host": [
54 | "localhost"
55 | ],
56 | "port": "3000",
57 | "path": [
58 | "api",
59 | "v2",
60 | "user"
61 | ]
62 | }
63 | },
64 | "status": "OK",
65 | "code": 200,
66 | "_postman_previewlanguage": "json",
67 | "header": [
68 | {
69 | "key": "Access-Control-Allow-Origin",
70 | "value": "*"
71 | },
72 | {
73 | "key": "Content-Security-Policy",
74 | "value": "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
75 | },
76 | {
77 | "key": "Cross-Origin-Embedder-Policy",
78 | "value": "require-corp"
79 | },
80 | {
81 | "key": "Cross-Origin-Opener-Policy",
82 | "value": "same-origin"
83 | },
84 | {
85 | "key": "Cross-Origin-Resource-Policy",
86 | "value": "same-origin"
87 | },
88 | {
89 | "key": "X-DNS-Prefetch-Control",
90 | "value": "off"
91 | },
92 | {
93 | "key": "X-Frame-Options",
94 | "value": "SAMEORIGIN"
95 | },
96 | {
97 | "key": "Strict-Transport-Security",
98 | "value": "max-age=15552000; includeSubDomains"
99 | },
100 | {
101 | "key": "X-Download-Options",
102 | "value": "noopen"
103 | },
104 | {
105 | "key": "X-Content-Type-Options",
106 | "value": "nosniff"
107 | },
108 | {
109 | "key": "Origin-Agent-Cluster",
110 | "value": "?1"
111 | },
112 | {
113 | "key": "X-Permitted-Cross-Domain-Policies",
114 | "value": "none"
115 | },
116 | {
117 | "key": "Referrer-Policy",
118 | "value": "no-referrer"
119 | },
120 | {
121 | "key": "X-XSS-Protection",
122 | "value": "0"
123 | },
124 | {
125 | "key": "X-Powered-By",
126 | "value": "Express"
127 | },
128 | {
129 | "key": "Content-Type",
130 | "value": "application/json; charset=utf-8"
131 | },
132 | {
133 | "key": "Content-Length",
134 | "value": "176"
135 | },
136 | {
137 | "key": "ETag",
138 | "value": "W/\"b0-w6gYrn5jqf0/wOU50K5ms8oLe/o\""
139 | },
140 | {
141 | "key": "Date",
142 | "value": "Tue, 14 Feb 2023 10:05:37 GMT"
143 | },
144 | {
145 | "key": "Connection",
146 | "value": "keep-alive"
147 | },
148 | {
149 | "key": "Keep-Alive",
150 | "value": "timeout=5"
151 | }
152 | ],
153 | "cookie": [],
154 | "body": "{\n \"email\": \"ahmedeid6842@gmail.com\",\n \"userName\": \"ahmed eid\",\n \"_id\": \"63eb5cf125f5d12af42dc344\",\n \"createdAt\": \"2023-02-14T10:05:37.406Z\",\n \"updatedAt\": \"2023-02-14T10:05:37.406Z\",\n \"__v\": 0\n}"
155 | }
156 | ]
157 | },
158 | {
159 | "name": "get user",
160 | "request": {
161 | "method": "GET",
162 | "header": [],
163 | "url": {
164 | "raw": "localhost:3000/api/v2/user?userName=ahmed eid",
165 | "host": [
166 | "localhost"
167 | ],
168 | "port": "3000",
169 | "path": [
170 | "api",
171 | "v2",
172 | "user"
173 | ],
174 | "query": [
175 | {
176 | "key": "userName",
177 | "value": "ahmed eid"
178 | }
179 | ]
180 | }
181 | },
182 | "response": [
183 | {
184 | "name": "New Request",
185 | "originalRequest": {
186 | "method": "GET",
187 | "header": [],
188 | "url": {
189 | "raw": "localhost:3000/api/v2/user?userName=ahmed eid",
190 | "host": [
191 | "localhost"
192 | ],
193 | "port": "3000",
194 | "path": [
195 | "api",
196 | "v2",
197 | "user"
198 | ],
199 | "query": [
200 | {
201 | "key": "userName",
202 | "value": "ahmed eid"
203 | }
204 | ]
205 | }
206 | },
207 | "status": "OK",
208 | "code": 200,
209 | "_postman_previewlanguage": "json",
210 | "header": [
211 | {
212 | "key": "Access-Control-Allow-Origin",
213 | "value": "*"
214 | },
215 | {
216 | "key": "Content-Security-Policy",
217 | "value": "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
218 | },
219 | {
220 | "key": "Cross-Origin-Embedder-Policy",
221 | "value": "require-corp"
222 | },
223 | {
224 | "key": "Cross-Origin-Opener-Policy",
225 | "value": "same-origin"
226 | },
227 | {
228 | "key": "Cross-Origin-Resource-Policy",
229 | "value": "same-origin"
230 | },
231 | {
232 | "key": "X-DNS-Prefetch-Control",
233 | "value": "off"
234 | },
235 | {
236 | "key": "X-Frame-Options",
237 | "value": "SAMEORIGIN"
238 | },
239 | {
240 | "key": "Strict-Transport-Security",
241 | "value": "max-age=15552000; includeSubDomains"
242 | },
243 | {
244 | "key": "X-Download-Options",
245 | "value": "noopen"
246 | },
247 | {
248 | "key": "X-Content-Type-Options",
249 | "value": "nosniff"
250 | },
251 | {
252 | "key": "Origin-Agent-Cluster",
253 | "value": "?1"
254 | },
255 | {
256 | "key": "X-Permitted-Cross-Domain-Policies",
257 | "value": "none"
258 | },
259 | {
260 | "key": "Referrer-Policy",
261 | "value": "no-referrer"
262 | },
263 | {
264 | "key": "X-XSS-Protection",
265 | "value": "0"
266 | },
267 | {
268 | "key": "X-Powered-By",
269 | "value": "Express"
270 | },
271 | {
272 | "key": "Content-Type",
273 | "value": "application/json; charset=utf-8"
274 | },
275 | {
276 | "key": "Content-Length",
277 | "value": "178"
278 | },
279 | {
280 | "key": "ETag",
281 | "value": "W/\"b2-1gzroPLpvbvMv9SjmqAGR6Qgx74\""
282 | },
283 | {
284 | "key": "Date",
285 | "value": "Tue, 14 Feb 2023 10:10:09 GMT"
286 | },
287 | {
288 | "key": "Connection",
289 | "value": "keep-alive"
290 | },
291 | {
292 | "key": "Keep-Alive",
293 | "value": "timeout=5"
294 | }
295 | ],
296 | "cookie": [],
297 | "body": "[\n {\n \"_id\": \"63eb5cf125f5d12af42dc344\",\n \"email\": \"ahmedeid6842@gmail.com\",\n \"userName\": \"ahmed eid\",\n \"createdAt\": \"2023-02-14T10:05:37.406Z\",\n \"updatedAt\": \"2023-02-14T10:05:37.406Z\",\n \"__v\": 0\n }\n]"
298 | }
299 | ]
300 | },
301 | {
302 | "name": "login",
303 | "request": {
304 | "method": "POST",
305 | "header": [],
306 | "body": {
307 | "mode": "raw",
308 | "raw": "{\r\n \"email\": \"ahmedeid6842@gmail.com\",\r\n \"password\":\"Aa12345@\"\r\n}",
309 | "options": {
310 | "raw": {
311 | "language": "json"
312 | }
313 | }
314 | },
315 | "url": {
316 | "raw": "localhost:3000/api/v2/session/login",
317 | "host": [
318 | "localhost"
319 | ],
320 | "port": "3000",
321 | "path": [
322 | "api",
323 | "v2",
324 | "session",
325 | "login"
326 | ]
327 | }
328 | },
329 | "response": [
330 | {
331 | "name": "login",
332 | "originalRequest": {
333 | "method": "POST",
334 | "header": [],
335 | "body": {
336 | "mode": "raw",
337 | "raw": "{\r\n \"email\": \"ahmedeid6842@gmail.com\",\r\n \"password\":\"Aa12345@\"\r\n}",
338 | "options": {
339 | "raw": {
340 | "language": "json"
341 | }
342 | }
343 | },
344 | "url": {
345 | "raw": "localhost:3000/api/v2/session/login",
346 | "host": [
347 | "localhost"
348 | ],
349 | "port": "3000",
350 | "path": [
351 | "api",
352 | "v2",
353 | "session",
354 | "login"
355 | ]
356 | }
357 | },
358 | "status": "OK",
359 | "code": 200,
360 | "_postman_previewlanguage": "html",
361 | "header": [
362 | {
363 | "key": "Access-Control-Allow-Origin",
364 | "value": "*"
365 | },
366 | {
367 | "key": "Content-Security-Policy",
368 | "value": "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
369 | },
370 | {
371 | "key": "Cross-Origin-Embedder-Policy",
372 | "value": "require-corp"
373 | },
374 | {
375 | "key": "Cross-Origin-Opener-Policy",
376 | "value": "same-origin"
377 | },
378 | {
379 | "key": "Cross-Origin-Resource-Policy",
380 | "value": "same-origin"
381 | },
382 | {
383 | "key": "X-DNS-Prefetch-Control",
384 | "value": "off"
385 | },
386 | {
387 | "key": "X-Frame-Options",
388 | "value": "SAMEORIGIN"
389 | },
390 | {
391 | "key": "Strict-Transport-Security",
392 | "value": "max-age=15552000; includeSubDomains"
393 | },
394 | {
395 | "key": "X-Download-Options",
396 | "value": "noopen"
397 | },
398 | {
399 | "key": "X-Content-Type-Options",
400 | "value": "nosniff"
401 | },
402 | {
403 | "key": "Origin-Agent-Cluster",
404 | "value": "?1"
405 | },
406 | {
407 | "key": "X-Permitted-Cross-Domain-Policies",
408 | "value": "none"
409 | },
410 | {
411 | "key": "Referrer-Policy",
412 | "value": "no-referrer"
413 | },
414 | {
415 | "key": "X-XSS-Protection",
416 | "value": "0"
417 | },
418 | {
419 | "key": "X-Powered-By",
420 | "value": "Express"
421 | },
422 | {
423 | "key": "Set-Cookie",
424 | "value": "accessjwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2M2ViNWNmMTI1ZjVkMTJhZjQyZGMzNDQiLCJ1c2VyTmFtZSI6ImFobWVkIGVpZCIsImlhdCI6MTY3NjM2OTQ5NCwiZXhwIjoxNjc2MzcxMjk0fQ.oJSYLvS2AEoLhyIr-QDHftzcshWnnWIT_vvRNcYulkU; Path=/; HttpOnly; SameSite=Strict"
425 | },
426 | {
427 | "key": "Content-Type",
428 | "value": "text/html; charset=utf-8"
429 | },
430 | {
431 | "key": "Content-Length",
432 | "value": "17"
433 | },
434 | {
435 | "key": "ETag",
436 | "value": "W/\"11-Ap1JuyXO6ObAbFgtY1hwfdGNTUk\""
437 | },
438 | {
439 | "key": "Date",
440 | "value": "Tue, 14 Feb 2023 10:11:34 GMT"
441 | },
442 | {
443 | "key": "Connection",
444 | "value": "keep-alive"
445 | },
446 | {
447 | "key": "Keep-Alive",
448 | "value": "timeout=5"
449 | }
450 | ],
451 | "cookie": [],
452 | "body": "welcome ahmed eid"
453 | }
454 | ]
455 | },
456 | {
457 | "name": "logout",
458 | "request": {
459 | "method": "GET",
460 | "header": [],
461 | "url": {
462 | "raw": "localhost:3000/api/v2/session/logout",
463 | "host": [
464 | "localhost"
465 | ],
466 | "port": "3000",
467 | "path": [
468 | "api",
469 | "v2",
470 | "session",
471 | "logout"
472 | ]
473 | }
474 | },
475 | "response": [
476 | {
477 | "name": "logout",
478 | "originalRequest": {
479 | "method": "GET",
480 | "header": [],
481 | "url": {
482 | "raw": "localhost:3000/api/v2/session/logout",
483 | "host": [
484 | "localhost"
485 | ],
486 | "port": "3000",
487 | "path": [
488 | "api",
489 | "v2",
490 | "session",
491 | "logout"
492 | ]
493 | }
494 | },
495 | "status": "OK",
496 | "code": 200,
497 | "_postman_previewlanguage": "html",
498 | "header": [
499 | {
500 | "key": "Access-Control-Allow-Origin",
501 | "value": "*"
502 | },
503 | {
504 | "key": "Content-Security-Policy",
505 | "value": "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
506 | },
507 | {
508 | "key": "Cross-Origin-Embedder-Policy",
509 | "value": "require-corp"
510 | },
511 | {
512 | "key": "Cross-Origin-Opener-Policy",
513 | "value": "same-origin"
514 | },
515 | {
516 | "key": "Cross-Origin-Resource-Policy",
517 | "value": "same-origin"
518 | },
519 | {
520 | "key": "X-DNS-Prefetch-Control",
521 | "value": "off"
522 | },
523 | {
524 | "key": "X-Frame-Options",
525 | "value": "SAMEORIGIN"
526 | },
527 | {
528 | "key": "Strict-Transport-Security",
529 | "value": "max-age=15552000; includeSubDomains"
530 | },
531 | {
532 | "key": "X-Download-Options",
533 | "value": "noopen"
534 | },
535 | {
536 | "key": "X-Content-Type-Options",
537 | "value": "nosniff"
538 | },
539 | {
540 | "key": "Origin-Agent-Cluster",
541 | "value": "?1"
542 | },
543 | {
544 | "key": "X-Permitted-Cross-Domain-Policies",
545 | "value": "none"
546 | },
547 | {
548 | "key": "Referrer-Policy",
549 | "value": "no-referrer"
550 | },
551 | {
552 | "key": "X-XSS-Protection",
553 | "value": "0"
554 | },
555 | {
556 | "key": "X-Powered-By",
557 | "value": "Express"
558 | },
559 | {
560 | "key": "Set-Cookie",
561 | "value": "accessjwt=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
562 | },
563 | {
564 | "key": "Content-Type",
565 | "value": "text/html; charset=utf-8"
566 | },
567 | {
568 | "key": "Content-Length",
569 | "value": "10"
570 | },
571 | {
572 | "key": "ETag",
573 | "value": "W/\"a-Tulnr8I5a7ilRNX7HnW4Gbpf/9I\""
574 | },
575 | {
576 | "key": "Date",
577 | "value": "Tue, 14 Feb 2023 10:12:42 GMT"
578 | },
579 | {
580 | "key": "Connection",
581 | "value": "keep-alive"
582 | },
583 | {
584 | "key": "Keep-Alive",
585 | "value": "timeout=5"
586 | }
587 | ],
588 | "cookie": [],
589 | "body": "logged out"
590 | }
591 | ]
592 | },
593 | {
594 | "name": "create Blog",
595 | "request": {
596 | "method": "POST",
597 | "header": [],
598 | "body": {
599 | "mode": "raw",
600 | "raw": "{\r\n \"title\":\"first blog\",\r\n \"description\":\"this is my first blog\",\r\n \"tags\":\"sports\"\r\n}",
601 | "options": {
602 | "raw": {
603 | "language": "json"
604 | }
605 | }
606 | },
607 | "url": {
608 | "raw": "localhost:3000/api/v2/blog/",
609 | "host": [
610 | "localhost"
611 | ],
612 | "port": "3000",
613 | "path": [
614 | "api",
615 | "v2",
616 | "blog",
617 | ""
618 | ]
619 | }
620 | },
621 | "response": [
622 | {
623 | "name": "create Blog",
624 | "originalRequest": {
625 | "method": "POST",
626 | "header": [],
627 | "body": {
628 | "mode": "raw",
629 | "raw": "{\r\n \"title\":\"first blog\",\r\n \"description\":\"this is my first blog\",\r\n \"tags\":\"sports\"\r\n}",
630 | "options": {
631 | "raw": {
632 | "language": "json"
633 | }
634 | }
635 | },
636 | "url": {
637 | "raw": "localhost:3000/api/v2/blog/",
638 | "host": [
639 | "localhost"
640 | ],
641 | "port": "3000",
642 | "path": [
643 | "api",
644 | "v2",
645 | "blog",
646 | ""
647 | ]
648 | }
649 | },
650 | "status": "Created",
651 | "code": 201,
652 | "_postman_previewlanguage": "json",
653 | "header": [
654 | {
655 | "key": "Access-Control-Allow-Origin",
656 | "value": "*"
657 | },
658 | {
659 | "key": "Content-Security-Policy",
660 | "value": "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
661 | },
662 | {
663 | "key": "Cross-Origin-Embedder-Policy",
664 | "value": "require-corp"
665 | },
666 | {
667 | "key": "Cross-Origin-Opener-Policy",
668 | "value": "same-origin"
669 | },
670 | {
671 | "key": "Cross-Origin-Resource-Policy",
672 | "value": "same-origin"
673 | },
674 | {
675 | "key": "X-DNS-Prefetch-Control",
676 | "value": "off"
677 | },
678 | {
679 | "key": "X-Frame-Options",
680 | "value": "SAMEORIGIN"
681 | },
682 | {
683 | "key": "Strict-Transport-Security",
684 | "value": "max-age=15552000; includeSubDomains"
685 | },
686 | {
687 | "key": "X-Download-Options",
688 | "value": "noopen"
689 | },
690 | {
691 | "key": "X-Content-Type-Options",
692 | "value": "nosniff"
693 | },
694 | {
695 | "key": "Origin-Agent-Cluster",
696 | "value": "?1"
697 | },
698 | {
699 | "key": "X-Permitted-Cross-Domain-Policies",
700 | "value": "none"
701 | },
702 | {
703 | "key": "Referrer-Policy",
704 | "value": "no-referrer"
705 | },
706 | {
707 | "key": "X-XSS-Protection",
708 | "value": "0"
709 | },
710 | {
711 | "key": "X-Powered-By",
712 | "value": "Express"
713 | },
714 | {
715 | "key": "Content-Type",
716 | "value": "application/json; charset=utf-8"
717 | },
718 | {
719 | "key": "Content-Length",
720 | "value": "262"
721 | },
722 | {
723 | "key": "ETag",
724 | "value": "W/\"106-gyr4kFgfre2/ajPsCR7IZlylCcM\""
725 | },
726 | {
727 | "key": "Date",
728 | "value": "Tue, 14 Feb 2023 10:15:27 GMT"
729 | },
730 | {
731 | "key": "Connection",
732 | "value": "keep-alive"
733 | },
734 | {
735 | "key": "Keep-Alive",
736 | "value": "timeout=5"
737 | }
738 | ],
739 | "cookie": [],
740 | "body": "{\n \"title\": \"first blog\",\n \"description\": \"this is my first blog\",\n \"tags\": \"sports\",\n \"author\": {\n \"userName\": \"ahmed eid\",\n \"_id\": \"63eb5cf125f5d12af42dc344\"\n },\n \"_id\": \"63eb5f3f25f5d12af42dc34a\",\n \"createdAt\": \"2023-02-14T10:15:27.515Z\",\n \"updatedAt\": \"2023-02-14T10:15:27.515Z\",\n \"__v\": 0\n}"
741 | }
742 | ]
743 | },
744 | {
745 | "name": "get Blog",
746 | "request": {
747 | "method": "GET",
748 | "header": [],
749 | "url": {
750 | "raw": "localhost:3000/api/v2/blog?author=ahmed eid",
751 | "host": [
752 | "localhost"
753 | ],
754 | "port": "3000",
755 | "path": [
756 | "api",
757 | "v2",
758 | "blog"
759 | ],
760 | "query": [
761 | {
762 | "key": "author",
763 | "value": "ahmed eid"
764 | }
765 | ]
766 | }
767 | },
768 | "response": [
769 | {
770 | "name": "get Blog",
771 | "originalRequest": {
772 | "method": "GET",
773 | "header": [],
774 | "url": {
775 | "raw": "localhost:3000/api/v2/blog?author=ahmed eid",
776 | "host": [
777 | "localhost"
778 | ],
779 | "port": "3000",
780 | "path": [
781 | "api",
782 | "v2",
783 | "blog"
784 | ],
785 | "query": [
786 | {
787 | "key": "author",
788 | "value": "ahmed eid"
789 | }
790 | ]
791 | }
792 | },
793 | "status": "OK",
794 | "code": 200,
795 | "_postman_previewlanguage": "json",
796 | "header": [
797 | {
798 | "key": "Access-Control-Allow-Origin",
799 | "value": "*"
800 | },
801 | {
802 | "key": "Content-Security-Policy",
803 | "value": "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
804 | },
805 | {
806 | "key": "Cross-Origin-Embedder-Policy",
807 | "value": "require-corp"
808 | },
809 | {
810 | "key": "Cross-Origin-Opener-Policy",
811 | "value": "same-origin"
812 | },
813 | {
814 | "key": "Cross-Origin-Resource-Policy",
815 | "value": "same-origin"
816 | },
817 | {
818 | "key": "X-DNS-Prefetch-Control",
819 | "value": "off"
820 | },
821 | {
822 | "key": "X-Frame-Options",
823 | "value": "SAMEORIGIN"
824 | },
825 | {
826 | "key": "Strict-Transport-Security",
827 | "value": "max-age=15552000; includeSubDomains"
828 | },
829 | {
830 | "key": "X-Download-Options",
831 | "value": "noopen"
832 | },
833 | {
834 | "key": "X-Content-Type-Options",
835 | "value": "nosniff"
836 | },
837 | {
838 | "key": "Origin-Agent-Cluster",
839 | "value": "?1"
840 | },
841 | {
842 | "key": "X-Permitted-Cross-Domain-Policies",
843 | "value": "none"
844 | },
845 | {
846 | "key": "Referrer-Policy",
847 | "value": "no-referrer"
848 | },
849 | {
850 | "key": "X-XSS-Protection",
851 | "value": "0"
852 | },
853 | {
854 | "key": "X-Powered-By",
855 | "value": "Express"
856 | },
857 | {
858 | "key": "Content-Type",
859 | "value": "application/json; charset=utf-8"
860 | },
861 | {
862 | "key": "Content-Length",
863 | "value": "264"
864 | },
865 | {
866 | "key": "ETag",
867 | "value": "W/\"108-mREcmU6TZVgjisZBq70QwtEPnqQ\""
868 | },
869 | {
870 | "key": "Date",
871 | "value": "Tue, 14 Feb 2023 10:17:18 GMT"
872 | },
873 | {
874 | "key": "Connection",
875 | "value": "keep-alive"
876 | },
877 | {
878 | "key": "Keep-Alive",
879 | "value": "timeout=5"
880 | }
881 | ],
882 | "cookie": [],
883 | "body": "[\n {\n \"author\": {\n \"userName\": \"ahmed eid\",\n \"_id\": \"63eb5cf125f5d12af42dc344\"\n },\n \"_id\": \"63eb5f3f25f5d12af42dc34a\",\n \"title\": \"first blog\",\n \"description\": \"this is my first blog\",\n \"tags\": \"sports\",\n \"createdAt\": \"2023-02-14T10:15:27.515Z\",\n \"updatedAt\": \"2023-02-14T10:15:27.515Z\",\n \"__v\": 0\n }\n]"
884 | }
885 | ]
886 | },
887 | {
888 | "name": "delete Blog",
889 | "request": {
890 | "method": "DELETE",
891 | "header": [],
892 | "url": {
893 | "raw": "localhost:3000/api/v2/blog/63eb5f3f25f5d12af42dc34a",
894 | "host": [
895 | "localhost"
896 | ],
897 | "port": "3000",
898 | "path": [
899 | "api",
900 | "v2",
901 | "blog",
902 | "63eb5f3f25f5d12af42dc34a"
903 | ]
904 | }
905 | },
906 | "response": [
907 | {
908 | "name": "delete Blog",
909 | "originalRequest": {
910 | "method": "DELETE",
911 | "header": [],
912 | "url": {
913 | "raw": "localhost:3000/api/v2/blog/63eb5f3f25f5d12af42dc34a",
914 | "host": [
915 | "localhost"
916 | ],
917 | "port": "3000",
918 | "path": [
919 | "api",
920 | "v2",
921 | "blog",
922 | "63eb5f3f25f5d12af42dc34a"
923 | ]
924 | }
925 | },
926 | "status": "OK",
927 | "code": 200,
928 | "_postman_previewlanguage": "html",
929 | "header": [
930 | {
931 | "key": "Access-Control-Allow-Origin",
932 | "value": "*"
933 | },
934 | {
935 | "key": "Content-Security-Policy",
936 | "value": "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
937 | },
938 | {
939 | "key": "Cross-Origin-Embedder-Policy",
940 | "value": "require-corp"
941 | },
942 | {
943 | "key": "Cross-Origin-Opener-Policy",
944 | "value": "same-origin"
945 | },
946 | {
947 | "key": "Cross-Origin-Resource-Policy",
948 | "value": "same-origin"
949 | },
950 | {
951 | "key": "X-DNS-Prefetch-Control",
952 | "value": "off"
953 | },
954 | {
955 | "key": "X-Frame-Options",
956 | "value": "SAMEORIGIN"
957 | },
958 | {
959 | "key": "Strict-Transport-Security",
960 | "value": "max-age=15552000; includeSubDomains"
961 | },
962 | {
963 | "key": "X-Download-Options",
964 | "value": "noopen"
965 | },
966 | {
967 | "key": "X-Content-Type-Options",
968 | "value": "nosniff"
969 | },
970 | {
971 | "key": "Origin-Agent-Cluster",
972 | "value": "?1"
973 | },
974 | {
975 | "key": "X-Permitted-Cross-Domain-Policies",
976 | "value": "none"
977 | },
978 | {
979 | "key": "Referrer-Policy",
980 | "value": "no-referrer"
981 | },
982 | {
983 | "key": "X-XSS-Protection",
984 | "value": "0"
985 | },
986 | {
987 | "key": "X-Powered-By",
988 | "value": "Express"
989 | },
990 | {
991 | "key": "Content-Type",
992 | "value": "text/html; charset=utf-8"
993 | },
994 | {
995 | "key": "Content-Length",
996 | "value": "19"
997 | },
998 | {
999 | "key": "ETag",
1000 | "value": "W/\"13-ndEqHlUkDs4b09hcACaI0QZx65k\""
1001 | },
1002 | {
1003 | "key": "Date",
1004 | "value": "Tue, 14 Feb 2023 10:18:17 GMT"
1005 | },
1006 | {
1007 | "key": "Connection",
1008 | "value": "keep-alive"
1009 | },
1010 | {
1011 | "key": "Keep-Alive",
1012 | "value": "timeout=5"
1013 | }
1014 | ],
1015 | "cookie": [],
1016 | "body": "deleted succesfully"
1017 | }
1018 | ]
1019 | }
1020 | ]
1021 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:19-alpine3.16
2 |
3 | RUN addgroup app && adduser -S -G app app
4 | USER app
5 |
6 | WORKDIR /app
7 |
8 | COPY package*.json .
9 | RUN npm install
10 | COPY . .
11 |
12 | EXPOSE 3000
13 |
14 | CMD ["npm","start"]
15 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 |
5 |
6 | # Affable
7 |
8 | Affable manages your Blog through REST API and you can also see others' Blogs .
9 |
10 |
11 | ## Demo
12 |
13 | Insert gif or link to demo
14 |
15 | ## Requirements
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ## Environment Variables
25 |
26 | To run this project, you will need to add the following environment variables to your **config/default.json** file
27 |
28 | `PORT`
29 |
30 | `HOST`
31 |
32 | `MONGO_URI`
33 |
34 | `saltWorkFactor`
35 |
36 | `jwtSecret`
37 |
38 | you can also take **config/test.json** as reference
39 | ## Installation
40 |
41 | Install my-project with npm
42 |
43 | ```bash
44 | npm install
45 | npm run dev #if your are a developer
46 | npm run start
47 | ```
48 | ## Usage
49 | Import this [JSON file](Blog.postman_collection.json) into Postman Collection, and you will be able to use all REST APIs.
50 |
51 | If you don't know how to do it, watch this [video](https://www.youtube.com/watch?v=bzquMXmCLUQ).
52 |
53 | ## API Reference
54 |
55 | ### Authentication
56 |
57 | #### Create User
58 | ```http
59 | POST /api/v2/user/
60 | ```
61 | | Request Body | Type | Description |
62 | | :-------- | :------- | :------------------------- |
63 | | `userName` | `string` | **Required** .user's username |
64 | | `email` | `string` | **Required** .user's email |
65 | | `password` | `string` | **Required** .user's password |
66 | | `passwordConfirmation` | `string` | **Required** |
67 |
68 | #### Login
69 | ```http
70 | POST /api/v2/session/login
71 | ```
72 | | Request Body | Type | Description |
73 | | :-------- | :------- | :------------------------- |
74 | | `email` | `string` | **Required** .email of exist user |
75 | | `passwrod` | `string` | **Required** .password of that email |
76 |
77 | #### Find User
78 | ```http
79 | GET /api/v2/user?
80 | ```
81 | | Request Query | Type | Description |
82 | | :-------- | :------- | :------------------------- |
83 | | `userName` | `string` | username of selected user |
84 | | `email` | `string` | email of selected user |
85 | | `id` | `Int` | id of selected user |
86 |
87 | #### Logout
88 | ```http
89 | GET /api/v2/session/logout
90 | ```
91 | ### CRUD Blog
92 | #### Create Blog
93 | ```http
94 | POST /api/v2/blog/
95 | ```
96 | | Constraints | Type | Description |
97 | | :-------- | :------- | :------------------------- |
98 | | `isAuthenticated` | `middleware`| **Required** you must be logged in to create a post |
99 |
100 | | Request Body | Type | Description |
101 | | :-------- | :------- | :------------------------- |
102 | | `title` | `string` | **Required** .main title of blog |
103 | | `description` | `string` | describe your blog |
104 | | `tags` | `string` | tag your post to specific topic ["programming", "health", "sports"] |
105 |
106 | #### Find Blog
107 | ```http
108 | GET /api/v2/blog?
109 | ```
110 | | Request Query | Type | Description |
111 | | :-------- | :------- | :------------------------- |
112 | | `id` | `string` | get blog with it's id |
113 | | `author` | `string` | get blogs for specific author |
114 |
115 | #### Delete Blog
116 | ```http
117 | DELETE /api/v2/blog/${id}
118 | ```
119 | | Constraints | Type | Description |
120 | | :-------- | :------- | :------------------------- |
121 | | `isAuthenticated` | `middleware`| **Required** you must be logged in to create a blog |
122 | | `isAuthorized` | `middleware`| **Required** you must be that owner of the blog |
123 |
124 | | Request Parameters | Type | Description |
125 | | :-------- | :------- | :------------------------- |
126 | | `id` | `string` | **Required** .id of deleted blog|
127 |
128 | ## Contributing
129 |
130 | Contributions are always welcome!
131 |
132 | ## Authors
133 |
134 | - [@AhmedEid](https://github.com/ahmedeid6842/)
135 |
136 | ## Lessons Learned
137 |
138 | - How to transition from javascript to typescript and reap the benefits of typescript.
139 | - How to use Zod for validation.
140 | - How to migrate MongDB and typescript.
141 | - There is always something new to learn.
142 |
143 |
144 |
--------------------------------------------------------------------------------
/config/test.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | PORT: 3000,
3 | HOST: "localhost",
4 | MONGO_URI: "Your Mongo URI",
5 | saltWorkFactor: "bcrypt salt",
6 | jwtSecret: "json web token secret",
7 | };
8 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | backend:
4 | build: ./
5 | ports:
6 | - "3000:3000"
7 | environment:
8 | - MONGO_URI=mongodb://db/blog
9 | depends_on:
10 | - db
11 | db:
12 | image: mongo:4.0-xenial
13 | ports:
14 | - 27017:27017
15 | volumes:
16 | - blog:/data/db
17 | volumes:
18 | blog:
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blog",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "ts-node src/server.ts",
8 | "dev": "nodemon src/server.ts"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/ahmedeid6842/Blog.git"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/ahmedeid6842/Blog/issues"
19 | },
20 | "homepage": "https://github.com/ahmedeid6842/Blog#readme",
21 | "dependencies": {
22 | "@types/cors": "^2.8.13",
23 | "bcrypt": "^5.1.0",
24 | "config": "^3.3.9",
25 | "cookie-parser": "^1.4.6",
26 | "cors": "^2.8.5",
27 | "express": "^4.18.2",
28 | "helmet": "^6.0.1",
29 | "jsonwebtoken": "^9.0.0",
30 | "lodash": "^4.17.21",
31 | "mongoose": "^6.8.4",
32 | "morgan": "^1.10.0",
33 | "winston": "^3.8.2",
34 | "zod": "^3.20.2"
35 | },
36 | "devDependencies": {
37 | "@types/bcrypt": "^5.0.0",
38 | "@types/config": "^3.3.0",
39 | "@types/cookie-parser": "^1.4.3",
40 | "@types/express": "^4.17.15",
41 | "@types/jsonwebtoken": "^9.0.1",
42 | "@types/lodash": "^4.14.191",
43 | "@types/mongoose": "^5.11.97",
44 | "@types/morgan": "^1.9.4",
45 | "@types/node": "^18.11.18",
46 | "ts-node": "^10.9.1",
47 | "typescript": "^4.9.4"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import cors from "cors";
3 | import morgan from "morgan";
4 | import helmet from "helmet";
5 | import route from "./routes/index";
6 | import cookiePrser from "cookie-parser";
7 | import { errorHandler } from "./middleware/errorHandler";
8 | import { notFound } from "./middleware/notFound";
9 | const app = express();
10 |
11 | app.use(express.json());
12 | app.use(cookiePrser());
13 | app.use(cors());
14 | app.use(morgan("dev"));
15 | app.use(helmet());
16 |
17 | app.use("/api/v2", route);
18 |
19 | app.use(notFound);
20 | app.use(errorHandler);
21 |
22 | export default app;
23 |
--------------------------------------------------------------------------------
/src/controller/blog.controller.ts:
--------------------------------------------------------------------------------
1 | import { NextFunction, Request, Response } from "express";
2 | import {
3 | createBlogInput,
4 | deleteBlogInput,
5 | getBlogInput,
6 | } from "../schema/blog.schema";
7 | import {
8 | createBlog,
9 | deleteBlog,
10 | getBlog,
11 | } from "../service/blog.service";
12 |
13 | export async function ceateBlogHandler(
14 | req: Request<{}, {}, createBlogInput>,
15 | res: Response,
16 | next: NextFunction
17 | ) {
18 | /**
19 | * DONE: user must be authenticated use isAuth middleware
20 | * DONE: validate req.body by zod
21 | * DONE: pass createBlogInput as generator to requesut
22 | * DONE: call create Blog service function
23 | */
24 | try {
25 | const Blog = await createBlog({
26 | ...req.body,
27 | author: {
28 | _id: res.locals.user._id,
29 | userName: res.locals.user.userName,
30 | },
31 | });
32 | return res.status(201).send(Blog);
33 | } catch (error: any) {
34 | next(error);
35 | }
36 | }
37 |
38 | export async function getBlogHandler(
39 | req: Request<{}, {}, {}, getBlogInput>,
40 | res: Response,
41 | next: NextFunction
42 | ) {
43 | /**
44 | * DONE: validate user req.query by zod
45 | * DONE: call get blog service function
46 | * DONE: if not blog found return 404
47 | *
48 | */
49 | try {
50 | const blogs = await getBlog(req.query);
51 | if (!blogs) {
52 | return res.status(404).send("no blogs found");
53 | }
54 | return res.status(200).send(blogs);
55 | } catch (error) {
56 | next(error);
57 | }
58 | }
59 |
60 | export async function deleteBlogHandler(
61 | req: Request,
62 | res: Response,
63 | next: NextFunction
64 | ) {
65 | /**
66 | * DONE: user must be authenticated and authorized
67 | * DONE: validate req.query by zod
68 | * DONE: call delete blog service functionn
69 | */
70 | try {
71 | let blog = await deleteBlog(req.params, res.locals.user._id);
72 | if (!blog) {
73 | return res.status(404).send("no blog found");
74 | }
75 | return res.status(200).send("deleted succesfully");
76 | } catch (error: any) {
77 | next(error);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/controller/session.controller.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { loginUserInput } from "../schema/user.schema";
3 | import { getUser } from "../service/user.service";
4 | import { signJWT } from "../utils/jwt.utils";
5 | import logger from "../utils/logger";
6 |
7 | export async function loginUserHandler(
8 | req: Request<{}, loginUserInput>,
9 | res: Response
10 | ) {
11 | /**
12 | * DONE: validate req.body by zod loginUserSchema
13 | * DONE: call get user service
14 | * DONE: call compare password method in mongoose user Schema
15 | * DONE: generate access Token
16 | * DONE: logg error
17 | */
18 | try {
19 | let user = await getUser({ email: req.body.email }, true);
20 | if (!user) {
21 | return res.status(404).send(`no user found`);
22 | }
23 |
24 | let valid = await user[0].comparePassword(req.body.password);
25 | if (!valid) {
26 | return res.status(404).send(`no user found`);
27 | }
28 |
29 | let accessToken = signJWT(
30 | { _id: user[0]._id, userName: user[0].userName },
31 | { expiresIn: "30m" }
32 | );
33 |
34 | res.cookie("accessjwt", accessToken, {
35 | httpOnly: true,
36 | sameSite: "strict",
37 | });
38 |
39 | return res.send(`welcome ${user[0].userName}`);
40 | } catch (error: any) {
41 | logger.error(error.message);
42 | return res.status(500);
43 | }
44 | }
45 |
46 | export async function logoutUserHandler(req: Request, res: Response) {
47 | /**
48 | * DONE: clear user cookie
49 | */
50 | res.clearCookie("accessjwt");
51 | return res.send("logged out");
52 | }
53 |
--------------------------------------------------------------------------------
/src/controller/user.controller.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { omit } from "lodash";
3 | import { DocumentDefinition } from "mongoose";
4 | import { UserDocument } from "../model/user.models";
5 | import { createUserInput, getUserInput } from "../schema/user.schema";
6 | import { createUser, getUser } from "../service/user.service";
7 | import logger from "../utils/logger";
8 |
9 | export async function createUserHandler(
10 | req: Request<{}, {}, createUserInput>,
11 | res: Response
12 | ) {
13 | try {
14 | /**
15 | * DONE: validate req.body by zod create user schema
16 | * DONE: pass createUserInput as generator to Request
17 | * DONE: call create user service
18 | * DONE: log error using winston
19 | */
20 | const user = await createUser(omit(req.body, "passwordConfirmation"));
21 | return res.send(user);
22 | } catch (error: any) {
23 | logger.error(error.message);
24 | return res.status(500);
25 | }
26 | }
27 |
28 |
29 | export async function getUserHandler(
30 | req: Request<
31 | {},
32 | DocumentDefinition>,
33 | {},
34 | getUserInput
35 | >,
36 | res: Response
37 | ) {
38 | /**
39 | * DONE: validate req.query by zod getUserQuery
40 | * DONE: pass UserDocument as generator response and getUserinput and genertor query
41 | * DONE: call get user service
42 | * DONE: logg error
43 | */ try {
44 | const user = await getUser(req.query);
45 | if (!user) {
46 | return res.status(404).send(`no user with ${req.query}`);
47 | }
48 | return res.send(user);
49 | } catch (error: any) {
50 | logger.error(error.message);
51 | return res.status(500);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/interface/ErrorResponse.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export default interface ErrorResponse {
4 | stack?: string;
5 | message: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/interface/RequestValidators.ts:
--------------------------------------------------------------------------------
1 | import { AnyZodObject } from "zod";
2 |
3 | export default interface RequestValidators {
4 | params?: AnyZodObject;
5 | body?: AnyZodObject;
6 | query?: AnyZodObject;
7 | }
8 |
--------------------------------------------------------------------------------
/src/interface/jwtPayload.ts:
--------------------------------------------------------------------------------
1 | export interface jwtPayLoadInterface {
2 | _id: string;
3 | userName: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/middleware/errorHandler.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import ErrorResponse from "../interface/ErrorResponse";
3 | import logger from "../utils/logger";
4 | export function errorHandler(
5 | err: Error,
6 | req: Request,
7 | res: Response,
8 | next: NextFunction
9 | ) {
10 | const statusCode = res.statusCode !== 200 ? res.statusCode : 500;
11 | logger.error(err.stack);
12 | res.status(statusCode);
13 | res.json({
14 | message: err.message,
15 | stack: process.env.NODE_ENV === "production" ? "🥞" : err.stack,
16 | });
17 | }
18 |
--------------------------------------------------------------------------------
/src/middleware/isAuth.ts:
--------------------------------------------------------------------------------
1 | //check if user is authenticated and deseralize user
2 | import { verifyJWT } from "../utils/jwt.utils";
3 | import { Request, Response, NextFunction } from "express";
4 |
5 | export const isAuth = (req: Request, res: Response, next: NextFunction) => {
6 | const { accessjwt } = req.cookies;
7 | if (!accessjwt) return res.status(401).send("no token provided");
8 |
9 | const { valid, expired, decoded } = verifyJWT(accessjwt);
10 | if (!valid) {
11 | if (expired) {
12 | return res.status(400).send("you session has ended please log again");
13 | }
14 | return res.status(400).send("invalid token");
15 | }
16 |
17 | res.locals.user = decoded;
18 | return next();
19 | };
20 |
--------------------------------------------------------------------------------
/src/middleware/notFound.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 |
3 | export function notFound(req: Request, res: Response, next: NextFunction) {
4 | res.status(404);
5 | const error = new Error(`🔍 - Not Found - ${req.originalUrl}`);
6 | next(error);
7 | }
8 |
--------------------------------------------------------------------------------
/src/middleware/validateResource.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import RequestValidator from "../interface/RequestValidators";
3 | import { AnyZodObject } from "zod";
4 |
5 | const validate =
6 | (validators: RequestValidator) =>
7 | async (req: Request, res: Response, next: NextFunction) => {
8 | try {
9 | if (validators.params) {
10 | req.params = await validators.params.parseAsync(req.params);
11 | }
12 | if (validators.body) {
13 | req.body = await validators.body.parseAsync(req.body);
14 | }
15 | if (validators.query) {
16 | req.query = await validators.query.parseAsync(req.query);
17 | }
18 | return next();
19 | } catch (error: any) {
20 | return res.status(400).send(error);
21 | }
22 | };
23 |
24 | export default validate;
25 |
--------------------------------------------------------------------------------
/src/model/blog.models.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { mongo } from "mongoose";
2 |
3 | export interface BlogDocument extends mongoose.Document {
4 | title: string;
5 | description?: string;
6 | tags?: "programming" | "health" | "sports" | undefined;
7 | author: {
8 | userName: string;
9 | _id: mongoose.Schema.Types.ObjectId;
10 | };
11 | createdAt: Date;
12 | updatedAt: Date;
13 | }
14 |
15 | const BlogSchema = new mongoose.Schema(
16 | {
17 | title: {
18 | type: String,
19 | required: true,
20 | },
21 | description: {
22 | type: String,
23 | },
24 | tags: {
25 | type: String,
26 | enum: ["programming", "health", "sports"],
27 | },
28 | author: {
29 | userName: {
30 | type: String,
31 | required: true,
32 | },
33 | _id: {
34 | type: mongoose.Schema.Types.ObjectId,
35 | required: true,
36 | ref: "user",
37 | },
38 | },
39 | },
40 | { timestamps: true }
41 | );
42 |
43 | const blog = mongoose.model("blog", BlogSchema);
44 | export default blog;
45 |
--------------------------------------------------------------------------------
/src/model/user.models.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import bcrypt from "bcrypt";
3 | import config from "config";
4 |
5 | export interface UserDocument extends mongoose.Document {
6 | email: string;
7 | userName: string;
8 | password: string;
9 | createdAt: Date;
10 | updatedAt: Date;
11 | comparePassword(userPassword: string): Promise;
12 | }
13 |
14 | const UserSchema = new mongoose.Schema(
15 | {
16 | email: {
17 | type: String,
18 | required: true,
19 | unique: true,
20 | },
21 | userName: {
22 | type: String,
23 | required: true,
24 | },
25 | password: {
26 | type: String,
27 | required: true,
28 | },
29 | },
30 | { timestamps: true }
31 | );
32 |
33 | UserSchema.pre("save", async function (next) {
34 | let user = this as UserDocument;
35 | if (!user.isModified("password")) return next();
36 | const salt = await bcrypt.genSalt(config.get("saltWorkFactor"));
37 | const hash = await bcrypt.hash(user.password, salt);
38 | user.password = hash;
39 | return next();
40 | });
41 |
42 | UserSchema.methods.comparePassword = async function (
43 | userPassword: string
44 | ): Promise {
45 | const user = this as UserDocument;
46 | return await bcrypt.compare(userPassword, user.password).catch((e) => false);
47 | };
48 |
49 | const User = mongoose.model("user", UserSchema);
50 | export default User;
51 |
--------------------------------------------------------------------------------
/src/routes/blog.routes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import {
3 | ceateBlogHandler,
4 | getBlogHandler,
5 | deleteBlogHandler,
6 | } from "../controller/blog.controller";
7 | import { isAuth } from "../middleware/isAuth";
8 | import validateResource from "../middleware/validateResource";
9 | import {
10 | createBlogSchema,
11 | getBlogQuery,
12 | deleteBlogParams,
13 | } from "../schema/blog.schema";
14 |
15 | const route = Router();
16 |
17 | //DONE: route -> create a new blog , autheticated user
18 | route.post(
19 | "/",
20 | isAuth,
21 | validateResource({ body: createBlogSchema }),
22 | ceateBlogHandler
23 | );
24 |
25 | //DONE: route[query] -> get pulgin by title or id or author
26 | route.get("/", validateResource({ query: getBlogQuery }), getBlogHandler);
27 |
28 | //DONE: route[params] -> delete blog by it's id , authorized user
29 | route.delete(
30 | "/:id",
31 | isAuth,
32 | validateResource({ params: deleteBlogParams }),
33 | deleteBlogHandler
34 | );
35 |
36 | export default route;
37 |
--------------------------------------------------------------------------------
/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 |
3 | import user from "./user.routes";
4 | import blog from "./blog.routes";
5 | import session from "./session.routes";
6 |
7 | const app = express();
8 |
9 | app.use("/user", user);
10 | app.use("/blog", blog);
11 | app.use("/session", session);
12 |
13 | export default app;
14 |
--------------------------------------------------------------------------------
/src/routes/session.routes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import {
3 | loginUserHandler,
4 | logoutUserHandler,
5 | } from "../controller/session.controller";
6 | import { loginUserSchema } from "../schema/user.schema";
7 | import validateResource from "../middleware/validateResource";
8 |
9 | const route = Router();
10 |
11 | //DONE: route -> authenticate user and create session
12 | route.post(
13 | "/login",
14 | validateResource({ body: loginUserSchema }),
15 | loginUserHandler
16 | );
17 |
18 | //DONE: route -> logout and remove sessions
19 | route.get("/logout", logoutUserHandler);
20 |
21 | export default route;
22 |
--------------------------------------------------------------------------------
/src/routes/user.routes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import validateResource from "../middleware/validateResource";
3 | import { createUserSchema, getUserQuery } from "../schema/user.schema";
4 | import {
5 | createUserHandler,
6 | getUserHandler,
7 | } from "../controller/user.controller";
8 |
9 | const route = Router();
10 |
11 | //DONE: route -> create new user
12 | route.post(
13 | "/",
14 | validateResource({ body: createUserSchema }),
15 | createUserHandler
16 | );
17 |
18 | //DONE: route[query] -> get user by name or id
19 | route.get("/", validateResource({ query: getUserQuery }), getUserHandler);
20 |
21 | export default route;
22 |
--------------------------------------------------------------------------------
/src/schema/blog.schema.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import * as zod from "zod";
3 | import { string } from "zod";
4 |
5 | export const createBlogSchema = zod.object({
6 | title: zod
7 | .string({
8 | required_error: "title is required",
9 | })
10 | .min(5)
11 | .max(100),
12 | description: zod.string().min(5).max(255).optional(),
13 | tags: zod.enum(["programming", "health", "sports"]).optional(),
14 | });
15 |
16 | export const getBlogQuery = zod.object({
17 | title: string().optional(),
18 | id: zod
19 | .string()
20 | .min(24)
21 | .max(24)
22 | .optional()
23 | .refine((val) => {
24 | try {
25 | return new mongoose.Schema.Types.ObjectId(val as string);
26 | } catch (error) {
27 | return false;
28 | }
29 | }),
30 | author: zod.string().optional(),
31 | });
32 |
33 | export const deleteBlogParams = zod.object({
34 | id: zod
35 | .string()
36 | .min(24)
37 | .max(24)
38 | .refine((val) => {
39 | try {
40 | return new mongoose.Schema.Types.ObjectId(val as string);
41 | } catch (error) {
42 | return false;
43 | }
44 | }),
45 | });
46 |
47 | export type createBlogInput = zod.infer;
48 | export type getBlogInput = zod.infer;
49 | export type deleteBlogInput = zod.infer;
50 |
--------------------------------------------------------------------------------
/src/schema/user.schema.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import * as zod from "zod";
3 |
4 | export const createUserSchema = zod.object({
5 | userName: zod.string({
6 | required_error: "user name is required",
7 | }),
8 | password: zod
9 | .string({
10 | required_error: "password is requierd",
11 | })
12 | .min(6, "password too short - should be 6 chars minimum"),
13 | passwordConfirmation: zod.string({
14 | required_error: "password confiramtion is required",
15 | }),
16 | email: zod
17 | .string({
18 | required_error: "email is required",
19 | })
20 | .email("Not a valid email"),
21 | });
22 |
23 | export const getUserQuery = zod.object({
24 | userName: zod.string().optional(),
25 | id: zod
26 | .string()
27 | .min(24)
28 | .max(24)
29 | .optional()
30 | .refine((val) => {
31 | try {
32 | return new mongoose.Schema.Types.ObjectId(val as string);
33 | } catch (error) {
34 | return false;
35 | }
36 | }),
37 | });
38 |
39 | export const loginUserSchema = zod.object({
40 | email: zod
41 | .string({
42 | required_error: "email is required",
43 | })
44 | .email("must be valid email"),
45 | password: zod.string({
46 | required_error: "password is required",
47 | }),
48 | });
49 |
50 | export type createUserInput = zod.infer;
51 | export type getUserInput = zod.infer;
52 | export type loginUserInput = zod.infer;
53 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import config from "config";
2 |
3 | import MongoConnect from "./utils/connectDB"
4 | import app from "./app";
5 |
6 |
7 | const port = config.get("PORT") || 3000;
8 | const host = config.get("HOST") || "localhost";
9 |
10 |
11 |
12 | MongoConnect()
13 | .then(() => {
14 | app.listen(port, host, () => {
15 | console.log(`server starting at http://${host}:${port} 🚀🎉`);
16 | });
17 | })
18 | .catch((error) => {
19 | console.log(error);
20 | process.exit(1);
21 | });
22 |
--------------------------------------------------------------------------------
/src/service/blog.service.ts:
--------------------------------------------------------------------------------
1 | import { omit } from "lodash";
2 | import mongoose, { DocumentDefinition, ObjectId } from "mongoose";
3 | import Blog, { BlogDocument } from "../model/blog.models";
4 | import { deleteBlogInput, getBlogInput } from "../schema/blog.schema";
5 |
6 | export async function createBlog(
7 | input: DocumentDefinition>
8 | ) {
9 | try {
10 | let blog = await Blog.create(input);
11 | return blog;
12 | } catch (error: any) {
13 | throw new Error(error);
14 | }
15 | }
16 |
17 | export async function getBlog(input: getBlogInput) {
18 | try {
19 | let blogs: any;
20 | if (input.author) {
21 | let userName = input.author;
22 | input = omit(input, "author");
23 | blogs = await Blog.find({
24 | $and: [{ ...input }, { "author.userName": userName }],
25 | });
26 | } else {
27 | blogs = await Blog.find(input);
28 | }
29 | if (blogs.length === 0) {
30 | return false;
31 | }
32 | return blogs;
33 | } catch (error: any) {
34 | throw new Error(error);
35 | }
36 | }
37 |
38 | export async function deleteBlog(
39 | BlogID: deleteBlogInput,
40 | userID: ObjectId
41 | ) {
42 | try {
43 | let blog = await Blog.findOneAndDelete({
44 | _id: BlogID.id,
45 | "author._id": userID,
46 | });
47 | if (!blog) {
48 | return false;
49 | }
50 | return true;
51 | } catch (error: any) {
52 | throw new Error(error);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/service/user.service.ts:
--------------------------------------------------------------------------------
1 | import { omit } from "lodash";
2 | import { DocumentDefinition } from "mongoose";
3 | import User, { UserDocument } from "../model/user.models";
4 |
5 | export async function createUser(
6 | input: DocumentDefinition<
7 | Omit
8 | >
9 | ) {
10 | try {
11 | let user = await User.create(input);
12 | return omit(user.toObject(), "password");
13 | } catch (error: any) {
14 | throw new Error(error);
15 | }
16 | }
17 |
18 |
19 | export async function getUser(
20 | input: Partial<
21 | DocumentDefinition<
22 | Omit
23 | >
24 | >,
25 | password?: boolean
26 | ) {
27 | /**
28 | * DONE: make get user reusable so it can take any argument and search with it
29 | * DONE: using Partial typescript mapped to make all property optaion
30 | */
31 | try {
32 | let users = await User.find(input);
33 | if (users.length == 0) {
34 | return false;
35 | }
36 |
37 | if (password) {
38 |
39 | // if password = true then return password with document
40 | return users;
41 | }
42 |
43 | return users.map((user) => {
44 | return omit(user.toObject(), "password");
45 | });
46 | } catch (error: any) {
47 | throw new Error(error);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/utils/connectDB.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import config from "config";
3 |
4 | function connect() {
5 | // const MONGO_URI = config.get("MONGO_URI");
6 | return mongoose
7 | .connect(process.env.MONGO_URI as string)
8 | .then(() => {
9 | console.log(`Connected to DataBase ${process.env.MONGO_URI}`);
10 | })
11 | .catch((error) => {
12 | console.log("Failed to Connect to DataBase");
13 | process.exit(1);
14 | });
15 | }
16 |
17 | export default connect;
18 |
--------------------------------------------------------------------------------
/src/utils/jwt.utils.ts:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import config from "config";
3 |
4 | const jwtSecret = config.get("jwtSecret");
5 |
6 | export function signJWT(
7 | payload: Object,
8 | options?: jwt.SignOptions | undefined
9 | ) {
10 | let token = jwt.sign(payload, jwtSecret, options);
11 | return token;
12 | }
13 |
14 | export function verifyJWT(Token: string): {
15 | valid: Boolean;
16 | expired: Boolean;
17 | decoded: Object | null;
18 | } {
19 | try {
20 | const decoded = jwt.verify(Token, jwtSecret);
21 | return {
22 | valid: true,
23 | expired: false,
24 | decoded,
25 | };
26 | } catch (error: any) {
27 | return {
28 | valid: false,
29 | expired: error.message === "jwt expired",
30 | decoded: null,
31 | };
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
1 | import { createLogger, format, transports } from "winston";
2 | const log = createLogger({
3 | format: format.combine(
4 | format.colorize(),
5 | format.timestamp(),
6 | format.printf(({ timestamp, level, message }) => {
7 | return `[${timestamp}] ${level} : ${message}`;
8 | })
9 | ),
10 | transports: [new transports.Console()],
11 | });
12 |
13 | export default log;
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "commonjs", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "resolveJsonModule": true, /* Enable importing .json files. */
39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45 |
46 | /* Emit */
47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
52 | // "outDir": "./", /* Specify an output folder for all emitted files. */
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 |
78 | /* Type Checking */
79 | "strict": true, /* Enable all strict type-checking options. */
80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | }
103 | }
104 |
--------------------------------------------------------------------------------