From b8024408f4ac769cdf39bd8bfdbf3fe282af6e09 Mon Sep 17 00:00:00 2001 From: vd77-1 Date: Sun, 24 May 2026 13:37:00 +0530 Subject: [PATCH 1/3] fix: add /docs pages to resolve 404 error --- frontend/src/app/docs/architecture/page.tsx | 79 ++++++++++ frontend/src/app/docs/backend/page.tsx | 79 ++++++++++ frontend/src/app/docs/frontend/page.tsx | 79 ++++++++++ frontend/src/app/docs/page.tsx | 157 ++++++++++++++++++++ frontend/src/app/docs/setup/page.tsx | 79 ++++++++++ frontend/src/content/ARCHITECTURE.md | 34 +++++ frontend/src/content/BACKEND.md | 47 ++++++ frontend/src/content/ENVIRONMENT_SETUP.md | 38 +++++ frontend/src/content/FRONTEND.md | 44 ++++++ 9 files changed, 636 insertions(+) create mode 100644 frontend/src/app/docs/architecture/page.tsx create mode 100644 frontend/src/app/docs/backend/page.tsx create mode 100644 frontend/src/app/docs/frontend/page.tsx create mode 100644 frontend/src/app/docs/page.tsx create mode 100644 frontend/src/app/docs/setup/page.tsx create mode 100644 frontend/src/content/ARCHITECTURE.md create mode 100644 frontend/src/content/BACKEND.md create mode 100644 frontend/src/content/ENVIRONMENT_SETUP.md create mode 100644 frontend/src/content/FRONTEND.md diff --git a/frontend/src/app/docs/architecture/page.tsx b/frontend/src/app/docs/architecture/page.tsx new file mode 100644 index 0000000..fc336a0 --- /dev/null +++ b/frontend/src/app/docs/architecture/page.tsx @@ -0,0 +1,79 @@ +import fs from "fs" +import path from "path" +import Link from "next/link" +import Header from "../../../components/Home/Header" +import Footer from "../../../components/Home/Footer" + +export const metadata = { + title: "Architecture | NPMChat Docs", +} + +export default function ArchitecturePage() { + const content = fs.readFileSync( + path.resolve(process.cwd(), "..", "docs", "ARCHITECTURE.md"), + "utf-8" + ) + + return ( +
+
+ +
+
+ +
+ + ← All Docs + + / + architecture +
+ +
+
+
+ +
+ ⚙️ Setup + 🔧 Backend + 🖥️ Frontend +
+ +
+
+ +
+
+ ) +} + +function renderMarkdown(md: string): string { + return md + + .replace(/```[\w]*\n([\s\S]*?)```/g, '
$1
') + + .replace(/`([^`]+)`/g, '$1') + + .replace(/\*\*(.+?)\*\*/g, '$1') + + .replace(/^### (.+)$/gm, '

$1

') + .replace(/^## (.+)$/gm, '

$1

') + .replace(/^# (.+)$/gm, '

$1

') + + .replace(/^---$/gm, '
') + + .replace(/^- (.+)$/gm, '
  • $1
  • ') + .replace(/^\d+\. (.+)$/gm, '
  • $1
  • ') + + .replace(/^(?!<)(.+)$/gm, '

    $1

    ') + + .replace(/]*>\s*<\/p>/g, '') + + .replace(/(]*>.*<\/li>\n?)+/g, '
      $&
    '); +} \ No newline at end of file diff --git a/frontend/src/app/docs/backend/page.tsx b/frontend/src/app/docs/backend/page.tsx new file mode 100644 index 0000000..d36f730 --- /dev/null +++ b/frontend/src/app/docs/backend/page.tsx @@ -0,0 +1,79 @@ +import fs from "fs" +import path from "path" +import Link from "next/link" +import Header from "../../../components/Home/Header" +import Footer from "../../../components/Home/Footer" + +export const metadata = { + title: "Backend | NPMChat Docs", +} + +export default function BackendPage() { + const content = fs.readFileSync( + path.resolve(process.cwd(), "..", "docs", "BACKEND.md"), + "utf-8" + ) + + return ( +
    +
    + +
    +
    + +
    + + ← All Docs + + / + backend +
    + +
    +
    +
    + +
    + ⚙️ Setup + 🏗️ Architecture + 🖥️ Frontend +
    + +
    +
    + +
    +
    + ) +} + +function renderMarkdown(md: string): string { + return md + + .replace(/```[\w]*\n([\s\S]*?)```/g, '
    $1
    ') + + .replace(/`([^`]+)`/g, '$1') + + .replace(/\*\*(.+?)\*\*/g, '$1') + + .replace(/^### (.+)$/gm, '

    $1

    ') + .replace(/^## (.+)$/gm, '

    $1

    ') + .replace(/^# (.+)$/gm, '

    $1

    ') + + .replace(/^---$/gm, '
    ') + + .replace(/^- (.+)$/gm, '
  • $1
  • ') + .replace(/^\d+\. (.+)$/gm, '
  • $1
  • ') + + .replace(/^(?!<)(.+)$/gm, '

    $1

    ') + + .replace(/]*>\s*<\/p>/g, '') + + .replace(/(]*>.*<\/li>\n?)+/g, '
      $&
    '); +} diff --git a/frontend/src/app/docs/frontend/page.tsx b/frontend/src/app/docs/frontend/page.tsx new file mode 100644 index 0000000..2733acf --- /dev/null +++ b/frontend/src/app/docs/frontend/page.tsx @@ -0,0 +1,79 @@ +import fs from "fs" +import path from "path" +import Link from "next/link" +import Header from "../../../components/Home/Header" +import Footer from "../../../components/Home/Footer" + +export const metadata = { + title: "Frontend | NPMChat Docs", +} + +export default function FrontendPage() { + const content = fs.readFileSync( + path.resolve(process.cwd(), "..", "docs", "FRONTEND.md"), + "utf-8" + ) + + return ( +
    +
    + +
    +
    + +
    + + ← All Docs + + / + frontend +
    + +
    +
    +
    + +
    + ⚙️ Setup + 🏗️ Architecture + 🔧 Backend +
    + +
    +
    + +
    +
    + ) +} + +function renderMarkdown(md: string): string { + return md + + .replace(/```[\w]*\n([\s\S]*?)```/g, '
    $1
    ') + + .replace(/`([^`]+)`/g, '$1') + + .replace(/\*\*(.+?)\*\*/g, '$1') + + .replace(/^### (.+)$/gm, '

    $1

    ') + .replace(/^## (.+)$/gm, '

    $1

    ') + .replace(/^# (.+)$/gm, '

    $1

    ') + + .replace(/^---$/gm, '
    ') + + .replace(/^- (.+)$/gm, '
  • $1
  • ') + .replace(/^\d+\. (.+)$/gm, '
  • $1
  • ') + + .replace(/^(?!<)(.+)$/gm, '

    $1

    ') + + .replace(/]*>\s*<\/p>/g, '') + + .replace(/(]*>.*<\/li>\n?)+/g, '
      $&
    '); +} \ No newline at end of file diff --git a/frontend/src/app/docs/page.tsx b/frontend/src/app/docs/page.tsx new file mode 100644 index 0000000..c95481c --- /dev/null +++ b/frontend/src/app/docs/page.tsx @@ -0,0 +1,157 @@ +import Link from "next/link" +import Header from "../../components/Home/Header" +import Footer from "../../components/Home/Footer" +import { + Settings, + BookOpen, + Server, + Layout, +} from "lucide-react" + +const docs = [ + { + href: "/docs/setup", + icon: , + title: "Setup Guide", + description: + "Get NPMChat running locally from scratch. Prerequisites, environment variables, and step-by-step instructions to have everything up and running.", + bgColor: "bg-[#fef6e4]", + details: ["Prerequisites", "Backend setup", "Frontend setup", "Env variables"], + }, + { + href: "/docs/architecture", + icon: , + title: "Architecture", + description: + "High-level overview of the NPMChat monorepo structure, data flow between frontend and backend, and key design decisions.", + bgColor: "bg-[#e9d5ff]", + details: ["Monorepo structure", "Data flow", "Design decisions", "Tech stack"], + }, + { + href: "/docs/backend", + icon: , + title: "Backend", + description: + "Express.js API reference covering all routes, Socket.IO events, MongoDB models, and environment configuration.", + bgColor: "bg-[#d9f99d]", + details: ["API routes", "Socket.IO events", "MongoDB models", "Configuration"], + }, + { + href: "/docs/frontend", + icon: , + title: "Frontend", + description: + "Next.js app folder structure, key pages, routing conventions, and how to add new pages or components to the project.", + bgColor: "bg-[#e0f2fe]", + details: ["Folder structure", "Key routes", "Components", "Adding new pages"], + }, +] + +export const metadata = { + title: "Documentation | NPMChat", + description: "NPMChat developer documentation", +} + +export default function DocsPage() { + return ( +
    +
    + + {/* Hero */} +
    +
    +
    + + DEVELOPER DOCS + +
    +

    + Build with NPMChat +

    +

    + Everything you need to understand, run, and contribute to NPMChat — + from local setup to architecture deep dives. +

    +
    +
    + + {/* Doc Cards */} +
    +
    +
    +

    + Documentation +

    +

    + Four guides covering everything from first setup to internal architecture +

    +
    + +
    + {docs.map((doc, index) => ( + +
    +
    + {doc.icon} +
    +

    + {doc.title} +

    +

    + {doc.description} +

    +
    + {doc.details.map((detail, idx) => ( +
    +
    + {detail} +
    + ))} +
    +
    + Read docs → +
    +
    + + ))} +
    +
    +
    + + {/* CTA */} +
    +
    +

    + Ready to Contribute? +

    +

    + NPMChat is open source. Pick up an issue, read the docs, and send a PR. +

    +
    + + View on GitHub + + + Get Started + +
    +
    +
    + +
    +
    + ) +} diff --git a/frontend/src/app/docs/setup/page.tsx b/frontend/src/app/docs/setup/page.tsx new file mode 100644 index 0000000..d08fc70 --- /dev/null +++ b/frontend/src/app/docs/setup/page.tsx @@ -0,0 +1,79 @@ +import fs from "fs" +import path from "path" +import Link from "next/link" +import Header from "../../../components/Home/Header" +import Footer from "../../../components/Home/Footer" + +export const metadata = { + title: "Setup Guide | NPMChat Docs", +} + +export default function SetupPage() { + const content = fs.readFileSync( + path.resolve(process.cwd(), "..", "docs", "ENVIRONMENT_SETUP.md"), + "utf-8" + ) + + return ( +
    +
    + +
    +
    + +
    + + ← All Docs + + / + setup +
    + +
    +
    +
    + +
    + 🏗️ Architecture + 🔧 Backend + 🖥️ Frontend +
    + +
    +
    + +
    +
    + ) +} + +function renderMarkdown(md: string): string { + return md + + .replace(/```[\w]*\n([\s\S]*?)```/g, '
    $1
    ') + + .replace(/`([^`]+)`/g, '$1') + + .replace(/\*\*(.+?)\*\*/g, '$1') + + .replace(/^### (.+)$/gm, '

    $1

    ') + .replace(/^## (.+)$/gm, '

    $1

    ') + .replace(/^# (.+)$/gm, '

    $1

    ') + + .replace(/^---$/gm, '
    ') + + .replace(/^- (.+)$/gm, '
  • $1
  • ') + .replace(/^\d+\. (.+)$/gm, '
  • $1
  • ') + + .replace(/^(?!<)(.+)$/gm, '

    $1

    ') + + .replace(/]*>\s*<\/p>/g, '') + + .replace(/(]*>.*<\/li>\n?)+/g, '
      $&
    '); +} \ No newline at end of file diff --git a/frontend/src/content/ARCHITECTURE.md b/frontend/src/content/ARCHITECTURE.md new file mode 100644 index 0000000..5ac58a4 --- /dev/null +++ b/frontend/src/content/ARCHITECTURE.md @@ -0,0 +1,34 @@ +# Architecture Overview + +NPMChat is a full-stack real-time chat application built using the MERN stack (with Next.js) and Socket.io. + +## System Components + +### 1. Backend (Node.js/Express) +The backend acts as a RESTful API and a WebSocket server. +- **REST API:** Handles authentication, user profile management, and message history retrieval. +- **WebSocket (Socket.io):** Handles real-time message delivery, online status tracking, and "typing" indicators. +- **Database (MongoDB):** Stores user profiles and message history. + +### 2. Frontend (Next.js/React) +A modern, responsive UI built with TypeScript and Tailwind CSS. +- **State Management:** Uses React Context API (`AuthContext` and `MessageContext`) for global state. +- **Real-time Integration:** Uses `socket.io-client` to maintain a persistent connection with the backend. +- **Styling:** Follows a Neo-Brutalist design language. + +## Data Flow +1. **Auth:** User logs in -> Backend returns JWT -> Frontend stores JWT in LocalStorage -> Frontend sends JWT in `Authorization` header for subsequent requests. +2. **Messaging:** + - User sends message (via REST POST). + - Backend saves to DB. + - Backend emits `newMessage` via Socket.io to the recipient. + - Recipient's frontend receives socket event and updates local message state. + +## Deployment Strategy +- **Frontend:** Vercel — handles Next.js SSR, static assets, and edge routing. +- **Backend:** Render Web Service (Docker) — required for persistent Socket.IO WebSocket connections. See [DEPLOYMENT.md](/DEPLOYMENT.md) for setup. +- **Database:** MongoDB Atlas (free tier available). +- **CI:** GitHub Actions — validates Docker build and health check on every PR. See `.github/workflows/backend-ci.yml`. + +> **Note:** Vercel cannot host the backend because its serverless functions do not support persistent WebSocket connections. See [DEPLOYMENT.md](/DEPLOYMENT.md) for details. + diff --git a/frontend/src/content/BACKEND.md b/frontend/src/content/BACKEND.md new file mode 100644 index 0000000..35aa2a6 --- /dev/null +++ b/frontend/src/content/BACKEND.md @@ -0,0 +1,47 @@ +# Backend Documentation + +The backend is a Node.js application using Express and Mongoose. + +## Tech Stack +- **Framework:** Express.js (ES Modules) +- **Database:** MongoDB +- **Real-time:** Socket.io +- **Auth:** JWT (JSON Web Tokens) & Bcrypt.js +- **File Storage:** Cloudinary + +## API Reference + +### Auth Routes (`/api/v1/auth`) +| Method | Endpoint | Description | Auth Required | +|--------|----------|-------------|---------------| +| POST | `/signup` | Create a new user account | No | +| POST | `/login` | Authenticate user & get token | No | +| GET | `/check-auth` | Verify current token & get user data | Yes | +| PUT | `/update-profile` | Update user bio, name, or avatar | Yes | + +### Message Routes (`/api/v1/messages`) +| Method | Endpoint | Description | Auth Required | +|--------|----------|-------------|---------------| +| GET | `/` | Get list of users for sidebar with unseen counts | Yes | +| GET | `/:userId` | Get chat history between two users | Yes | +| POST | `/send/:receiverId` | Send a new text/image message | Yes | +| PUT | `/mark-as-seen/:messageId` | Mark a specific message as read | Yes | + +## Database Models + +### User Schema +- `email`: String (Unique, Lowercase) +- `password`: String (Hashed) +- `name`: String +- `avatarUrl`: String (Cloudinary URL) +- `bio`: String + +### Message Schema +- `senderId`: ObjectId (Ref: User) +- `receiverId`: ObjectId (Ref: User) +- `text`: String +- `image`: String (Cloudinary URL) +- `seen`: Boolean (Default: false) + +## Middleware +- `protectRoute`: Verifies the JWT in the `Authorization` header. If valid, attaches the user object to `req.user`. diff --git a/frontend/src/content/ENVIRONMENT_SETUP.md b/frontend/src/content/ENVIRONMENT_SETUP.md new file mode 100644 index 0000000..e53f3b3 --- /dev/null +++ b/frontend/src/content/ENVIRONMENT_SETUP.md @@ -0,0 +1,38 @@ +# Environment Setup + +To run NPMChat locally, follow these steps. + +## Prerequisites +- Node.js (v18+) +- MongoDB (Local or Atlas) +- Cloudinary Account (for image uploads) + +## Backend Setup +1. Navigate to the `backend/` directory. +2. Create a `.env` file based on `.env.example`: +```env +PORT=8080 +MONGODB_URI=your_mongodb_connection_string +JWT_SECRET=your_super_secret_key +CLOUDINARY_CLOUD_NAME=your_name +CLOUDINARY_API_KEY=your_key +CLOUDINARY_API_SECRET=your_secret +CLIENT_URL=http://localhost:3000 +NODE_ENV=development +``` +3. Install dependencies: `npm install` +4. Start development server: `npm run dev` + +## Frontend Setup +1. Navigate to the `frontend/` directory. +2. Create a `.env.local` file: +```env +NEXT_PUBLIC_API_URL=http://localhost:8080 +``` +3. Install dependencies: `npm install` +4. Start development server: `npm run dev` + +## Troubleshooting +- **CORS Issues:** Ensure `CLIENT_URL` in the backend `.env` matches your frontend URL. +- **Socket Connection:** If the socket fails to connect, verify that the backend is running on the expected port (default: 8080). +- **Image Uploads:** If profile pictures fail to save, check your Cloudinary credentials. diff --git a/frontend/src/content/FRONTEND.md b/frontend/src/content/FRONTEND.md new file mode 100644 index 0000000..87e27a5 --- /dev/null +++ b/frontend/src/content/FRONTEND.md @@ -0,0 +1,44 @@ +# Frontend Documentation + +The frontend is a Next.js application built with TypeScript and Tailwind CSS. + +## Tech Stack +- **Framework:** Next.js 15 (App Router) +- **Styling:** Tailwind CSS + Lucide Icons +- **State:** React Context API +- **Networking:** Fetch API (Custom wrapper) + Socket.io-client +- **Animations:** Lenis (Smooth Scroll) + Framer Motion (planned) + +## Project Structure +- `src/app`: Routes and Page layouts. +- `src/components`: UI components (organized by feature). +- `src/lib`: Utility functions and shared helpers. +- `src/app/AuthContext.tsx`: Manages user login/logout/session. +- `src/app/MessageContext.tsx`: Manages active chat state, socket events, and message history. + +## State Management + +### AuthContext +Stores the `user` object and provides methods: +- `login(data)` +- `signup(data)` +- `logout()` +- `updateProfile(data)` + +### MessageContext +The "Engine" of the chat UI: +- Manages `socket` connection. +- Stores `users` (sidebar list) and `messages` (active conversation). +- Handles real-time listeners for `newMessage` and `getOnlineUsers`. + +## Real-time Logic +The socket is initialized in `MessageContext` as soon as the user is authenticated. +- **Connection:** `io(BACKEND_URL, { query: { userId } })` +- **Events Emitted:** `send-message` +- **Events Received:** `newMessage`, `getOnlineUsers`, `messageSeen` + +## Styling Guidelines +The project uses a **Neo-Brutalist** aesthetic: +- Thick black borders (`border-2 border-black`). +- Harsh shadows (`shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]`). +- High contrast colors (Vibrant greens, purples, and yellows). From e1c8dd1b7d887b4bf1673f5387f6886a6a475a91 Mon Sep 17 00:00:00 2001 From: vd77-1 Date: Wed, 27 May 2026 01:10:10 +0530 Subject: [PATCH 2/3] feat: add OAuth 2.0 login/signup with Google and GitHub --- backend/.env.example | 7 +- backend/lib/passport.js | 83 ++++++++++++++++++++ backend/models/User.js | 17 +++- backend/package-lock.json | 98 ++++++++++++++++++++++++ backend/package.json | 3 + backend/routes/auth.oauth.routes.js | 45 +++++++++++ backend/server.js | 10 ++- frontend/src/app/login/page.tsx | 5 +- frontend/src/app/oauth-callback/page.tsx | 34 ++++++++ frontend/src/app/signup/page.tsx | 6 +- frontend/src/components/OAuthButtons.tsx | 41 ++++++++++ 11 files changed, 339 insertions(+), 10 deletions(-) create mode 100644 backend/lib/passport.js create mode 100644 backend/routes/auth.oauth.routes.js create mode 100644 frontend/src/app/oauth-callback/page.tsx create mode 100644 frontend/src/components/OAuthButtons.tsx diff --git a/backend/.env.example b/backend/.env.example index 1f7dd3c..1acef4c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -5,4 +5,9 @@ MONGODB_URI=your_mongodb_connection_string JWT_SECRET=your_jwt_secret CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name CLOUDINARY_API_KEY=your_cloudinary_api_key -CLOUDINARY_API_SECRET=your_cloudinary_api_secret \ No newline at end of file +CLOUDINARY_API_SECRET=your_cloudinary_api_secret + +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret_key +GITHUB_CLIENT_ID=your_github_client_id +GITHUB_CLIENT_SECRET=your_github_client_secret_key \ No newline at end of file diff --git a/backend/lib/passport.js b/backend/lib/passport.js new file mode 100644 index 0000000..6c5f64b --- /dev/null +++ b/backend/lib/passport.js @@ -0,0 +1,83 @@ +import passport from 'passport' +import { Strategy as GoogleStrategy } from 'passport-google-oauth20' +import { Strategy as GitHubStrategy } from 'passport-github2' +import User from '../models/User.js' + +passport.use(new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: `${process.env.BACKEND_URL}/api/v1/auth/google/callback`, + }, + async (_accessToken, _refreshToken, profile, done) => { + try { + + let user = await User.findOne({ googleId: profile.id }) + + + if (!user) { + const email = profile.emails?.[0]?.value + user = email ? await User.findOne({ email }) : null + + if (user) { + + user.googleId = profile.id + if (!user.avatarUrl) user.avatarUrl = profile.photos?.[0]?.value + await user.save() + } else { + + user = await User.create({ + googleId: profile.id, + email: profile.emails?.[0]?.value, + name: profile.displayName || profile.emails?.[0]?.value?.split('@')[0] || 'User', + avatarUrl: profile.photos?.[0]?.value, + password: null, + }) + } + } + + done(null, user) + } catch (err) { + done(err, null) + } + } +)) + +passport.use(new GitHubStrategy( + { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: `${process.env.BACKEND_URL}/api/v1/auth/github/callback`, + scope: ['user:email'], + }, + async (_accessToken, _refreshToken, profile, done) => { + try { + let user = await User.findOne({ githubId: profile.id }) + + if (!user) { + const email = profile.emails?.[0]?.value + user = email ? await User.findOne({ email }) : null + + if (user) { + user.githubId = profile.id + if (!user.avatarUrl) user.avatarUrl = profile.photos?.[0]?.value + await user.save() + } else { + user = await User.create({ + githubId: profile.id, + email: profile.emails?.[0]?.value, + name: profile.displayName || profile.username || profile.emails?.[0]?.value?.split('@')[0] || 'User', + avatarUrl: profile.photos?.[0]?.value, + password: null, + }) + } + } + + done(null, user) + } catch (err) { + done(err, null) + } + } +)) + +export default passport \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js index 3620078..0b9b397 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -10,8 +10,9 @@ const userSchema = new mongoose.Schema( }, password: { type: String, - required: true, + required: false, minlength: 6, + default: null, }, name: { type: String, @@ -19,15 +20,25 @@ const userSchema = new mongoose.Schema( }, avatarUrl: { type: String, - default: "", // Default profile picture URL + default: "", }, bio: { type: String, }, + googleId: { + type: String, + default: null, + index: true, + }, + githubId: { + type: String, + default: null, + index: true, + }, }, { timestamps: true }, ) const User = mongoose.model("User", userSchema) -export default User +export default User \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index c6e35c8..8f79487 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,9 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.16.1", "nodemon": "^3.1.10", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "socket.io": "^4.8.1", "zod": "^4.4.3" }, @@ -792,6 +795,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcryptjs": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", @@ -2659,6 +2671,12 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2761,6 +2779,75 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-github2": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", + "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2784,6 +2871,11 @@ "dev": true, "license": "MIT" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -3574,6 +3666,12 @@ "node": ">= 0.6" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/backend/package.json b/backend/package.json index 03e0ac6..af08077 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,9 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.16.1", "nodemon": "^3.1.10", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "socket.io": "^4.8.1", "zod": "^4.4.3" }, diff --git a/backend/routes/auth.oauth.routes.js b/backend/routes/auth.oauth.routes.js new file mode 100644 index 0000000..08ffa47 --- /dev/null +++ b/backend/routes/auth.oauth.routes.js @@ -0,0 +1,45 @@ +import express from 'express' +import passport from '../lib/passport.js' +import { generateToken } from '../lib/utils.js' + +const oauthRouter = express.Router() + +// Google + +oauthRouter.get( + '/google', + passport.authenticate('google', { scope: ['profile', 'email'], session: false }) +) + +oauthRouter.get( + '/google/callback', + passport.authenticate('google', { + session: false, + failureRedirect: `${process.env.CLIENT_URL}/login?error=oauth_failed`, + }), + (req, res) => { + const token = generateToken(req.user._id) + // Redirect to frontend callback page with token in query param + res.redirect(`${process.env.CLIENT_URL}/oauth-callback?token=${token}`) + } +) + +// GitHub +oauthRouter.get( + '/github', + passport.authenticate('github', { scope: ['user:email'], session: false }) +) + +oauthRouter.get( + '/github/callback', + passport.authenticate('github', { + session: false, + failureRedirect: `${process.env.CLIENT_URL}/login?error=oauth_failed`, + }), + (req, res) => { + const token = generateToken(req.user._id) + res.redirect(`${process.env.CLIENT_URL}/oauth-callback?token=${token}`) + } +) + +export default oauthRouter \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 4897a03..d242603 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,5 +1,9 @@ -import express from "express" import "dotenv/config" + +import passport from './lib/passport.js' +import oauthRouter from './routes/auth.oauth.routes.js' +import express from "express" + import cors from "cors" import http from "http" import helmet from "helmet" @@ -96,7 +100,7 @@ io.on("connection", (socket) => { }) app.use(express.json({ limit: "4mb" })) - +app.use(passport.initialize()) // 4. Routes with Rate Limiting applied app.use("/api/status", (req, res) => { res.status(200).json({ status: "ok" }) @@ -118,8 +122,8 @@ app.use("/api/health", (req, res) => { // Apply strict limiter to auth routes, and standard limiter to message routes app.use("/api/v1/auth", authLimiter, userRouter) +app.use("/api/v1/auth", authLimiter, oauthRouter) app.use("/api/v1/messages", standardLimiter, messageRouter) - app.use("/", (req, res) => { res.send("NPMChat API is running") }) diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 4ed8195..2bad39f 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -1,4 +1,5 @@ "use client" +import OAuthButtons from '@/components/OAuthButtons' import React, { useState } from "react" import Link from "next/link" import { useRouter } from "next/navigation" @@ -112,7 +113,7 @@ function LoginPageContent() { className="mt-2 border-2 border-black bg-[#b39ddb] text-black font-extrabold text-lg py-2 rounded-none transition-all cursor-[url('/custom-cursor-click.svg'),_pointer] hover:bg-[#39ff14] hover:text-white focus:outline-none" style={{ boxShadow: `4px 4px 0 0 ${accent}` }} > - {loading ? "Logging In..." : "Login 2"} + {loading ? "Logging In..." : "Login"}
    + + {/* Floating accent shape bottom left */}
    diff --git a/frontend/src/app/oauth-callback/page.tsx b/frontend/src/app/oauth-callback/page.tsx new file mode 100644 index 0000000..352a1bb --- /dev/null +++ b/frontend/src/app/oauth-callback/page.tsx @@ -0,0 +1,34 @@ +'use client' +import { useEffect } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { useAuth } from '../AuthContext' + +export default function OAuthCallback() { + const router = useRouter() + const params = useSearchParams() + const { checkAuth, setToken } = useAuth() + + useEffect(() => { + const token = params.get('token') + const error = params.get('error') + + if (error) { + router.replace('/login?error=oauth_failed') + return + } + + if (token) { + localStorage.setItem('token', token) + checkAuth().then(() => router.replace('/chat')) + } + }, []) + + return ( +
    +
    + Signing you in... +
    +
    + ) +} \ No newline at end of file diff --git a/frontend/src/app/signup/page.tsx b/frontend/src/app/signup/page.tsx index f7847f4..d65567d 100644 --- a/frontend/src/app/signup/page.tsx +++ b/frontend/src/app/signup/page.tsx @@ -7,7 +7,7 @@ import ProtectedRoute from "../../components/ProtectedRoute" import zxcvbn from "zxcvbn" import { toast } from "sonner" import { Eye, EyeOff } from "lucide-react" - +import OAuthButtons from '@/components/OAuthButtons' const accent = "#b39ddb" // pastel purple const accentGreen = "#39ff14" // neon green @@ -173,7 +173,7 @@ function SignupPageContent() { className="mt-2 border-2 border-black bg-[#39ff14] text-black font-extrabold text-lg py-2 rounded-none transition-all cursor-[url('/custom-cursor-click.svg'),_pointer] hover:bg-[#b39ddb] hover:text-white focus:outline-none" style={{ boxShadow: `4px 4px 0 0 ${accentGreen}` }} > - {loading ? "Signing Up..." : "Sign Up 2"} + {loading ? "Signing Up..." : "Sign Up"}
    + + {/* Floating accent shape bottom right */}
    diff --git a/frontend/src/components/OAuthButtons.tsx b/frontend/src/components/OAuthButtons.tsx new file mode 100644 index 0000000..a8412a7 --- /dev/null +++ b/frontend/src/components/OAuthButtons.tsx @@ -0,0 +1,41 @@ +'use client' +import React from 'react' +import { FaGithub } from 'react-icons/fa' + +const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8080' + +interface Props { + label?: string +} + +export default function OAuthButtons({ label = 'Continue' }: Props) { + return ( +
    +
    + + ) +} From 9b85cc190cf9e5577f32976d19b676fa526ef67a Mon Sep 17 00:00:00 2001 From: vd77-1 Date: Wed, 27 May 2026 01:37:24 +0530 Subject: [PATCH 3/3] fix: guard passport OAuth strategies when env vars are missing --- backend/lib/passport.js | 135 +++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 64 deletions(-) diff --git a/backend/lib/passport.js b/backend/lib/passport.js index 6c5f64b..13e2989 100644 --- a/backend/lib/passport.js +++ b/backend/lib/passport.js @@ -3,81 +3,88 @@ import { Strategy as GoogleStrategy } from 'passport-google-oauth20' import { Strategy as GitHubStrategy } from 'passport-github2' import User from '../models/User.js' -passport.use(new GoogleStrategy( - { - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: `${process.env.BACKEND_URL}/api/v1/auth/google/callback`, - }, - async (_accessToken, _refreshToken, profile, done) => { - try { +const googleConfigured = process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET +const githubConfigured = process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET - let user = await User.findOne({ googleId: profile.id }) +if (googleConfigured) { + passport.use(new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: `${process.env.BACKEND_URL}/api/v1/auth/google/callback`, + }, + async (_accessToken, _refreshToken, profile, done) => { + try { + let user = await User.findOne({ googleId: profile.id }) + if (!user) { + const email = profile.emails?.[0]?.value + user = email ? await User.findOne({ email }) : null - if (!user) { - const email = profile.emails?.[0]?.value - user = email ? await User.findOne({ email }) : null - - if (user) { - - user.googleId = profile.id - if (!user.avatarUrl) user.avatarUrl = profile.photos?.[0]?.value - await user.save() - } else { - - user = await User.create({ - googleId: profile.id, - email: profile.emails?.[0]?.value, - name: profile.displayName || profile.emails?.[0]?.value?.split('@')[0] || 'User', - avatarUrl: profile.photos?.[0]?.value, - password: null, - }) + if (user) { + user.googleId = profile.id + if (!user.avatarUrl) user.avatarUrl = profile.photos?.[0]?.value + await user.save() + } else { + user = await User.create({ + googleId: profile.id, + email: profile.emails?.[0]?.value, + name: profile.displayName || profile.emails?.[0]?.value?.split('@')[0] || 'User', + avatarUrl: profile.photos?.[0]?.value, + password: null, + }) + } } - } - done(null, user) - } catch (err) { - done(err, null) + done(null, user) + } catch (err) { + done(err, null) + } } - } -)) + )) +} else { + console.warn(' Google OAuth not configured. Skipping Google strategy.') +} -passport.use(new GitHubStrategy( - { - clientID: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackURL: `${process.env.BACKEND_URL}/api/v1/auth/github/callback`, - scope: ['user:email'], - }, - async (_accessToken, _refreshToken, profile, done) => { - try { - let user = await User.findOne({ githubId: profile.id }) +if (githubConfigured) { + passport.use(new GitHubStrategy( + { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: `${process.env.BACKEND_URL}/api/v1/auth/github/callback`, + scope: ['user:email'], + }, + async (_accessToken, _refreshToken, profile, done) => { + try { + let user = await User.findOne({ githubId: profile.id }) - if (!user) { - const email = profile.emails?.[0]?.value - user = email ? await User.findOne({ email }) : null + if (!user) { + const email = profile.emails?.[0]?.value + user = email ? await User.findOne({ email }) : null - if (user) { - user.githubId = profile.id - if (!user.avatarUrl) user.avatarUrl = profile.photos?.[0]?.value - await user.save() - } else { - user = await User.create({ - githubId: profile.id, - email: profile.emails?.[0]?.value, - name: profile.displayName || profile.username || profile.emails?.[0]?.value?.split('@')[0] || 'User', - avatarUrl: profile.photos?.[0]?.value, - password: null, - }) + if (user) { + user.githubId = profile.id + if (!user.avatarUrl) user.avatarUrl = profile.photos?.[0]?.value + await user.save() + } else { + user = await User.create({ + githubId: profile.id, + email: profile.emails?.[0]?.value, + name: profile.displayName || profile.username || profile.emails?.[0]?.value?.split('@')[0] || 'User', + avatarUrl: profile.photos?.[0]?.value, + password: null, + }) + } } - } - done(null, user) - } catch (err) { - done(err, null) + done(null, user) + } catch (err) { + done(err, null) + } } - } -)) + )) +} else { + console.warn(' GitHub OAuth not configured. Skipping GitHub strategy.') +} export default passport \ No newline at end of file