├── .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": "![Logo](https://user-images.githubusercontent.com/57197702/217839044-f0e66ebc-ff6f-4abf-b0e9-f926cc56f192.PNG)\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 | ![Logo](https://user-images.githubusercontent.com/57197702/217839044-f0e66ebc-ff6f-4abf-b0e9-f926cc56f192.PNG) 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 | --------------------------------------------------------------------------------