diff --git a/README.md b/README.md
index d4d04bd..fdd7d32 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,30 @@
-# Blogify π
+# OG Docs π
-**A modern, collaborative blogging platform built for speed, security, and developer-centric content control.**
+**A modern, real-time collaborative documentation platform built for speed, security, and developer-centric content control.**
## About The Project
-**Blogify** is a full-stack web application designed to streamline the way developers write and share articles. The project focuses on structured content delivery, moving away from standard plain-text storage to a robust, custom-tailored data architecture.
+**OG Docs** is a full-stack web application designed to streamline the way teams and developers write, edit, and share documentation together. The platform emphasizes structured content delivery and real-time collaboration, moving beyond traditional plain-text storage toward a robust, custom-tailored data architecture.
-Built as a high-performance **MERN** application, Blogify provides a seamless writing experience while giving authors granular control over their content's metadata and presentation.
+Built as a high-performance **MERN** application, OG Docs enables multiple collaborators to work simultaneously in a shared workspace, with changes reflected instantly for everyoneβensuring smooth, efficient, and transparent collaboration.
---
## π Key Features
-* **Custom Slate.js Engine:** Uses a custom-built implementation to structure content into JSON format, ensuring consistent styling and portability.
+* **Real-Time Collaborative Workspace:** A dedicated editing area where multiple collaborators can make changes simultaneously, with updates reflected in real time.
+* **Custom Slate.js Engine:** A custom-built implementation that structures content into JSON format for consistent styling and long-term portability.
* **Modern UI/UX:** A clean, responsive interface built with **React 19**, **Tailwind CSS 4.0**, and **CoreUI** components.
-* **Secure Authentication:** Integrated **Google OAuth** for quick and secure user access.
+* **Secure Authentication:** Integrated **Google OAuth** for fast and secure user access.
* **Developer-Friendly Build:** Optimized with **Vite** for near-instant hot module replacement (HMR).
-* **Robust Security:** Hardened backend using **Helmet**, **HPP**, **Rate Limiting**, and **Zod** for schema validation.
+* **Robust Security:** Hardened backend using **Helmet**, **HPP**, **Rate Limiting**, and **Zod** for strict schema validation.
---
## π The Tech Stack
### Frontend
+
* **Library:** React 19
* **Styling:** Tailwind CSS 4.0 & CoreUI
* **Editor:** Slate.js (Custom JSON Content Engine)
@@ -30,12 +32,14 @@ Built as a high-performance **MERN** application, Blogify provides a seamless wr
* **Build Tool:** Vite
### Backend
+
* **Runtime:** Node.js
* **Framework:** Express.js (v5)
* **Authentication:** JWT & Google Auth Library
* **Validation:** Zod
### Database
+
* **Database:** MongoDB
* **ODM:** Mongoose
@@ -43,51 +47,56 @@ Built as a high-performance **MERN** application, Blogify provides a seamless wr
## βοΈ How It Works
-The core logic of Blogify revolves around a structured data pipeline:
-
-1. **The Input:** Authors use a custom editor powered by **Slate.js**. Instead of generating messy HTML, it outputs a clean **JSON tree** representing the document structure.
-2. **The Storage:** This JSON object is validated via **Zod** and stored in **MongoDB** through **Mongoose** models.
-3. **The Rendering:** On the frontend, our custom engine traverses the JSON tree and dynamically "paints" the content using Tailwind-styled React components.
-
+The core logic of OG Docs revolves around a structured, collaborative data pipeline:
+1. **The Input:** Contributors write using a custom editor powered by **Slate.js**. Instead of generating raw HTML, the editor produces a clean **JSON tree** representing the document structure.
+2. **The Collaboration Layer:** Multiple users can edit the same document simultaneously, with real-time updates synchronized across all active sessions.
+3. **The Storage:** The JSON structure is validated using **Zod** and securely stored in **MongoDB** via **Mongoose** models.
+4. **The Rendering:** On the frontend, a custom rendering engine traverses the JSON tree and dynamically paints the content using Tailwind-styled React components.
---
## π¦ Installation & Setup
-Follow these steps to get a copy of Blogify running locally.
+Follow these steps to run OG Docs locally.
### Prerequisites
+
* [Node.js](https://nodejs.org/) (Latest LTS)
-* [MongoDB](https://www.mongodb.com/) account or local instance.
+* [MongoDB](https://www.mongodb.com/) account or local instance
### Steps
-1. **Clone the Repository**
- ```bash
- git clone [https://github.com/shamil-tp/Blogify.git](https://github.com/shamil-tp/Blogify.git)
- cd blogify
- ```
+1. **Clone the Repository**
+
+ ```bash
+ git clone https://github.com/shamil-tp/Blogify.git
+ cd blogify
+ ```
+
+2. **Setup Backend**
+
+ ```bash
+ cd backend
+ npm install
+ ```
+
+ Create a `.env` file in the `backend` folder and add your `MONGODB_URI`, `JWT_SECRET`, and `PORT`.
+
+3. **Setup Frontend**
-2. **Setup Backend**
- ```bash
- cd backend
- npm install
- ```
- Create a `.env` file in the `backend` folder and add your `MONGODB_URI`, `JWT_SECRET`, and `PORT`.
+ ```bash
+ cd ../frontend
+ npm install
+ ```
-3. **Setup Frontend**
- ```bash
- cd ../frontend
- npm install
- ```
+4. **Run Development Servers**
-4. **Run Development Servers**
- * **Backend:** `npm run test` (uses nodemon)
- * **Frontend:** `npm run dev` (uses vite)
+ * **Backend:** `npm run test` (nodemon)
+ * **Frontend:** `npm run dev` (Vite)
-5. **View the App**
- Open `http://localhost:5173` for the frontend.
+5. **View the App**
+ Open `http://localhost:5173` in your browser.
---
@@ -95,6 +104,176 @@ Follow these steps to get a copy of Blogify running locally.
This project was collaboratively designed and developed by:
-* **Shamil** - [GitHub](https://github.com/shamil-tp)
-* **Sinan** - [GitHub](https://github.com/sinanrahman/)
-* **Ranfees** - [GitHub](https://github.com/Ranfees)
\ No newline at end of file
+* **Sinan** β [GitHub](https://github.com/sinanrahman/)
+* **Hana** β [GitHub](https://github.com/Hana-Haris3)
+* **Salih** β [GitHub](https://github.com/salih85)
+
+---
+
+OG Docs is built with collaboration at its coreβdesigned to help teams write, iterate, and ship documentation together, faster and cleaner.
+
+
+
+
+
+
+
+
+
+
+frontend/src/
+βββ collaboration/
+β βββ socket.js
+β βββ ydoc.js
+β βββ awareness.js
+β βββ collabTypes.js
+β
+βββ hooks/
+β βββ useAuth.jsx
+β βββ useCollaboration.js β NEW
+β
+βββ components/
+β βββ GridEditor/
+β βββ GridEditor.jsx
+β βββ withCollaboration.js β NEW
+β βββ TextWidget.jsx
+β βββ ImageWidget.jsx
+β βββ VideoWidget.jsx
+
+
+
+
+
+
+
+
+backend/
+βββ socket/
+β βββ index.js
+β βββ auth.js
+β βββ rooms.js
+β βββ handlers/
+β βββ joinDoc.js
+β βββ syncUpdate.js
+β βββ awareness.js
+β βββ disconnect.js
+β
+βββ collaboration/
+β βββ yjs/
+β β βββ createDoc.js
+β β βββ applyUpdate.js
+β βββ persistence/
+β βββ loadSnapshot.js
+β βββ saveSnapshot.js
+
+
+
+
+
+π€ Teammate A β Backend (Realtime Engine)
+π― Responsibility
+
+Everything related to:
+
+Socket.IO
+
+Yjs document sync
+
+MongoDB snapshots
+
+π Files they touch
+backend/
+βββ socket/
+βββ collaboration/
+βββ models/Blog.js
+
+
+They do NOT touch frontend.
+
+π§ Their Tasks (Very Exact)
+A1 β Socket.IO server
+
+Attach Socket.IO to Express
+
+JWT auth on connection
+
+Rooms per blogId
+
+A2 β Yjs document manager
+
+Create Yjs doc per blog
+
+Load collabSnapshot
+
+Keep docs in memory
+
+A3 β Sync protocol
+
+Receive Yjs updates
+
+Broadcast to room
+
+Prevent echo loops
+
+A4 β Persistence
+
+Save snapshot when:
+
+last user leaves
+
+OR every 30s
+
+π¦ Output
+
+socket.emit("doc:update", update)
+socket.emit("doc:sync", state)
+
+
+
+
+
+π€ Teammate B β Frontend (Quill + Yjs)
+π― Responsibility
+
+Bind Quill editor β Yjs β Socket.IO.
+
+π Files they touch
+frontend/src/
+βββ collaboration/
+β βββ socket.js
+β βββ ydoc.js
+β βββ awareness.js
+β
+βββ components/GridEditor/
+β βββ GridEditor.jsx
+β βββ withCollaboration.js
+
+
+They do NOT touch backend.
+
+π§ Their Tasks (Very Exact)
+B1 β Yjs + Quill binding
+
+Create Y.Doc
+
+Create Y.Text
+
+Bind using y-quill
+
+import { QuillBinding } from 'y-quill'
+
+B2 β Socket sync
+
+Send Yjs updates to backend
+
+Apply remote updates
+
+B3 β Presence & cursors
+
+Use quill-cursors
+
+Show colored cursors per user
+
+π¦ Output
+
+
You have been invited to ${role} the blog "${blog.title || 'Untitled Blog'}".
Access it here: ${inviteLink}
`; + + try { + await sendEmail(email, subject, text, html); + } catch (emailError) { + console.error("Detailed Email Error:", emailError); + return res.status(200).json({ + success: true, + message: `Collaborator added, but email failed: ${emailError.message}. Check your RESEND_API_KEY settings.` + }); + } + + res.status(200).json({ success: true, message: "Invitation sent successfully" }); + } catch (e) { + console.error(e); + res.status(500).json({ message: "Sharing failed", error: e.message }); + } +}; + +exports.removeCollaborator = async (req, res) => { + try { + const { email } = req.body; + const blogId = req.params.postId; + const userId = req.user._id; + + const blog = await Blog.findById(blogId); + + if (!blog) { + return res.status(404).json({ message: "Blog not found" }); + } + + // Only author can remove collaborators + if (blog.author.toString() !== userId.toString()) { + return res.status(403).json({ message: "Only the author can remove collaborators" }); + } + + blog.collaborators = blog.collaborators.filter(c => c.email !== email); + await blog.save(); + + res.status(200).json({ success: true, message: "Collaborator removed successfully" }); + } catch (e) { + console.error(e); + res.status(500).json({ message: "Action failed", error: e.message }); + } +}; \ No newline at end of file diff --git a/backend/index.js b/backend/index.js index f42d254..72fdb14 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,7 +1,9 @@ require("dotenv").config(); const express = require("express"); const cors = require("cors"); -const connectDB = require("./config/db"); +const helmet = require("helmet"); +const rateLimit = require("express-rate-limit"); +const connectDB = require("./config/db"); const authRoutes = require("./routes/authRoutes"); const blogRoutes = require("./routes/blogRoutes"); @@ -9,39 +11,92 @@ const blogRoutes = require("./routes/blogRoutes"); const app = express(); const PORT = process.env.PORT || 3000; -const helmet = require('helmet') -const rateLimit = require('express-rate-limit') -const hpp = require('hpp') +// 1. CORS - MUST BE FIRST to catch all requests and provide headers +app.use( + cors({ + origin: (origin, callback) => { + if (!origin) return callback(null, true); + + const allowedOrigins = [ + "http://localhost:5173", + "http://localhost:5174", + "http://localhost:5175", + process.env.FRONTEND_URL, + "https://og-doc.vercel.app", + ]; + const isAllowed = allowedOrigins.some(ao => ao && ao.replace(/\/$/, '').toLowerCase() === origin.replace(/\/$/, '').toLowerCase()) || + origin.endsWith(".onrender.com") || + origin.includes("ngrok-free") || + origin.endsWith(".vercel.app"); + + if (isAllowed) { + callback(null, true); + } else { + console.error("β CORS Blocked:", origin); + callback(new Error('CORS not allowed for this origin')); + } + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept'], + exposedHeaders: ['Set-Cookie'] + }) +); + +// 2. Security Headers (configured to allow cross-origin communication) +app.use(helmet({ + crossOriginOpenerPolicy: { policy: "same-origin-allow-popups" }, + crossOriginResourcePolicy: { policy: "cross-origin" }, + crossOriginEmbedderPolicy: false, + contentSecurityPolicy: false, + referrerPolicy: { policy: "strict-origin-when-cross-origin" }, +})); + +// 3. Rate Limiting (moved below CORS so preflight OPTIONS requests aren't blocked silently) const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, - limit: 100, - standardHeaders: 'draft-8', - legacyHeaders: false, - ipv6Subnet: 56, + windowMs: 15 * 60 * 1000, + limit: 500, // Increased limit for testing + standardHeaders: 'draft-8', + legacyHeaders: false, }) - app.use(limiter) -app.use(helmet()) - -app.use( - cors({ - origin: ["http://localhost:5173", "http://localhost:5174", "http://localhost:5175",process.env.FRONTEND_URL,process.env.LIBRARY_URL], - credentials:true - }) -); +const hpp = require('hpp') +const http = require("http") +const initSocket = require("./socket"); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(hpp()) connectDB() +// Health check endpoint +app.get('/health', (req, res) => { + res.status(200).json({ status: 'ok', message: 'Server is running' }); +}); + +// Root endpoint +app.get('/', (req, res) => { + res.status(200).json({ + message: 'ogDoc API Server', + version: '1.0.0', + endpoints: { + auth: '/api/auth', + blog: '/api/blog' + } + }); +}); + app.use("/api/auth", authRoutes); -app.use("/api", blogRoutes); +app.use("/api/blog", blogRoutes); if (require.main === module) { - app.listen(PORT); -} + const server = http.createServer(app); + initSocket(server); + + console.log(`Attempting to listen on port ${PORT}`); + server.listen(PORT, () => console.log(`Server running on ${PORT}`)); +} module.exports = app; \ No newline at end of file diff --git a/backend/middlewares/authMiddleware.js b/backend/middlewares/authMiddleware.js index d567e8d..1ec8fa1 100644 --- a/backend/middlewares/authMiddleware.js +++ b/backend/middlewares/authMiddleware.js @@ -15,12 +15,10 @@ const protect = async (req, res, next) => { next(); } catch (error) { console.log("Auth Middleware: Token Verification Failed:", error.message); - res.status(401).json({ message: "Not authorized" }); + return res.status(401).json({ message: "Not authorized" }); } - } - - if (!token) { - res.status(401).json({ message: "Not authorized, no token" }); + } else { + return res.status(401).json({ message: "Not authorized, no token" }); } }; diff --git a/backend/models/Blog.js b/backend/models/Blog.js index f80246e..3246292 100644 --- a/backend/models/Blog.js +++ b/backend/models/Blog.js @@ -1,3 +1,34 @@ +// const mongoose = require('mongoose') + +// const blogSchema = new mongoose.Schema( +// { +// author: { +// type: mongoose.Schema.Types.ObjectId, +// ref: 'User', +// required: true +// }, +// title: { +// type: String, +// required: true, +// trim: true +// }, +// slug: { +// type: String, +// required: true, +// unique: true +// }, +// content: { +// type: Array, +// required: true, +// default: [] +// } +// }, +// { timestamps: true } +// ) + +// module.exports = mongoose.model('Blog', blogSchema) + + const mongoose = require('mongoose') const blogSchema = new mongoose.Schema( @@ -7,21 +38,55 @@ const blogSchema = new mongoose.Schema( ref: 'User', required: true }, + title: { type: String, - required: true, - trim: true + trim: true, + required: function () { + return this.published === true + } }, slug: { type: String, required: true, unique: true }, + content: { type: Array, - required: true, default: [] - } + }, + published: { + type: Boolean, + default: false + }, + + // Yjs snapshot for collaboration + collabSnapshot: { + type: Buffer, // store encoded Yjs state + default: null + }, + + // Optional: track who is currently editing + activeEditors: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + } + ], + + // Collaboration versioning (future-proof) + collabVersion: { + type: Number, + default: 1 + }, + + collaborators: [ + { + email: { type: String, required: true }, + role: { type: String, enum: ['view', 'edit'], default: 'view' } + } + ] }, { timestamps: true } ) diff --git a/backend/package-lock.json b/backend/package-lock.json index 87a95d9..9d9dbe6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,7 +20,12 @@ "jsonwebtoken": "^9.0.3", "mongoose": "^9.1.5", "nanoid": "^3.3.11", + "nodemailer": "^8.0.1", + "resend": "^6.9.2", "slugify": "^1.6.6", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", + "yjs": "^13.6.29", "zod": "^4.3.6" }, "devDependencies": { @@ -78,6 +83,36 @@ "node": ">=14" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.2.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz", + "integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -195,6 +230,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -553,6 +597,91 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/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/engine.io/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/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -665,6 +794,12 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -1213,6 +1348,16 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors β€", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -1289,6 +1434,27 @@ "node": ">=18.0.0" } }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors β€", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -1615,6 +1781,15 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", @@ -1759,6 +1934,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postal-mime": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz", + "integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==", + "license": "MIT-0" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1840,6 +2021,27 @@ "node": ">=8.10.0" } }, + "node_modules/resend": { + "version": "6.9.2", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.9.2.tgz", + "integrity": "sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.3", + "svix": "1.84.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -2093,6 +2295,105 @@ "node": ">=8.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/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/socket.io/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/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -2102,6 +2403,16 @@ "memory-pager": "^1.0.2" } }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -2220,6 +2531,16 @@ "node": ">=4" } }, + "node_modules/svix": { + "version": "1.84.1", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz", + "integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2285,6 +2606,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2294,6 +2621,19 @@ "node": ">= 0.8" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2446,6 +2786,52 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yjs": { + "version": "13.6.29", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz", + "integrity": "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors β€", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/backend/package.json b/backend/package.json index a62d360..5733a4d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,7 +23,12 @@ "jsonwebtoken": "^9.0.3", "mongoose": "^9.1.5", "nanoid": "^3.3.11", + "nodemailer": "^8.0.1", + "resend": "^6.9.2", "slugify": "^1.6.6", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", + "yjs": "^13.6.29", "zod": "^4.3.6" }, "devDependencies": { diff --git a/backend/routes/blogRoutes.js b/backend/routes/blogRoutes.js index 8a9d114..7805c6e 100644 --- a/backend/routes/blogRoutes.js +++ b/backend/routes/blogRoutes.js @@ -1,17 +1,38 @@ const express = require("express"); const { protect } = require("../middlewares/authMiddleware"); -const { addBlog, getBlog, getUserBlogs, deleteUserPost, getBlogById, updateBlog } = require("../controllers/blogController"); +const { + addBlog, + getBlog, + getUserBlogs, + deleteUserPost, + getBlogById, + updateBlog, + createDraft, + shareBlog, + removeCollaborator +} = require("../controllers/blogController"); const router = express.Router(); -router.get("/viewblog/:slug", getBlog); +/** + * ALL ROUTES HERE ARE PREFIXED WITH /api/blog (via index.js) + */ + +// Debugging +router.get("/ping", (req, res) => res.json({ message: "Blog routes active" })); -router.post("/blog/postblog", protect, addBlog); -router.get("/blog/user-blogs",protect,getUserBlogs) -router.get("/blog/deleteblog/:postId",protect,deleteUserPost) -router.get("/blog/:postId",getBlogById) -router.post("/blog/updateblog/:postId", protect, updateBlog); +// MAIN SHARE ROUTE +router.post("/share-post/:postId", protect, shareBlog); +router.post("/remove-collaborator/:postId", protect, removeCollaborator); +// Other Routes +router.post("/create-draft", protect, createDraft); +router.get("/user-blogs", protect, getUserBlogs); +router.get("/deleteblog/:postId", protect, deleteUserPost); +router.get("/:postId", getBlogById); +router.post("/updateblog/:postId", protect, updateBlog); +router.post("/postblog", protect, addBlog); +router.get("/viewblog/:slug", getBlog); module.exports = router; diff --git a/backend/socket/auth.js b/backend/socket/auth.js new file mode 100644 index 0000000..80f153a --- /dev/null +++ b/backend/socket/auth.js @@ -0,0 +1,26 @@ +// socket/auth.js +const jwt = require("jsonwebtoken"); +const User = require("../models/User"); + +const authSocket = async (socket, next) => { + try { + const token = socket.handshake.auth?.token; + + if (token) { + // JWT user + const decoded = jwt.verify(token, process.env.JWT_SECRET); + socket.user = await User.findById(decoded.userId).select("-password"); + } else { + // Guest user (anyone with link) + const ip = socket.handshake.address.replace(/[:.]/g, ""); // sanitize IP + socket.user = { _id: `guest-${ip}-${socket.id}`, name: `Guest-${ip}` }; + } + + next(); + } catch (err) { + console.log("Socket Auth Error:", err.message); + next(new Error("Authentication failed")); + } +}; + +module.exports = authSocket; diff --git a/backend/socket/handlers/awareness.js b/backend/socket/handlers/awareness.js new file mode 100644 index 0000000..16ec4a9 --- /dev/null +++ b/backend/socket/handlers/awareness.js @@ -0,0 +1,11 @@ +const rooms = require("../rooms"); + +module.exports = (io, socket, { blogId, awareness }) => { + if (!rooms[blogId]) return; + + // Broadcast awareness (cursor, selection) + socket.to(blogId).emit("doc:awareness", { + userId: socket.user._id, + awareness + }); +}; diff --git a/backend/socket/handlers/disconnect.js b/backend/socket/handlers/disconnect.js new file mode 100644 index 0000000..c50b445 --- /dev/null +++ b/backend/socket/handlers/disconnect.js @@ -0,0 +1,22 @@ +const { leaveRoom, rooms } = require("../rooms"); +const { saveSnapshot } = require("../../collaboration/persistence/saveSnapshot"); +const { docs } = require("../../collaboration/yjs/createDoc"); + +const disconnect = async (io, socket) => { + rooms.forEach(async (socketsSet, blogId) => { + if (socketsSet.has(socket.id)) { + leaveRoom(blogId, socket); + + // if last user left, persist snapshot + if (!rooms.has(blogId)) { + const ydoc = docs.get(blogId); + if (ydoc) { + await saveSnapshot(blogId, ydoc); + // Note: docs.delete(blogId) should probably happen here if we want to free memory + } + } + } + }); +}; + +module.exports = { disconnect }; diff --git a/backend/socket/handlers/joinDoc.js b/backend/socket/handlers/joinDoc.js new file mode 100644 index 0000000..c28bc94 --- /dev/null +++ b/backend/socket/handlers/joinDoc.js @@ -0,0 +1,32 @@ +const Y = require("yjs"); +const { createDoc } = require("../../collaboration/yjs/createDoc"); +const { joinRoom } = require("../rooms"); + +const joinDoc = async (io, socket, blogId) => { + socket.join(blogId); + joinRoom(blogId, socket); + + // Load or create Yjs document + const ydoc = await createDoc(blogId); + + // Send current Yjs state to this socket + const state = Y.encodeStateAsUpdate(ydoc); + socket.emit("doc:sync", state); + + // Build users list WITH socketId + const usersInRoom = Array.from( + io.sockets.adapter.rooms.get(blogId) || [] + ).map((id) => { + const s = io.sockets.sockets.get(id); + return { + name: s.user?.name || "Anonymous", + email: s.user?.email || null, + socketId: id, + }; + }); + + // Emit users to everyone in the room + io.to(blogId).emit("doc:users", usersInRoom); +}; + +module.exports = { joinDoc }; diff --git a/backend/socket/handlers/syncUpdate.js b/backend/socket/handlers/syncUpdate.js new file mode 100644 index 0000000..34df314 --- /dev/null +++ b/backend/socket/handlers/syncUpdate.js @@ -0,0 +1,16 @@ +// socket/handlers/syncUpdate.js +const { applyUpdate } = require("../../collaboration/yjs/applyUpdate"); +const { docs } = require("../../collaboration/yjs/createDoc"); + +const syncUpdate = (io, socket, blogId, update) => { + const ydoc = docs.get(blogId); + if (!ydoc) return; + + // Apply update to Yjs doc + applyUpdate(ydoc, update); + + // Broadcast to everyone else + socket.to(blogId).emit("doc:update", update); +}; + +module.exports = { syncUpdate }; diff --git a/backend/socket/index.js b/backend/socket/index.js new file mode 100644 index 0000000..ac29123 --- /dev/null +++ b/backend/socket/index.js @@ -0,0 +1,96 @@ +const { Server } = require("socket.io"); +const authSocket = require("./auth"); +const { joinDoc } = require("./handlers/joinDoc"); +const { syncUpdate } = require("./handlers/syncUpdate"); +const { disconnect } = require("./handlers/disconnect"); + +const initSocket = (server) => { + const io = new Server(server, { + cors: { + origin: [ + "http://localhost:5173", + "http://localhost:5174", + "http://localhost:5175", + "http://192.168.29.93:5173", + ], + credentials: true + }, + }); + + io.use(authSocket); + + io.on("connection", (socket) => { + console.log("Socket connected:", socket.id, socket.user?.name); + + // ================= DOC COLLAB ================= + socket.on("doc:join", async (blogId) => { + await joinDoc(io, socket, blogId); + }); + + socket.on("doc:update", async (data) => { + await syncUpdate(io, socket, data.blogId, data.update); + }); + + socket.on("doc:awareness", ({ blogId, awareness }) => { + socket.to(blogId).emit("doc:awareness", awareness); + }); + + // ================= VIDEO CALL ================= + socket.on("call:join", (blogId) => { + socket.join(`call-${blogId}`); + socket.to(`call-${blogId}`).emit("call:user-joined", { + socketId: socket.id, + }); + }); + + socket.on("call:signal", ({ to, signal }) => { + io.to(to).emit("call:signal", { + from: socket.id, + signal, + }); + }); + + socket.on("call:leave", (blogId) => { + socket.leave(`call-${blogId}`); + socket.to(`call-${blogId}`).emit("call:user-left", socket.id); + }); + + + // INVITE + socket.on("call:invite", ({ blogId, to }) => { + io.to(to).emit("call:incoming", { + from: socket.id, + blogId, + user: socket.user, + }); + }); + + // KICK + socket.on("doc:kick", ({ blogId, socketId }) => { + io.to(socketId).emit("doc:kicked"); + }); + + // ACCEPT + socket.on("call:accept", ({ to, blogId }) => { + io.to(to).emit("call:accepted", { + from: socket.id, + blogId, + }); + }); + + // DECLINE + socket.on("call:decline", ({ to }) => { + io.to(to).emit("call:declined", { + from: socket.id, + }); + }); + + + socket.on("disconnect", () => { + disconnect(io, socket); + console.log("Socket disconnected:", socket.id); + }); + }); +}; + +module.exports = initSocket; diff --git a/backend/socket/registerHandlers.js b/backend/socket/registerHandlers.js new file mode 100644 index 0000000..22a96eb --- /dev/null +++ b/backend/socket/registerHandlers.js @@ -0,0 +1,20 @@ +// const joinDoc = require("./handlers/joinDoc"); +// const syncUpdate = require("./handlers/syncUpdate"); +// const awareness = require("./handlers/awareness"); +// const disconnect = require("./handlers/disconnect"); + +// module.exports = (io, socket) => { +// socket.on("doc:join", (data) => joinDoc(io, socket, data)); +// socket.on("doc:update", (data) => syncUpdate(io, socket, data)); +// socket.on("doc:awareness", (data) => awareness(io, socket, data)); +// socket.on("disconnect", () => disconnect(io, socket)); +// }; +const joinDoc = require('./handlers/joinDoc'); +const syncUpdate = require('./handlers/syncUpdate'); +const disconnect = require('./handlers/disconnect'); + +module.exports = (io, socket) => { + socket.on('doc:join', (data) => joinDoc(io, socket, data)); + socket.on('sync:update', (data) => syncUpdate(io, socket, data)); + socket.on('disconnect', () => disconnect(io, socket)); +}; diff --git a/backend/socket/rooms.js b/backend/socket/rooms.js new file mode 100644 index 0000000..8096759 --- /dev/null +++ b/backend/socket/rooms.js @@ -0,0 +1,14 @@ +const rooms = new Map(); // blogId -> Set of socket ids + +const joinRoom = (blogId, socket) => { + if (!rooms.has(blogId)) rooms.set(blogId, new Set()); + rooms.get(blogId).add(socket.id); +}; + +const leaveRoom = (blogId, socket) => { + if (!rooms.has(blogId)) return; + rooms.get(blogId).delete(socket.id); + if (rooms.get(blogId).size === 0) rooms.delete(blogId); +}; + +module.exports = { joinRoom, leaveRoom, rooms }; diff --git a/backend/utils/emailService.js b/backend/utils/emailService.js new file mode 100644 index 0000000..4efcf2b --- /dev/null +++ b/backend/utils/emailService.js @@ -0,0 +1,37 @@ +const { Resend } = require("resend"); + +const resend = new Resend(process.env.RESEND_API_KEY); + +const sendEmail = async (to, subject, text, html) => { + console.log("Resend Service Attempt:", { + apiKey: process.env.RESEND_API_KEY ? "DETECTED" : "MISSING", + to + }); + + if (!process.env.RESEND_API_KEY) { + throw new Error("Missing RESEND_API_KEY environment variable"); + } + + try { + const { data, error } = await resend.emails.send({ + from: "ogDoc@shamiltp.me", + to, + subject, + text, + html, + }); + + if (error) { + console.error("Resend API Error:", error); + throw new Error(error.message); + } + + console.log("Email sent successfully via Resend:", data.id); + return data; + } catch (error) { + console.error("Resend execution error:", error); + throw error; + } +}; + +module.exports = { sendEmail }; diff --git a/backend/utils/tokenUtil.js b/backend/utils/tokenUtil.js index df16a2e..bd42159 100644 --- a/backend/utils/tokenUtil.js +++ b/backend/utils/tokenUtil.js @@ -1,7 +1,7 @@ const jwt = require("jsonwebtoken"); const generateAccessToken = (userId) => { - return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: "15m" }); + return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: "24h" }); }; const generateRefreshToken = (userId) => { diff --git a/diagnose-404.ps1 b/diagnose-404.ps1 new file mode 100644 index 0000000..96474cb --- /dev/null +++ b/diagnose-404.ps1 @@ -0,0 +1,69 @@ +# 404 Error Diagnostic Script +# Run this to help identify what's causing the 404 error + +Write-Host "ogDoc 404 Error Diagnostic Tool" -ForegroundColor Cyan +Write-Host "===================================" -ForegroundColor Cyan +Write-Host "" + +# Test 1: Check Backend Health +Write-Host "1. Testing Backend Health..." -ForegroundColor Yellow +try { + $backendHealth = Invoke-RestMethod -Uri "https://ogdoc.onrender.com/health" -Method Get -TimeoutSec 10 + if ($backendHealth.status -eq "ok") { + Write-Host " OK: Backend is healthy!" -ForegroundColor Green + Write-Host " Response: $($backendHealth | ConvertTo-Json -Compress)" -ForegroundColor Gray + } else { + Write-Host " Warning: Backend returned unexpected response" -ForegroundColor Yellow + } +} catch { + Write-Host " Error: Backend health check failed!" -ForegroundColor Red + Write-Host " Message: $($_.Exception.Message)" -ForegroundColor Red +} +Write-Host "" + +# Test 2: Check Local Backend +Write-Host "2. Testing Local Backend (Port 5001)..." -ForegroundColor Yellow +try { + $localBackend = Invoke-RestMethod -Uri "http://localhost:5001/health" -Method Get -TimeoutSec 2 + Write-Host " OK: Local backend is running on port 5001" -ForegroundColor Green +} catch { + Write-Host " Info: Local backend not running on 5001" -ForegroundColor Gray +} +Write-Host "" + +# Test 3: Check Frontend +Write-Host "3. Testing Frontend..." -ForegroundColor Yellow +try { + $frontendResponse = Invoke-WebRequest -Uri "https://ogdoc-1.onrender.com/" -Method Get -TimeoutSec 10 -UseBasicParsing + Write-Host " OK: Frontend is accessible!" -ForegroundColor Green + Write-Host " Status Code: $($frontendResponse.StatusCode)" -ForegroundColor Gray +} catch { + Write-Host " Error: Frontend failed!" -ForegroundColor Red + Write-Host " Message: $($_.Exception.Message)" -ForegroundColor Red +} +Write-Host "" + +# Test 4: Check if local dev server is running +Write-Host "4. Checking Local Dev Server (Port 5173)..." -ForegroundColor Yellow +try { + $localResponse = Invoke-WebRequest -Uri "http://localhost:5173/" -Method Get -TimeoutSec 2 -UseBasicParsing + Write-Host " OK: Local dev server is running on port 5173" -ForegroundColor Green +} catch { + Write-Host " Info: Local dev server not running" -ForegroundColor Gray +} +Write-Host "" + +Write-Host "Summary & Next Steps:" -ForegroundColor Cyan +Write-Host "========================" -ForegroundColor Cyan +Write-Host "" +Write-Host "If you're seeing 404 errors, please provide:" -ForegroundColor White +Write-Host "1. The EXACT URL showing the 404 (from network tab)" -ForegroundColor White +Write-Host "2. Check if VITE_BACKEND_URL in frontend/.env matches your intended backend" -ForegroundColor White +Write-Host "" +Write-Host "Common Issues:" -ForegroundColor Yellow +Write-Host "- Testing locally without rebuilding: Run 'npm run dev' in frontend folder" -ForegroundColor Gray +Write-Host "- Render still deploying: Wait 2-3 minutes after git push" -ForegroundColor Gray +Write-Host "- Browser cache: Hard refresh with Ctrl+Shift+R" -ForegroundColor Gray +Write-Host "- Missing render.json: Ensure it's in your build output" -ForegroundColor Gray +Write-Host "" + diff --git a/frontend/index.html b/frontend/index.html index 089e053..14c7e9d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,10 +2,13 @@ - + + + + -