From a8416ebe8b2d48ef2c4a32b0ad5bf9a998720377 Mon Sep 17 00:00:00 2001 From: Joyosmit Date: Wed, 29 May 2024 18:25:10 +0530 Subject: [PATCH 1/8] Added OTP verification for email via nodemailer --- backend/package-lock.json | 289 ++++++++++++++++++++++++ backend/package.json | 3 + backend/prisma/schema.prisma | 1 + backend/src/helpers/sendMail.ts | 24 ++ backend/src/middleware/auth.ts | 1 + backend/src/routes/user/controller.ts | 89 +++++++- backend/src/routes/user/route.ts | 4 +- backend/src/routes/user/zodSchema.ts | 4 +- frontend/src/App.tsx | 11 +- frontend/src/pages/Otp.jsx | 11 + frontend/src/pages/Signup.tsx | 307 ++++++++++++++++---------- 11 files changed, 623 insertions(+), 121 deletions(-) create mode 100644 backend/src/helpers/sendMail.ts create mode 100644 frontend/src/pages/Otp.jsx diff --git a/backend/package-lock.json b/backend/package-lock.json index a704f2d5..81eb0247 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,9 +17,12 @@ "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", + "nodemailer": "^6.9.13", + "nodemon": "^3.1.1", "prisma": "^5.14.0", "ts-node": "^10.9.2", "typescript": "^5.4.5", @@ -341,6 +344,18 @@ "node": ">=8" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -387,6 +402,17 @@ "node": ">= 10.0.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -419,6 +445,17 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -450,6 +487,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -525,6 +585,12 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -697,6 +763,17 @@ "node": ">= 0.10.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -757,6 +834,19 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -822,6 +912,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -833,6 +934,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -941,6 +1050,11 @@ "node": ">=0.10.0" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -963,6 +1077,25 @@ "node": ">= 0.10" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -971,6 +1104,25 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1219,6 +1371,62 @@ } } }, + "node_modules/nodemailer": { + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.1.tgz", + "integrity": "sha512-k43xGaDtaDIcufn0Fc6fTtsdKSkV/hQzoQFigNH//GaKta28yoKVYXCnV+KXRqfT/YzsFaQU9VdeEG+HEyxr6A==", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -1233,6 +1441,14 @@ "node": ">=6" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -1301,6 +1517,17 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/prisma": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.14.0.tgz", @@ -1328,6 +1555,11 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -1377,6 +1609,17 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -1516,6 +1759,17 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1556,6 +1810,17 @@ "node": ">=8" } }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -1572,6 +1837,17 @@ "node": ">=10" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1580,6 +1856,14 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -1651,6 +1935,11 @@ "node": ">=14.17" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/backend/package.json b/backend/package.json index a114d7e3..13aebdd1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,9 +21,12 @@ "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", + "nodemailer": "^6.9.13", + "nodemon": "^3.1.1", "prisma": "^5.14.0", "ts-node": "^10.9.2", "typescript": "^5.4.5", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f1074440..546eccb2 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -18,6 +18,7 @@ model User { username String email String passwordHash String + otp Int? posts Post[] @relation("authorPosts") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/backend/src/helpers/sendMail.ts b/backend/src/helpers/sendMail.ts new file mode 100644 index 00000000..90aa1196 --- /dev/null +++ b/backend/src/helpers/sendMail.ts @@ -0,0 +1,24 @@ +// import nodemailer from 'nodemailer'; +// @ts-nocheck +import nodemailer from "nodemailer"; +// nodemailer = require('nodemailer'); + +export const sendVerificationEmail = async (email: string, otp: number) => { + console.log("Email: ",process.env.EMAIL_USER, process.env.EMAIL_PASS) + let transporter = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + let info = await transporter.sendMail({ + from: '"Your App" ', + to: email, + subject: "Email Verification", + text: `Your OTP for email verification is ${otp}`, + }); + + console.log("Message sent: %s", info.messageId); +}; diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 822b52ca..f1831147 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -28,6 +28,7 @@ const authMiddleware = (req:UserAuthRequest, res:Response, next: NextFunction) = } req.userId = decodedValue.id; + console.log("IN JWT:'",req.userId) next(); } diff --git a/backend/src/routes/user/controller.ts b/backend/src/routes/user/controller.ts index 1694f2b2..39c39f12 100644 --- a/backend/src/routes/user/controller.ts +++ b/backend/src/routes/user/controller.ts @@ -4,6 +4,9 @@ import { signinBodySchema, signupBodySchema } from "./zodSchema"; import { createHash, validatePassword } from "../../helpers/hash"; import { createJWT } from "../../helpers/jwt"; import { UserAuthRequest } from "../../helpers/types"; +import crypto from "crypto"; +import { sendVerificationEmail } from "../../helpers/sendMail"; +import { z } from "zod"; export const userSignupController = async (req: Request, res: Response) => { try { @@ -45,11 +48,16 @@ export const userSignupController = async (req: Request, res: Response) => { const passwordHash = await createHash(data.password); + const otp = crypto.randomInt(100000, 999999); // Generate a 6-digit OTP + + const user = await prisma.user.create({ data: { email: data.email, passwordHash, username: data.username, + // @ts-ignore + otp, }, select: { id: true, @@ -58,6 +66,9 @@ export const userSignupController = async (req: Request, res: Response) => { }, }); + // Send OTP to user's email + await sendVerificationEmail(user.email, otp); + const token = createJWT({ id: user.id, username: user.username, @@ -69,14 +80,90 @@ export const userSignupController = async (req: Request, res: Response) => { token: token, }); } catch (error) { + console.log(error) return res.status(500).json({ error: { - message: "An unexpected exception occurred!", + message: "An unexpected exception occurred! Brooo", + display: error }, }); } }; +// import prisma from "../../db"; + +const otpVerificationSchema = z.object({ + userId: z.string(), + otp: z.number(), +}); + +export const verifyOtpController = async (req: Request, res: Response) => { + try { + const payload = req.body; + const result = otpVerificationSchema.safeParse(payload); + + if (!result.success) { + const formattedError: any = {}; + result.error.errors.forEach((e) => { + formattedError[e.path[0]] = e.message; + }); + return res.status(400).json({ + error: { ...formattedError, message: "Validation error." }, + }); + } + + const { userId, otp } = result.data; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + otp: true, + createdAt: true, + }, + }); + + if (!user) { + return res.status(404).json({ + error: { message: "User not found." }, + }); + } + + if (user.otp !== otp) { + return res.status(400).json({ + error: { message: "Invalid OTP." }, + }); + } + + const otpAge = Date.now() - new Date(user.createdAt).getTime(); + const otpExpiry = 10 * 60 * 1000; // 10 minutes + + if (otpAge > otpExpiry) { + return res.status(400).json({ + error: { message: "OTP has expired." }, + }); + } + + await prisma.user.update({ + where: { id: userId }, + data: { + otp: null, + }, + }); + + return res.status(200).json({ + message: "Email verified successfully.", + }); + } catch (error) { + console.error("OTP verification error:", error); + return res.status(500).json({ + error: { message: "An unexpected error occurred." }, + }); + } +}; + + + export const userSigninController = async (req: Request, res: Response) => { try { const payload = req.body; diff --git a/backend/src/routes/user/route.ts b/backend/src/routes/user/route.ts index d16b277e..24f1253c 100644 --- a/backend/src/routes/user/route.ts +++ b/backend/src/routes/user/route.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { userProfileController, userSigninController, userSignupController } from "./controller"; +import { userProfileController, userSigninController, userSignupController, verifyOtpController } from "./controller"; import authMiddleware from "../../middleware/auth" const userRouter = Router(); @@ -8,6 +8,8 @@ userRouter.post('/signup', userSignupController) userRouter.post('/signin', userSigninController) +userRouter.post('/verify', verifyOtpController) + userRouter.get('/me', authMiddleware, userProfileController); export default userRouter; \ No newline at end of file diff --git a/backend/src/routes/user/zodSchema.ts b/backend/src/routes/user/zodSchema.ts index 336de0f5..6ea69134 100644 --- a/backend/src/routes/user/zodSchema.ts +++ b/backend/src/routes/user/zodSchema.ts @@ -5,7 +5,7 @@ export const signupBodySchema = zod.object({ .string() .min(5, { message: "Username too short!" }) .max(30, { message: "Username too long!" }), - email: zod.string().email().max(30, { message: "Email too long!" }), + email: zod.string().email().max(80, { message: "Email too long!" }), password: zod .string() .min(8, { message: "Password too short!" }) @@ -13,7 +13,7 @@ export const signupBodySchema = zod.object({ }); export const signinBodySchema = zod.object({ - email: zod.string().email().max(30, { message: "Email too long!" }), + email: zod.string().email().max(80, { message: "Email too long!" }), password: zod .string() .min(8, { message: "Password too short!" }) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5c73538f..86ab8618 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,10 +12,12 @@ import { RecoilRoot } from "recoil"; import NonAuthenticatedRoute from "./components/NonAuthenticatedRoute"; import AuthenticatedRoute from "./components/AuthenticatedRoute"; import Profile from "./pages/Profile"; +// @ts-expect-error +import OTP from "./pages/Otp.jsx"; import React from "react"; import Loader from "./components/Loader"; -// import axios from "axios"; -// axios.defaults.baseURL = "http://localhost:3001/"; +import axios from "axios"; +axios.defaults.baseURL = "http://localhost:3001/"; function App() { return ( @@ -27,6 +29,7 @@ function App() {
} /> + } /> } /> } /> + // - + // } /> { + return ( +
+ OTP dedo +
+ ) +} + +export default Otp diff --git a/frontend/src/pages/Signup.tsx b/frontend/src/pages/Signup.tsx index 2edbe554..5cacac57 100644 --- a/frontend/src/pages/Signup.tsx +++ b/frontend/src/pages/Signup.tsx @@ -5,125 +5,206 @@ import { useSetRecoilState } from "recoil"; import { tokenState } from "../store/atoms/auth"; const Signup = () => { - const [username, setUsername] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState({ - username: "", - email: "", - password: "", - message: "", - }); - const setTokenState = useSetRecoilState(tokenState); - const navigate = useNavigate(); + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState({ + username: "", + email: "", + password: "", + message: "", + }); + const [userId, setUserId] = useState(null); + const [otpStr, setOtpStr] = useState(""); // to store otp string + const [otpSent, setOtpSent] = useState(false); // to store otp sent status - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - try { - const response = await axios.post("/api/v1/user/signup", { - username, - email, - password, - }); + const setTokenState = useSetRecoilState(tokenState); + const navigate = useNavigate(); - console.log(response); - setTokenState(response.data?.token); - localStorage.setItem("token",response.data?.token || ""); - navigate('/app'); - } catch (e) { - const axiosError = e as AxiosError<{ - error: { - message: string; - }; - }>; - // console.log(e); - setError((e) => { - if (axiosError?.response?.data?.error) - return axiosError?.response?.data?.error as typeof e; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const response = await axios.post("/api/v1/user/signup", { + username, + email, + password, + }); - e.message = "An unexpected error occurred"; - return e; - }); - } - }; + console.log(response); + setTokenState(response.data?.token); + setUserId(response.data?.user?.id); // to send while verifying otp + localStorage.setItem("token", response.data?.token || ""); + // navigate('/app'); + } catch (e) { + const axiosError = e as AxiosError<{ + error: { + message: string; + }; + }>; + // console.log(e); + setError((e) => { + if (axiosError?.response?.data?.error) + return axiosError?.response?.data?.error as typeof e; - return ( -
-

- Sign Up -

-

- {error.message} -

-
-
- + e.message = "An unexpected error occurred"; + return e; + }); + } - setUsername(e.target.value)} - required - /> -
-

- {error.username} -

-
- - setEmail(e.target.value)} - required - /> -

- {error.email} -

-
-
- - setPassword(e.target.value)} - required - /> -
-

- {error.password} -

- -
-

- Already have an account?{" "} - - Sign In - -

-
- ); + // try{ + // const response = await axios.post("/api/v1/user/verify", { + // userId, + // otp: parseInt(otpStr), + // }) + // console.log(response); + // navigate('/app'); + // }catch(e){ + // const axiosError = e as AxiosError<{ + // error: { + // message: string; + // }; + // }>; + // // console.log(e); + // setError((e) => { + // if (axiosError?.response?.data?.error) + // return axiosError?.response?.data?.error as typeof e; + + // e.message = "An unexpected error occurred"; + // return e; + // }); + // } + + }; + + const handleOtpSubmit = async (e: React.MouseEvent) => { + e.preventDefault(); + try { + const response = await axios.post("/api/v1/user/verify", { + userId, + otp: parseInt(otpStr), + }) + console.log(response); + navigate('/app'); + } catch (e) { + const axiosError = e as AxiosError<{ + error: { + message: string; + }; + }>; + // console.log(e); + setError((e) => { + if (axiosError?.response?.data?.error) + return axiosError?.response?.data?.error as typeof e; + + e.message = "An unexpected error occurred"; + return e; + }); + } + } + + return ( +
+

+ Sign Up +

+

+ {error.message} +

+
+
+ + + setUsername(e.target.value)} + required + /> +
+

+ {error.username} +

+
+ + setEmail(e.target.value)} + required + /> +

+ {error.email} +

+
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + setOtpStr(e.target.value)} + required + /> + +

+ {error.email} +

+
+

+ {error.password} +

+ +
+

+ Already have an account?{" "} + + Sign In + +

+
+ ); }; export default Signup; From a97996724e4f39fd4427237af6995dabff1973c4 Mon Sep 17 00:00:00 2001 From: Joyosmit Date: Wed, 29 May 2024 18:29:26 +0530 Subject: [PATCH 2/8] Real --- frontend/src/pages/Signup.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/Signup.tsx b/frontend/src/pages/Signup.tsx index c738af91..56767b6b 100644 --- a/frontend/src/pages/Signup.tsx +++ b/frontend/src/pages/Signup.tsx @@ -77,6 +77,7 @@ const Signup = () => { }; + // abcd const handleOtpSubmit = async (e: React.MouseEvent) => { e.preventDefault(); try { From c3d199c03c80583834f0fb51255b7590e682ca00 Mon Sep 17 00:00:00 2001 From: Joyosmit Date: Wed, 29 May 2024 18:31:31 +0530 Subject: [PATCH 3/8] Made Verify OTP button functional --- frontend/src/pages/Signup.tsx | 177 +++++++++++++++++++--------------- 1 file changed, 101 insertions(+), 76 deletions(-) diff --git a/frontend/src/pages/Signup.tsx b/frontend/src/pages/Signup.tsx index 56767b6b..5cacac57 100644 --- a/frontend/src/pages/Signup.tsx +++ b/frontend/src/pages/Signup.tsx @@ -77,7 +77,6 @@ const Signup = () => { }; - // abcd const handleOtpSubmit = async (e: React.MouseEvent) => { e.preventDefault(); try { @@ -104,82 +103,108 @@ const Signup = () => { } } - return ( -
-

- Sign Up -

-

- {error.message} -

-
-
- + return ( +
+

+ Sign Up +

+

+ {error.message} +

+ +
+ - setUsername(e.target.value)} - required - /> -
-

- {error.username} -

-
- - setEmail(e.target.value)} - required - /> -

- {error.email} -

-
-
- - setPassword(e.target.value)} - required - /> -
-

- {error.password} -

- - -

- Already have an account?{" "} - - Sign In - -

-
- ); + setUsername(e.target.value)} + required + /> +
+

+ {error.username} +

+
+ + setEmail(e.target.value)} + required + /> +

+ {error.email} +

+
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + setOtpStr(e.target.value)} + required + /> + +

+ {error.email} +

+
+

+ {error.password} +

+ + +

+ Already have an account?{" "} + + Sign In + +

+
+ ); }; export default Signup; From 6b4f92e78a46522c1f6830d0db51e5395c280d07 Mon Sep 17 00:00:00 2001 From: Joyosmit Date: Wed, 29 May 2024 19:41:46 +0530 Subject: [PATCH 4/8] Fully functional OTP verification for signup --- backend/src/helpers/sendMail.ts | 73 ++++++++++++++++++++++- backend/src/routes/user/controller.ts | 82 +++++++++++++++++-------- frontend/src/pages/Signup.tsx | 86 +++++++++++---------------- frontend/src/store/atoms/auth.ts | 2 +- 4 files changed, 164 insertions(+), 79 deletions(-) diff --git a/backend/src/helpers/sendMail.ts b/backend/src/helpers/sendMail.ts index 90aa1196..7ac262e4 100644 --- a/backend/src/helpers/sendMail.ts +++ b/backend/src/helpers/sendMail.ts @@ -4,7 +4,7 @@ import nodemailer from "nodemailer"; // nodemailer = require('nodemailer'); export const sendVerificationEmail = async (email: string, otp: number) => { - console.log("Email: ",process.env.EMAIL_USER, process.env.EMAIL_PASS) + console.log("Email: ", process.env.EMAIL_USER, process.env.EMAIL_PASS) let transporter = nodemailer.createTransport({ service: 'Gmail', auth: { @@ -14,10 +14,79 @@ export const sendVerificationEmail = async (email: string, otp: number) => { }); let info = await transporter.sendMail({ - from: '"Your App" ', + from: '"Style Share" ', to: email, subject: "Email Verification", text: `Your OTP for email verification is ${otp}`, + // This html is a simple html template for the email body + html: ` + + + + + + OTP Verification + + + +
+
+

OTP Verification

+
+
+

Hello,

+

Thank you for signing up. To complete your registration, please use the following OTP (One Time Password) to verify your email address:

+
${otp}
+

This OTP is valid for 10 minutes. Please do not share it with anyone.

+
+ +
+ + + + ` }); console.log("Message sent: %s", info.messageId); diff --git a/backend/src/routes/user/controller.ts b/backend/src/routes/user/controller.ts index 39c39f12..c3c9d900 100644 --- a/backend/src/routes/user/controller.ts +++ b/backend/src/routes/user/controller.ts @@ -39,39 +39,63 @@ export const userSignupController = async (req: Request, res: Response) => { }); if (existingUser) { - return res.status(411).json({ - error: { - message: "Username or email already in use.", - }, - }); + if (!existingUser.otp) { + return res.status(411).json({ + error: { + message: "Username or email already in use.", + }, + }); + + } } - const passwordHash = await createHash(data.password); - const otp = crypto.randomInt(100000, 999999); // Generate a 6-digit OTP + const passwordHash = await createHash(data.password); - const user = await prisma.user.create({ - data: { - email: data.email, - passwordHash, - username: data.username, - // @ts-ignore - otp, - }, - select: { - id: true, - email: true, - username: true, - }, - }); + const otp = crypto.randomInt(100000, 999999); // Generate a 6-digit OTP + let user; + if(!existingUser){ + user = await prisma.user.create({ + data: { + email: data.email, + passwordHash, + username: data.username, + // @ts-ignore + otp, + }, + select: { + id: true, + email: true, + username: true, + }, + }); + } + else if(existingUser && existingUser.otp){ + user = await prisma.user.update({ + where: { + id: existingUser.id + }, + data: { + otp: otp, + passwordHash, + }, + select: { + id: true, + email: true, + username: true, + } + }) + } + + // Send OTP to user's email - await sendVerificationEmail(user.email, otp); + await sendVerificationEmail(user!.email, otp); const token = createJWT({ - id: user.id, - username: user.username, + id: user!.id, + username: user!.username, }); res.status(201).json({ @@ -186,6 +210,7 @@ export const userSigninController = async (req: Request, res: Response) => { email: data.email, }, select: { + otp: true, id: true, username: true, email: true, @@ -201,6 +226,15 @@ export const userSigninController = async (req: Request, res: Response) => { }); } + if (user.otp) { + console.log("Email really not verified") + return res.status(411).json({ + error: { + message: "Email not verified", + }, + }); + } + const matchPassword = await validatePassword( data.password, user.passwordHash diff --git a/frontend/src/pages/Signup.tsx b/frontend/src/pages/Signup.tsx index 5cacac57..8d1c5d98 100644 --- a/frontend/src/pages/Signup.tsx +++ b/frontend/src/pages/Signup.tsx @@ -16,7 +16,8 @@ const Signup = () => { }); const [userId, setUserId] = useState(null); const [otpStr, setOtpStr] = useState(""); // to store otp string - const [otpSent, setOtpSent] = useState(false); // to store otp sent status + // const [otpSent, setOtpSent] = useState(false); // to store otp sent status + const [token, setToken] = useState(""); // to store token const setTokenState = useSetRecoilState(tokenState); @@ -32,10 +33,8 @@ const Signup = () => { }); console.log(response); - setTokenState(response.data?.token); setUserId(response.data?.user?.id); // to send while verifying otp - localStorage.setItem("token", response.data?.token || ""); - // navigate('/app'); + setToken(response.data?.token); } catch (e) { const axiosError = e as AxiosError<{ error: { @@ -51,30 +50,6 @@ const Signup = () => { return e; }); } - - // try{ - // const response = await axios.post("/api/v1/user/verify", { - // userId, - // otp: parseInt(otpStr), - // }) - // console.log(response); - // navigate('/app'); - // }catch(e){ - // const axiosError = e as AxiosError<{ - // error: { - // message: string; - // }; - // }>; - // // console.log(e); - // setError((e) => { - // if (axiosError?.response?.data?.error) - // return axiosError?.response?.data?.error as typeof e; - - // e.message = "An unexpected error occurred"; - // return e; - // }); - // } - }; const handleOtpSubmit = async (e: React.MouseEvent) => { @@ -85,6 +60,8 @@ const Signup = () => { otp: parseInt(otpStr), }) console.log(response); + setTokenState(token); + localStorage.setItem("token", token || ""); navigate('/app'); } catch (e) { const axiosError = e as AxiosError<{ @@ -162,27 +139,37 @@ const Signup = () => { />
- - setOtpStr(e.target.value)} - required - /> + +
+ setOtpStr(e.target.value)} + required + /> + + +

{error.email}

@@ -190,12 +177,7 @@ const Signup = () => {

{error.password}

- +

Already have an account?{" "} diff --git a/frontend/src/store/atoms/auth.ts b/frontend/src/store/atoms/auth.ts index 84255199..0acd6b76 100644 --- a/frontend/src/store/atoms/auth.ts +++ b/frontend/src/store/atoms/auth.ts @@ -19,7 +19,7 @@ export const userState = selector({ Authorization: `Bearer ${token}`, } }) - + console.log("DATA:",data) return data?.user; } catch (error) { return null; From b2cc680100ebdf2146617a04cdb3a37e03f51c7d Mon Sep 17 00:00:00 2001 From: Joyosmit Date: Thu, 30 May 2024 10:39:06 +0530 Subject: [PATCH 5/8] Sending JWT only after OTP verification --- backend/src/helpers/sendMail.ts | 4 ++-- backend/src/middleware/auth.ts | 2 +- backend/src/routes/user/controller.ts | 16 +++++++++------- frontend/src/App.tsx | 4 ++-- frontend/src/pages/Signup.tsx | 5 +++-- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/backend/src/helpers/sendMail.ts b/backend/src/helpers/sendMail.ts index 7ac262e4..32715c6a 100644 --- a/backend/src/helpers/sendMail.ts +++ b/backend/src/helpers/sendMail.ts @@ -4,7 +4,7 @@ import nodemailer from "nodemailer"; // nodemailer = require('nodemailer'); export const sendVerificationEmail = async (email: string, otp: number) => { - console.log("Email: ", process.env.EMAIL_USER, process.env.EMAIL_PASS) +// console.log("Email: ", process.env.EMAIL_USER, process.env.EMAIL_PASS) let transporter = nodemailer.createTransport({ service: 'Gmail', auth: { @@ -89,5 +89,5 @@ export const sendVerificationEmail = async (email: string, otp: number) => { ` }); - console.log("Message sent: %s", info.messageId); +// console.log("Message sent: %s", info.messageId); }; diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index f1831147..1fcedb60 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -28,7 +28,7 @@ const authMiddleware = (req:UserAuthRequest, res:Response, next: NextFunction) = } req.userId = decodedValue.id; - console.log("IN JWT:'",req.userId) + // console.log("IN JWT:'",req.userId) next(); } diff --git a/backend/src/routes/user/controller.ts b/backend/src/routes/user/controller.ts index c3c9d900..f29c1345 100644 --- a/backend/src/routes/user/controller.ts +++ b/backend/src/routes/user/controller.ts @@ -80,6 +80,7 @@ export const userSignupController = async (req: Request, res: Response) => { data: { otp: otp, passwordHash, + username: data.username, }, select: { id: true, @@ -93,15 +94,11 @@ export const userSignupController = async (req: Request, res: Response) => { // Send OTP to user's email await sendVerificationEmail(user!.email, otp); - const token = createJWT({ - id: user!.id, - username: user!.username, - }); + res.status(201).json({ message: "User created Successfully.", user, - token: token, }); } catch (error) { console.log(error) @@ -119,6 +116,7 @@ export const userSignupController = async (req: Request, res: Response) => { const otpVerificationSchema = z.object({ userId: z.string(), otp: z.number(), + username: z.string(), }); export const verifyOtpController = async (req: Request, res: Response) => { @@ -136,7 +134,7 @@ export const verifyOtpController = async (req: Request, res: Response) => { }); } - const { userId, otp } = result.data; + const { userId, otp, username } = result.data; const user = await prisma.user.findUnique({ where: { id: userId }, @@ -167,7 +165,10 @@ export const verifyOtpController = async (req: Request, res: Response) => { error: { message: "OTP has expired." }, }); } - + const token = createJWT({ + id: user!.id, + username: username, + }); await prisma.user.update({ where: { id: userId }, data: { @@ -177,6 +178,7 @@ export const verifyOtpController = async (req: Request, res: Response) => { return res.status(200).json({ message: "Email verified successfully.", + token, }); } catch (error) { console.error("OTP verification error:", error); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 86ab8618..e861883e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -43,9 +43,9 @@ function App() { + - // + } /> { console.log(response); setUserId(response.data?.user?.id); // to send while verifying otp - setToken(response.data?.token); } catch (e) { const axiosError = e as AxiosError<{ error: { @@ -58,9 +57,11 @@ const Signup = () => { const response = await axios.post("/api/v1/user/verify", { userId, otp: parseInt(otpStr), + username: username }) console.log(response); - setTokenState(token); + setToken(response.data?.token); + setTokenState(response.data?.token); localStorage.setItem("token", token || ""); navigate('/app'); } catch (e) { From d13248f2d87a0d4efb166a28d63f0e8cdcdbba0b Mon Sep 17 00:00:00 2001 From: Joyosmit Date: Fri, 31 May 2024 10:48:48 +0530 Subject: [PATCH 6/8] Commented out the baseURL localhost in App.tsx --- frontend/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e861883e..ac1d0847 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,8 +16,8 @@ import Profile from "./pages/Profile"; import OTP from "./pages/Otp.jsx"; import React from "react"; import Loader from "./components/Loader"; -import axios from "axios"; -axios.defaults.baseURL = "http://localhost:3001/"; +// import axios from "axios"; +// axios.defaults.baseURL = "http://localhost:3001/"; function App() { return ( From 99034a05db5b5e3d2e3d79d11231e813577a20ec Mon Sep 17 00:00:00 2001 From: Rajveer <142779809+root-0101@users.noreply.github.com> Date: Fri, 31 May 2024 11:51:45 +0000 Subject: [PATCH 7/8] update home --- frontend/src/pages/Home.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 166a1d38..a0fc2a86 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,9 +1,9 @@ import { Link } from "react-router-dom"; -const Home = () => { +function Home() { return (

-
+

Welcome to Style Share

@@ -14,12 +14,13 @@ const Home = () => { Explore

+ {/*
*/}
- {/* Code */} + Code
); -}; +} export default Home; From 03ef2d708489d1b79f9a81c4575810a0638ec54d Mon Sep 17 00:00:00 2001 From: Rajveer <142779809+root-0101@users.noreply.github.com> Date: Fri, 31 May 2024 12:42:20 +0000 Subject: [PATCH 8/8] Modify card --- frontend/src/pages/Posts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Posts.tsx b/frontend/src/pages/Posts.tsx index a2ea6335..42136c1a 100644 --- a/frontend/src/pages/Posts.tsx +++ b/frontend/src/pages/Posts.tsx @@ -129,7 +129,7 @@ const Posts = () => {
{filteredPosts.map((post) => ( -
+

{post.title}

{post.description.length > 100 ? `${post.description.slice(0, 100)}...` : post.description}

By: {post.author.username}