Initial push from server

This commit is contained in:
Manesh 2026-02-21 19:07:05 +00:00
parent cd1005fe78
commit f18b02e88d
49 changed files with 5668 additions and 0 deletions

132
.gitignore copy Normal file
View 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.*

273
DM_API_TEST_EXAMPLES.md Normal file
View File

@ -0,0 +1,273 @@
# DM Automation API - HTTPie Test Examples
## Prerequisites
```bash
# Install httpie if not already installed
pip install httpie
# Set your base URL and user email
export BASE_URL="https://api.socialbuddy.co"
export USER_EMAIL="alaguraj0361@gmail.com"
```
## 1. Test Auto-Reply (Static Template)
Test the static reply generator without connecting to Instagram:
```bash
http POST $BASE_URL/api/dm/messages/test-autoreply \
messageText="What are your prices?"
```
Expected response:
```json
{
"originalMessage": "What are your prices?",
"autoReply": "For pricing information, please check our website or DM us with specific requirements."
}
```
## 2. Get Conversations
Fetch all Instagram DM conversations for a user:
```bash
http GET "$BASE_URL/api/dm/messages/conversations?userId=$USER_EMAIL"
```
With pagination:
```bash
http GET "$BASE_URL/api/dm/messages/conversations?userId=$USER_EMAIL&after=CURSOR_VALUE"
```
Expected response:
```json
{
"data": [
{
"id": "conversation_id_123",
"updated_time": "2026-01-20T10:30:00+0000",
"participants": {...},
"messages": {...}
}
],
"paging": {
"cursors": {
"before": "...",
"after": "..."
},
"next": "..."
}
}
```
## 3. Get Messages in a Conversation
Fetch messages from a specific conversation:
```bash
export CONVERSATION_ID="your_conversation_id_here"
http GET "$BASE_URL/api/dm/messages/conversations/$CONVERSATION_ID/messages?userId=$USER_EMAIL"
```
With pagination:
```bash
http GET "$BASE_URL/api/dm/messages/conversations/$CONVERSATION_ID/messages?userId=$USER_EMAIL&after=CURSOR_VALUE"
```
Expected response:
```json
{
"data": [
{
"id": "message_id_123",
"created_time": "2026-01-20T10:25:00+0000",
"from": {...},
"to": {...},
"message": "Hello, I have a question"
}
],
"paging": {...}
}
```
## 4. Send a Message
Send a DM reply to a specific Instagram user:
```bash
export RECIPIENT_ID="instagram_scoped_user_id"
http POST "$BASE_URL/api/dm/messages/send?userId=$USER_EMAIL" \
recipientId="$RECIPIENT_ID" \
message="Thank you for reaching out! How can I help you today?"
```
Expected response:
```json
{
"recipient_id": "instagram_scoped_user_id",
"message_id": "mid.message_id_123"
}
```
## 5. Mark Conversation as Read
Mark a conversation as read:
```bash
http POST "$BASE_URL/api/dm/messages/conversations/$CONVERSATION_ID/read?userId=$USER_EMAIL"
```
Expected response:
```json
{
"success": true
}
```
## Webhook Setup (For Auto-Reply Automation)
### Step 1: Verify Webhook (Facebook will call this during setup)
```bash
# Facebook will make this GET request:
# GET /api/dm/webhook?hub.mode=subscribe&hub.verify_token=your_verify_token_123&hub.challenge=CHALLENGE_STRING
```
### Step 2: Test Webhook Locally with ngrok
```bash
# Install ngrok
npm install -g ngrok
# Expose local server
ngrok http 7002
# Set webhook URL in Meta App Dashboard:
# https://your-ngrok-url.ngrok.io/api/dm/webhook
```
### Step 3: Simulate Incoming Message (Testing)
```bash
http POST http://localhost:7002/api/dm/webhook \
'X-Hub-Signature-256:sha256=YOUR_SIGNATURE_HERE' \
object="instagram" \
entry:='[{
"id": "page_id",
"time": 1234567890,
"messaging": [{
"sender": {"id": "sender_instagram_id"},
"recipient": {"id": "page_instagram_id"},
"timestamp": 1234567890,
"message": {
"mid": "message_id",
"text": "Hello, I need help"
}
}]
}]'
```
## Error Handling Examples
### Missing userId
```bash
http GET "$BASE_URL/api/dm/messages/conversations"
# Response: 500
# {"error": "userId is required as query parameter"}
```
### No Channel Connected
```bash
http GET "$BASE_URL/api/dm/messages/conversations?userId=nonexistent@example.com"
# Response: 500
# {"error": "Connect a channel first"}
```
### Invalid Conversation ID
```bash
http GET "$BASE_URL/api/dm/messages/conversations/invalid_id/messages?userId=$USER_EMAIL"
# Response: 500
# {"error": "Unsupported get request..."}
```
## Testing Workflow (End-to-End)
```bash
# 1. First, ensure user has connected their Instagram account
# (Use the social OAuth flow first)
# 2. Verify connection
http GET "$BASE_URL/api/social/auth/status?userId=$USER_EMAIL"
# 3. Get conversations
http GET "$BASE_URL/api/dm/messages/conversations?userId=$USER_EMAIL"
# 4. Pick a conversation ID from response
export CONVO_ID="123456789"
# 5. Get messages in that conversation
http GET "$BASE_URL/api/dm/messages/conversations/$CONVO_ID/messages?userId=$USER_EMAIL"
# 6. Send a reply (get recipient ID from conversation data)
export RECIPIENT="recipient_instagram_scoped_id"
http POST "$BASE_URL/api/dm/messages/send?userId=$USER_EMAIL" \
recipientId="$RECIPIENT" \
message="Thanks for your message!"
# 7. Mark conversation as read
http POST "$BASE_URL/api/dm/messages/conversations/$CONVO_ID/read?userId=$USER_EMAIL"
```
## Environment Variables Required
Add these to your `.env` file:
```bash
# Webhook Configuration
WEBHOOK_VERIFY_TOKEN=your_custom_verify_token_123
PAGE_ACCESS_TOKEN=your_page_access_token_here # Temporary, will be replaced by user-specific tokens
# OAuth (already configured)
OAUTH_APP_ID=your_facebook_app_id
OAUTH_APP_SECRET=your_facebook_app_secret
# Graph API
GRAPH_VER=v21.0
```
## Common Issues & Solutions
### Issue: "Connect a channel first"
**Solution:** User needs to complete the Instagram connection flow:
```bash
http GET "$BASE_URL/api/social/auth/login"
# Follow OAuth flow, then connect a channel
```
### Issue: "Invalid signature" on webhook
**Solution:** Ensure `OAUTH_APP_SECRET` matches your Meta App settings and raw body middleware is working.
### Issue: Empty conversations list
**Solution:**
1. Verify Instagram Professional/Business account is linked to Facebook Page
2. Check that `instagram_manage_messages` permission is granted
3. Ensure page has at least one conversation/message
### Issue: Cannot send message
**Solution:**
1. Recipient must have messaged you first (Instagram limitation)
2. Check that recipient ID is the Instagram-scoped ID (IGSID), not username
3. Verify page token has `pages_manage_engagement` permission
## Meta App Configuration Checklist
- [ ] App has Instagram Basic Display API enabled
- [ ] App has Instagram Graph API enabled
- [ ] App has Messenger Platform enabled
- [ ] Permissions granted:
- `instagram_manage_messages`
- `pages_manage_engagement`
- `pages_read_engagement`
- `pages_show_list`
- `instagram_basic`
- [ ] Webhook subscribed to `messages` field
- [ ] Webhook verify token matches `WEBHOOK_VERIFY_TOKEN` in .env
- [ ] Page is connected and has Instagram Professional/Business account

2
README copy.md Normal file
View File

@ -0,0 +1,2 @@
# socialbuddy_app_backend

13
config/db.js Normal file
View File

@ -0,0 +1,13 @@
const mongoose = require("mongoose");
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, { dbName: "SocialBuddy" });
console.log("✅ MongoDB connected");
} catch (err) {
console.error("❌ MongoDB connection error:", err);
process.exit(1);
}
};
module.exports = { connectDB };

51
config/passport.js Normal file
View 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.socialbuddy.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;

66
data/store.json Normal file
View File

@ -0,0 +1,66 @@
{
"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"
},
"info@metatroncubesolutions.com": {
"longUserToken": "EAAH1s4xuDg8BQnXFK9ynDX0ZBLWGBrftzTcWHptrYIINEy01ZCMCQhPcHrPIPkp5Ho0FsAKIz2A7nIPlnDnMrNPWLt4qUU2bHXljY7sOs04w9w6b8qMsAs0wVIBfQVgjYcQo6krZAvHs7e1XqeqnVyNPaqr5MNwOPmoScaaz8xmUHJf4sZAW47yXQdag",
"longUserTokenExpiresAt": null,
"pageId": "109240551620324",
"pageToken": "EAAH1s4xuDg8BQkZC55qjeeEvHte7JWqlFtzGEWV1obZC4h57sZCIlIZC40ur7ZCq99UHNoaf2uKPrL40NGVHeZBxqVXSZCqkUvNw2lAGY8AkIiRZAGTKzfnLvh1moSV655QKfHZAsX67AZCJHZAT4w7QT0Nk25sY3xUfz65TZCVNI6dLE5LWRVR4aUDcnsYyzGNop6KrsSMSDcwZD",
"igUserId": "17841440545601461"
},
"alaguraj0361@gmail.com1": {
"longUserToken": "EAAH1s4xuDg8BQoW5CDQtJvmLnqhNzHY5ux40vTuApkw2W5hoZA5F9vXXBSMTDFghyZAzJSG8GlP3uOZBuFdS2uDxO2t6IdeZBi9rHxmTUnbgBRinBOPBehmzRXafYFYohiyQ6ve0CsAlRijSPBXwDZBRAaP0XqXLZAzTfZBNjzwoq2DXKIvvUJCSjR6y2hhxOiE",
"longUserTokenExpiresAt": null,
"pageId": "109240551620324",
"pageToken": "EAAH1s4xuDg8BQiuAU2jT4O9G2ZCOChSMZAThq5turitdjld1mrZBeIaNJOrwGtulvb661SiO8uZC3y9mTrbB9RapcCNiCta7ZCIVK2LixDweZBoIPtPLTvhA1HwQWp1jXCYqqiqmFiJZATCKoxgTiXNk4cK6HSKs1j1ZC8YZAgltr2P6QQ8h2j6mR8HaRZBPDrGZABaYFq7BpAZD",
"igUserId": "17841440545601461"
},
"gogreenpackscanada@gmail.com": {
"longUserToken": "EAAH1s4xuDg8BQsZBYfXghYZAgZBtx5JvX7b3Y4CMo7QEFZAXjFmOdZBopWJ3r3XfIu6ZBe3yeZAZAnDrFqot1dquGYk5ki9xw3GgToncdEG6LebVZAvQHbjEGlZB1iO7E8FgS6ZAwenw9CZCAbI3iDhkL0bCPxK679TjbsByztUWWipW6F2CgGWTPMvQL5RsDim7KThM",
"longUserTokenExpiresAt": null
},
"admin@gmail.com": {
"longUserToken": "EAAH1s4xuDg8BQZC6CGyQZAtJtf1zB3FgzjZC08Lyrz7bJUzXqZAw9GnJSZBGIfOPvQQeLYOQdKoSFeNdPZBANLrC9MGnbVAhih5h7ZBJ1g7XhhulZCANsyNJZCqY6OPy0ROb7t25UxAzuAq4ip980c04f1uQFuCaMm8uGhSPauxQbSfOvbYvJxZB80cZCxjWdqZB",
"longUserTokenExpiresAt": null,
"pageId": "631970130002549",
"pageToken": "EAAH1s4xuDg8BQZB6FUjZAnpzdgXVAkZBFY2cqb8pK16KtfWmL7nqZAVkAsmMwZB1nXVpTHEIBqwTPaL3AoYX7WYy2zjL3jo5xQX7mn1GiDZCMN2ZAvZCpCREEZAkNuhKU3SMv7AD5sVC1cK9gsj3tIfLX40dh5v3okNtdZCE6QYo4DuT41sT7YrMfjjEnDJlTa97FQRXsGEtwb",
"igUserId": "17841474871998063"
}
}

1925
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "socialbuddy_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"
}
}

68
server.js Normal file
View File

@ -0,0 +1,68 @@
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 socialRoutes = require("./src/routes/social.routes");
const authRoutes = require("./src/routes/auth.routes");
const paymentRoutes = require("./src/routes/payment.routes");
const userRoutes = require("./src/routes/user.routes");
const dmWebhookRoutes = require("./src/routes/dm_webhook.routes");
const dmMessagesRoutes = require("./src/routes/dm_messages.routes");
const { captureDmRawBody } = require("./src/middlewares/dm_rawBody.middleware");
const app = express();
const PORT = process.env.PORT || 7002;
// 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://devapp.socialbuddy.co"],
credentials: true,
})
);
// ⚠ STRIPE WEBHOOK MUST USE RAW BODY ⚠
app.use(
"/api/payment/webhook",
express.raw({ type: "application/json" })
);
// Capture raw body for DM webhook before JSON parsing
app.use(captureDmRawBody);
// ------------------ 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/social", socialRoutes);
app.use("/api/auth", authRoutes);
app.use("/api/users", userRoutes);
app.use("/api/payment", paymentRoutes);
app.use("/api/dm/webhook", dmWebhookRoutes);
app.use("/api/dm/messages", dmMessagesRoutes);
app.get("/", (_req, res) => {
res.send("SocialBuddy backend is running...");
});
// ------------------ Start Server ------------------
app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));

View File

@ -0,0 +1,37 @@
const { getPageConnection } = require('../services/tokenStore.service');
const { getBusinessAccountProfile } = require('../services/account.service');
// async function getAccount(req, res) {
// console.log('✅ Account endpoint hit!'); // Add this
// const userId =req.query.userId
// const { pageToken, igUserId } = await getPageConnection(userId);
// if (!pageToken || !igUserId) return res.status(400).json({ error: 'Connect a channel first' });
// const profile = await getBusinessAccountProfile(igUserId, pageToken);
// return res.json(profile);
// }
// module.exports = { getAccount };
// account.controller.js - Add temporarily
async function getAccount(req, res) {
console.log('🔍 Account endpoint hit');
const userId =req.query.userId
console.log('👤 User ID for Account :', userId) ;
try {
const { pageToken, igUserId } = await getPageConnection(userId);
console.log('🔍 Page connection found:', { hasToken: !!pageToken, igUserId });
if (!pageToken || !igUserId) {
console.log('❌ No connection found for user:', userId);
return res.status(400).json({ error: 'Connect a channel first' });
}
const profile = await getBusinessAccountProfile(igUserId, pageToken);
console.log('✅ Profile fetched successfully');
return res.json(profile);
} catch (error) {
console.error('💥 Account error:', error.message);
return res.status(500).json({ error: error.message });
}
}
module.exports = { getAccount };

View File

@ -0,0 +1,28 @@
const { buildLoginUrl, getShortLivedUserToken, exchangeToLongLivedUserToken } = require('../services/oauth.service');
const { saveUserToken } = require('../services/tokenStore.service');
async function login(req, res) {
return res.redirect(buildLoginUrl());
}
async function callback(req, res) {
const { code, error } = req.query;
if (error) return res.status(400).send(`OAuth error: ${error}`);
if (!code) return res.status(400).send('Missing code');
const short = await getShortLivedUserToken(code);
const long = await exchangeToLongLivedUserToken(short.access_token);
const userId =req.query.userId
const expiresAt = new Date(Date.now() + (long.expires_in * 1000));
await saveUserToken(userId, long.access_token, expiresAt);
// 🔥 Redirect to Next.js page after success
return res.redirect('https://devapp.socialbuddy.co/social-media-connect');
// return res.json({ ok: true, token_type: long.token_type, expires_in: long.expires_in });
}
module.exports = { login, callback };

View File

@ -0,0 +1,117 @@
const { buildLoginUrl, getShortLivedUserToken, exchangeToLongLivedUserToken } = require('../services/oauth.service');
const { saveUserToken, getUserToken, deleteUserCompletely } = require('../services/tokenStore.service');
const jwt = require('jsonwebtoken');
async function login(req, res) {
return res.redirect(buildLoginUrl());
}
async function status(req, res) {
const userId = req.query.userId
console.log("Checking OAuth status for user:", userId);
const userToken = await getUserToken(userId);
if (userToken) {
return res.json({ connected: true, userId: userId });
} else {
return res.json({ connected: false, userId: userId });
}
}
async function disconnect(req, res) {
try {
const userId = req.body.userId
console.log("Checking disconnect status for user:", userId);
await deleteUserCompletely(userId);
res.json({ ok: true, message: 'User Facebook Account disconnected successfully' });
} catch (error) {
console.error("Error disconnecting user:", error);
return res.status(500).json({ error: 'Failed to disconnect user' });
}
}
// async function callback(req, res) {
// const { code, error } = req.query;
// if (error) return res.status(400).send(`OAuth error: ${error}`);
// if (!code) return res.status(400).send('Missing code');
// const short = await getShortLivedUserToken(code);
// const long = await exchangeToLongLivedUserToken(short.access_token);
// const userId =req.query.userId
// const expiresAt = new Date(Date.now() + (long.expires_in * 1000));
// await saveUserToken(userId, long.access_token, expiresAt);
// // 🔥 Redirect to Next.js page after success
// return res.redirect('https://devapp.socialbuddy.co/social-media-connect');
// // return res.json({ ok: true, token_type: long.token_type, expires_in: long.expires_in });
// }
async function callback(req, res) {
const { code, error } = req.query;
if (error) return res.status(400).send(`OAuth error: ${error}`);
if (!code) return res.status(400).send('Missing code');
// 1) Exchange auth code -> tokens (as you already do)
const short = await getShortLivedUserToken(code);
const long = await exchangeToLongLivedUserToken(short.access_token);
// // You *can* skip saving here if you really want FE to send it back
// const userId =req.query.userId
// 2) Build a payload with what FE needs
const payload = {
// userId,
access_token: long.access_token,
token_type: long.token_type,
expires_in: long.expires_in,
// include other fields if you want:
refresh_token: long.refresh_token,
scope: long.scope,
};
// 3) Sign it as a short-lived token (5 minutes, for example)
const tempToken = jwt.sign(payload, process.env.OAUTH_TEMP_SECRET, {
expiresIn: '5m',
});
// 4) Redirect with this signed blob as a query param
const url = new URL('https://devapp.socialbuddy.co/social-media-connect');
url.searchParams.set('auth', tempToken);
return res.redirect(url.toString());
}
async function save_oauth(req, res) {
const { auth, userId } = req.body;
if (!auth) return res.status(400).json({ error: 'Missing auth token' });
try {
// 1) Decode & verify
const decoded = jwt.verify(auth, process.env.OAUTH_TEMP_SECRET);
const { access_token, token_type, expires_in } = decoded;
const expiresAt = new Date(Date.now() + expires_in * 1000);
// 2) Save token in DB (your existing helper)
await saveUserToken(userId, access_token, expiresAt);
return res.json({ ok: true });
} catch (err) {
console.error('Finalize social auth error:', err);
return res.status(400).json({ error: 'Invalid or expired auth token' });
}
}
module.exports = { login, callback, save_oauth, status, disconnect };

View File

@ -0,0 +1,236 @@
// src/controllers/automation.controller.js
const { getPageConnection } = require('../services/tokenStore.service');
const { getBusinessAccountProfile } = require('../services/account.service');
const { getIGMedia } = require('../services/media.service');
const { getIGComments, replyToIGComment } = require('../services/comments.service');
const { generateReply, filterRecentComments, sleep } = require('../services/automation.service');
// In-memory store for replied comments (replace with DB in production)
const repliedCommentsStore = new Map(); // userId -> Set of commentIds
/**
* Load replied comments for a user
*/
function loadRepliedComments(userId) {
if (!repliedCommentsStore.has(userId)) {
repliedCommentsStore.set(userId, new Set());
}
return repliedCommentsStore.get(userId);
}
/**
* Save replied comment ID for a user
*/
function saveRepliedComment(userId, commentId) {
const replied = loadRepliedComments(userId);
replied.add(commentId);
}
/**
* POST /api/social/automation/auto-reply
* Automated comment reply system
* Body: { limit: 10 } (optional - number of posts to check)
*/
async function autoReplyComments(req, res) {
console.log('\n🚀 Starting Instagram Comment Auto-Reply Automation...');
const userId =req.query.userId
const { limit = 10 } = req.body;
const replied = loadRepliedComments(userId);
const results = [];
let totalProcessed = 0;
let totalReplied = 0;
try {
// Step 1: Get page connection
const { pageToken, igUserId } = await getPageConnection(userId);
if (!pageToken || !igUserId) {
return res.status(400).json({ error: 'Connect a channel first' });
}
// Step 2: Get account details
console.log('📊 Fetching Instagram account details...');
const account = await getBusinessAccountProfile(igUserId, pageToken);
const accountUsername = (account.username || '').toLowerCase();
const accountBio = account.biography || '';
console.log(`✅ Account: @${account.username}`);
console.log(` Followers: ${account.followers_count}`);
console.log(` Bio: ${accountBio.substring(0, 50)}...`);
// Step 3: Fetch recent media posts
console.log('\n📸 Fetching recent media posts...');
const mediaResp = await getIGMedia(igUserId, pageToken, limit);
const mediaList = mediaResp.data || [];
console.log(`✅ Found ${mediaList.length} recent posts`);
// Step 4: Process each post
for (const [index, media] of mediaList.entries()) {
const postId = media.id;
const postCaption = media.caption || '';
const postPermalink = media.permalink || '';
console.log(`\n📝 Post ${index + 1}/${mediaList.length}`);
console.log(` Caption: ${postCaption.substring(0, 60)}...`);
console.log(` Comments: ${media.comments_count}`);
if (media.comments_count === 0) {
console.log(' ⏭️ No comments, skipping...');
continue;
}
// Step 5: Fetch comments for this post
console.log(' 💬 Fetching comments...');
const commentsResp = await getIGComments(postId, pageToken);
const allComments = commentsResp.data || [];
// Step 6: Filter to last 24 hours only
const recentComments = filterRecentComments(allComments);
console.log(`${recentComments.length} comments in last 24 hours`);
// Step 7: Process each recent comment
for (const comment of recentComments) {
const commentId = comment.id;
const commentText = comment.text || '';
const commenter = (comment.username || '').toLowerCase();
totalProcessed++;
// Skip if already replied
if (replied.has(commentId)) {
console.log(` ⏭️ Already replied to comment by @${commenter}`);
continue;
}
// Skip if comment is from the account itself
if (commenter === accountUsername) {
console.log(` ⏭️ Skipping own comment`);
continue;
}
console.log(`\n 💡 New comment by @${commenter}:`);
console.log(` "${commentText.substring(0, 80)}..."`);
try {
// Step 8: Generate AI reply
console.log(' 🤖 Generating AI reply...');
const replyText = await generateReply(
commentText,
accountBio,
account.username,
postCaption,
commenter
);
console.log(` 📝 Reply: "${replyText}"`);
// Step 9: Post the reply
console.log(' 📤 Posting reply...');
const replyRes = await replyToIGComment(commentId, replyText, pageToken);
const replyId = replyRes?.id || null;
if (replyId) {
// Mark both comment and reply as processed
saveRepliedComment(userId, commentId);
saveRepliedComment(userId, replyId);
totalReplied++;
results.push({
commentId,
commenter,
commentText,
replyText,
replyId,
postCaption: postCaption.substring(0, 50),
status: 'success'
});
console.log(` ✅ Successfully replied! (ID: ${replyId})`);
} else {
console.log(' ⚠️ Reply posted but no ID returned');
}
// Rate limiting: wait 1.2 seconds between replies
await sleep(1200);
} catch (err) {
const errorMsg = err.response?.data?.error?.message || err.message;
console.error(` ❌ Failed to reply: ${errorMsg}`);
results.push({
commentId,
commenter,
commentText,
error: errorMsg,
status: 'failed'
});
}
}
}
// Step 10: Summary
console.log('\n' + '='.repeat(60));
console.log('✨ AUTOMATION COMPLETE ✨');
console.log('='.repeat(60));
console.log(`📊 Total comments processed: ${totalProcessed}`);
console.log(`✅ Successfully replied: ${totalReplied}`);
console.log(`❌ Failed: ${results.filter(r => r.status === 'failed').length}`);
console.log(`⏭️ Skipped (already replied): ${totalProcessed - results.length}`);
console.log('='.repeat(60) + '\n');
return res.json({
success: true,
summary: {
totalProcessed,
totalReplied,
failed: results.filter(r => r.status === 'failed').length,
skipped: totalProcessed - results.length,
postsChecked: mediaList.length
},
results
});
} catch (error) {
console.error('\n❌ AUTOMATION ERROR:', error.message);
return res.status(500).json({
error: error.message,
details: error.response?.data
});
}
}
/**
* GET /api/social/automation/replied-comments
* Get list of replied comment IDs for current user
*/
async function getRepliedComments(req, res) {
const userId =req.query.userId
const replied = loadRepliedComments(userId);
return res.json({
count: replied.size,
commentIds: Array.from(replied)
});
}
/**
* DELETE /api/social/automation/replied-comments
* Clear replied comments history for current user
*/
async function clearRepliedComments(req, res) {
const userId =req.query.userId
repliedCommentsStore.set(userId, new Set());
console.log(`🗑️ Cleared replied comments for user: ${userId}`);
return res.json({
success: true,
message: 'Replied comments history cleared'
});
}
module.exports = {
autoReplyComments,
getRepliedComments,
clearRepliedComments
};

View File

@ -0,0 +1,45 @@
const { listChannelsForUser, getChannelDetails } = require('../services/channels.service');
const { getUserToken, savePageConnection } = require('../services/tokenStore.service');
async function listChannels(req, res) {
const userId = req.query.userId
const userToken = await getUserToken(userId);
if (!userToken) return res.status(401).json({ error: 'Connect first' });
const channels = await listChannelsForUser(userToken);
const rows = await Promise.all(
channels.map(async (c) => {
const det = await getChannelDetails(c.id, userToken);
return {
id: c.id,
name: c.name,
has_linked_account: Boolean(det.instagram_business_account?.id),
};
})
);
return res.json(rows);
}
async function connectChannel(req, res) {
const { channel_id } = req.body;
if (!channel_id) return res.status(400).json({ error: 'channel_id required' });
const userId = req.query.userId
const userToken = await getUserToken(userId);
if (!userToken) return res.status(401).json({ error: 'Connect first' });
const det = await getChannelDetails(channel_id, userToken);
const pageToken = det.access_token;
const businessAccountId = det.instagram_business_account?.id;
if (!pageToken || !businessAccountId) {
return res.status(400).json({ error: 'Selected channel missing linked business account or token' });
}
console.log('🔗 Connecting channel:', { userId, channel_id, pageToken, businessAccountId });
await savePageConnection(userId, channel_id, pageToken, businessAccountId);
return res.json({ ok: true, channel_id, business_account_id: businessAccountId });
}
module.exports = { listChannels, connectChannel };

View File

@ -0,0 +1,215 @@
// src/controllers/comments.controller.js
const { getPageConnection } = require('../services/tokenStore.service');
const {
getIGComments,
replyToIGComment,
deleteIGComment,
hideIGComment,
postIGComment
} = require('../services/comments.service');
/**
* GET /api/social/media/:mediaId/comments
* Get comments on a specific media post
* Query params: after (pagination cursor)
*/
async function listComments(req, res) {
console.log('💬 Comments list endpoint hit');
const userId = req.query.userId
const { mediaId } = req.params;
const { after } = req.query;
if (!mediaId) {
return res.status(400).json({ error: 'mediaId parameter required' });
}
try {
const { pageToken, igUserId } = await getPageConnection(userId);
if (!pageToken || !igUserId) {
return res.status(400).json({ error: 'Connect a channel first' });
}
const comments = await getIGComments(mediaId, pageToken, after);
console.log(`✅ Fetched ${comments.data.length} comments`);
return res.json({
data: comments.data,
paging: comments.paging,
count: comments.data.length
});
} catch (error) {
console.error('💥 Comments list error:', error.message);
return res.status(500).json({
error: error.message,
details: error.response?.data
});
}
}
/**
* POST /api/social/comments/:commentId/reply
* Reply to a specific comment
* Body: { message: "reply text" }
*/
async function replyComment(req, res) {
console.log('💬 Reply to comment endpoint hit');
const userId = req.query.userId
const { commentId } = req.params;
const { message } = req.body;
if (!commentId) {
return res.status(400).json({ error: 'commentId parameter required' });
}
if (!message || message.trim() === '') {
return res.status(400).json({ error: 'message is required in body' });
}
try {
const { pageToken, igUserId } = await getPageConnection(userId);
if (!pageToken || !igUserId) {
return res.status(400).json({ error: 'Connect a channel first' });
}
const reply = await replyToIGComment(commentId, message, pageToken);
console.log('✅ Reply posted successfully');
return res.json({
success: true,
reply_id: reply.id
});
} catch (error) {
console.error('💥 Reply error:', error.message);
return res.status(500).json({
error: error.message,
details: error.response?.data
});
}
}
/**
* DELETE /api/social/comments/:commentId
* Delete a comment
*/
async function deleteComment(req, res) {
console.log('🗑️ Delete comment endpoint hit');
const userId = req.query.userId
const { commentId } = req.params;
if (!commentId) {
return res.status(400).json({ error: 'commentId parameter required' });
}
try {
const { pageToken, igUserId } = await getPageConnection(userId);
if (!pageToken || !igUserId) {
return res.status(400).json({ error: 'Connect a channel first' });
}
await deleteIGComment(commentId, pageToken);
console.log('✅ Comment deleted successfully');
return res.json({ success: true });
} catch (error) {
console.error('💥 Delete comment error:', error.message);
return res.status(500).json({
error: error.message,
details: error.response?.data
});
}
}
/**
* POST /api/social/comments/:commentId/hide
* Hide or unhide a comment
* Body: { hide: true/false }
*/
async function toggleHideComment(req, res) {
console.log('👁️ Toggle hide comment endpoint hit');
const userId = req.query.userId
const { commentId } = req.params;
const { hide } = req.body;
if (!commentId) {
return res.status(400).json({ error: 'commentId parameter required' });
}
if (typeof hide !== 'boolean') {
return res.status(400).json({ error: 'hide must be true or false in body' });
}
try {
const { pageToken, igUserId } = await getPageConnection(userId);
if (!pageToken || !igUserId) {
return res.status(400).json({ error: 'Connect a channel first' });
}
await hideIGComment(commentId, hide, pageToken);
console.log(`✅ Comment ${hide ? 'hidden' : 'unhidden'} successfully`);
return res.json({
success: true,
hidden: hide
});
} catch (error) {
console.error('💥 Hide comment error:', error.message);
return res.status(500).json({
error: error.message,
details: error.response?.data
});
}
}
/**
* POST /api/social/media/:mediaId/comments
* Post a new top-level comment on a media post
* Body: { message: "comment text" }
*/
async function createComment(req, res) {
console.log('💬 Create top-level comment endpoint hit');
const userId = req.query.userId;
const { mediaId } = req.params;
const { message } = req.body;
if (!mediaId) {
return res.status(400).json({ error: 'mediaId parameter required' });
}
if (!message || message.trim() === '') {
return res.status(400).json({ error: 'message is required in body' });
}
try {
const { pageToken, igUserId } = await getPageConnection(userId);
if (!pageToken || !igUserId) {
return res.status(400).json({ error: 'Connect a channel first' });
}
const comment = await postIGComment(mediaId, message, pageToken);
console.log('✅ Top-level comment posted successfully');
return res.json({
success: true,
comment_id: comment.id
});
} catch (error) {
console.error('💥 Create comment error:', error.message);
return res.status(500).json({
error: error.message,
details: error.response?.data
});
}
}
module.exports = {
listComments,
replyComment,
deleteComment,
toggleHideComment,
createComment
};

View File

@ -0,0 +1,169 @@
const messagesService = require('../services/dm_messages.service');
const autoreplyService = require('../services/dm_autoreply.service');
const { getPageConnection } = require('../services/tokenStore.service');
async function resolveTokens(req) {
const userId = req?.query?.userId || req?.body?.userId;
const bodyToken = req?.body?.pageAccessToken;
const bodyIgUserId = req?.body?.igUserId;
console.log('🔍 Resolving tokens for userId:', userId, bodyToken, bodyIgUserId);
if (bodyToken) {
return {
pageAccessToken: bodyToken,
igUserId: bodyIgUserId || null,
};
}
if (!userId) {
throw new Error('userId is required as query parameter');
}
const { pageToken, igUserId } = await getPageConnection(userId);
if (!pageToken || !igUserId) {
throw new Error('Connect a channel first');
}
return {
pageAccessToken: pageToken,
igUserId,
};
}
async function getConversations(req, res) {
console.log('💬 Get conversations endpoint hit');
const userId = req.query.userId;
console.log('👤 User ID:', userId);
try {
const { after } = req.query;
const tokens = await resolveTokens(req);
if (!tokens) throw new Error('Connect a channel first');
const { pageAccessToken, igUserId } = tokens;
const conversations = await messagesService.getConversations(
igUserId,
pageAccessToken,
after
);
console.log(`✅ Fetched ${conversations.data?.length || 0} conversations`);
res.json(conversations);
} catch (error) {
console.error('💥 Get conversations error:', error.message);
res.status(500).json({ error: error.message });
}
}
async function getMessages(req, res) {
console.log('💬 Get messages endpoint hit');
const userId = req.query.userId;
const { conversationId } = req.params;
console.log('👤 User ID:', userId);
console.log('💭 Conversation ID:', conversationId);
try {
const { after } = req.query;
const tokens = await resolveTokens(req);
if (!tokens) throw new Error('Connect a channel first');
const { pageAccessToken } = tokens;
const messages = await messagesService.getMessages(
conversationId,
pageAccessToken,
after
);
console.log(`✅ Fetched ${messages.data?.length || 0} messages`);
res.json(messages);
} catch (error) {
console.error('💥 Get messages error:', error.message);
res.status(500).json({ error: error.message });
}
}
async function sendMessage(req, res) {
console.log('📤 Send message endpoint hit');
const userId = req.query.userId || req.body.userId;
const { recipientId, message } = req.body;
console.log('👤 User ID:', userId);
console.log('📨 Recipient ID:', recipientId);
try {
if (!recipientId || !message) {
return res.status(400).json({ error: 'recipientId and message are required in body' });
}
const tokens = await resolveTokens(req);
if (!tokens) throw new Error('Connect a channel first');
const { pageAccessToken } = tokens;
const result = await messagesService.sendMessage(
recipientId,
message,
pageAccessToken
);
console.log(`✅ Message sent successfully`);
res.json(result);
} catch (error) {
console.error('💥 Send message error:', error.message);
res.status(500).json({ error: error.message });
}
}
async function markAsRead(req, res) {
console.log('👁️ Mark as read endpoint hit');
const userId = req.query.userId || req.body.userId;
const { conversationId } = req.params;
console.log('👤 User ID:', userId);
console.log('💭 Conversation ID:', conversationId);
try {
const tokens = await resolveTokens(req);
if (!tokens) throw new Error('Connect a channel first');
const { pageAccessToken } = tokens;
const result = await messagesService.markAsRead(
conversationId,
pageAccessToken
);
console.log(`✅ Conversation marked as read`);
res.json(result);
} catch (error) {
console.error('💥 Mark as read error:', error.message);
res.status(500).json({ error: error.message });
}
}
async function testAutoReply(req, res) {
try {
const { messageText } = req.body;
if (!messageText) {
return res.status(400).json({ error: 'messageText is required' });
}
const reply = autoreplyService.generateStaticReply(messageText);
res.json({
originalMessage: messageText,
autoReply: reply,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
}
module.exports = {
getConversations,
getMessages,
sendMessage,
markAsRead,
testAutoReply,
};

View File

@ -0,0 +1,51 @@
const webhookService = require('../services/dm_webhook.service');
const autoreplyService = require('../services/dm_autoreply.service');
function verifyWebhook(req, res) {
try {
const challenge = webhookService.verifyWebhook(req.query);
if (challenge) {
return res.status(200).send(challenge);
}
res.status(403).send('Verification failed');
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async function handleWebhook(req, res) {
try {
const signature = req.headers['x-hub-signature-256'];
const rawBody = req.rawBody;
if (!webhookService.verifySignature(rawBody, signature)) {
return res.status(403).send('Invalid signature');
}
res.status(200).send('EVENT_RECEIVED');
const messages = webhookService.processWebhookEvent(req.body);
if (!messages.length) {
return;
}
const pageAccessToken = process.env.PAGE_ACCESS_TOKEN;
if (!pageAccessToken) {
return;
}
for (const message of messages) {
await autoreplyService.handleIncomingMessage(message, pageAccessToken);
}
} catch (error) {
// Already acknowledged the webhook; log only
console.error('Webhook handling error:', error.message);
}
}
module.exports = {
verifyWebhook,
handleWebhook,
};

View File

@ -0,0 +1,75 @@
// src/controllers/media.controller.js
const { getPageConnection } = require('../services/tokenStore.service');
const { getIGMedia, getMediaDetails } = require('../services/media.service');
/**
* GET /api/social/media
* Get Instagram media posts for connected account
* Query params: limit (default 50), after (pagination cursor)
*/
async function listMedia(req, res) {
console.log('📸 Media list endpoint hit');
const userId =req.query.userId
const { limit = 50, after } = req.query;
try {
const { pageToken, igUserId } = await getPageConnection(userId);
if (!pageToken || !igUserId) {
return res.status(400).json({ error: 'Connect a channel first' });
}
const media = await getIGMedia(igUserId, pageToken, parseInt(limit), after);
console.log(`✅ Fetched ${media.data.length} media items`);
return res.json({
data: media.data,
paging: media.paging,
count: media.data.length
});
} catch (error) {
console.error('💥 Media list error:', error.message);
return res.status(500).json({
error: error.message,
details: error.response?.data
});
}
}
/**
* GET /api/social/media/:mediaId
* Get details of a specific media post
*/
async function getMedia(req, res) {
console.log('📸 Single media endpoint hit');
const userId =req.query.userId
const { mediaId } = req.params;
if (!mediaId) {
return res.status(400).json({ error: 'mediaId parameter required' });
}
try {
const { pageToken, igUserId } = await getPageConnection(userId);
if (!pageToken || !igUserId) {
return res.status(400).json({ error: 'Connect a channel first' });
}
const media = await getMediaDetails(mediaId, pageToken);
console.log('✅ Media details fetched');
return res.json(media);
} catch (error) {
console.error('💥 Media details error:', error.message);
return res.status(500).json({
error: error.message,
details: error.response?.data
});
}
}
module.exports = {
listMedia,
getMedia
};

View File

@ -0,0 +1,465 @@
const Stripe = require("stripe");
const Payment = require("../models/payment.module");
const User = require("../models/user.model");
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2022-11-15",
});
/* -----------------------------------------------
PLAN PRICE ID MAPPING
------------------------------------------------ */
const PLAN_PRICE_MAP = {
basic_monthly: process.env.STRIPE_PRICE_SB_BASIC_MONTHLY,
standard_monthly: process.env.STRIPE_PRICE_SB_STANDARD_MONTHLY,
premium_monthly: process.env.STRIPE_PRICE_SB_PREMIUM_MONTHLY,
basic_yearly: process.env.STRIPE_PRICE_SB_BASIC_YEARLY,
standard_yearly: process.env.STRIPE_PRICE_SB_STANDARD_YEARLY,
premium_yearly: process.env.STRIPE_PRICE_SB_PREMIUM_YEARLY,
};
/* -----------------------------------------------------
CREATE CHECKOUT SESSION SUBSCRIPTIONS
------------------------------------------------------ */
async function createCheckoutSession(req, res) {
try {
const { email, planId, userId } = req.body;
if (!email || !planId || !userId) {
return res.status(400).json({ error: "email, planId & userId required" });
}
const priceId = PLAN_PRICE_MAP[planId];
if (!priceId)
return res.status(400).json({ error: "Invalid planId" });
const price = await stripe.prices.retrieve(priceId);
const session = await stripe.checkout.sessions.create({
mode: "subscription",
customer_email: email,
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.FRONTEND_URL}/payment/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.FRONTEND_URL}/payment/cancel`,
metadata: { email, planId, userId },
});
await Payment.create({
userId,
email,
planId,
amount: price.unit_amount || 0,
stripeSessionId: session.id,
status: "pending",
});
res.json({ url: session.url });
} catch (err) {
console.error("Checkout Error:", err);
res.status(500).json({ error: "Internal Server Error" });
}
}
/* -----------------------------------------------------
PAYMENT INTENT (ONE-TIME ADD-ON)
------------------------------------------------------ */
async function createPaymentIntent(req, res) {
try {
const { email, planId, userId } = req.body;
if (!email || !planId || !userId) {
return res.status(400).json({ error: "email, planId & userId required" });
}
const priceId = PLAN_PRICE_MAP[planId];
if (!priceId)
return res.status(400).json({ error: "Invalid planId" });
const price = await stripe.prices.retrieve(priceId);
const paymentIntent = await stripe.paymentIntents.create({
amount: price.unit_amount,
currency: price.currency,
metadata: { email, planId, userId },
automatic_payment_methods: { enabled: true },
});
await Payment.create({
userId,
email,
amount: price.unit_amount,
planId,
stripePaymentIntentId: paymentIntent.id,
status: "pending",
});
res.json({ clientSecret: paymentIntent.client_secret });
} catch (err) {
console.error("PaymentIntent Error:", err);
res.status(500).json({ error: "Internal Server Error" });
}
}
/* -----------------------------------------------------
STRIPE WEBHOOK HANDLER
------------------------------------------------------ */
async function handleWebhook(req, res) {
let event;
try {
const signature = req.headers["stripe-signature"];
event = stripe.webhooks.constructEvent(
req.rawBody,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
switch (event.type) {
/* -------------------------
CHECKOUT SUCCESS
---------------------------- */
case "checkout.session.completed": {
const session = event.data.object;
const customerId = session.customer;
// Fetch subscription details from Stripe
const subscription = await stripe.subscriptions.retrieve(
session.subscription
);
// Update Payment Record
const updatedPayment = await Payment.findOneAndUpdate(
{ stripeSessionId: session.id },
{
subscriptionId: session.subscription,
status: "active",
subscriptionStartDate: new Date(subscription.current_period_start * 1000),
subscriptionEndDate: new Date(subscription.current_period_end * 1000),
},
{ new: true }
);
// Update User Record with Stripe Customer ID
if (updatedPayment && updatedPayment.userId && customerId) {
await User.findByIdAndUpdate(updatedPayment.userId, {
stripeCustomerId: customerId
});
}
break;
}
/* -------------------------
PAYMENT FAILED
---------------------------- */
case "invoice.payment_failed":
await Payment.findOneAndUpdate(
{ subscriptionId: event.data.object.subscription },
{ status: "failed" }
);
break;
/* -------------------------
SUBSCRIPTION CANCELLED
---------------------------- */
case "customer.subscription.deleted": {
const sub = event.data.object;
await Payment.findOneAndUpdate(
{ subscriptionId: sub.id },
{
status: "canceled",
subscriptionEndDate: new Date(sub.canceled_at * 1000),
}
);
break;
}
}
res.json({ received: true });
}
/* -----------------------------------------------------
CANCEL SUBSCRIPTION (MANUAL CANCEL FROM UI)
------------------------------------------------------ */
async function cancelSubscription(req, res) {
try {
const { session_id } = req.body;
if (!session_id)
return res.status(400).json({ error: "session_id required" });
const payment = await Payment.findOne({ stripeSessionId: session_id });
if (!payment)
return res.status(404).json({ error: "Subscription not found" });
// If it's a Stripe subscription
if (payment.subscriptionId && payment.planId !== "free_trial") {
const canceledSub = await stripe.subscriptions.cancel(payment.subscriptionId);
await Payment.findOneAndUpdate(
{ stripeSessionId: session_id },
{
status: "canceled",
subscriptionEndDate: new Date(canceledSub.canceled_at * 1000),
}
);
}
// If it's a Free Trial (Mock)
else if (payment.planId === "free_trial") {
await Payment.findOneAndUpdate(
{ stripeSessionId: session_id },
{
status: "canceled",
subscriptionEndDate: new Date(), // End immediately
}
);
// Also update User model
await User.findByIdAndUpdate(payment.userId, {
isTrialActive: false,
trialEndsAt: new Date() // Expire immediately
});
}
res.json({ message: "Subscription/Trial cancelled successfully" });
} catch (err) {
console.error("Cancel subscription error:", err);
res.status(500).json({ error: "Internal Server Error" });
}
}
/* -----------------------------------------------------
GET PAYMENT DETAILS
------------------------------------------------------ */
async function getPaymentDetails(req, res) {
try {
const { session_id } = req.query;
if (!session_id)
return res.status(400).json({ error: "session_id required" });
const payment = await Payment.findOne({ stripeSessionId: session_id });
if (!payment)
return res.status(404).json({ error: "Payment not found" });
res.json({
success: true,
message: "Payment details fetched successfully",
data: {
id: payment._id,
userId: payment.userId,
email: payment.email,
planId: payment.planId,
amount: payment.amount / 100 || 0,
status: payment.status,
stripeSessionId: payment.stripeSessionId,
subscriptionId: payment.subscriptionId,
subscriptionStartDate: payment.subscriptionStartDate,
subscriptionEndDate: payment.subscriptionEndDate,
createdAt: payment.createdAt,
},
});
} catch (err) {
console.error("Payment details fetch error:", err);
res.status(500).json({ error: "Internal Server Error" });
}
}
/* -----------------------------------------------------
ACTIVATE FREE TRIAL (7 DAYS)
------------------------------------------------------ */
async function activateTrial(req, res) {
try {
const { userId, email } = req.body;
if (!userId || !email) {
return res.status(400).json({ error: "userId & email required" });
}
// Check if user already had a trial
const user = await User.findById(userId);
if (!user) return res.status(404).json({ error: "User not found" });
// If user has already used trial logic (if you want to restrict it once)
// For now, let's assume if they have a 'free_trial' payment record, they can't do it again.
const existingTrial = await Payment.findOne({ userId, planId: "free_trial" });
if (existingTrial) {
return res.status(400).json({ error: "Trial already activated previously." });
}
// Create 7-day trial dates
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 7);
// Create Payment Record (dummy so frontend sees it as active subscription)
const payment = await Payment.create({
userId,
email,
planId: "free_trial",
amount: 0,
status: "active",
subscriptionStartDate: startDate,
subscriptionEndDate: endDate,
stripeSessionId: `trial_${userId}_${Date.now()}` // Mock ID
});
// Update User model
user.trialEndsAt = endDate;
user.isTrialActive = true;
await user.save();
res.json({
success: true,
message: "7-day free trial activated!",
payment: {
id: payment._id,
planId: payment.planId,
amount: payment.amount,
status: payment.status,
sessionId: payment.stripeSessionId,
createdAt: payment.createdAt
}
});
} catch (err) {
console.error("Activate trial error:", err);
res.status(500).json({ error: "Internal Server Error" });
}
}
/* -----------------------------------------------------
GET USER SUBSCRIPTION STATUS (For Pricing Page)
------------------------------------------------------ */
async function getUserSubscriptionStatus(req, res) {
try {
const { userId } = req.query;
if (!userId) return res.status(400).json({ error: "userId required" });
// Check if user ever had a free trial
const trialRecord = await Payment.findOne({ userId, planId: "free_trial" });
const hasUsedTrial = !!trialRecord;
// Check for currently active subscription (Trial or Paid)
// We look for status 'active' and end date in the future
const activeSub = await Payment.findOne({
userId,
status: "active",
subscriptionEndDate: { $gt: new Date() }
}).sort({ createdAt: -1 });
let isTrialActive = false;
let trialEndsAt = null;
let currentPlan = null;
let stripeSessionId = null;
if (activeSub) {
currentPlan = activeSub.planId;
stripeSessionId = activeSub.stripeSessionId;
if (activeSub.planId === "free_trial") {
isTrialActive = true;
trialEndsAt = activeSub.subscriptionEndDate;
}
}
res.json({
hasUsedTrial,
isTrialActive,
trialStartDate: trialRecord ? trialRecord.subscriptionStartDate : null,
trialEndsAt,
currentPlan,
stripeSessionId
});
} catch (err) {
console.error("Get sub status error:", err);
res.status(500).json({ error: "Internal Server Error" });
}
}
/* -----------------------------------------------------
CREATE CUSTOMER PORTAL SESSION (Manage Billing)
------------------------------------------------------ */
async function createPortalSession(req, res) {
try {
const { userId } = req.body;
if (!userId) return res.status(400).json({ error: "userId required" });
const user = await User.findById(userId);
if (!user || !user.stripeCustomerId) {
return res.status(404).json({ error: "No Stripe customer found for this user" });
}
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.FRONTEND_URL}/account-settings`,
});
res.json({ url: session.url });
} catch (err) {
console.error("Portal Session Error:", err);
res.status(500).json({ error: "Internal Server Error" });
}
}
/* -----------------------------------------------------
GET BILLING INFO (Last 4 digits)
------------------------------------------------------ */
async function getBillingInfo(req, res) {
try {
const { userId } = req.query;
if (!userId) return res.status(400).json({ error: "userId required" });
const user = await User.findById(userId);
if (!user) return res.status(404).json({ error: "User not found" });
if (!user.stripeCustomerId) {
return res.json({ hasPaymentMethod: false });
}
// List payment methods
const paymentMethods = await stripe.customers.listPaymentMethods(
user.stripeCustomerId,
{ type: 'card' }
);
if (paymentMethods.data && paymentMethods.data.length > 0) {
const pm = paymentMethods.data[0]; // Just take the first one
return res.json({
hasPaymentMethod: true,
brand: pm.card.brand,
last4: pm.card.last4,
exp_month: pm.card.exp_month,
exp_year: pm.card.exp_year
});
}
res.json({ hasPaymentMethod: false });
} catch (err) {
console.error("Get billing info error:", err);
res.status(500).json({ error: "Internal Server Error" });
}
}
/* ----------------------------------------------------- */
module.exports = {
createCheckoutSession,
createPaymentIntent,
handleWebhook,
cancelSubscription,
getPaymentDetails,
activateTrial,
getUserSubscriptionStatus,
createPortalSession,
getBillingInfo
};

View File

@ -0,0 +1,364 @@
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");
const Payment = require("../models/payment.module.js");
// ======================= 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" }
);
// ⭐ Fetch payment by userId OR email
const payment = await Payment.findOne({
$or: [
{ userId: user._id },
{ email: user.email }
]
})
.sort({ createdAt: -1 })
.select("-__v");
return res.json({
message: "Login success",
token,
user: {
id: user._id,
name: user.name,
email: user.email,
mobileNumber: user.mobileNumber,
role: user.role,
},
payment: payment
? {
id: payment._id,
planId: payment.planId,
amount: payment.amount/100,
status: payment.status,
sessionId: payment.stripeSessionId,
subscriptionId: payment.subscriptionId,
createdAt: payment.createdAt,
}
: null
});
} 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" });
}
}
// -----------------------------------
// 🔹 Google Login Step 1
// -----------------------------------
async function googleAuth(req, res, next) {
passport.authenticate("google", {
scope: ["profile", "email"],
})(req, res, next);
}
// -----------------------------------
// 🔹 Google Callback Step 2
// -----------------------------------
async function googleAuthCallback(req, res) {
try {
const user = req.user;
const token = jwt.sign(
{ userid: user._id, email: user.email },
process.env.JWT_SECRET || "SECRET_KEY",
{ expiresIn: "1h" }
);
// ✅ Localhost-safe cookie
res.cookie("token", token, {
httpOnly: true,
secure: false,
sameSite: "lax",
maxAge: 24 * 60 * 60 * 1000,
});
// ✅ Redirect to frontend
const frontendUrl = new URL("https://devapp.socialbuddy.co/login");
frontendUrl.searchParams.append("userid", user._id.toString());
frontendUrl.searchParams.append("email", user.email);
return res.redirect(frontendUrl.toString());
} catch (err) {
console.error("❌ Google login callback error:", err);
return res.redirect(
"https://devapp.socialbuddy.co/login?error=google_auth_failed"
);
}
}
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,
googleAuth,
googleAuthCallback,
// CRUD
createUser,
getUsers,
getUserById,
updateUser,
deleteUser,
};

65
src/lib/http.js Normal file
View 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 };

View 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 };

View 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 };

View File

@ -0,0 +1,24 @@
/**
* Capture raw body for DM webhook signature verification.
* Must run before JSON parsing. Limited to DM webhook route to avoid overhead.
*/
function captureDmRawBody(req, res, next) {
const path = req.originalUrl || req.url;
if (path.startsWith('/api/dm/webhook')) {
let data = '';
req.setEncoding('utf8');
req.on('data', (chunk) => {
data += chunk;
});
req.on('end', () => {
req.rawBody = data;
next();
});
} else {
next();
}
}
module.exports = { captureDmRawBody };

View 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 };

View File

@ -0,0 +1,22 @@
const mongoose = require("mongoose");
const PaymentSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
email: { type: String, required: true },
planId: { type: String, required: true },
stripeSessionId: { type: String },
stripePaymentIntentId: { type: String },
subscriptionId: { type: String },
amount: { type: Number },
status: { type: String, default: "pending" },
// 🔥 Added fields
subscriptionStartDate: { type: Date },
subscriptionEndDate: { type: Date },
}, { timestamps: true });
module.exports = mongoose.model("Payment", PaymentSchema);

27
src/models/user.model.js Normal file
View 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);

43
src/routes/auth.routes.js Normal file
View File

@ -0,0 +1,43 @@
const express = require("express");
const passport = require("passport");
const {
signup,
login,
changePassword,
forgotPassword,
resetPassword,
googleAuth,
googleAuthCallback,
} = 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);
// ✅ Google OAuth
router.get("/google", googleAuth);
router.get(
"/google/callback",
passport.authenticate("google", {
failureRedirect: "https://devapp.socialbuddy.co/login?error=google_auth_failed",
}),
googleAuthCallback
);
// router.get("/protected", protectedRoute);
// example protected route
router.get("/profile", authMiddleware, (req, res) => {
res.json({ user: req.user });
});
module.exports = router;

View File

@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const messagesController = require('../controllers/dm_messages.controller');
router.get('/conversations', messagesController.getConversations);
router.get('/conversations/:conversationId/messages', messagesController.getMessages);
router.post('/send', messagesController.sendMessage);
router.post('/conversations/:conversationId/read', messagesController.markAsRead);
router.post('/test-autoreply', messagesController.testAutoReply);
module.exports = router;

View File

@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const webhookController = require('../controllers/dm_webhook.controller');
router.get('/', webhookController.verifyWebhook);
router.post('/', webhookController.handleWebhook);
module.exports = router;

View File

@ -0,0 +1,26 @@
const express = require("express");
const router = express.Router();
const controller = require("../controllers/payment.controller");
router.post("/create-checkout-session", controller.createCheckoutSession);
router.post("/create-payment-intent", controller.createPaymentIntent);
router.post("/activate-trial", controller.activateTrial);
router.get("/status", controller.getUserSubscriptionStatus);
// Get payment details
router.get("/details", controller.getPaymentDetails);
// Cancel subscription manually
router.post("/cancel", controller.cancelSubscription);
// Stripe Webhook
router.post("/webhook", express.raw({ type: "application/json" }), controller.handleWebhook);
// Portal & Billing
router.post("/create-portal-session", controller.createPortalSession);
router.get("/billing-info", controller.getBillingInfo);
module.exports = router;

View File

@ -0,0 +1,58 @@
// src/routes/social.routes.js
const express = require('express');
const router = express.Router();
// Controllers
const { login, callback, save_oauth, status, disconnect } = require('../controllers/auth.controller');
const { listChannels, connectChannel } = require('../controllers/channels.controller');
const { getAccount } = require('../controllers/account.controller');
const { listMedia, getMedia } = require('../controllers/media.controller');
const {
listComments,
replyComment,
deleteComment,
toggleHideComment,
createComment
} = require('../controllers/comments.controller');
const {
autoReplyComments,
getRepliedComments,
clearRepliedComments
} = require('../controllers/automation.controller');
// ===== Auth Routes =====
router.get('/auth/status', status);
router.get('/auth/login', login);
router.get('/auth/callback', callback);
router.post('/auth/save-oauth', save_oauth);
router.post('/auth/disconnect', disconnect);
// ===== Channel Routes =====
router.get('/channels', listChannels);
router.post('/connect', connectChannel);
// ===== Account Routes =====
router.get('/account', getAccount);
// ===== Media Routes =====
router.get('/media', listMedia); // GET /api/social/media?limit=50&after=cursor
router.get('/media/:mediaId', getMedia); // GET /api/social/media/{mediaId}
// ===== Comments Routes =====
router.get('/media/:mediaId/comments', listComments); // GET /api/social/media/{mediaId}/comments
router.post('/media/:mediaId/comments', createComment); // POST /api/social/media/{mediaId}/comments
router.post('/comments/:commentId/reply', replyComment); // POST /api/social/comments/{commentId}/reply
router.delete('/comments/:commentId', deleteComment); // DELETE /api/social/comments/{commentId}
router.post('/comments/:commentId/hide', toggleHideComment); // POST /api/social/comments/{commentId}/hide
// ===== Automation Routes =====
router.post('/automation/auto-reply', autoReplyComments); // POST /api/social/automation/auto-reply
router.get('/automation/replied-comments', getRepliedComments); // GET /api/social/automation/replied-comments
router.delete('/automation/replied-comments', clearRepliedComments); // DELETE /api/social/automation/replied-comments
module.exports = router;

19
src/routes/user.routes.js Normal file
View 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;

View File

@ -0,0 +1,17 @@
const { http } = require('../lib/http');
const GRAPH = 'https://graph.facebook.com';
const GRAPH_VER = process.env.GRAPH_VER || 'v21.0';
async function getBusinessAccountProfile(businessAccountId, pageAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/${businessAccountId}`;
const { data } = await http.get(url, {
params: {
fields: 'id,username,biography,profile_picture_url,website,followers_count,follows_count,media_count',
access_token: pageAccessToken,
},
});
if (data.error) throw new Error(data.error.message);
return data;
}
module.exports = { getBusinessAccountProfile };

0
src/services/automation Normal file
View File

View File

@ -0,0 +1,93 @@
// src/services/automation.service.js
const axios = require("axios");
// Your AI Core endpoint
const AI_CORE_URL =
process.env.AI_CORE_URL || "https://llm.thedomainnest.com/chat-json";
// Optional: if you protect your endpoint with a token later, set AI_CORE_API_KEY in env
const AI_CORE_API_KEY = process.env.AI_CORE_API_KEY || "";
/**
* Generate AI reply using your AI Core endpoint
* @param {string} commentText - The comment text
* @param {string} accountBio - Instagram account biography
* @param {string} accountUsername - Instagram username
* @param {string} postCaption - Post caption
* @returns {Promise<string>} Generated reply text
*/
async function generateReply(commentText, accountBio, accountUsername, postCaption, commentedby="") {
await sleep(500);
const prompt = `You are managing an Instagram account.
- Biography: "${accountBio}"
- Username: "${accountUsername}"
- Post Caption: "${postCaption}"
A user commented: "${commentText}"
- Commenter: "${commentedby}"
Generate a warm, on-brand, concise, and helpful reply and be more friendly with the commenter.. while addressing the commenter, always wish in a friendly and differnet manner not only with hi and hello and at starting and with a little bit sarcasm (max 2-3 sentences) addressing the commenter. and dont include the source of information in the reply.`;
try {
const response = await axios.post(
AI_CORE_URL,
{
message: prompt,
mode: "fast", // you can change to "quality" etc. if your server supports it
},
{
headers: {
"Content-Type": "application/json",
...(AI_CORE_API_KEY ? { Authorization: `Bearer ${AI_CORE_API_KEY}` } : {}),
},
timeout: Number(process.env.AI_CORE_TIMEOUT_MS || 600000),
}
);
// Your response format:
// { mode_used, model_used, reply, image_context, file_used }
const reply =
response.data?.reply ||
response.data?.message ||
response.data?.text ||
"Thanks for your comment!";
console.log("🤖 AI Core reply:", response.data);
return String(reply).trim().replace(/\s+/g, " ");
} catch (error) {
const errPayload = error.response?.data || error.message;
console.error("❌ AI Core error:", errPayload);
return "Thank you for your comment! We appreciate your engagement. 💙";
}
}
/**
* Filter comments from last 24 hours
* @param {Array} comments - Array of comment objects
* @returns {Array} Filtered comments
*/
function filterRecentComments(comments) {
const cutoff = Date.now() - 24 * 60 * 60 * 1000; // 24 hours ago
return (comments || []).filter((c) => {
const commentTime = new Date(c.timestamp).getTime();
return commentTime >= cutoff;
});
}
/**
* Sleep helper
* @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = {
generateReply,
filterRecentComments,
sleep,
};

View File

@ -0,0 +1,85 @@
// src/services/automation.service.js
const axios = require('axios');
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || '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';
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
/**
* Generate AI reply using OpenRouter
* @param {string} commentText - The comment text
* @param {string} accountBio - Instagram account biography
* @param {string} accountUsername - Instagram username
* @param {string} postCaption - Post caption
* @returns {Promise<string>} Generated reply text
*/
async function generateReply(commentText, accountBio, accountUsername, postCaption) {
await sleep(500);
const prompt = `You are managing an Instagram account.
- Biography: "${accountBio}"
- Username: "${accountUsername}"
- Post Caption: "${postCaption}"
A user commented: "${commentText}"
Generate a warm, on-brand, concise, and helpful reply (max 2-3 sentences).`;
try {
const response = await axios.post(
OPENROUTER_URL,
{
model: "openai/gpt-4o-mini",
messages: [
{
role: 'user',
content: prompt,
},
],
max_tokens: 150,
temperature: 0.7,
},
{
headers: {
Authorization: `Bearer ${OPENROUTER_API_KEY}`,
'HTTP-Referer': process.env.APP_URL || 'https://socialbuddy.co',
'X-Title': 'Instagram Reply Generator',
'Content-Type': 'application/json',
},
}
);
const reply = response.data?.choices?.[0]?.message?.content || 'Thanks for your comment!';
return reply.trim().replace(/\s+/g, ' ');
} catch (error) {
console.error('❌ OpenRouter error:', error.response?.data || error.message);
return 'Thank you for your comment! We appreciate your engagement. 💙';
}
}
/**
* Filter comments from last 24 hours
* @param {Array} comments - Array of comment objects
* @returns {Array} Filtered comments
*/
function filterRecentComments(comments) {
const cutoff = Date.now() - 24 * 60 * 60 * 1000; // 24 hours ago
return (comments || []).filter(c => {
const commentTime = new Date(c.timestamp).getTime();
return commentTime >= cutoff;
});
}
/**
* Sleep helper
* @param {number} ms - Milliseconds to sleep
* @returns {Promise}
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
generateReply,
filterRecentComments,
sleep
};

View File

@ -0,0 +1,24 @@
const { http } = require('../lib/http');
const GRAPH = 'https://graph.facebook.com';
const GRAPH_VER = process.env.GRAPH_VER || 'v21.0';
async function listChannelsForUser(userAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/me/accounts`;
const { data } = await http.get(url, { params: { access_token: userAccessToken }});
if (data.error) throw new Error(data.error.message);
return data.data || [];
}
async function getChannelDetails(channelId, userAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/${channelId}`;
const { data } = await http.get(url, {
params: {
fields: 'name,access_token,instagram_business_account',
access_token: userAccessToken,
},
});
if (data.error) throw new Error(data.error.message);
return data;
}
module.exports = { listChannelsForUser, getChannelDetails };

View File

@ -0,0 +1,105 @@
// src/services/comments.service.js
const { http } = require('../lib/http');
const GRAPH = 'https://graph.facebook.com';
const GRAPH_VER = process.env.GRAPH_VER || 'v21.0';
/**
* Get comments on an Instagram media post
* @param {string} mediaId - Instagram media ID
* @param {string} pageAccessToken - Page access token
* @param {string|null} after - Cursor for pagination
* @returns {Promise<Object>} Comments data with pagination
*/
async function getIGComments(mediaId, pageAccessToken, after = null) {
const url = `${GRAPH}/${GRAPH_VER}/${mediaId}/comments`;
const params = {
fields: 'id,text,timestamp,username,like_count,hidden,replies{id,text,timestamp,username,like_count,hidden}',
limit: 100,
access_token: pageAccessToken
};
if (after) params.after = after;
const { data } = await http.get(url, { params });
if (data.error) throw new Error(data.error.message);
return data; // { data: [...], paging: { cursors, next } }
}
/**
* Reply to an Instagram comment
* @param {string} commentId - Instagram comment ID
* @param {string} message - Reply message text
* @param {string} pageAccessToken - Page access token
* @returns {Promise<Object>} Reply response with new comment ID
*/
async function replyToIGComment(commentId, message, pageAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/${commentId}/replies`;
const { data } = await http.post(url, null, {
params: {
message,
access_token: pageAccessToken
}
});
if (data.error) throw new Error(data.error.message);
return data; // { id: 'new_comment_id' }
}
/**
* Delete an Instagram comment
* @param {string} commentId - Instagram comment ID
* @param {string} pageAccessToken - Page access token
* @returns {Promise<Object>} Deletion response
*/
async function deleteIGComment(commentId, pageAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/${commentId}`;
const { data } = await http.delete(url, {
params: { access_token: pageAccessToken }
});
if (data.error) throw new Error(data.error.message);
return data; // { success: true }
}
/**
* Hide/Unhide an Instagram comment
* @param {string} commentId - Instagram comment ID
* @param {boolean} hide - true to hide, false to unhide
* @param {string} pageAccessToken - Page access token
* @returns {Promise<Object>} Response
*/
async function hideIGComment(commentId, hide, pageAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/${commentId}`;
const { data } = await http.post(url, null, {
params: {
hide,
access_token: pageAccessToken
}
});
if (data.error) throw new Error(data.error.message);
return data; // { success: true }
}
/**
* Post a top-level comment on an Instagram media post
* @param {string} mediaId - Instagram media ID
* @param {string} message - Comment text
* @param {string} pageAccessToken - Page access token
* @returns {Promise<Object>} Response with new comment ID
*/
async function postIGComment(mediaId, message, pageAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/${mediaId}/comments`;
const { data } = await http.post(url, null, {
params: {
message,
access_token: pageAccessToken
}
});
if (data.error) throw new Error(data.error.message);
return data; // { id: 'new_comment_id' }
}
module.exports = {
getIGComments,
replyToIGComment,
deleteIGComment,
hideIGComment,
postIGComment
};

View File

@ -0,0 +1,78 @@
const messagesService = require('./dm_messages.service');
// Static reply templates (ASCII only)
const REPLY_TEMPLATES = {
GREETING: 'Hi! Thanks for reaching out. I will get back to you soon.',
BUSINESS_HOURS: 'Thanks for your message. Our business hours are 9 AM - 6 PM. We will respond during business hours.',
DEFAULT: 'Thank you for your message. We have received it and will reply shortly.',
KEYWORDS: {
price: 'For pricing information, please check our website or DM us with specific requirements.',
hours: "We are open Monday-Friday, 9 AM - 6 PM.",
support: 'Our support team will assist you shortly. Please describe your issue in detail.',
order: 'To check your order status, please provide your order number.',
},
};
function detectKeyword(text) {
const lowerText = text.toLowerCase();
for (const keyword of Object.keys(REPLY_TEMPLATES.KEYWORDS)) {
if (lowerText.includes(keyword)) {
return keyword;
}
}
return null;
}
function generateStaticReply(messageText) {
const keyword = detectKeyword(messageText);
if (keyword) {
return REPLY_TEMPLATES.KEYWORDS[keyword];
}
const now = new Date();
const hour = now.getHours();
const isBusinessHours = hour >= 9 && hour < 18;
if (!isBusinessHours) {
return REPLY_TEMPLATES.BUSINESS_HOURS;
}
return REPLY_TEMPLATES.DEFAULT;
}
async function handleIncomingMessage(incomingMessage, pageAccessToken) {
try {
const { senderId, text, messageId } = incomingMessage;
const replyText = generateStaticReply(text);
const result = await messagesService.sendMessage(
senderId,
replyText,
pageAccessToken
);
return {
success: true,
originalMessageId: messageId,
replyMessageId: result.message_id,
replyText,
};
} catch (error) {
return {
success: false,
error: error.message,
originalMessageId: incomingMessage.messageId,
};
}
}
async function generateLLMReply() {
throw new Error('LLM integration not yet implemented');
}
module.exports = {
generateStaticReply,
handleIncomingMessage,
generateLLMReply,
REPLY_TEMPLATES,
};

View File

@ -0,0 +1,113 @@
const { http } = require('../lib/http');
const GRAPH = 'https://graph.facebook.com';
const GRAPH_VER = process.env.GRAPH_VER || 'v21.0';
/**
* Get conversations for an Instagram account
* @param {string} igUserId - Instagram Business Account ID
* @param {string} pageAccessToken - Page access token
* @param {string|null} after - Cursor for pagination
* @returns {Promise<Object>} Conversations data
*/
async function getConversations(igUserId, pageAccessToken, after = null) {
const url = `${GRAPH}/${GRAPH_VER}/${igUserId}/conversations`;
const params = {
fields: 'id,updated_time,participants,messages{id,created_time,from,to,message}',
platform: 'instagram',
limit: 25,
access_token: pageAccessToken,
};
if (after) params.after = after;
const { data } = await http.get(url, { params });
if (data.error) throw new Error(data.error.message);
return data;
}
/**
* Get messages in a conversation
* @param {string} conversationId - Conversation ID
* @param {string} pageAccessToken - Page access token
* @param {string|null} after - Cursor for pagination
* @returns {Promise<Object>} Messages data
*/
async function getMessages(conversationId, pageAccessToken, after = null) {
const url = `${GRAPH}/${GRAPH_VER}/${conversationId}/messages`;
const params = {
fields: 'id,created_time,from,to,message',
limit: 50,
access_token: pageAccessToken,
};
if (after) params.after = after;
const { data } = await http.get(url, { params });
if (data.error) throw new Error(data.error.message);
return data;
}
/**
* Send a message reply
* @param {string} recipientId - Instagram-scoped user ID (IGSID)
* @param {string} message - Message text to send
* @param {string} pageAccessToken - Page access token
* @returns {Promise<Object>} Response with message ID
*/
async function sendMessage(recipientId, message, pageAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/me/messages`;
const { data } = await http.post(
url,
{
recipient: { id: recipientId },
message: { text: message },
},
{
params: { access_token: pageAccessToken },
}
);
if (data.error) throw new Error(data.error.message);
return data;
}
/**
* Mark a conversation as read
* @param {string} conversationId - Conversation ID
* @param {string} pageAccessToken - Page access token
* @returns {Promise<Object>} Response
*/
async function markAsRead(conversationId, pageAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/${conversationId}`;
const { data } = await http.post(url, null, {
params: {
action: 'mark_read',
access_token: pageAccessToken,
},
});
if (data.error) throw new Error(data.error.message);
return data;
}
/**
* Get a single message details
* @param {string} messageId - Message ID
* @param {string} pageAccessToken - Page access token
* @returns {Promise<Object>} Message details
*/
async function getMessageDetails(messageId, pageAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/${messageId}`;
const { data } = await http.get(url, {
params: {
fields: 'id,created_time,from,to,message,story,attachments',
access_token: pageAccessToken,
},
});
if (data.error) throw new Error(data.error.message);
return data;
}
module.exports = {
getConversations,
getMessages,
sendMessage,
markAsRead,
getMessageDetails,
};

View File

@ -0,0 +1,80 @@
const crypto = require('crypto');
const VERIFY_TOKEN = process.env.WEBHOOK_VERIFY_TOKEN || 'your_verify_token_123';
const APP_SECRET = process.env.OAUTH_APP_SECRET;
function verifyWebhook(query) {
const mode = query['hub.mode'];
const token = query['hub.verify_token'];
const challenge = query['hub.challenge'];
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
return challenge;
}
return null;
}
function verifySignature(rawBody, signature) {
if (!rawBody || !signature || !APP_SECRET) return false;
const signatureHash = signature.split('sha256=')[1];
if (!signatureHash) return false;
const expectedSignature = crypto
.createHmac('sha256', APP_SECRET)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHash, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
function processWebhookEvent(body) {
const processedMessages = [];
if (body.object !== 'instagram') {
return processedMessages;
}
body.entry?.forEach((entry) => {
entry.messaging?.forEach((messagingEvent) => {
if (messagingEvent.message) {
const message = {
senderId: messagingEvent.sender.id,
recipientId: messagingEvent.recipient.id,
timestamp: messagingEvent.timestamp,
messageId: messagingEvent.message.mid,
text: messagingEvent.message.text || '',
isEcho: messagingEvent.message.is_echo || false,
attachments: messagingEvent.message.attachments || [],
};
if (!message.isEcho) {
processedMessages.push(message);
}
}
if (messagingEvent.message?.reply_to) {
const storyReply = {
senderId: messagingEvent.sender.id,
recipientId: messagingEvent.recipient.id,
timestamp: messagingEvent.timestamp,
messageId: messagingEvent.message.mid,
text: messagingEvent.message.text || '',
replyToStoryId: messagingEvent.message.reply_to.story.id,
};
processedMessages.push(storyReply);
}
});
});
return processedMessages;
}
module.exports = {
verifyWebhook,
verifySignature,
processWebhookEvent,
};

View File

@ -0,0 +1,49 @@
// src/services/media.service.js
const { http } = require('../lib/http');
const GRAPH = 'https://graph.facebook.com';
const GRAPH_VER = process.env.GRAPH_VER || 'v21.0';
/**
* Get Instagram media posts
* @param {string} igUserId - Instagram Business Account ID
* @param {string} pageAccessToken - Page access token
* @param {number} limit - Number of posts to fetch (default 50)
* @param {string|null} after - Cursor for pagination
* @returns {Promise<Object>} Media data with pagination
*/
async function getIGMedia(igUserId, pageAccessToken, limit = 50, after = null) {
const url = `${GRAPH}/${GRAPH_VER}/${igUserId}/media`;
const params = {
fields: 'id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,comments_count,like_count',
limit,
access_token: pageAccessToken
};
if (after) params.after = after;
const { data } = await http.get(url, { params });
if (data.error) throw new Error(data.error.message);
return data; // { data: [...], paging: { cursors, next } }
}
/**
* Get a single media item details
* @param {string} mediaId - Instagram media ID
* @param {string} pageAccessToken - Page access token
* @returns {Promise<Object>} Media details
*/
async function getMediaDetails(mediaId, pageAccessToken) {
const url = `${GRAPH}/${GRAPH_VER}/${mediaId}`;
const { data } = await http.get(url, {
params: {
fields: 'id,caption,media_type,media_url,permalink,thumbnail_url,timestamp,comments_count,like_count,children{id,media_type,media_url}',
access_token: pageAccessToken
}
});
if (data.error) throw new Error(data.error.message);
return data;
}
module.exports = {
getIGMedia,
getMediaDetails
};

View File

@ -0,0 +1,76 @@
const fetch = global.fetch || require('node-fetch');
const { URLSearchParams } = require('url');
const APP_ID = process.env.OAUTH_APP_ID;
const APP_SECRET = process.env.OAUTH_APP_SECRET;
const REDIRECT_URI = process.env.OAUTH_REDIRECT_URI;
const GRAPH_VER = process.env.GRAPH_VER || 'v21.0';
function buildLoginUrl() {
// const scope = [
// 'pages_show_list',
// 'instagram_basic',
// 'instagram_manage_comments',
// 'instagram_manage_messages',
// 'instagram_manage_insights',
// 'instagram_content_publish',
// 'pages_manage_metadata',
// 'pages_read_engagement',
// 'business_management',
// ].join(',');
const scope = [
'pages_show_list',
'pages_read_engagement',
'pages_read_user_content',
'pages_manage_posts',
'pages_manage_engagement',
'pages_manage_metadata',
'pages_messaging', // ✅ ADD THIS!
'instagram_basic',
'instagram_content_publish',
'instagram_manage_comments',
'instagram_manage_insights',
'instagram_manage_messages',
'business_management'
].join(',');
const qs = new URLSearchParams({
client_id: APP_ID,
redirect_uri: REDIRECT_URI,
scope,
response_type: 'code',
state: 'oauth_state',
});
return `https://www.facebook.com/${GRAPH_VER}/dialog/oauth?${qs.toString()}`;
}
async function getShortLivedUserToken(code) {
const qs = new URLSearchParams({
client_id: APP_ID,
client_secret: APP_SECRET,
redirect_uri: REDIRECT_URI,
code,
});
const res = await fetch(`https://graph.facebook.com/${GRAPH_VER}/oauth/access_token?${qs}`);
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message || 'Failed to get user token');
return data; // { access_token, token_type, expires_in }
}
async function exchangeToLongLivedUserToken(shortToken) {
const qs = new URLSearchParams({
grant_type: 'fb_exchange_token',
client_id: APP_ID,
client_secret: APP_SECRET,
fb_exchange_token: shortToken,
});
const res = await fetch(`https://graph.facebook.com/${GRAPH_VER}/oauth/access_token?${qs}`);
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message || 'Failed to exchange long-lived token');
return data; // { access_token, token_type, expires_in }
}
module.exports = { buildLoginUrl, getShortLivedUserToken, exchangeToLongLivedUserToken };

View File

@ -0,0 +1,22 @@
const store = new Map(); // swap with DB when ready
async function saveUserToken(userId, longUserToken, expiresAt) {
const cur = store.get(userId) || {};
store.set(userId, { ...cur, longUserToken, longUserTokenExpiresAt: expiresAt });
}
async function getUserToken(userId) {
return store.get(userId)?.longUserToken || null;
}
async function savePageConnection(userId, pageId, pageToken, businessAccountId) {
const cur = store.get(userId) || {};
store.set(userId, { ...cur, pageId, pageToken, igUserId: businessAccountId });
}
async function getPageConnection(userId) {
const cur = store.get(userId) || {};
return { pageId: cur.pageId, pageToken: cur.pageToken, igUserId: cur.igUserId };
}
module.exports = { saveUserToken, getUserToken, savePageConnection, getPageConnection };

View File

@ -0,0 +1,105 @@
// src/services/tokenStore.service.js
const fs = require("fs");
const path = require("path");
const STORE_FILE = path.resolve("data/store.json");
// Ensure file + folder exists
function ensureStoreFile() {
fs.mkdirSync(path.dirname(STORE_FILE), { recursive: true });
if (!fs.existsSync(STORE_FILE)) {
fs.writeFileSync(STORE_FILE, "{}");
}
}
// Read JSON fresh every time
function readStore() {
ensureStoreFile();
try {
const raw = fs.readFileSync(STORE_FILE, "utf8");
if (!raw.trim()) return {}; // empty file
return JSON.parse(raw);
} catch (err) {
console.error("Error reading store.json:", err);
return {};
}
}
// Write JSON fresh every time
function writeStore(data) {
try {
ensureStoreFile();
fs.writeFileSync(STORE_FILE, JSON.stringify(data, null, 2));
} catch (err) {
console.error("Error writing store.json:", err);
}
}
// --------------------------------------------------
// USER TOKEN FUNCTIONS — ALWAYS READ JSON FRESH
// --------------------------------------------------
async function saveUserToken(userId, longUserToken, expiresAt) {
console.log("Saving user token for userId:", userId);
console.log("Token expires at:",expiresAt);
console.log("Token value:", longUserToken);
const store = readStore();
store[userId] = store[userId] || {};
store[userId].longUserToken = longUserToken;
store[userId].longUserTokenExpiresAt = expiresAt;
writeStore(store);
}
async function getUserToken(userId) {
const store = readStore();
return store[userId]?.longUserToken || null;
}
// --------------------------------------------------
// PAGE CONNECTION FUNCTIONS — ALWAYS READ JSON FRESH
// --------------------------------------------------
async function savePageConnection(userId, pageId, pageToken, businessAccountId) {
const store = readStore();
store[userId] = store[userId] || {};
store[userId].pageId = pageId;
store[userId].pageToken = pageToken;
store[userId].igUserId = businessAccountId;
writeStore(store);
}
async function getPageConnection(userId) {
const store = readStore();
const cur = store[userId] || {};
return {
pageId: cur.pageId,
pageToken: cur.pageToken,
igUserId: cur.igUserId,
};
}
async function deleteUserCompletely(userId) {
const store = readStore();
if (store[userId]) {
delete store[userId];
writeStore(store);
}
}
module.exports = {
saveUserToken,
getUserToken,
savePageConnection,
getPageConnection,
deleteUserCompletely
};

84
src/utils/mailer.js Normal file
View 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.socialbuddy.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://devapp.socialbuddy.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 didnt 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,
};

10
src/utils/stripe.js Normal file
View File

@ -0,0 +1,10 @@
const Stripe = require("stripe");
const dotenv = require("dotenv");
dotenv.config();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-06-20",
});
module.exports = { stripe };

View 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://devapp.socialbuddy.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 didnt create this account, please ignore this email.</p>
<p style="font-size:13px;color:#777;"> The Team</p>
</div>
</div>
</div>
`;