Initial push from server
This commit is contained in:
parent
cd1005fe78
commit
f18b02e88d
132
.gitignore copy
Normal file
132
.gitignore copy
Normal file
@ -0,0 +1,132 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
273
DM_API_TEST_EXAMPLES.md
Normal file
273
DM_API_TEST_EXAMPLES.md
Normal 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
2
README copy.md
Normal file
@ -0,0 +1,2 @@
|
||||
# socialbuddy_app_backend
|
||||
|
||||
13
config/db.js
Normal file
13
config/db.js
Normal file
@ -0,0 +1,13 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGODB_URI, { dbName: "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
51
config/passport.js
Normal file
@ -0,0 +1,51 @@
|
||||
const passport = require("passport");
|
||||
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
||||
const { getPool } = require("./db.js");
|
||||
|
||||
|
||||
|
||||
// 🧠 Save user to DB or fetch if exists
|
||||
async function findOrCreateUser(profile, provider) {
|
||||
const email = profile.emails?.[0]?.value || `${profile.id}@facebook.com`;
|
||||
const name = profile.displayName;
|
||||
const pool = await getPool();
|
||||
|
||||
const [existing] = await pool.execute(
|
||||
"SELECT * FROM tbl_userlogin WHERE email = ? LIMIT 1",
|
||||
[email]
|
||||
);
|
||||
|
||||
if (existing.length) return existing[0];
|
||||
|
||||
const userid = uuidv4();
|
||||
|
||||
await pool.execute(
|
||||
"INSERT INTO tbl_userlogin (userid, name, email, password, provider) VALUES (?, ?, ?, ?, ?)",
|
||||
[userid, name, email, null, provider]
|
||||
);
|
||||
|
||||
return { userid, name, email, provider };
|
||||
}
|
||||
|
||||
// 🔹 Google
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: "657565817895-sqmh7bd4cef3j7c7cmuno1tpbhg7fjbo.apps.googleusercontent.com",
|
||||
clientSecret: "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",
|
||||
callbackURL: `https://api.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
66
data/store.json
Normal 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
1925
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "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
68
server.js
Normal 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}`));
|
||||
37
src/controllers/account.controller.js
Normal file
37
src/controllers/account.controller.js
Normal 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 };
|
||||
28
src/controllers/auth.controller copy.js
Normal file
28
src/controllers/auth.controller copy.js
Normal 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 };
|
||||
117
src/controllers/auth.controller.js
Normal file
117
src/controllers/auth.controller.js
Normal 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 };
|
||||
236
src/controllers/automation.controller.js
Normal file
236
src/controllers/automation.controller.js
Normal 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
|
||||
};
|
||||
45
src/controllers/channels.controller.js
Normal file
45
src/controllers/channels.controller.js
Normal 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 };
|
||||
215
src/controllers/comments.controller.js
Normal file
215
src/controllers/comments.controller.js
Normal 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
|
||||
};
|
||||
169
src/controllers/dm_messages.controller.js
Normal file
169
src/controllers/dm_messages.controller.js
Normal 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,
|
||||
};
|
||||
51
src/controllers/dm_webhook.controller.js
Normal file
51
src/controllers/dm_webhook.controller.js
Normal 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,
|
||||
};
|
||||
75
src/controllers/media.controller.js
Normal file
75
src/controllers/media.controller.js
Normal 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
|
||||
};
|
||||
465
src/controllers/payment.controller.js
Normal file
465
src/controllers/payment.controller.js
Normal 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
|
||||
};
|
||||
364
src/controllers/userauth.controller.js
Normal file
364
src/controllers/userauth.controller.js
Normal 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
65
src/lib/http.js
Normal file
@ -0,0 +1,65 @@
|
||||
// const axios = require('axios');
|
||||
|
||||
// const http = axios.create({
|
||||
// timeout: 15000,
|
||||
// maxRedirects: 2,
|
||||
// validateStatus: (s) => s >= 200 && s < 500,
|
||||
// });
|
||||
|
||||
// http.interceptors.response.use(async (res) => {
|
||||
// if (res.status === 429 || res.status >= 500) {
|
||||
// const retryAfter = Number(res.headers['retry-after'] || 1);
|
||||
// await new Promise(r => setTimeout(r, retryAfter * 1000));
|
||||
// }
|
||||
// return res;
|
||||
// });
|
||||
|
||||
// module.exports = { http };
|
||||
const axios = require('axios');
|
||||
|
||||
const http = axios.create({
|
||||
timeout: 15000,
|
||||
maxRedirects: 2,
|
||||
validateStatus: (s) => s >= 200 && s < 500,
|
||||
});
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
console.log(`📤 ${config.method.toUpperCase()} ${config.url}`);
|
||||
return config;
|
||||
});
|
||||
|
||||
http.interceptors.response.use(async (res) => {
|
||||
console.log(`📥 ${res.status} ${res.config.url}`);
|
||||
|
||||
// Log errors for debugging
|
||||
if (res.status >= 400) {
|
||||
console.error('❌ HTTP Error Response:', {
|
||||
status: res.status,
|
||||
url: res.config.url,
|
||||
data: res.data
|
||||
});
|
||||
}
|
||||
|
||||
// Retry logic for rate limits and server errors
|
||||
if (res.status === 429 || res.status >= 500) {
|
||||
const retryAfter = Number(res.headers['retry-after'] || 1);
|
||||
console.log(`⏳ Retrying after ${retryAfter}s...`);
|
||||
await new Promise(r => setTimeout(r, retryAfter * 1000));
|
||||
}
|
||||
|
||||
return res;
|
||||
}, (error) => {
|
||||
// Handle network errors (DNS, timeout, connection refused)
|
||||
if (error.code === 'ENOTFOUND') {
|
||||
console.error('🌐 DNS Error: Cannot resolve hostname', error.config?.url);
|
||||
} else if (error.code === 'ETIMEDOUT') {
|
||||
console.error('⏱️ Timeout Error:', error.config?.url);
|
||||
} else if (error.code === 'ECONNREFUSED') {
|
||||
console.error('🚫 Connection Refused:', error.config?.url);
|
||||
} else {
|
||||
console.error('💥 Request Error:', error.message);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
module.exports = { http };
|
||||
8
src/middlewares/admin.middleware.js
Normal file
8
src/middlewares/admin.middleware.js
Normal file
@ -0,0 +1,8 @@
|
||||
function adminOnly(req, res, next) {
|
||||
if (req.user.role !== "admin") {
|
||||
return res.status(403).json({ error: "Admin access required" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { adminOnly };
|
||||
19
src/middlewares/auth.middleware.js
Normal file
19
src/middlewares/auth.middleware.js
Normal file
@ -0,0 +1,19 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
function authMiddleware(req, res, next) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header || !header.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Missing token" });
|
||||
}
|
||||
|
||||
const token = header.split(" ")[1];
|
||||
|
||||
try {
|
||||
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
||||
next();
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error: "Invalid or expired token" });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { authMiddleware };
|
||||
24
src/middlewares/dm_rawBody.middleware.js
Normal file
24
src/middlewares/dm_rawBody.middleware.js
Normal 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 };
|
||||
6
src/middlewares/pageSpeedErrorHandler.js
Normal file
6
src/middlewares/pageSpeedErrorHandler.js
Normal file
@ -0,0 +1,6 @@
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ message: err.message || "Internal Server Error" });
|
||||
};
|
||||
|
||||
module.exports = { errorHandler };
|
||||
22
src/models/payment.module.js
Normal file
22
src/models/payment.module.js
Normal 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
27
src/models/user.model.js
Normal file
@ -0,0 +1,27 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const userSchema = new mongoose.Schema(
|
||||
{
|
||||
name: { type: String },
|
||||
email: { type: String, required: true, unique: true, lowercase: true },
|
||||
mobileNumber: { type: String },
|
||||
passwordHash: { type: String, required: true },
|
||||
|
||||
// ⭐ ADD ROLE
|
||||
role: {
|
||||
type: String,
|
||||
enum: ["customer", "admin", "partner"],
|
||||
default: "customer",
|
||||
},
|
||||
resetPasswordToken: { type: String },
|
||||
resetPasswordExpires: { type: Date },
|
||||
|
||||
// ⭐ FREE TRIAL
|
||||
trialEndsAt: { type: Date },
|
||||
isTrialActive: { type: Boolean, default: false },
|
||||
stripeCustomerId: { type: String },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("User", userSchema);
|
||||
43
src/routes/auth.routes.js
Normal file
43
src/routes/auth.routes.js
Normal 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;
|
||||
11
src/routes/dm_messages.routes.js
Normal file
11
src/routes/dm_messages.routes.js
Normal 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;
|
||||
8
src/routes/dm_webhook.routes.js
Normal file
8
src/routes/dm_webhook.routes.js
Normal 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;
|
||||
26
src/routes/payment.routes.js
Normal file
26
src/routes/payment.routes.js
Normal 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;
|
||||
58
src/routes/social.routes.js
Normal file
58
src/routes/social.routes.js
Normal 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
19
src/routes/user.routes.js
Normal file
@ -0,0 +1,19 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const controller = require("../controllers/userauth.controller.js");
|
||||
const { authMiddleware } = require("../middlewares/auth.middleware.js");
|
||||
const { adminOnly } = require("../middlewares/admin.middleware.js");
|
||||
|
||||
// 🔐 ADMIN ONLY – USER MANAGEMENT
|
||||
// router.post("/create", authMiddleware, adminOnly, controller.createUser);
|
||||
// router.get("/", controller.getUsers);
|
||||
// router.get("/:id", controller.getUserById);
|
||||
// router.put("/:id", authMiddleware, adminOnly, controller.updateUser);
|
||||
// router.delete("/:id", authMiddleware, adminOnly, controller.deleteUser);
|
||||
router.post("/create", controller.createUser);
|
||||
router.get("/", controller.getUsers);
|
||||
router.get("/:id", controller.getUserById);
|
||||
router.put("/:id", controller.updateUser);
|
||||
router.delete("/:id", controller.deleteUser);
|
||||
module.exports = router;
|
||||
17
src/services/account.service.js
Normal file
17
src/services/account.service.js
Normal 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
0
src/services/automation
Normal file
93
src/services/automation.service.js
Normal file
93
src/services/automation.service.js
Normal 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,
|
||||
};
|
||||
85
src/services/automation.service_openrouter.js
Normal file
85
src/services/automation.service_openrouter.js
Normal 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
|
||||
};
|
||||
24
src/services/channels.service.js
Normal file
24
src/services/channels.service.js
Normal 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 };
|
||||
105
src/services/comments.service.js
Normal file
105
src/services/comments.service.js
Normal 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
|
||||
};
|
||||
78
src/services/dm_autoreply.service.js
Normal file
78
src/services/dm_autoreply.service.js
Normal 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,
|
||||
};
|
||||
113
src/services/dm_messages.service.js
Normal file
113
src/services/dm_messages.service.js
Normal 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,
|
||||
};
|
||||
80
src/services/dm_webhook.service.js
Normal file
80
src/services/dm_webhook.service.js
Normal 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,
|
||||
};
|
||||
49
src/services/media.service.js
Normal file
49
src/services/media.service.js
Normal 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
|
||||
};
|
||||
76
src/services/oauth.service.js
Normal file
76
src/services/oauth.service.js
Normal 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 };
|
||||
22
src/services/tokenStore.service copy.js
Normal file
22
src/services/tokenStore.service copy.js
Normal 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 };
|
||||
105
src/services/tokenStore.service.js
Normal file
105
src/services/tokenStore.service.js
Normal 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
84
src/utils/mailer.js
Normal file
@ -0,0 +1,84 @@
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
//
|
||||
// Create reusable transporter object
|
||||
//
|
||||
const mailer = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: 587, // STARTTLS
|
||||
secure: false, // must be false for port 587
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
name: "mail.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 didn’t create this account, please ignore this email.
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
console.log(`✅ Signup email sent to ${toEmail}`);
|
||||
} catch (err) {
|
||||
console.error("❌ Error sending signup email:", err);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Send reset-password email with code or link
|
||||
//
|
||||
async function sendResetPasswordMail(email, token) {
|
||||
try {
|
||||
const resetURL = `${process.env.FRONTEND_URL}/reset-password?email=${email}&token=${token}`;
|
||||
|
||||
await mailer.sendMail({
|
||||
from: `"Social Buddy" <${process.env.SMTP_USER}>`,
|
||||
to: email,
|
||||
subject: "Reset your password",
|
||||
html: `
|
||||
<p>You requested a password reset.</p>
|
||||
<p>Click here to reset: <a href="${resetURL}">${resetURL}</a></p>
|
||||
<p>This link is valid for 1 hour.</p>
|
||||
`,
|
||||
});
|
||||
|
||||
console.log(`✅ Reset password email sent to ${email}`);
|
||||
} catch (err) {
|
||||
console.error("❌ Error sending reset password email:", err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mailer,
|
||||
sendSignupMail,
|
||||
sendResetPasswordMail,
|
||||
};
|
||||
10
src/utils/stripe.js
Normal file
10
src/utils/stripe.js
Normal 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 };
|
||||
25
src/utils/templates/welcomeTemplate.js
Normal file
25
src/utils/templates/welcomeTemplate.js
Normal file
@ -0,0 +1,25 @@
|
||||
// utils/templates/welcomeTemplate.js
|
||||
export const welcomeTemplate = (name) => `
|
||||
<div style="font-family:Arial,sans-serif;line-height:1.6;background:#f9f9f9;padding:20px;">
|
||||
<div style="max-width:600px;margin:auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 6px rgba(0,0,0,0.1);">
|
||||
<div style="background:#00d1ff;color:#fff;padding:15px 25px;text-align:center;">
|
||||
<h2>Welcome to Social Buddy 🎉</h2>
|
||||
</div>
|
||||
<div style="padding:25px;">
|
||||
<h3>Hello ${name},</h3>
|
||||
<p>Thank you for registering with <strong>Our App</strong>!</p>
|
||||
<p>Your account has been successfully created. We're thrilled to have you on board.</p>
|
||||
|
||||
<div style="margin:30px 0;text-align:center;">
|
||||
<a href="https://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 didn’t create this account, please ignore this email.</p>
|
||||
<p style="font-size:13px;color:#777;">— The Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Loading…
x
Reference in New Issue
Block a user