diff --git a/README.md b/README.md index ed75e33..581da80 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,10 @@ NODE_ENV=development # ── Auth ────────────────────────────────────────────── JWT_SECRETKEY=your_super_secret_key GOOGLE_CLIENT_ID=your_google_client_id +GITHUB_CLIENT_ID=your_github_client_id +GITHUB_CLIENT_SECRET=your_github_secret +CLIENT_URL=http://localhost:5001 +BACKEND_URL_URL=http://localhost:5173 # ── Cloudinary ──────────────────────────────────────── CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name @@ -189,6 +193,8 @@ Create a `.env` file inside the `frontend/` directory: ```env VITE_VAPID_PUBLIC_KEY=your_vapid_public_key VITE_GOOGLE_CLIENT_ID=your_google_client_id +VITE_GITHUB_CLIENT_ID=your_github_client_id +VITE_API_URL= http://localhost:5001/api ``` > ⚠️ **Never** commit `.env` to version control. It is already listed in `.gitignore`. diff --git a/backend/.env.example b/backend/.env.example index 7b62781..6580a42 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -9,3 +9,7 @@ GOOGLE_CLIENT_ID=your_google_client_id VAPID_PUBLIC_KEY=your_vapid_public_key VAPID_PRIVATE_KEY=your_vapid_private_key VAPID_SUBJECT=mailto:your@email.com +GITHUB_CLIENT_ID=your_github_client_id +GITHUB_CLIENT_SECRET=your_github_secret +CLIENT_URL=http://localhost:5001 +BACKEND_URL_URL=http://localhost:5173 diff --git a/backend/package-lock.json b/backend/package-lock.json index 1ff3988..8039738 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "bcryptjs": "^3.0.3", "cloudinary": "^2.10.0", + "compression": "^1.8.1", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.4.2", @@ -1542,6 +1543,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/backend/package.json b/backend/package.json index 4996ced..d8ac45f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "dependencies": { "bcryptjs": "^3.0.3", "cloudinary": "^2.10.0", + "compression": "^1.8.1", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.4.2", diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index 9abeb65..7a1c6cf 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -1,10 +1,10 @@ -import bcrypt from "bcryptjs"; -import { OAuth2Client } from "google-auth-library"; -import User from "../models/user.model.js"; -import { generateTokenAndSetCookie, catchAsync } from "../lib/utils.js"; -import cloudinary from "../lib/cloudinary.js"; +import bcrypt from 'bcryptjs' +import { OAuth2Client } from 'google-auth-library' +import User from '../models/user.model.js' +import { generateTokenAndSetCookie, catchAsync } from '../lib/utils.js' +import cloudinary from '../lib/cloudinary.js' -let googleClient; +let googleClient /** * Lazy-initializes and returns a singleton instance of the Google OAuth2 client. @@ -12,15 +12,15 @@ let googleClient; * * @returns {OAuth2Client} The initialized Google Client instance. */ const getGoogleClient = () => { - if (!googleClient) { - const clientId = (process.env.GOOGLE_CLIENT_ID || "").trim(); - if (!clientId) { - console.warn("WARNING: GOOGLE_CLIENT_ID environment variable is not set!"); - } - googleClient = new OAuth2Client(clientId); + if (!googleClient) { + const clientId = (process.env.GOOGLE_CLIENT_ID || '').trim() + if (!clientId) { + console.warn('WARNING: GOOGLE_CLIENT_ID environment variable is not set!') } - return googleClient; -}; + googleClient = new OAuth2Client(clientId) + } + return googleClient +} /** * Handles local user registration (Signup). @@ -29,33 +29,35 @@ const getGoogleClient = () => { * @param {Object} res - Express response object. */ export const signup = catchAsync(async (req, res) => { - const { name, email, password } = req.body; - const existing = await User.findOne({ email }); - if (existing) { - return res.status(409).json({ message: "An account with this email already exists" }); - } - - // Secure password hashing with 10 salt rounds - const hashedPassword = await bcrypt.hash(password, 10); - - // Sanitize string attributes before insertion - const user = await User.create({ - name: name.trim(), - email: email.toLowerCase().trim(), - password: hashedPassword - }); - - // Set secure HTTP-Only authentication cookie - generateTokenAndSetCookie(user._id, res); - - res.status(201).json({ - _id: user._id, - name: user.name, - email: user.email, - profilePicture: user.profilePicture, - statusMood: user.statusMood || null, - }); -}); + const { name, email, password } = req.body + const existing = await User.findOne({ email }) + if (existing) { + return res + .status(409) + .json({ message: 'An account with this email already exists' }) + } + + // Secure password hashing with 10 salt rounds + const hashedPassword = await bcrypt.hash(password, 10) + + // Sanitize string attributes before insertion + const user = await User.create({ + name: name.trim(), + email: email.toLowerCase().trim(), + password: hashedPassword, + }) + + // Set secure HTTP-Only authentication cookie + generateTokenAndSetCookie(user._id, res) + + res.status(201).json({ + _id: user._id, + name: user.name, + email: user.email, + profilePicture: user.profilePicture, + statusMood: user.statusMood || null, + }) +}) /** * Handles local user authentication (Login). @@ -64,28 +66,28 @@ export const signup = catchAsync(async (req, res) => { * @param {Object} res - Express response object. */ export const login = catchAsync(async (req, res) => { - const { email, password } = req.body; - const user = await User.findOne({ email: email.toLowerCase().trim() }); - if (!user || !user.password) { - return res.status(401).json({ message: "Invalid email or password" }); - } - - // Timing-safe verification of the incoming password against hash - const valid = await bcrypt.compare(password, user.password); - if (!valid) { - return res.status(401).json({ message: "Invalid email or password" }); - } - - generateTokenAndSetCookie(user._id, res); - - res.status(200).json({ - _id: user._id, - name: user.name, - email: user.email, - profilePicture: user.profilePicture, - statusMood: user.statusMood || null, - }); -}); + const { email, password } = req.body + const user = await User.findOne({ email: email.toLowerCase().trim() }) + if (!user || !user.password) { + return res.status(401).json({ message: 'Invalid email or password' }) + } + + // Timing-safe verification of the incoming password against hash + const valid = await bcrypt.compare(password, user.password) + if (!valid) { + return res.status(401).json({ message: 'Invalid email or password' }) + } + + generateTokenAndSetCookie(user._id, res) + + res.status(200).json({ + _id: user._id, + name: user.name, + email: user.email, + profilePicture: user.profilePicture, + statusMood: user.statusMood || null, + }) +}) /** * Handles single-tap Google OAuth2 federated authentication transactions. @@ -94,51 +96,68 @@ export const login = catchAsync(async (req, res) => { * @param {Object} res - Express response object. */ export const googleAuth = catchAsync(async (req, res) => { - const { credential } = req.body; - if (!credential) { - return res.status(400).json({ message: "Google credential is required" }); + const { credential } = req.body + if (!credential) { + return res.status(400).json({ message: 'Google credential is required' }) + } + + const client = getGoogleClient() + const cleanClientId = (process.env.GOOGLE_CLIENT_ID || '').trim() + + // Verify token authenticity against Google's authorization servers + const ticket = await client.verifyIdToken({ + idToken: credential, + audience: cleanClientId, + }) + + const { sub: googleId, email, name, picture } = ticket.getPayload() + let user = await User.findOne({ $or: [{ googleId }, { email }] }) + + if (!user) { + // Provision a new password-less account for federated identities + user = await User.create({ + googleId, + name, + email, + password: null, + profilePicture: picture || '', + }) + } else if (!user.googleId) { + // Link existing local account to Google identity context + user.googleId = googleId + if (!user.profilePicture && picture) { + user.profilePicture = picture } + await user.save() + } + + generateTokenAndSetCookie(user._id, res) - const client = getGoogleClient(); - const cleanClientId = (process.env.GOOGLE_CLIENT_ID || "").trim(); - - // Verify token authenticity against Google's authorization servers - const ticket = await client.verifyIdToken({ - idToken: credential, - audience: cleanClientId, - }); - - const { sub: googleId, email, name, picture } = ticket.getPayload(); - let user = await User.findOne({ $or: [{ googleId }, { email }] }); - - if (!user) { - // Provision a new password-less account for federated identities - user = await User.create({ - googleId, - name, - email, - password: null, - profilePicture: picture || "", - }); - } else if (!user.googleId) { - // Link existing local account to Google identity context - user.googleId = googleId; - if (!user.profilePicture && picture) { - user.profilePicture = picture; - } - await user.save(); - } - - generateTokenAndSetCookie(user._id, res); - - res.status(200).json({ - _id: user._id, - name: user.name, - email: user.email, - profilePicture: user.profilePicture, - statusMood: user.statusMood || null, - }); -}); + res.status(200).json({ + _id: user._id, + name: user.name, + email: user.email, + profilePicture: user.profilePicture, + statusMood: user.statusMood || null, + }) +}) + +export const githubRedirect = (req, res) => { + const clientId = process.env.GITHUB_CLIENT_ID + + const redirectUri = `${process.env.BACKEND_URL}/api/auth/github/callback` + + console.log('client_id:',clientId); + console.log('Backendurl:',process.env.BACKEND_URL) + console.log('redirecturl:',redirectUri); + + return res.redirect( + `https://github.com/login/oauth/authorize` + + `?client_id=${clientId}` + + `&redirect_uri=${encodeURIComponent(redirectUri)}` + + `&scope=read:user user:email`, + ) +} /** * Destroys the active user authentication session. @@ -147,13 +166,15 @@ export const googleAuth = catchAsync(async (req, res) => { * @param {Object} res - Express response object. */ export function logout(req, res) { - res.cookie("jwt", "", { - maxAge: 0, - httpOnly: true, - sameSite: "strict", - secure: process.env.NODE_ENV !== "development" || process.env.FORCE_SECURE_COOKIES === "true" - }); - res.status(200).json({ message: "Logged out" }); + res.cookie('jwt', '', { + maxAge: 0, + httpOnly: true, + sameSite: 'strict', + secure: + process.env.NODE_ENV !== 'development' || + process.env.FORCE_SECURE_COOKIES === 'true', + }) + res.status(200).json({ message: 'Logged out' }) } /** @@ -163,24 +184,30 @@ export function logout(req, res) { * @param {Object} res - Express response object. */ export const updateProfile = catchAsync(async (req, res) => { - const { name } = req.body; - if (typeof name !== "string" || name.trim().length < 2 || name.trim().length > 50) { - return res.status(400).json({ message: "Name must be between 2 and 50 characters" }); - } - const updates = { name: name.trim() }; + const { name } = req.body + if ( + typeof name !== 'string' || + name.trim().length < 2 || + name.trim().length > 50 + ) { + return res + .status(400) + .json({ message: 'Name must be between 2 and 50 characters' }) + } + const updates = { name: name.trim() } - // Enforce strong projection filters during model update cycles - const user = await User.findByIdAndUpdate( - req.userId, - { $set: updates }, - { new: true } - ).select("-password -__v"); + // Enforce strong projection filters during model update cycles + const user = await User.findByIdAndUpdate( + req.userId, + { $set: updates }, + { new: true }, + ).select('-password -__v') - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - res.status(200).json(user); -}); + if (!user) { + return res.status(404).json({ message: 'User not found' }) + } + res.status(200).json(user) +}) /** * Intercepts incoming profile photo attachments and synchronizes storage maps with Cloudinary. @@ -189,35 +216,37 @@ export const updateProfile = catchAsync(async (req, res) => { * @param {Object} res - Express response object. */ export const updateProfilePicture = catchAsync(async (req, res) => { - // GSSoC Issue #47 Fix - if (!req.userId) { - return res.status(401).json({ message: "Unauthorized: Invalid user session ID" }); - } - const { profilePicture } = req.body; - if (!profilePicture) { - return res.status(400).json({ message: "No image provided" }); - } + // GSSoC Issue #47 Fix + if (!req.userId) { + return res + .status(401) + .json({ message: 'Unauthorized: Invalid user session ID' }) + } + const { profilePicture } = req.body + if (!profilePicture) { + return res.status(400).json({ message: 'No image provided' }) + } - // Validate base64 payload size (roughly: length * 3/4) is under 5MB (5,242,880 bytes) - const approximateSizeBytes = (profilePicture.length * 3) / 4; - if (approximateSizeBytes > 5 * 1024 * 1024) { - return res.status(400).json({ message: "Image size exceeds the 5MB limit" }); - } + // Validate base64 payload size (roughly: length * 3/4) is under 5MB (5,242,880 bytes) + const approximateSizeBytes = (profilePicture.length * 3) / 4 + if (approximateSizeBytes > 5 * 1024 * 1024) { + return res.status(400).json({ message: 'Image size exceeds the 5MB limit' }) + } + + const upload = await cloudinary.uploader.upload(profilePicture) - const upload = await cloudinary.uploader.upload(profilePicture); - - // Strip sensitive credentials from returning memory layers - const user = await User.findByIdAndUpdate( - req.userId, - { profilePicture: upload.secure_url }, - { new: true } - ).select("-password -__v"); - - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - res.status(200).json(user); -}); + // Strip sensitive credentials from returning memory layers + const user = await User.findByIdAndUpdate( + req.userId, + { profilePicture: upload.secure_url }, + { new: true }, + ).select('-password -__v') + + if (!user) { + return res.status(404).json({ message: 'User not found' }) + } + res.status(200).json(user) +}) /** * Re-validates active tracking sessions during client hydration or reload cycles. @@ -225,12 +254,12 @@ export const updateProfilePicture = catchAsync(async (req, res) => { * @param {Object} res - Express response object. */ export const checkAuth = catchAsync(async (req, res) => { - const user = await User.findById(req.userId).select("-password -__v"); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - res.status(200).json(user); -}); + const user = await User.findById(req.userId).select('-password -__v') + if (!user) { + return res.status(404).json({ message: 'User not found' }) + } + res.status(200).json(user) +}) /** * Registers Web Push subscription payloads to enable native system push features. @@ -239,12 +268,12 @@ export const checkAuth = catchAsync(async (req, res) => { * @param {Object} res - Express response object. */ export const subscribeToPush = catchAsync(async (req, res) => { - const { subscription } = req.body; - - // HARDENING FIX: Explicitly strip credentials and metadata parameters from returning mutations - await User.findByIdAndUpdate(req.userId, { - pushSubscription: subscription - }).select("-password -__v"); - - res.status(200).json({ message: "Push subscription saved" }); -}); \ No newline at end of file + const { subscription } = req.body + + // HARDENING FIX: Explicitly strip credentials and metadata parameters from returning mutations + await User.findByIdAndUpdate(req.userId, { + pushSubscription: subscription, + }).select('-password -__v') + + res.status(200).json({ message: 'Push subscription saved' }) +}) diff --git a/backend/src/controllers/githubCallback.controller.js b/backend/src/controllers/githubCallback.controller.js new file mode 100644 index 0000000..aaf45b7 --- /dev/null +++ b/backend/src/controllers/githubCallback.controller.js @@ -0,0 +1,73 @@ +import { catchAsync, generateTokenAndSetCookie } from '../lib/utils.js' +import User from '../models/user.model.js' +import axios from "axios"; + +export const githubCallback = catchAsync(async (req, res) => { + const code = req.query.code + if (!code) { + return res.status(400).json({ message: 'Github code is required' }) + } + const response = await axios.post( + 'https://github.com/login/oauth/access_token', + { + client_id: (process.env.GITHUB_CLIENT_ID || '').trim(), + client_secret: (process.env.GITHUB_CLIENT_SECRET || '').trim(), + code, + }, + { + headers: { + Accept: 'application/json', + }, + }, + ) + const { access_token } = response.data + if (!access_token) { + return res + .status(401) + .json({ message: 'Failed to authenticate with GitHub' }) + } + const userResponse = await axios.get('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${access_token}`, + }, + }) + const emailResponse = await axios.get('https://api.github.com/user/emails', { + headers: { + Authorization: `Bearer ${access_token}`, + }, + }) + const verifiedEmail = emailResponse.data.find( + (email) => email.primary && email.verified, + ) + if (!verifiedEmail) { + return res.status(400).json({ message: 'no verified email' }) + } + const email = (verifiedEmail.email || '').toLowerCase().trim() + const { login, id, avatar_url } = userResponse.data + const githubId = String(id) + const name = login + const profilePicture = avatar_url + let user = await User.findOne({ + $or: [{ githubId }, { email }], + }) + if (!user) { + // Provision a new password-less account for federated identities + user = await User.create({ + githubId, + name: name || login, + email, + password: null, + profilePicture: avatar_url || '', + }) + } else if (!user.githubId) { + // Link existing local account to Google identity context + user.githubId = githubId + if (!user.profilePicture && avatar_url) { + user.profilePicture = avatar_url + } + await user.save() + } + generateTokenAndSetCookie(user._id, res) + + res.redirect(process.env.CLIENT_URL) +}) diff --git a/backend/src/index.js b/backend/src/index.js index fa41d96..91159fe 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -89,7 +89,7 @@ app.use((req, res, next) => { // Rate Limiting Policy Declarations const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, - max: 20, + max: 100, message: { message: "Too many attempts, please try again later" }, standardHeaders: true, legacyHeaders: false, @@ -120,7 +120,7 @@ app.use((err, req, res, next) => { console.error("Centralized Route Error Intercepted:", err.stack || err); const statusCode = err.status || err.statusCode || 500; - + // Evaluate execution scope to mask internal error details from HTTP clients in production if (process.env.NODE_ENV === "production") { return res.status(statusCode).json({ @@ -167,4 +167,4 @@ process.on("uncaughtException", (err) => { } stopScheduler(); process.exit(1); -}); \ No newline at end of file +}); diff --git a/backend/src/lib/socket.js b/backend/src/lib/socket.js index 92f5f85..cbc4398 100644 --- a/backend/src/lib/socket.js +++ b/backend/src/lib/socket.js @@ -1,192 +1,216 @@ -import { Server } from "socket.io"; -import http from "http"; -import express from "express"; -import jwt from "jsonwebtoken"; -import Message from "../models/message.model.js"; -import User from "../models/user.model.js"; +import { Server } from 'socket.io' +import http from 'http' +import express from 'express' +import jwt from 'jsonwebtoken' +import Message from '../models/message.model.js' +import User from '../models/user.model.js' -const app = express(); -const server = http.createServer(app); +const app = express() +const server = http.createServer(app) const io = new Server(server, { - cors: { - // Allow any origin so the app works on phones/tablets on local network - // and in all dev environments. Lock this down to your domain in production. - origin: process.env.ALLOWED_ORIGINS - ? process.env.ALLOWED_ORIGINS.split(",") - : true, - credentials: true, - }, -}); + cors: { + // Allow any origin so the app works on phones/tablets on local network + // and in all dev environments. Lock this down to your domain in production. + origin: process.env.ALLOWED_ORIGINS + ? process.env.ALLOWED_ORIGINS.split(',') + : true, + credentials: true, + }, +}) // Authenticate WebSocket connections via JWT from handshake cookie io.use((socket, next) => { - const cookieHeader = socket.handshake.headers.cookie; - if (!cookieHeader) return next(new Error("Authentication required")); + const cookieHeader = socket.handshake.headers.cookie + if (!cookieHeader) return next(new Error('Authentication required')) - const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); - const token = match ? match[1] : null; - if (!token) return next(new Error("Authentication required")); + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/) + const token = match ? match[1] : null + if (!token) return next(new Error('Authentication required')) - try { - const decoded = jwt.verify(token, process.env.JWT_SECRETKEY); - socket.userId = decoded.userId; - next(); - } catch { - next(new Error("Invalid or expired token")); - } -}); + try { + const decoded = jwt.verify(token, process.env.JWT_SECRETKEY) + socket.userId = decoded.userId + next() + } catch { + next(new Error('Invalid or expired token')) + } +}) -const userSocketMap = {}; +const userSocketMap = {} -export const getReceiverSocketIds = (userId) => - userSocketMap[userId] ? [...userSocketMap[userId]] : []; +export const getReceiverSocketIds = (userId) => + userSocketMap[userId] ? [...userSocketMap[userId]] : [] export const broadcastStatusMoodUpdate = ({ userId, statusMood }) => { - io.emit("statusMoodUpdated", { userId, statusMood }); -}; + io.emit('statusMoodUpdated', { userId, statusMood }) +} /** * 🛠️ Security Helper: Validates if two users can communicate. - * Adjust the database query inside based on whether you track relationships via + * Adjust the database query inside based on whether you track relationships via * a Friends/Block schema or directly within the User model. */ const canCommunicate = async (senderId, receiverId) => { - if (!senderId || !receiverId || senderId === receiverId) return false; - - try { - const receiver = await User.findById(receiverId); - if (!receiver) return false; + if (!senderId || !receiverId || senderId === receiverId) return false - // Example: If your User schema has a 'blockedUsers' array - if (receiver.blockedUsers && receiver.blockedUsers.includes(senderId)) { - return false; - } + try { + const receiver = await User.findById(receiverId) + if (!receiver) return false - return true; - } catch (error) { - console.error("Authorization check failed:", error); - return false; + // Example: If your User schema has a 'blockedUsers' array + if (receiver.blockedUsers && receiver.blockedUsers.includes(senderId)) { + return false } -}; -io.on("connection", (socket) => { -const getActiveContacts = async (userId) => { + return true + } catch (error) { + console.error('Authorization check failed:', error) + return false + } +} + +io.on('connection', (socket) => { + const getActiveContacts = async (userId) => { try { - const [senders, receivers] = await Promise.all([ - Message.distinct("senderId", { receiverId: userId }), - Message.distinct("receiverId", { senderId: userId }) - ]); - const merged = [...senders, ...receivers].map(id => id.toString()); - return [...new Set(merged)]; + const [senders, receivers] = await Promise.all([ + Message.distinct('senderId', { receiverId: userId }), + Message.distinct('receiverId', { senderId: userId }), + ]) + const merged = [...senders, ...receivers].map((id) => id.toString()) + return [...new Set(merged)] } catch (err) { - console.error("Error fetching active contacts:", err); - return []; + console.error('Error fetching active contacts:', err) + return [] } -} + } -io.on("connection", (socket) => { - const userId = socket.userId; + io.on('connection', (socket) => { + const userId = socket.userId // Early guard return: Prevent state pollution / memory leaks from unauthenticated sockets if (!userId) { - console.warn(`[Socket.io] Connection rejected: Missing userId for socket ${socket.id}`); - return socket.disconnect(true); + console.warn( + `[Socket.io] Connection rejected: Missing userId for socket ${socket.id}`, + ) + return socket.disconnect(true) } // Now safe to assume userId exists - if (!userSocketMap[userId]) userSocketMap[userId] = []; - userSocketMap[userId].push(socket.id); - + if (!userSocketMap[userId]) userSocketMap[userId] = [] + userSocketMap[userId].push(socket.id) + // Update lastSeen with a throttle mechanism to protect against connection churn - throttledUpdateLastSeen(userId); + // throttledUpdateLastSeen(userId) // Mark offline pending messages as delivered Message.updateMany( - { receiverId: userId, status: "sent" }, - { $set: { status: "delivered" } } - ).then(async (res) => { + { receiverId: userId, status: 'sent' }, + { $set: { status: 'delivered' } }, + ) + .then(async (res) => { if (res.modifiedCount > 0) { - const senders = await Message.distinct("senderId", { receiverId: userId, status: "delivered" }); - senders.forEach(senderIdStr => { - const senderSockets = getReceiverSocketIds(senderIdStr.toString()); - senderSockets.forEach(s => io.to(s).emit("messagesDelivered", { receiverId: userId })); - }); + const senders = await Message.distinct('senderId', { + receiverId: userId, + status: 'delivered', + }) + senders.forEach((senderIdStr) => { + const senderSockets = getReceiverSocketIds(senderIdStr.toString()) + senderSockets.forEach((s) => + io.to(s).emit('messagesDelivered', { receiverId: userId }), + ) + }) } - }).catch(console.error); + }) + .catch(console.error) - io.emit("getOnlineUsers", Object.keys(userSocketMap)); + io.emit('getOnlineUsers', Object.keys(userSocketMap)) // Typing indicators (🔒 Protected) - socket.on("typing", async ({ receiverId }) => { - if (!(await canCommunicate(userId, receiverId))) return; + socket.on('typing', async ({ receiverId }) => { + if (!(await canCommunicate(userId, receiverId))) return - const receiverSockets = getReceiverSocketIds(receiverId); - receiverSockets.forEach(s => io.to(s).emit("userTyping", { senderId: userId })); - }); + const receiverSockets = getReceiverSocketIds(receiverId) + receiverSockets.forEach((s) => + io.to(s).emit('userTyping', { senderId: userId }), + ) + }) - socket.on("stopTyping", async ({ receiverId }) => { - if (!(await canCommunicate(userId, receiverId))) return; + socket.on('stopTyping', async ({ receiverId }) => { + if (!(await canCommunicate(userId, receiverId))) return - const receiverSockets = getReceiverSocketIds(receiverId); - receiverSockets.forEach(s => io.to(s).emit("userStoppedTyping", { senderId: userId })); - }); + const receiverSockets = getReceiverSocketIds(receiverId) + receiverSockets.forEach((s) => + io.to(s).emit('userStoppedTyping', { senderId: userId }), + ) + }) // WebRTC Signaling (🔒 Protected) - socket.on("callUser", async ({ userToCall, signalData, type }) => { - try { - if (!(await canCommunicate(userId, userToCall))) return; - - const sender = await User.findById(userId).select("name"); - if (!sender) return; - const receiverSockets = getReceiverSocketIds(userToCall); - receiverSockets.forEach(s => io.to(s).emit("incomingCall", { signal: signalData, from: userId, name: sender.name, type })); - } catch (err) { - console.error("Error in callUser:", err); - } - }); - - socket.on("answerCall", async ({ to, signal }) => { - if (!(await canCommunicate(userId, to))) return; - - const receiverSockets = getReceiverSocketIds(to); - receiverSockets.forEach(s => io.to(s).emit("callAccepted", signal)); - }); - - socket.on("iceCandidate", async ({ to, candidate }) => { - if (!(await canCommunicate(userId, to))) return; - - const receiverSockets = getReceiverSocketIds(to); - receiverSockets.forEach(s => io.to(s).emit("iceCandidate", candidate)); - }); - - socket.on("endCall", async ({ to }) => { - if (!(await canCommunicate(userId, to))) return; - - const receiverSockets = getReceiverSocketIds(to); - receiverSockets.forEach(s => io.to(s).emit("callEnded")); - }); - - socket.on("rejectCall", async ({ to }) => { - if (!(await canCommunicate(userId, to))) return; - - const receiverSockets = getReceiverSocketIds(to); - receiverSockets.forEach(s => io.to(s).emit("callRejected")); - }); - - socket.on("disconnect", async () => { - userSocketMap[userId] = userSocketMap[userId]?.filter(id => id !== socket.id) || []; - - if (userSocketMap[userId].length === 0) { - delete userSocketMap[userId]; - // Update lastSeen when they completely disconnect (if not updated recently) - await throttledUpdateLastSeen(userId); - // Clean up our local cache memory since the user is fully offline - lastDbUpdateCache.delete(userId); - } - - io.emit("getOnlineUsers", Object.keys(userSocketMap)); - }); -}); - -export { io, app, server }; + socket.on('callUser', async ({ userToCall, signalData, type }) => { + try { + if (!(await canCommunicate(userId, userToCall))) return + + const sender = await User.findById(userId).select('name') + if (!sender) return + const receiverSockets = getReceiverSocketIds(userToCall) + receiverSockets.forEach((s) => + io + .to(s) + .emit('incomingCall', { + signal: signalData, + from: userId, + name: sender.name, + type, + }), + ) + } catch (err) { + console.error('Error in callUser:', err) + } + }) + + socket.on('answerCall', async ({ to, signal }) => { + if (!(await canCommunicate(userId, to))) return + + const receiverSockets = getReceiverSocketIds(to) + receiverSockets.forEach((s) => io.to(s).emit('callAccepted', signal)) + }) + + socket.on('iceCandidate', async ({ to, candidate }) => { + if (!(await canCommunicate(userId, to))) return + + const receiverSockets = getReceiverSocketIds(to) + receiverSockets.forEach((s) => io.to(s).emit('iceCandidate', candidate)) + }) + + socket.on('endCall', async ({ to }) => { + if (!(await canCommunicate(userId, to))) return + + const receiverSockets = getReceiverSocketIds(to) + receiverSockets.forEach((s) => io.to(s).emit('callEnded')) + }) + + socket.on('rejectCall', async ({ to }) => { + if (!(await canCommunicate(userId, to))) return + + const receiverSockets = getReceiverSocketIds(to) + receiverSockets.forEach((s) => io.to(s).emit('callRejected')) + }) + + socket.on('disconnect', async () => { + userSocketMap[userId] = + userSocketMap[userId]?.filter((id) => id !== socket.id) || [] + + if (userSocketMap[userId].length === 0) { + delete userSocketMap[userId] + // Update lastSeen when they completely disconnect (if not updated recently) + // await throttledUpdateLastSeen(userId) + // Clean up our local cache memory since the user is fully offline + lastDbUpdateCache.delete(userId) + } + + io.emit('getOnlineUsers', Object.keys(userSocketMap)) + }) + }) +}) + +export {io,app,server}; diff --git a/backend/src/models/user.model.js b/backend/src/models/user.model.js index 5f6aead..2c57095 100644 --- a/backend/src/models/user.model.js +++ b/backend/src/models/user.model.js @@ -25,6 +25,11 @@ const userSchema = new mongoose.Schema({ default: null, sparse: true, }, + githubId: { + type: String, + unique: true, + sparse: true + }, profilePicture: { type: String, default: "", @@ -57,4 +62,4 @@ const userSchema = new mongoose.Schema({ userSchema.index({ name: "text" }); const User = mongoose.model("User", userSchema); -export default User; \ No newline at end of file +export default User; diff --git a/backend/src/routes/auth.route.js b/backend/src/routes/auth.route.js index 3db8be9..8b9bc30 100644 --- a/backend/src/routes/auth.route.js +++ b/backend/src/routes/auth.route.js @@ -1,17 +1,30 @@ -import express from "express"; -import protectRoute from "../middleware/auth.middleware.js"; -import { validateSignup, validateLogin } from "../middleware/validate.js"; -import { signup, login, logout, googleAuth, updateProfile, updateProfilePicture, checkAuth, subscribeToPush } from "../controllers/auth.controller.js"; +import express from 'express' +import protectRoute from '../middleware/auth.middleware.js' +import { validateSignup, validateLogin } from '../middleware/validate.js' +import { + signup, + login, + logout, + googleAuth, + updateProfile, + updateProfilePicture, + checkAuth, + subscribeToPush, + githubRedirect, +} from '../controllers/auth.controller.js' +import { githubCallback } from '../controllers/githubCallback.controller.js' -const router = express.Router(); +const router = express.Router() -router.post("/signup", validateSignup, signup); -router.post("/login", validateLogin, login); -router.post("/google", googleAuth); -router.post("/logout", logout); -router.put("/update-profile", protectRoute, updateProfile); -router.put("/update-profile-picture", protectRoute, updateProfilePicture); -router.get("/check", protectRoute, checkAuth); -router.post("/push-subscribe", protectRoute, subscribeToPush); +router.post('/signup', validateSignup, signup) +router.post('/login', validateLogin, login) +router.post('/google', googleAuth) +router.get('/github', githubRedirect) +router.get('/github/callback', githubCallback) +router.post('/logout', logout) +router.put('/update-profile', protectRoute, updateProfile) +router.put('/update-profile-picture', protectRoute, updateProfilePicture) +router.get('/check', protectRoute, checkAuth) +router.post('/push-subscribe', protectRoute, subscribeToPush) -export default router; \ No newline at end of file +export default router diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..b77d343 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,4 @@ +VITE_VAPID_PUBLIC_KEY=your_vapid_public_key +VITE_GOOGLE_CLIENT_ID=your_google_client_id +VITE_GITHUB_CLIENT_ID=your_github_client_id +VITE_API_URL= http://localhost:5001/api diff --git a/frontend/components/chat/Sidebar.jsx b/frontend/components/chat/Sidebar.jsx index 3358f5c..0e5c3ae 100644 --- a/frontend/components/chat/Sidebar.jsx +++ b/frontend/components/chat/Sidebar.jsx @@ -1,68 +1,75 @@ -import { useEffect, useRef, useState } from "react" -import { Search, PenSquare, MonitorSmartphone, Palette } from "lucide-react" -import useChatStore from "../../src/store/useChatStore" -import useAuthStore from "../../src/store/useAuthStore" -import { getSocket } from "../../lib/socket" -import Avatar from "./Avatar" -import NewChatModal from "./NewChatModal" -import { getStatusMoodLabel } from "../../src/lib/statusMoods" +import { useEffect, useRef, useState } from 'react' +import { Search, PenSquare, MonitorSmartphone, Palette } from 'lucide-react' +import useChatStore from '../../src/store/useChatStore' +import useAuthStore from '../../src/store/useAuthStore' +import { getSocket } from '../../lib/socket' +import Avatar from './Avatar' +import NewChatModal from './NewChatModal' +import { getStatusMoodLabel } from '../../src/lib/statusMoods' const formatTime = (d) => - new Date(d).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) - + new Date(d).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) +//! need to see this file // Sidebar with conversation list, search, and new chat modal -export default function Sidebar({ selectedUser, onSelectUser, isMobileHidden }) { - const { users, getUsers, isUsersLoading, typingUsers } = useChatStore() - const { onlineUsers } = useAuthStore() - const [search, setSearch] = useState("") - const [showNewChat, setShowNewChat] = useState(false) - const [selectedFolder, setSelectedFolder] = useState("All") +export default function Sidebar({ + selectedUser, + onSelectUser, + isMobileHidden, +}) { + const { users, getUsers, isUsersLoading, typingUsers } = useChatStore() + const { onlineUsers } = useAuthStore() + const [search, setSearch] = useState('') + const [showNewChat, setShowNewChat] = useState(false) + const [selectedFolder, setSelectedFolder] = useState('All') - const previousOnlineRef = useRef(onlineUsers) + const previousOnlineRef = useRef(onlineUsers) - useEffect(() => { - getUsers() - }, [getUsers]) + useEffect(() => { + getUsers() + }, [getUsers]) - useEffect(() => { - const socket = getSocket() - if (!socket) return - socket.on("getOnlineUsers", (ids) => { - const currentOnline = useAuthStore.getState().onlineUsers; - const wentOffline = currentOnline.filter(id => !ids.includes(id)); - if (wentOffline.length > 0) { - const now = new Date().toISOString(); - useChatStore.setState(state => ({ - users: state.users.map(u => wentOffline.includes(u._id) ? { ...u, lastSeen: now } : u), - selectedUser: state.selectedUser && wentOffline.includes(state.selectedUser._id) - ? { ...state.selectedUser, lastSeen: now } - : state.selectedUser - })); - } - useAuthStore.getState().setOnlineUsers(ids) - }) - return () => socket.off("getOnlineUsers") - }, []) + useEffect(() => { + const socket = getSocket() + if (!socket) return + socket.on('getOnlineUsers', (ids) => { + const currentOnline = useAuthStore.getState().onlineUsers + const wentOffline = currentOnline.filter((id) => !ids.includes(id)) + if (wentOffline.length > 0) { + const now = new Date().toISOString() + useChatStore.setState((state) => ({ + users: state.users.map((u) => + wentOffline.includes(u._id) ? { ...u, lastSeen: now } : u, + ), + selectedUser: + state.selectedUser && wentOffline.includes(state.selectedUser._id) + ? { ...state.selectedUser, lastSeen: now } + : state.selectedUser, + })) + } + useAuthStore.getState().setOnlineUsers(ids) + }) + return () => socket.off('getOnlineUsers') + }, []) - const totalUnread = users.reduce( + const totalUnread = users.reduce( (sum, user) => sum + (user.unreadCount || 0), - 0 -) + 0, + ) -const activeChats = onlineUsers.length + const activeChats = onlineUsers.length - const filtered = users.filter(u => - ( - selectedFolder === "All" || - (selectedFolder === "Unread" && u.unreadCount > 0) || - u.folder === selectedFolder - ) && - u.name.toLowerCase().includes(search.toLowerCase()) -) + const filtered = users.filter( + (u) => + (selectedFolder === 'All' || + (selectedFolder === 'Unread' && u.unreadCount > 0) || + u.folder === selectedFolder) && + u.name.toLowerCase().includes(search.toLowerCase()), + ) - return ( - - ) + {showNewChat && ( + setShowNewChat(false)} + /> + )} + + ) } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e0604a9..16cdaef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -287,9 +287,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -793,6 +793,40 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.0-rc.17", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8a0a0c0..33a66bf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,5 +36,13 @@ "postcss": "^8.5.13", "tailwindcss": "^4.2.4", "vite": "^8.0.10" - } + }, + "description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.", + "main": "eslint.config.js", + "directories": { + "lib": "lib" + }, + "keywords": [], + "author": "", + "license": "ISC" } diff --git a/frontend/pages/LoginPage.jsx b/frontend/pages/LoginPage.jsx index fbfb9b8..6615764 100644 --- a/frontend/pages/LoginPage.jsx +++ b/frontend/pages/LoginPage.jsx @@ -1,190 +1,251 @@ -import { useState, useEffect } from "react" -import { Link, useNavigate } from "react-router-dom" -import { MessageSquare, Mail, Lock, Eye, EyeOff, Loader2 } from "lucide-react" -import useAuthStore from "../src/store/useAuthStore" -import toast from "react-hot-toast" +import { useState, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { MessageSquare, Mail, Lock, Eye, EyeOff, Loader2 } from 'lucide-react' +import useAuthStore from '../src/store/useAuthStore' +import toast from 'react-hot-toast' export default function LoginPage() { - const navigate = useNavigate() - const { login, googleLogin, isLoading } = useAuthStore() - const [showPassword, setShowPassword] = useState(false) - const [formData, setFormData] = useState({ email: "", password: "" }) - - const handleSubmit = async (e) => { - e.preventDefault() - try { - await login(formData) - navigate("/") - } catch { - // toast already shown in store - } + const navigate = useNavigate() + const { login, googleLogin, isLoading } = useAuthStore() + const [showPassword, setShowPassword] = useState(false) + const [formData, setFormData] = useState({ email: '', password: '' }) + + const handleSubmit = async (e) => { + e.preventDefault() + try { + await login(formData) + navigate('/') + } catch { + // toast already shown in store } - - const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID - const isGoogleConfigured = clientId && clientId !== "your_google_client_id_here" - - useEffect(() => { - if (!isGoogleConfigured) return - - let interval - const initGoogleBtn = () => { - const btnEl = document.getElementById("google-signin-button") - if (window.google?.accounts?.id && btnEl) { - clearInterval(interval) - window.google.accounts.id.initialize({ - client_id: clientId, - callback: async (response) => { - try { - await googleLogin(response.credential) - navigate("/") - } catch { - // toast already shown in store - } - }, - }) - window.google.accounts.id.renderButton(btnEl, { - theme: "outline", - size: "large", - width: btnEl.clientWidth || 380, - text: "continue_with", - shape: "rectangular", - }) + } + + const googleclientId = import.meta.env.VITE_GOOGLE_CLIENT_ID + const githubclientId = import.meta.env.VITE_GITHUB_CLIENT_ID + const isGoogleConfigured = googleclientId !== 'your_google_client_id_here' + const isGithubConfigured = githubclientId !== 'your_github_client_id_here' + + useEffect(() => { + if (!isGoogleConfigured) return + + let interval + const initGoogleBtn = () => { + const btnEl = document.getElementById('google-signin-button') + if (window.google?.accounts?.id && btnEl) { + clearInterval(interval) + window.google.accounts.id.initialize({ + client_id: googleclientId, + callback: async (response) => { + try { + await googleLogin(response.credential) + navigate('/') + } catch { + // toast already shown in store } - } + }, + }) + window.google.accounts.id.renderButton(btnEl, { + theme: 'outline', + size: 'large', + width: btnEl.clientWidth || 380, + text: 'continue_with', + shape: 'rectangular', + }) + } + } + + interval = setInterval(initGoogleBtn, 100) + initGoogleBtn() - interval = setInterval(initGoogleBtn, 100) - initGoogleBtn() + return () => clearInterval(interval) + }, [isGoogleConfigured, googleclientId, googleLogin, navigate]) - return () => clearInterval(interval) - }, [isGoogleConfigured, clientId, googleLogin, navigate]) + const handleGoogleLogin = () => { + toast.error('Google Sign-In is not configured') + } - const handleGoogleLogin = () => { - toast.error("Google Sign-In is not configured") + const handleGithubLogin = () => { + if (!isGithubConfigured) { + toast.error('Github Sign-In is not configured') + return } + window.location.href = `${import.meta.env.VITE_API_URL}/auth/github` + } + + return ( +
+
+
+
+
+
+ +
+

+ Welcome back +

+

+ Sign in to your chatter-box account +

+
+
+ +
+
+ + +
- return ( -
-
-
-
-
-
- -
-

Welcome back

-

Sign in to your chatter-box account

-
-
- - -
- - -
- -
- - -
- - - -
OR
- {isGoogleConfigured ? ( -
-
-
- ) : ( - - )} - - -
-

- Don't have an account?{" "} - - Create one - -

-
-
+
+ +
-
-
-
- -
-
- -
-

Stay connected

-

- Chat in real-time with friends and teams. Fast, secure, and available on any device. -

-
+ + +
OR
+ {isGoogleConfigured ? ( +
+
+
+ ) : ( + + )} + +
+
+ +
+ + +
+

+ Don't have an account?{' '} + + Create one + +

+
+
+
+ +
+
+
+ +
+
+ +
+

Stay connected

+

+ Chat in real-time with friends and teams. Fast, secure, and + available on any device. +

- ) -} \ No newline at end of file +
+
+ ) +} diff --git a/frontend/pages/SignUpPage.jsx b/frontend/pages/SignUpPage.jsx index a1bb95b..907c24f 100644 --- a/frontend/pages/SignUpPage.jsx +++ b/frontend/pages/SignUpPage.jsx @@ -1,285 +1,362 @@ -import { useState, useEffect } from "react" -import { Link, useNavigate } from "react-router-dom" -import { MessageSquare, User, Mail, Lock, Eye, EyeOff, Loader2 } from "lucide-react" -import useAuthStore from "../src/store/useAuthStore" -import toast from "react-hot-toast" +import { useState, useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { + MessageSquare, + User, + Mail, + Lock, + Eye, + EyeOff, + Loader2, +} from 'lucide-react' +import useAuthStore from '../src/store/useAuthStore' +import toast from 'react-hot-toast' export default function SignUpPage() { - const navigate = useNavigate() - const { signup, googleLogin, isLoading } = useAuthStore() - const [showPassword, setShowPassword] = useState(false) - const [formData, setFormData] = useState({name: "",email: "",password: "",confirmPassword: ""}) + const navigate = useNavigate() + const { signup, googleLogin, githubLogin, isLoading } = useAuthStore() + const [showPassword, setShowPassword] = useState(false) + const [formData, setFormData] = useState({ + name: '', + email: '', + password: '', + confirmPassword: '', + }) - const passwordStrength = [ - formData.password.length >= 8, - /[A-Z]/.test(formData.password), - /[a-z]/.test(formData.password), - /\d/.test(formData.password), - /[@#$!%&*]/.test(formData.password), - ].filter(Boolean).length + const passwordStrength = [ + formData.password.length >= 8, + /[A-Z]/.test(formData.password), + /[a-z]/.test(formData.password), + /\d/.test(formData.password), + /[@#$!%&*]/.test(formData.password), + ].filter(Boolean).length + const handleSubmit = async (e) => { + e.preventDefault() - const handleSubmit = async (e) => { - e.preventDefault() + const passwordRegex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$!%&*])[A-Za-z\d@#$!%&*]{8,}$/ - const passwordRegex = - /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$!%&*])[A-Za-z\d@#$!%&*]{8,}$/ - - if (formData.password !== formData.confirmPassword) { - toast.error("Passwords do not match") - return - } + if (formData.password !== formData.confirmPassword) { + toast.error('Passwords do not match') + return + } - try { - await signup(formData) - navigate("/") - } catch { - // toast already shown in store - } + try { + await signup(formData) + navigate('/') + } catch { + // toast already shown in store } + } - const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID - const isGoogleConfigured = clientId && clientId !== "your_google_client_id_here" + const googleclientId = import.meta.env.VITE_GOOGLE_CLIENT_ID + const githubclientId = import.meta.env.VITE_GITHUB_CLIENT_ID + const isGoogleConfigured = + googleclientId && googleclientId !== 'your_google_client_id_here' + const isGithubConfigured = + githubclientId && githubclientId !== 'your_github_client_id_here' - useEffect(() => { - if (!isGoogleConfigured) return + useEffect(() => { + if (!isGoogleConfigured) return - let interval - const initGoogleBtn = () => { - const btnEl = document.getElementById("google-signup-button") - if (window.google?.accounts?.id && btnEl) { - clearInterval(interval) - window.google.accounts.id.initialize({ - client_id: clientId, - callback: async (response) => { - try { - await googleLogin(response.credential) - navigate("/") - } catch { - // toast already shown in store - } - }, - }) - window.google.accounts.id.renderButton(btnEl, { - theme: "outline", - size: "large", - width: btnEl.clientWidth || 380, - text: "signup_with", - shape: "rectangular", - }) + let interval + const initGoogleBtn = () => { + const btnEl = document.getElementById('google-signup-button') + if (window.google?.accounts?.id && btnEl) { + clearInterval(interval) + window.google.accounts.id.initialize({ + client_id: googleclientId, + callback: async (response) => { + try { + await googleLogin(response.credential) + navigate('/') + } catch { + // toast already shown in store } - } + }, + }) + window.google.accounts.id.renderButton(btnEl, { + theme: 'outline', + size: 'large', + width: btnEl.clientWidth || 380, + text: 'signup_with', + shape: 'rectangular', + }) + } + } - interval = setInterval(initGoogleBtn, 100) - initGoogleBtn() + interval = setInterval(initGoogleBtn, 100) + initGoogleBtn() - return () => clearInterval(interval) - }, [isGoogleConfigured, clientId, googleLogin, navigate]) + return () => clearInterval(interval) + }, [isGoogleConfigured, googleclientId, googleLogin, navigate]) - const handleGoogleSignup = () => { - toast.error("Google Sign-In is not configured") - } + const handleGoogleSignUp = () => { + toast.error('Google Sign-In is not configured') + } - return ( -
-
-
-
+ const handleGithubSignUp = () => { + if (!isGithubConfigured) { + toast.error('Github Sign-In is not configured') + return + } + window.location.href = + `https://github.com/login/oauth/authorize` + + `?client_id=${githubclientId}` + + `&scope=read:user user:email` + } -
-
- -
-

Join the conversation

-

- Create your free account and start messaging with anyone, anywhere, instantly. -

-
-
+ return ( +
+
+
+
-
-
-
-
-
- -
-

Create account

-

Get started with your free account

-
-
+
+
+ +
+

Join the conversation

+

+ Create your free account and start messaging with anyone, anywhere, + instantly. +

+
+
-
-
- - -
+
+
+
+
+
+ +
+

+ Create account +

+

+ Get started with your free account +

+
+
-
- - -
+ +
+ + +
-
- +
+ + +
- -
- +
+

Password must contain:

+
    +
  • At least 8 characters
  • +
  • One uppercase letter (A-Z)
  • +
  • One lowercase letter (a-z)
  • +
  • One number (0-9)
  • +
  • One special character (@, #, $, !, %, &, *)
  • +
+
+
- -
OR
- {isGoogleConfigured ? ( -
-
-
- ) : ( - - )} - + {formData.confirmPassword && ( +

+ {formData.password === formData.confirmPassword + ? 'Passwords match ✓' + : 'Passwords do not match'} +

+ )} +
+ -
-

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

-
-
+
OR
+ {isGoogleConfigured ? ( +
+
+
+ ) : ( + + )} +
+
+ +
+ + +
+

+ Already have an account?{' '} + + Sign in + +

+
- ) -} \ No newline at end of file +
+
+ ) +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7800fce..8ce6a12 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -59,4 +59,4 @@ const App = () => { ) } -export default App \ No newline at end of file +export default App diff --git a/frontend/src/store/useAuthStore.js b/frontend/src/store/useAuthStore.js index 4d66f98..b4cdeb7 100644 --- a/frontend/src/store/useAuthStore.js +++ b/frontend/src/store/useAuthStore.js @@ -162,4 +162,4 @@ const useAuthStore = create((set) => ({ }, })); -export default useAuthStore; \ No newline at end of file +export default useAuthStore; diff --git a/package-lock.json b/package-lock.json index 5968c91..7344816 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,341 @@ "": { "name": "chat-app", "version": "1.0.0", - "license": "ISC" + "license": "ISC", + "dependencies": { + "axios": "^1.17.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } } } } diff --git a/package.json b/package.json index 36bb16d..5d9f5a9 100644 --- a/package.json +++ b/package.json @@ -11,5 +11,8 @@ "keywords": [], "author": "", "license": "ISC", - "type": "commonjs" -} \ No newline at end of file + "type": "commonjs", + "dependencies": { + "axios": "^1.17.0" + } +}