first commit
This commit is contained in:
commit
ec29463fb6
132
.gitignore
vendored
Normal file
132
.gitignore
vendored
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# ---> Node
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
124
SOCIALBUDDY_INTEGRATION_DOCUMENT.md
Normal file
124
SOCIALBUDDY_INTEGRATION_DOCUMENT.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# 🤖 VG Products — Integration & Features Documentation
|
||||||
|
|
||||||
|
Welcome to the official documentation for **VG Products**. This document provides an in-depth explanation of all integration features, architectural components, and API capabilities currently implemented in the platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📑 Table of Contents
|
||||||
|
1. [Platform Overview](#-platform-overview)
|
||||||
|
2. [User Authentication & Security](#-user-authentication--security)
|
||||||
|
3. [Social Media Connectivity (Meta/Instagram)](#-social-media-connectivity-metainstagram)
|
||||||
|
4. [Content & Engagement Management](#-content--engagement-management)
|
||||||
|
5. [AI-Powered Automation Suite](#-ai-powered-automation-suite)
|
||||||
|
6. [Payment & Subscription Infrastructure](#-payment--subscription-infrastructure)
|
||||||
|
7. [API Reference List](#-api-reference-list)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 Platform Overview
|
||||||
|
VG Products is an all-in-one social media management and automation platform designed to help businesses manage their Instagram interaction through AI-driven insights and automated engagement. It bridges the gap between manual management and pure automation by providing a suite of tools for content monitoring and automated response systems.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 User Authentication & Security
|
||||||
|
|
||||||
|
### 🔹 Local Authentication
|
||||||
|
- **JWT Implementation**: Secure token-based authentication using `jsonwebtoken`.
|
||||||
|
- **Encryption**: Password hashing via `bcrypt` for secure user data storage.
|
||||||
|
- **Session Management**: Configurable JWT expiration (default 30 days).
|
||||||
|
|
||||||
|
### 🔹 Google OAuth Integration
|
||||||
|
- **One-Tap Login**: Seamless authentication via Google using `passport-google-oauth20`.
|
||||||
|
- **Auto-Provisioning**: Automatic user record creation upon successful third-party authentication.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Social Media Connectivity (Meta/Instagram)
|
||||||
|
|
||||||
|
### 🔹 Facebook Login & Instagram Graph API
|
||||||
|
VG Products uses the **Instagram Graph API** to interact with professional accounts.
|
||||||
|
- **Short-Lived to Long-Lived Token Exchange**: Automatically upgrades user access tokens to 60-day "Long-Lived" tokens for persistent connectivity.
|
||||||
|
- **Granular Permissions**: Requests only necessary scopes (`instagram_basic`, `instagram_manage_comments`, `pages_show_list`, etc.).
|
||||||
|
|
||||||
|
### 🔹 Channel & Account Management
|
||||||
|
- **Page Listing**: Fetches all Facebook Pages owned by the user.
|
||||||
|
- **IG Account Linking**: Identifies which Instagram Business accounts are linked to specific Facebook Pages.
|
||||||
|
- **Connection Persistence**: Securely stores page-level tokens for background automation tasks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Content & Engagement Management
|
||||||
|
|
||||||
|
### 🔹 Media Monitoring
|
||||||
|
- **Post Retrieval**: Fetches recent Instagram media (images, videos, reels) with metadata like captions, timestamps, and permalinks.
|
||||||
|
- **Detailed View**: Access specific media metrics and full-resolution content.
|
||||||
|
|
||||||
|
### 🔹 Comprehensive Comment System
|
||||||
|
- **Real-time Interaction**: Fetch comments for any Instagram post.
|
||||||
|
- **Standard Actions**: Support for replying to comments, deleting unwanted content, and toggling visibility (Hide/Unhide).
|
||||||
|
- **Engagement Tracking**: View comment counts and commenter details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 AI-Powered Automation Suite
|
||||||
|
|
||||||
|
The core value proposition of VG Products is its intelligent automation engine.
|
||||||
|
|
||||||
|
### 🔹 Smart Auto-Reply Engine
|
||||||
|
- **Context-Aware Processing**: Analyzes the post caption, the user's account bio, and the comment text to generate a relevant reply.
|
||||||
|
- **AI Generation**: Leverages advanced LLM logic to craft human-like responses that align with the brand's voice.
|
||||||
|
- **Intelligent Filtering**:
|
||||||
|
- **Self-Exclusion**: Automatically skips comments made by the account itself.
|
||||||
|
- **Time Window**: Defaults to processing comments within the last 24 hours to ensure relevance.
|
||||||
|
- **Duplicate Prevention**: Tracks `repliedComments` to avoid double-responding to the same user.
|
||||||
|
|
||||||
|
### 🔹 Automation Governance
|
||||||
|
- **Rate Limiting**: Built-in delays between replies to comply with Meta's rate limits and prevent "bot-like" behavior flags.
|
||||||
|
- **History Management**: Users can view automation summaries (Processed vs. Replied) and clear their reply history.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💳 Payment & Subscription Infrastructure
|
||||||
|
|
||||||
|
Powered by **Stripe**, VG Products features a robust monetization layer.
|
||||||
|
|
||||||
|
### 🔹 Tiered Subscription Plans
|
||||||
|
- **Basic, Standard, & Premium**: Flexible pricing tiers available in both **Monthly** and **Yearly** billing cycles.
|
||||||
|
- **Flexible Upgrades**: Managed through Stripe Checkout sessions.
|
||||||
|
|
||||||
|
### 🔹 Free Trial Activation
|
||||||
|
- **7-Day Trial**: One-time activation for new users to explore premium features with zero upfront cost.
|
||||||
|
- **Automated Expiry**: Features are automatically restricted once the trial period ends.
|
||||||
|
|
||||||
|
### 🔹 Billing Management
|
||||||
|
- **Self-Service Portal**: Direct integration with the **Stripe Billing Portal**, allowing users to manage payment methods (last 4 digits overview), download invoices, and cancel subscriptions without contacting support.
|
||||||
|
- **Webhook Synchronization**: Real-time updates for subscription status through Stripe Webhooks (Payment Succeeded, Failed, Cancelled).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 API Reference List
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- `GET /api/auth/status`: Check connectivity status.
|
||||||
|
- `GET /api/auth/login`: Initiate Facebook OAuth.
|
||||||
|
- `POST /api/auth/disconnect`: Revoke social connections.
|
||||||
|
|
||||||
|
### Social Meta-Data
|
||||||
|
- `GET /api/social/channels`: List available Facebook Pages.
|
||||||
|
- `POST /api/social/connect`: Link a specific Instagram Business account.
|
||||||
|
- `GET /api/social/media`: Fetch Instagram posts.
|
||||||
|
|
||||||
|
### Comments & Automation
|
||||||
|
- `POST /api/social/comments/:id/reply`: Manual reply.
|
||||||
|
- `DELETE /api/social/comments/:id`: Remove comment.
|
||||||
|
- `POST /api/social/automation/auto-reply`: Start the AI automation cycle.
|
||||||
|
- `GET /api/social/automation/replied-comments`: View reply history.
|
||||||
|
|
||||||
|
### Payments
|
||||||
|
- `POST /api/payment/create-checkout-session`: Start a subscription buy.
|
||||||
|
- `POST /api/payment/activate-trial`: Start 7-day trial.
|
||||||
|
- `POST /api/payment/create-portal-session`: Open Stripe Billing Portal.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Documentation Version: 1.0.0*
|
||||||
|
*Last Updated: 2026-02-20*
|
||||||
13
config/db.js
Normal file
13
config/db.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const connectDB = async () => {
|
||||||
|
try {
|
||||||
|
await mongoose.connect(process.env.MONGODB_URI, { dbName: "VG Products" });
|
||||||
|
console.log("✅ MongoDB connected");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ MongoDB connection error:", err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { connectDB };
|
||||||
51
config/passport.js
Normal file
51
config/passport.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const passport = require("passport");
|
||||||
|
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
||||||
|
const { getPool } = require("./db.js");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 🧠 Save user to DB or fetch if exists
|
||||||
|
async function findOrCreateUser(profile, provider) {
|
||||||
|
const email = profile.emails?.[0]?.value || `${profile.id}@facebook.com`;
|
||||||
|
const name = profile.displayName;
|
||||||
|
const pool = await getPool();
|
||||||
|
|
||||||
|
const [existing] = await pool.execute(
|
||||||
|
"SELECT * FROM tbl_userlogin WHERE email = ? LIMIT 1",
|
||||||
|
[email]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.length) return existing[0];
|
||||||
|
|
||||||
|
const userid = uuidv4();
|
||||||
|
|
||||||
|
await pool.execute(
|
||||||
|
"INSERT INTO tbl_userlogin (userid, name, email, password, provider) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
[userid, name, email, null, provider]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { userid, name, email, provider };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Google
|
||||||
|
passport.use(
|
||||||
|
new GoogleStrategy(
|
||||||
|
{
|
||||||
|
clientID: "657565817895-sqmh7bd4cef3j7c7cmuno1tpbhg7fjbo.apps.googleusercontent.com",
|
||||||
|
clientSecret: "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",
|
||||||
|
callbackURL: `https://api.VG Products.co/api/auth/google/callback`,
|
||||||
|
},
|
||||||
|
async (accessToken, refreshToken, profile, done) => {
|
||||||
|
const user = await findOrCreateUser(profile, "google");
|
||||||
|
done(null, user);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 🔹 Serialize/deserialize
|
||||||
|
passport.serializeUser((user, done) => done(null, user));
|
||||||
|
passport.deserializeUser((user, done) => done(null, user));
|
||||||
|
|
||||||
|
module.exports = passport;
|
||||||
62
data/store.json
Normal file
62
data/store.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"info@metatroncubesolutions.com": {
|
||||||
|
"longUserToken": "EAAH1s4xuDg8BQDjRlZACAb7BIoBwHOKMYZCbL4agvmtcRWyAouYZCJjg1MSQJr7O0VsnL91VnvJOrB2ZC2N4ZBZBpguGYSHgJYhTzO59fJ0H6RwllZAapjReBnjSWZAoqOHBsh5zd2XUdHM3ANahOhrwCOaAooZAd3w2kLZAsOJe7YmuwHOyxZBvsgYzWu1gam5tvHadHCvji9sSnW1FpSI",
|
||||||
|
"longUserTokenExpiresAt": null,
|
||||||
|
"pageId": "631970130002549",
|
||||||
|
"pageToken": "EAAH1s4xuDg8BQFqgeSlB3I3A3f4hZBSZCbL6PtIUFWRhIeTZBxBZAM7unQtBlNt3orBaBYNIBzyw3wDAqqEV19WKsicDZCMb2dhmCGmPpgnCd2fha7GIzUWoBz9bGlq6EF3XO0WeChfjFRM04eErhceIVVdL20SZCRj9a08ML6faYZBeX4iVNX6nwry4tZCLmgAAc7lY8yX5qNcVVUX1Vqoe",
|
||||||
|
"igUserId": "17841474871998063"
|
||||||
|
},
|
||||||
|
"null": {
|
||||||
|
"longUserToken": "EAAH1s4xuDg8BQOioF1dZCz50GaZC2Rzw1ZCGLj1RQ06sNspbZBnh3O6xnpzgd4q9PuE41Wxw20hDSKKUsHrZBJ3EFoJDbMTNsQbR6vrsUylbLsb41ZBAZAXwVyMU5LfauZAxAm488Qe8v54sHZB7j0s8QroFE2PClmxqvZBEsWvmpTx73ZATbyTjnE6ZBpt8oi2IwkT5",
|
||||||
|
"longUserTokenExpiresAt": null,
|
||||||
|
"pageId": "356453210890355",
|
||||||
|
"pageToken": "EAAH1s4xuDg8BQZAOdlPaZAyTZC1HMwjFyamNVcfvw5JWNgnYgZBJBaSRVd7EZB6uyy8DqmQjTMLQglXJhoIyATRagVn0ynkZA1oLtio8hjW5DGPUPCsaMmyOcdtzpXyY72ARCASwD4gJNMjKKuBq3vZCG2TcWUpw2N2ZBOqZAFELUCrNQ2gZAGisVNipOR4I2BTW3o8Ljk",
|
||||||
|
"igUserId": "17841467507578923"
|
||||||
|
},
|
||||||
|
"manesh@gmail.com": {
|
||||||
|
"longUserToken": "EAAH1s4xuDg8BQMcvltO48gREI4t0wEKeej2ehL1Kel3ZAU8nwfAUZBakiZCzIILyMm9fHX4aejCVKHtV17s2VJqLawgjhw6W1zeVtWnOZCHSiXtWio7oZCxzf6iH8m09ogAF4PsjnM1cvOBGazpf3525XhEYELnoJekDVeRYpDvOZCC2yn8OecbxgzhFM4FH4R",
|
||||||
|
"longUserTokenExpiresAt": null
|
||||||
|
},
|
||||||
|
"vijay@gmail.com": {
|
||||||
|
"longUserToken": "EAAH1s4xuDg8BQHHioWnhkoLTcrUCsohfVnZC6jUrckykslwpoZAnOXRAA5TCPCNPR3aU7TNap4VRDQwGhriUx1CIY0aCEiExkyPxCHWRS6D2dbJyz6ltFnhkPlyPaW5v7BKPZACiVVg8A7tP3sAmaUZBpetRg6ACESD8FyWmwTVQHPfpYs2uasfPme1Y3eCj",
|
||||||
|
"longUserTokenExpiresAt": null,
|
||||||
|
"pageId": "631970130002549",
|
||||||
|
"pageToken": "EAAH1s4xuDg8BQM3K7uiQVWdktgIFO4hjoCJSdmExSb0eQnF0eOKgxzeIJkZCGIAGb3JjeuXu3oGHTb0tSRJT3K3JZAGAbpcliCJccR56oi6epz31ZCOBYDEZAq3ClqcSfobxNC0Fwond0qPZAQZABfglE9z5iHZAov22WvNLaOO07TG9hWG3WADHexEQtutraxYfR4b",
|
||||||
|
"igUserId": "17841474871998063"
|
||||||
|
},
|
||||||
|
"partner@gmail.com": {
|
||||||
|
"longUserToken": "EAAH1s4xuDg8BQM48InpqtHcgXpnfnssHYTFpZBhR8ZAgbt1BVSOPmzpN2LBjKmoskfkGZBMUkDPNDc0ctVwMWrNUoeCJuRDReeoWzakYzvRhPJuBeUxoaZCmDcupPZCwpFsXkeh9hFtaYFz2hbS3ZCVtFc84fPuwTTYH6AL7WV3grQyOxGUTZAXh6b597D6",
|
||||||
|
"longUserTokenExpiresAt": null,
|
||||||
|
"pageId": "631970130002549",
|
||||||
|
"pageToken": "EAAH1s4xuDg8BQDGwkCZAZAsq3hCxnYUhZClRYl8DqKf0qi44iWtUW6FaD0wZBDz36esp7fpQMVKu2CooK0miuD0TK6cmQzN4A9V4DOe2dmwBYL886YE5SYXMrm7ZBlt4KNXmFZBGSoHB22xz9cV2jPuy7bNCmHjKxM5YS6NlSLo1pq7ZBWDUgZA0Sqz2e94G0R634xsaWZBNc",
|
||||||
|
"igUserId": "17841474871998063"
|
||||||
|
},
|
||||||
|
"trailuser@gmail.com": {
|
||||||
|
"longUserToken": "EAAH1s4xuDg8BQB4IKZCSADGUxNM3bZAFbV3Vzv94OCi3rKV2ERaIzZB3vm9fY7iMdcbPuhSpTZBfX13WZA2llzowkBIK7mCP9UXCHc9lMglBa5LfANgqefAMcEX0juRMs9EVSiqtY0qIZB4XsO0SXzekVlqM79hZCO5YlX5OL3BSGkJZA70G9wXORxm4vtcYsxXy",
|
||||||
|
"longUserTokenExpiresAt": null,
|
||||||
|
"pageId": "631970130002549",
|
||||||
|
"pageToken": "EAAH1s4xuDg8BQLc4vQhrTq89cnZCUnnrCWaDqKMS3wN98yMqGzlWseLrV7lCWyDvJ9w1YA5774kbhPzawViEgy9rrjZCjKGsrV4dLcxYOvcfozcDPGIBHnvluHAWcvUeAJ8HAH108sljDqDGiIRAwgtGyvGlZBG3YpAHwLbglcDUxpwtZAUMeor0xxF74IM1dRKY",
|
||||||
|
"igUserId": "17841474871998063"
|
||||||
|
},
|
||||||
|
"test@gmail.com": {
|
||||||
|
"longUserToken": "EAAH1s4xuDg8BQasZCppDmYQCrQEcemXWZC2yNKK3TFVQMnxGIqzZCG2RQSoBlpkNOHTB9HlnZCdHSoqUhkQ0ZA2HDHe3smxZBQF1wMZBm8eqaoTEraTJVPHU1gEqqa6OcdXWO6jlUEMSNhXfZCwphsGuMdYBtar5ouXgy9tVzed6NRwbdBCEVlPxMSJmseZBbgwtG",
|
||||||
|
"longUserTokenExpiresAt": null,
|
||||||
|
"pageId": "631970130002549",
|
||||||
|
"pageToken": "EAAH1s4xuDg8BQRwtXTkzGqcg0RQXJzM1WH3e0DlUNnu3EcZBMxg85vWkIRiNHDxWmMvCDbMSK85c88M6ANjoBQuSH25tUfZCmFkbz60CK1BfZCxwf1MTE6WZB1AOqmCOLibfrZAlngBHLSx0JGXZBMvy0un5IIPhsDWucqcGY8PudBZAlJ9KpZAJh9ZAAR5fzJpFzVGyA",
|
||||||
|
"igUserId": "17841474871998063"
|
||||||
|
},
|
||||||
|
"alaguraj0361@gmail.com": {
|
||||||
|
"longUserToken": "EAAH1s4xuDg8BQUzWCVGt435Gh3x7Pc1WORbvgZAVzB3f22UPZCbflbOyTZCXZCyHrjWuCvP9GndHmmUJOMHYtfA3ygySlnytDKpQ6fEpCiDDoSZCbK3mqI7IB0d5C74G71SZBnTaOXVEdl8k8ZBI7TqP152Qtqgia2r7hLaCM4ZBOuecBWLkWpPvL8S3KsavLy8hQYNsicapCgIukfGv",
|
||||||
|
"longUserTokenExpiresAt": null,
|
||||||
|
"pageId": "631970130002549",
|
||||||
|
"pageToken": "EAAH1s4xuDg8BQYq4IILSB8ZAtErj5eBLV79hHkOasDBbBF5QPX694k6pbLcAyXVworBz56qMBvH8EierFBqGy914Ft9lffEwTCsKQz3T1DYMQubrOeQojjAubyrt2nqFc20bzILy3drL1I2Dut5sQZCDE5ShsICo59Ghf6ZAPMlaby7ULW3G5KuEa8BsneDOahNMNIe5frts6zTZBFjidsJ0",
|
||||||
|
"igUserId": "17841474871998063"
|
||||||
|
},
|
||||||
|
"admin@gmail.com": {
|
||||||
|
"longUserToken": "EAAH1s4xuDg8BQa4MSdHEN7YhNz28JDFCM49Y6bAQZAWZAA5AgnpPW8ZBHL6qGE6LAjhNnpy8rEWwLj7qAtxFQehMhNoUxYRSIHYS6Y6PSyAYSHZBJDRuSOxz2h6hoy9hZCSEg1mq9ja2YAkrv3ZC05ZC5XBs5FxVQtM5nH64bBYAmzCdzLovmNlAXo8MQwRjh7QfBwCfoGlFP9k5ZCrA",
|
||||||
|
"longUserTokenExpiresAt": null,
|
||||||
|
"pageId": "631970130002549",
|
||||||
|
"pageToken": "EAAH1s4xuDg8BQZAhJxpI73iuNCjyGjO6ZBf05pZA8jiF2lsBesFmZCMwzd6YFaaZBLZBM1QipNIa4KwZBtM4KkRuezTtQI54jLMuwF3D1bGX7N0NGZAEeZBksp8HixJfuMbQboBKpiHWIuktsHuy5w83ZCra50eASaY5t81EU3qvvSq9S50pO8gGv3BdnsPefU0Hc0SkcF9Bm6uxlZAvnYg6nHY",
|
||||||
|
"igUserId": "17841474871998063"
|
||||||
|
}
|
||||||
|
}
|
||||||
1925
package-lock.json
generated
Normal file
1925
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "VG Products_app_backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "server.js",
|
||||||
|
"type": "commonjs",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"mongoose": "^8.19.3",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"nodemailer": "^7.0.10",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-google-oauth20": "^2.0.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"stripe": "^20.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
52
server.js
Normal file
52
server.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
require("dotenv").config();
|
||||||
|
const express = require("express");
|
||||||
|
const cors = require("cors");
|
||||||
|
const path = require("path");
|
||||||
|
const { connectDB } = require("./config/db");
|
||||||
|
const passport = require("./config/passport");
|
||||||
|
const authRoutes = require("./src/routes/auth.routes");
|
||||||
|
const userRoutes = require("./src/routes/user.routes");
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3050;
|
||||||
|
|
||||||
|
// Request Logger
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
console.log(`[REQUEST] ${req.method} ${req.url}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ------------------ Connect DB ------------------
|
||||||
|
connectDB()
|
||||||
|
.then(() => console.log("✅ Database connected successfully"))
|
||||||
|
.catch((err) => console.error("❌ Database connection failed:", err));
|
||||||
|
|
||||||
|
// ------------------ CORS ------------------
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: ["http://localhost:3500", "https://app.VG Products.co"],
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ------------------ Body Parser (After Webhook) ------------------
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// ------------------ Static Files ------------------
|
||||||
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
|
||||||
|
//passport
|
||||||
|
app.use(passport.initialize());
|
||||||
|
|
||||||
|
// ------------------ Routes ------------------
|
||||||
|
app.use("/api/auth", authRoutes);
|
||||||
|
app.use("/api/users", userRoutes);
|
||||||
|
|
||||||
|
app.get("/", (_req, res) => {
|
||||||
|
res.send("VG Products backend is running...");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ------------------ Start Server ------------------
|
||||||
|
app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));
|
||||||
293
src/controllers/userauth.controller.js
Normal file
293
src/controllers/userauth.controller.js
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
const bcrypt = require("bcrypt");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const User = require("../models/user.model.js");
|
||||||
|
const { sendResetPasswordMail, sendSignupMail } = require("../utils/mailer.js");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const passport = require("passport");
|
||||||
|
// ======================= SIGNUP =======================
|
||||||
|
async function signup(req, res) {
|
||||||
|
try {
|
||||||
|
const { name, email, mobileNumber, password } = req.body;
|
||||||
|
|
||||||
|
// Validate fields
|
||||||
|
if (!name || !email || !mobileNumber || !password) {
|
||||||
|
return res.status(400).json({ error: "All fields are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user exists
|
||||||
|
const exists = await User.findOne({ email });
|
||||||
|
if (exists) {
|
||||||
|
return res.status(400).json({ error: "User already exists" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const passwordHash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const user = await User.create({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
mobileNumber,
|
||||||
|
passwordHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send confirmation email (non-blocking)
|
||||||
|
sendSignupMail(email, name)
|
||||||
|
.then(() => console.log("Signup email sent to", email))
|
||||||
|
.catch((err) => console.error("Email send failed:", err));
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: "Signup success, email sent",
|
||||||
|
id: user._id,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Signup failed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================= LOGIN =======================
|
||||||
|
async function login(req, res) {
|
||||||
|
try {
|
||||||
|
console.log("LOGIN REQ BODY =>", req.body);
|
||||||
|
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
return res.status(400).json({ error: "Email & password required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({ email });
|
||||||
|
if (!user) return res.status(401).json({ error: "Invalid credentials" });
|
||||||
|
|
||||||
|
const match = await bcrypt.compare(password, user.passwordHash);
|
||||||
|
if (!match) return res.status(401).json({ error: "Invalid credentials" });
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: user._id, email: user.email },
|
||||||
|
process.env.JWT_SECRET,
|
||||||
|
{ expiresIn: "1h" }
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
message: "Login success",
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user._id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
mobileNumber: user.mobileNumber,
|
||||||
|
role: user.role,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("LOGIN ERROR:", err);
|
||||||
|
return res.status(500).json({ error: "Login failed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ======================= CHANGE PASSWORD =======================
|
||||||
|
async function changePassword(req, res) {
|
||||||
|
try {
|
||||||
|
const { currentPassword, newPassword } = req.body;
|
||||||
|
|
||||||
|
if (!currentPassword || !newPassword) {
|
||||||
|
return res.status(400).json({ error: "Current password and new password are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findById(req.user.id);
|
||||||
|
if (!user) return res.status(404).json({ error: "User not found" });
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(currentPassword, user.passwordHash);
|
||||||
|
if (!isMatch)
|
||||||
|
return res.status(401).json({ error: "Current password is incorrect" });
|
||||||
|
|
||||||
|
user.passwordHash = await bcrypt.hash(newPassword, 10);
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({ message: "Password updated successfully" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("changePassword error:", err);
|
||||||
|
res.status(500).json({ error: "Failed to change password" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================= FORGOT PASSWORD =======================
|
||||||
|
async function forgotPassword(req, res) {
|
||||||
|
try {
|
||||||
|
const { email } = req.body;
|
||||||
|
if (!email) return res.status(400).json({ error: "Email is required" });
|
||||||
|
|
||||||
|
const user = await User.findOne({ email });
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
return res.json({
|
||||||
|
message: "If the email is registered, a reset link has been sent.",
|
||||||
|
verificationCode: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4-digit numeric code
|
||||||
|
const verificationCode = Math.floor(1000 + Math.random() * 9000).toString();
|
||||||
|
|
||||||
|
user.resetPasswordToken = verificationCode;
|
||||||
|
user.resetPasswordExpires = Date.now() + 60 * 60 * 1000; // 1 hour
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
// Send code via email
|
||||||
|
await sendResetPasswordMail(email, verificationCode);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "If the email is registered, a reset link has been sent.",
|
||||||
|
verificationCode,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("forgotPassword error:", err);
|
||||||
|
res.status(500).json({ error: "Failed to send reset link" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================= RESET PASSWORD =======================
|
||||||
|
async function resetPassword(req, res) {
|
||||||
|
try {
|
||||||
|
const { token, newPassword } = req.body;
|
||||||
|
if (!token || !newPassword)
|
||||||
|
return res.status(400).json({ error: "Token and new password are required" });
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
resetPasswordToken: token,
|
||||||
|
resetPasswordExpires: { $gt: Date.now() },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) return res.status(400).json({ error: "Invalid or expired token" });
|
||||||
|
|
||||||
|
user.passwordHash = await bcrypt.hash(newPassword, 10);
|
||||||
|
user.resetPasswordToken = undefined;
|
||||||
|
user.resetPasswordExpires = undefined;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({ message: "Password has been reset successfully" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("resetPassword error:", err);
|
||||||
|
res.status(500).json({ error: "Failed to reset password" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function createUser(req, res) {
|
||||||
|
try {
|
||||||
|
const { name, email, mobileNumber, password, role } = req.body;
|
||||||
|
|
||||||
|
if (!name || !email || !password)
|
||||||
|
return res.status(400).json({ error: "Name, email & password required" });
|
||||||
|
|
||||||
|
const exists = await User.findOne({ email });
|
||||||
|
if (exists) return res.status(400).json({ error: "Email already exists" });
|
||||||
|
|
||||||
|
const passwordHash = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
const user = await User.create({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
mobileNumber,
|
||||||
|
passwordHash,
|
||||||
|
role
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "User created successfully",
|
||||||
|
user: {
|
||||||
|
id: user._id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
mobileNumber: user.mobileNumber,
|
||||||
|
role: user.role,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Create user error:", err);
|
||||||
|
res.status(500).json({ error: "Failed to create user" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getUsers(req, res) {
|
||||||
|
try {
|
||||||
|
const users = await User.find()
|
||||||
|
.select("name email mobileNumber role createdAt updatedAt");
|
||||||
|
|
||||||
|
res.json(users);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Get users error:", err);
|
||||||
|
res.status(500).json({ error: "Failed to fetch users" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getUserById(req, res) {
|
||||||
|
try {
|
||||||
|
const user = await User.findById(req.params.id)
|
||||||
|
.select("name email mobileNumber role createdAt updatedAt");
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(user);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Get user error:", err);
|
||||||
|
res.status(500).json({ error: "Failed to fetch user" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateUser(req, res) {
|
||||||
|
try {
|
||||||
|
const { name, email, mobileNumber, role } = req.body;
|
||||||
|
|
||||||
|
const updated = await User.findByIdAndUpdate(
|
||||||
|
req.params.id,
|
||||||
|
{ name, email, mobileNumber, role },
|
||||||
|
{ new: true }
|
||||||
|
).select("name email mobileNumber role");
|
||||||
|
|
||||||
|
if (!updated) return res.status(404).json({ error: "User not found" });
|
||||||
|
|
||||||
|
res.json({ message: "User updated", user: updated });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Update user error:", err);
|
||||||
|
res.status(500).json({ error: "Failed to update user" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function deleteUser(req, res) {
|
||||||
|
try {
|
||||||
|
const deleted = await User.findByIdAndDelete(req.params.id);
|
||||||
|
|
||||||
|
if (!deleted) return res.status(404).json({ error: "User not found" });
|
||||||
|
|
||||||
|
res.json({ message: "User deleted successfully" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Delete user error:", err);
|
||||||
|
res.status(500).json({ error: "Failed to delete user" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
signup,
|
||||||
|
login,
|
||||||
|
changePassword,
|
||||||
|
forgotPassword,
|
||||||
|
resetPassword,
|
||||||
|
|
||||||
|
// CRUD
|
||||||
|
createUser,
|
||||||
|
getUsers,
|
||||||
|
getUserById,
|
||||||
|
updateUser,
|
||||||
|
deleteUser,
|
||||||
|
};
|
||||||
65
src/lib/http.js
Normal file
65
src/lib/http.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// const axios = require('axios');
|
||||||
|
|
||||||
|
// const http = axios.create({
|
||||||
|
// timeout: 15000,
|
||||||
|
// maxRedirects: 2,
|
||||||
|
// validateStatus: (s) => s >= 200 && s < 500,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// http.interceptors.response.use(async (res) => {
|
||||||
|
// if (res.status === 429 || res.status >= 500) {
|
||||||
|
// const retryAfter = Number(res.headers['retry-after'] || 1);
|
||||||
|
// await new Promise(r => setTimeout(r, retryAfter * 1000));
|
||||||
|
// }
|
||||||
|
// return res;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// module.exports = { http };
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const http = axios.create({
|
||||||
|
timeout: 15000,
|
||||||
|
maxRedirects: 2,
|
||||||
|
validateStatus: (s) => s >= 200 && s < 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
http.interceptors.request.use((config) => {
|
||||||
|
console.log(`📤 ${config.method.toUpperCase()} ${config.url}`);
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
http.interceptors.response.use(async (res) => {
|
||||||
|
console.log(`📥 ${res.status} ${res.config.url}`);
|
||||||
|
|
||||||
|
// Log errors for debugging
|
||||||
|
if (res.status >= 400) {
|
||||||
|
console.error('❌ HTTP Error Response:', {
|
||||||
|
status: res.status,
|
||||||
|
url: res.config.url,
|
||||||
|
data: res.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry logic for rate limits and server errors
|
||||||
|
if (res.status === 429 || res.status >= 500) {
|
||||||
|
const retryAfter = Number(res.headers['retry-after'] || 1);
|
||||||
|
console.log(`⏳ Retrying after ${retryAfter}s...`);
|
||||||
|
await new Promise(r => setTimeout(r, retryAfter * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, (error) => {
|
||||||
|
// Handle network errors (DNS, timeout, connection refused)
|
||||||
|
if (error.code === 'ENOTFOUND') {
|
||||||
|
console.error('🌐 DNS Error: Cannot resolve hostname', error.config?.url);
|
||||||
|
} else if (error.code === 'ETIMEDOUT') {
|
||||||
|
console.error('⏱️ Timeout Error:', error.config?.url);
|
||||||
|
} else if (error.code === 'ECONNREFUSED') {
|
||||||
|
console.error('🚫 Connection Refused:', error.config?.url);
|
||||||
|
} else {
|
||||||
|
console.error('💥 Request Error:', error.message);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { http };
|
||||||
8
src/middlewares/admin.middleware.js
Normal file
8
src/middlewares/admin.middleware.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
function adminOnly(req, res, next) {
|
||||||
|
if (req.user.role !== "admin") {
|
||||||
|
return res.status(403).json({ error: "Admin access required" });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { adminOnly };
|
||||||
19
src/middlewares/auth.middleware.js
Normal file
19
src/middlewares/auth.middleware.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
|
||||||
|
function authMiddleware(req, res, next) {
|
||||||
|
const header = req.headers.authorization;
|
||||||
|
if (!header || !header.startsWith("Bearer ")) {
|
||||||
|
return res.status(401).json({ error: "Missing token" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = header.split(" ")[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(401).json({ error: "Invalid or expired token" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { authMiddleware };
|
||||||
6
src/middlewares/pageSpeedErrorHandler.js
Normal file
6
src/middlewares/pageSpeedErrorHandler.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const errorHandler = (err, req, res, next) => {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ message: err.message || "Internal Server Error" });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { errorHandler };
|
||||||
27
src/models/user.model.js
Normal file
27
src/models/user.model.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const userSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
name: { type: String },
|
||||||
|
email: { type: String, required: true, unique: true, lowercase: true },
|
||||||
|
mobileNumber: { type: String },
|
||||||
|
passwordHash: { type: String, required: true },
|
||||||
|
|
||||||
|
// ⭐ ADD ROLE
|
||||||
|
role: {
|
||||||
|
type: String,
|
||||||
|
enum: ["customer", "admin", "partner"],
|
||||||
|
default: "customer",
|
||||||
|
},
|
||||||
|
resetPasswordToken: { type: String },
|
||||||
|
resetPasswordExpires: { type: Date },
|
||||||
|
|
||||||
|
// ⭐ FREE TRIAL
|
||||||
|
trialEndsAt: { type: Date },
|
||||||
|
isTrialActive: { type: Boolean, default: false },
|
||||||
|
stripeCustomerId: { type: String },
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = mongoose.model("User", userSchema);
|
||||||
31
src/routes/auth.routes.js
Normal file
31
src/routes/auth.routes.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const passport = require("passport");
|
||||||
|
|
||||||
|
const {
|
||||||
|
signup,
|
||||||
|
login,
|
||||||
|
changePassword,
|
||||||
|
forgotPassword,
|
||||||
|
resetPassword,
|
||||||
|
} = require("../controllers/userauth.controller.js");
|
||||||
|
|
||||||
|
const { authMiddleware } = require("../middlewares/auth.middleware.js");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/signup", signup);
|
||||||
|
router.post("/login", login);
|
||||||
|
router.post("/change-password", authMiddleware, changePassword);
|
||||||
|
router.post("/forgot-password", forgotPassword);
|
||||||
|
router.post("/reset-password", resetPassword);
|
||||||
|
|
||||||
|
|
||||||
|
// router.get("/protected", protectedRoute);
|
||||||
|
|
||||||
|
|
||||||
|
// example protected route
|
||||||
|
router.get("/profile", authMiddleware, (req, res) => {
|
||||||
|
res.json({ user: req.user });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
19
src/routes/user.routes.js
Normal file
19
src/routes/user.routes.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const controller = require("../controllers/userauth.controller.js");
|
||||||
|
const { authMiddleware } = require("../middlewares/auth.middleware.js");
|
||||||
|
const { adminOnly } = require("../middlewares/admin.middleware.js");
|
||||||
|
|
||||||
|
// 🔐 ADMIN ONLY – USER MANAGEMENT
|
||||||
|
// router.post("/create", authMiddleware, adminOnly, controller.createUser);
|
||||||
|
// router.get("/", controller.getUsers);
|
||||||
|
// router.get("/:id", controller.getUserById);
|
||||||
|
// router.put("/:id", authMiddleware, adminOnly, controller.updateUser);
|
||||||
|
// router.delete("/:id", authMiddleware, adminOnly, controller.deleteUser);
|
||||||
|
router.post("/create", controller.createUser);
|
||||||
|
router.get("/", controller.getUsers);
|
||||||
|
router.get("/:id", controller.getUserById);
|
||||||
|
router.put("/:id", controller.updateUser);
|
||||||
|
router.delete("/:id", controller.deleteUser);
|
||||||
|
module.exports = router;
|
||||||
84
src/utils/mailer.js
Normal file
84
src/utils/mailer.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
const nodemailer = require("nodemailer");
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create reusable transporter object
|
||||||
|
//
|
||||||
|
const mailer = nodemailer.createTransport({
|
||||||
|
host: process.env.SMTP_HOST,
|
||||||
|
port: 587, // STARTTLS
|
||||||
|
secure: false, // must be false for port 587
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER,
|
||||||
|
pass: process.env.SMTP_PASS,
|
||||||
|
},
|
||||||
|
name: "mail.VG Products.co",
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
logger: true,
|
||||||
|
debug: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Send welcome / signup email
|
||||||
|
//
|
||||||
|
async function sendSignupMail(toEmail, name= "user") {
|
||||||
|
try {
|
||||||
|
await mailer.sendMail({
|
||||||
|
from: `"Social Buddy" <${process.env.SMTP_USER}>`,
|
||||||
|
to: toEmail,
|
||||||
|
subject: "Welcome to Social Buddy",
|
||||||
|
html: `
|
||||||
|
<div style="font-family:Arial;max-width:600px;margin:auto;padding:20px;border:1px solid #eee;">
|
||||||
|
<h2 style="color:#007bff;">Welcome to Social Buddy 🎉</h2>
|
||||||
|
<p>Hello <b>${name}</b>,</p>
|
||||||
|
<p>Your signup was successful! You can now log in and start using the app.</p>
|
||||||
|
|
||||||
|
<div style="margin-top:20px;">
|
||||||
|
<a href="https://app.VG Products.co/login"
|
||||||
|
style="background:#007bff;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">
|
||||||
|
Login Now
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:20px;color:#666;font-size:12px;">
|
||||||
|
If you didn’t create this account, please ignore this email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
console.log(`✅ Signup email sent to ${toEmail}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Error sending signup email:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Send reset-password email with code or link
|
||||||
|
//
|
||||||
|
async function sendResetPasswordMail(email, token) {
|
||||||
|
try {
|
||||||
|
const resetURL = `${process.env.FRONTEND_URL}/reset-password?email=${email}&token=${token}`;
|
||||||
|
|
||||||
|
await mailer.sendMail({
|
||||||
|
from: `"Social Buddy" <${process.env.SMTP_USER}>`,
|
||||||
|
to: email,
|
||||||
|
subject: "Reset your password",
|
||||||
|
html: `
|
||||||
|
<p>You requested a password reset.</p>
|
||||||
|
<p>Click here to reset: <a href="${resetURL}">${resetURL}</a></p>
|
||||||
|
<p>This link is valid for 1 hour.</p>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Reset password email sent to ${email}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Error sending reset password email:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mailer,
|
||||||
|
sendSignupMail,
|
||||||
|
sendResetPasswordMail,
|
||||||
|
};
|
||||||
25
src/utils/templates/welcomeTemplate.js
Normal file
25
src/utils/templates/welcomeTemplate.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// utils/templates/welcomeTemplate.js
|
||||||
|
export const welcomeTemplate = (name) => `
|
||||||
|
<div style="font-family:Arial,sans-serif;line-height:1.6;background:#f9f9f9;padding:20px;">
|
||||||
|
<div style="max-width:600px;margin:auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 6px rgba(0,0,0,0.1);">
|
||||||
|
<div style="background:#00d1ff;color:#fff;padding:15px 25px;text-align:center;">
|
||||||
|
<h2>Welcome to Social Buddy 🎉</h2>
|
||||||
|
</div>
|
||||||
|
<div style="padding:25px;">
|
||||||
|
<h3>Hello ${name},</h3>
|
||||||
|
<p>Thank you for registering with <strong>Our App</strong>!</p>
|
||||||
|
<p>Your account has been successfully created. We're thrilled to have you on board.</p>
|
||||||
|
|
||||||
|
<div style="margin:30px 0;text-align:center;">
|
||||||
|
<a href="https://app.VG Products.co/login"
|
||||||
|
style="background:#00d1ff;color:#fff;padding:10px 20px;text-decoration:none;border-radius:5px;">
|
||||||
|
Go to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:13px;color:#777;">If you didn’t create this account, please ignore this email.</p>
|
||||||
|
<p style="font-size:13px;color:#777;">— The Team</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
Loading…
x
Reference in New Issue
Block a user