diff --git a/backend/package-lock.json b/backend/package-lock.json index 2f0fb9d..ffe72d4 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@types/socket.io": "^3.0.1", "@tanstack/react-query": "^5.90.5", "bcryptjs": "^3.0.2", "body-parser": "^1.20.2", @@ -22,6 +23,8 @@ "morgan": "^1.10.0", "nodemailer": "^7.0.10", "nodemon": "^3.1.10", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "uuid": "^13.0.0", "zod": "^4.1.12" "winston": "^3.18.3" @@ -1489,6 +1492,12 @@ "react": "^18 || ^19" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1559,7 +1568,6 @@ "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1703,6 +1711,15 @@ "@types/node": "*" } }, + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, "node_modules/@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", @@ -1822,6 +1839,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -2239,6 +2265,103 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3610,6 +3733,154 @@ "node": ">=10" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4050,6 +4321,35 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index aa49157..77099ed 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,7 @@ "author": "", "license": "MIT", "dependencies": { + "@types/socket.io": "^3.0.1", "@tanstack/react-query": "^5.90.5", "bcryptjs": "^3.0.2", "body-parser": "^1.20.2", @@ -24,6 +25,8 @@ "morgan": "^1.10.0", "nodemailer": "^7.0.10", "nodemon": "^3.1.10", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "uuid": "^13.0.0", "zod": "^4.1.12" "winston": "^3.18.3" diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index f6c88df..4db7cda 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -34,16 +34,41 @@ export const register = async (req: Request, res: Response) => { if (existingUser) return res.status(400).json({ message: "User already exists" }); const verificationToken = crypto.randomBytes(20).toString("hex"); - const user = await User.create({ name, email, password, verificationToken }); - - const verificationLink = `${CLIENT_URL}/verify/${verificationToken}`; - await transporter.sendMail({ - to: email, - subject: "Verify your email", - html: `

Click here to verify your account.

`, + const isDevelopment = process.env.NODE_ENV !== "production"; + + const user = await User.create({ + name, + email, + password, + verificationToken, + isVerified: isDevelopment }); - res.status(201).json({ message: "User registered. Check your email for verification link." }); + if (isDevelopment) { + // In development, return verification token in response + res.status(201).json({ + message: "User registered and auto-verified (development mode).", + verificationToken: verificationToken, + verificationLink: `${CLIENT_URL}/verify/${verificationToken}` + }); + } else { + // In production, send verification email + const verificationLink = `${CLIENT_URL}/verify/${verificationToken}`; + try { + await transporter.sendMail({ + to: email, + subject: "Verify your email", + html: `

Click here to verify your account.

`, + }); + res.status(201).json({ message: "User registered. Check your email for verification link." }); + } catch (emailError) { + console.error("Email sending failed:", emailError); + res.status(201).json({ + message: "User registered. Email verification failed, but you can verify using the link below.", + verificationLink: verificationLink + }); + } + } } catch (error) { res.status(500).json({ message: "Registration failed", error }); } @@ -72,9 +97,17 @@ export const login = async (req: Request, res: Response) => { try { const { email, password } = req.body; const user = await User.findOne({ email }); + const isDevelopment = process.env.NODE_ENV !== "production"; if (!user) return res.status(400).json({ message: "Invalid credentials" }); - if (!user.isVerified) return res.status(403).json({ message: "Please verify your email" }); + if (!user.isVerified) { + if (isDevelopment) { + user.isVerified = true; + await user.save(); + } else { + return res.status(403).json({ message: "Please verify your email" }); + } + } const isMatch = await user.comparePassword(password); if (!isMatch) return res.status(400).json({ message: "Invalid credentials" }); diff --git a/backend/src/types.d.ts b/backend/src/types.d.ts index 5d111cf..a964b1c 100644 --- a/backend/src/types.d.ts +++ b/backend/src/types.d.ts @@ -1,5 +1,5 @@ declare namespace Express { export interface Request { - user?: { id: string; username: string; role: string }; + user?: { id: string; username?: string; role?: string }; } } \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 231274d..4b97817 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -56,6 +56,7 @@ "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", "recharts": "^2.15.4", + "socket.io-client": "^4.8.1", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "tailwindcss": "^4.1.14", @@ -3777,6 +3778,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -4898,7 +4905,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.0.tgz", "integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -6209,6 +6215,66 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -7850,7 +7916,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -8822,6 +8887,68 @@ "node": ">=8" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/sonner": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", @@ -9792,6 +9919,14 @@ "dev": true, "license": "MIT" }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index c5d64d9..ff72d6f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,6 +59,7 @@ "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", "recharts": "^2.15.4", + "socket.io-client": "^4.8.1", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "tailwindcss": "^4.1.14", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7712523..96b28d6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,7 @@ import { CartProvider } from "./components/CartContext"; import Cart from "./pages/Cart"; import Payment from "./pages/Payment"; import Dashboard from "./pages/Dashboard"; +import VerifyEmail from "./pages/VerifyEmail"; import { NotificationProvider } from "./components/NotificationContext"; import RequestReset from "./pages/RequestReset"; import ResetPassword from "./pages/ResetPassword"; @@ -32,6 +33,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 9d1b33e..dc2756f 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect, useRef } from "react"; import { Link, useNavigate, useLocation } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { Menu, X, Moon, Sun, ShoppingCart } from "lucide-react"; +import { Menu, X, Moon, Sun, ShoppingCart, LogOut, User } from "lucide-react"; import { useCart } from "./CartContext"; +import { isAuthenticated, getCurrentUser, logout } from "../lib/api"; const Navbar: React.FC = () => { const [isOpen, setIsOpen] = useState(false); @@ -42,11 +43,26 @@ const Navbar: React.FC = () => { // Cart integration const { state } = useCart(); + const [user, setUser] = useState(null); + const [authStatus, setAuthStatus] = useState(false); const totalCount = state.items.reduce((sum, it) => sum + (it.quantity || 0), 0); const prevCountRef = useRef(totalCount); const [bump, setBump] = useState(false); + // Check authentication status + useEffect(() => { + const checkAuth = async () => { + const authenticated = isAuthenticated(); + setAuthStatus(authenticated); + if (authenticated) { + const userData = await getCurrentUser(); + setUser(userData); + } + }; + checkAuth(); + }, [location.pathname]); // Re-check when route changes + useEffect(() => { const prev = prevCountRef.current; if (totalCount > prev) { @@ -120,19 +136,44 @@ const Navbar: React.FC = () => { - - + {authStatus ? ( + <> + + + + ) : ( + <> + + + + )} -
- - -
+ {authStatus ? ( +
+ + +
+ ) : ( +
+ + +
+ )} )} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 3912111..514cf23 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -2,6 +2,7 @@ export interface SignUpData { email: string; password: string; confirmPassword: string; + name?: string; } export interface SignInData { @@ -18,50 +19,110 @@ export interface AuthResponse { }; } -// Mock registration function +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:5000"; + +// Helper function to get current user +export const getCurrentUser = async () => { + const token = localStorage.getItem("accessToken"); + if (!token) return null; + + try { + const response = await fetch(`${API_BASE_URL}/api/users/me`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response.ok) { + return await response.json(); + } + return null; + } catch (error) { + return null; + } +}; + +export const isAuthenticated = (): boolean => { + return !!localStorage.getItem("accessToken"); +}; + +export const logout = () => { + localStorage.removeItem("accessToken"); + window.location.href = "/"; +}; + +// Real registration function export const mockRegister = async (data: SignUpData): Promise => { - return new Promise((resolve, reject) => { - setTimeout(() => { - // Simulate validation - if (data.email === "test@example.com") { - reject({ - success: false, - message: "Email already exists", - }); - } else { - resolve({ - success: true, - message: "Registration successful!", - user: { - email: data.email, - id: Math.random().toString(36).substring(7), - }, - }); - } - }, 1500); - }); + try { + const response = await fetch(`${API_BASE_URL}/api/users/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: data.name || data.email.split("@")[0], + email: data.email, + password: data.password, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: "Registration failed" })); + throw new Error(errorData.message || "Registration failed"); + } + + const result = await response.json(); + + // In development, log verification link if provided + if (result.verificationLink && import.meta.env.DEV) { + console.log("Verification link (development):", result.verificationLink); + } + + return { + success: true, + message: result.message || "Registration successful! Please check your email to verify your account.", + verificationLink: result.verificationLink, + }; + } catch (error: any) { + throw error; + } }; -// Mock login function +// Real login function export const mockLogin = async (data: SignInData): Promise => { - return new Promise((resolve, reject) => { - setTimeout(() => { - // Simulate authentication - if (data.email === "demo@peercall.com" && data.password === "password123") { - resolve({ - success: true, - message: "Login successful!", - user: { - email: data.email, - id: "demo-user-123", - }, - }); - } else { - reject({ - success: false, - message: "Invalid email or password", - }); - } - }, 1500); - }); + try { + const response = await fetch(`${API_BASE_URL}/api/users/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: data.email, + password: data.password, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: "Login failed" })); + throw new Error(errorData.message || "Login failed"); + } + + const result = await response.json(); + + // Store token + if (result.token) { + localStorage.setItem("accessToken", result.token); + } + + return { + success: true, + message: "Login successful!", + user: { + email: result.user?.email || data.email, + id: result.user?.id || "", + }, + }; + } catch (error: any) { + throw error; + } }; \ No newline at end of file diff --git a/frontend/src/pages/Signin.tsx b/frontend/src/pages/Signin.tsx index a108767..fc58835 100644 --- a/frontend/src/pages/Signin.tsx +++ b/frontend/src/pages/Signin.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { Button } from "../components/ui/button"; import { Card, @@ -17,6 +17,7 @@ import { Loader2 } from "lucide-react"; const SignIn = () => { const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); const { register, handleSubmit, @@ -27,13 +28,15 @@ const SignIn = () => { const onSubmit = async (data: SignInData) => { setIsLoading(true); try { - const response = await mockLogin(data); + await mockLogin(data); toast({ title: "Welcome back!", - description: response.message, + description: "Login successful!", }); reset(); - // Navigate to dashboard or home page here + navigate("/"); + // Reload to update navbar + window.location.reload(); } catch (error: any) { toast({ title: "Error", diff --git a/frontend/src/pages/Signup.tsx b/frontend/src/pages/Signup.tsx index 11a0803..839728d 100644 --- a/frontend/src/pages/Signup.tsx +++ b/frontend/src/pages/Signup.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { Button } from "../components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/ui/card"; import { InputField } from "../components/InputField"; @@ -9,6 +9,7 @@ import { mockRegister, SignUpData } from "../lib/api"; import { Loader2 } from "lucide-react"; interface SignUpFormData { + name?: string; email: string; password: string; confirmPassword: string; @@ -16,6 +17,7 @@ interface SignUpFormData { const SignUp = () => { const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); const { register, handleSubmit, @@ -29,12 +31,26 @@ const SignUp = () => { const onSubmit = async (data: SignUpFormData) => { setIsLoading(true); try { - const response = await mockRegister(data); + const result = await mockRegister({ + name: data.name, + email: data.email, + password: data.password, + confirmPassword: data.confirmPassword, + }); + + let description = result.message || "Registration successful!"; + + // In development, show verification link if provided + if (result.verificationLink && import.meta.env.DEV) { + description += ` Verification link: ${result.verificationLink}`; + } + toast({ title: "Success!", - description: response.message, + description: description, }); reset(); + navigate("/signin"); } catch (error: any) { toast({ title: "Error", @@ -64,6 +80,17 @@ const SignUp = () => {
+ + { + const { token } = useParams<{ token: string }>(); + const navigate = useNavigate(); + const [status, setStatus] = useState<"loading" | "success" | "error">("loading"); + const [message, setMessage] = useState(""); + + useEffect(() => { + const verifyEmail = async () => { + if (!token) { + setStatus("error"); + setMessage("No verification token provided"); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/users/verify/${token}`); + + if (response.ok) { + const data = await response.json(); + setStatus("success"); + setMessage(data.message || "Email verified successfully!"); + setTimeout(() => { + navigate("/signin"); + }, 2000); + } else { + const errorData = await response.json().catch(() => ({ message: "Verification failed" })); + setStatus("error"); + setMessage(errorData.message || "Invalid or expired verification token"); + } + } catch (error) { + setStatus("error"); + setMessage("Failed to verify email. Please try again."); + } + }; + + verifyEmail(); + }, [token, navigate]); + + return ( +
+
+
+
+
+
+ + + + + Email Verification + + + + {status === "loading" && ( + <> + +

Verifying your email...

+ + )} + + {status === "success" && ( + <> + +

{message}

+

Redirecting to sign in...

+ + )} + + {status === "error" && ( + <> + +

{message}

+
+ + +
+ + )} +
+
+
+ ); +}; + +export default VerifyEmail; +