const express = require("express"); const cors = require("cors"); const axios = require("axios"); const crypto = require("crypto"); const path = require("path"); const app = express(); app.use(cors()); app.use(express.static(path.join(__dirname, "public"))); // ── CONFIG ── const PAYSTACK_SECRET = process.env.PAYSTACK_SECRET; const PAYSTACK_BASE = "https://api.paystack.co"; const NOW_API_KEY = process.env.NOW_API_KEY; const NOW_IPN_SECRET = process.env.NOW_IPN_SECRET; const FREE_CREDITS = 5000; const REFERRAL_PCT = 0.10; const MIN_WITHDRAWAL = 10000; const FLW_SECRET_KEY = process.env.FLW_SECRET_KEY; const FLW_WEBHOOK_SECRET = process.env.FLW_WEBHOOK_SECRET; // ── EMAIL ── const nodemailer = require("nodemailer"); const emailTransporter = nodemailer.createTransport({ service: "gmail", auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS, } }); const audlabsTransporter = nodemailer.createTransport({ host: "mail.privateemail.com", port: 587, secure: false, auth: { user: process.env.AUDLABS_SMTP_USER, pass: process.env.AUDLABS_SMTP_PASS, }, tls: { rejectUnauthorized: false } }); async function sendWithdrawalAlert(data) { try { await emailTransporter.sendMail({ from: process.env.EMAIL_USER, to: process.env.EMAIL_USER, subject: `AudLabs — USDT Withdrawal Request: $${data.usdAmount}`, html: `

New USDT Withdrawal Request

User Email${data.email}
Amount (NGN)₦${data.amountNGN.toLocaleString()}
Amount (USDT)$${data.usdAmount} USDT
Wallet Address${data.walletAddress}
Exchange Rate₦${data.rateUsed} per $1
Time${new Date().toLocaleString()}

Please send the USDT to the wallet address above as soon as possible.

` }); console.log("✅ Withdrawal alert email sent"); } catch(e) { console.error("Email send failed:", e.message); } } // ── FIREBASE ── const admin = require("firebase-admin"); if (!admin.apps.length) { let privateKey = process.env.FIREBASE_PRIVATE_KEY || ""; if (privateKey.startsWith('"')) privateKey = privateKey.slice(1,-1); privateKey = privateKey.replace(/\\n/g, "\n"); try { admin.initializeApp({ credential: admin.credential.cert({ projectId: process.env.FIREBASE_PROJECT_ID || "voicegene", clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey, }) }); console.log("✅ Firebase initialized"); } catch(e) { console.error("❌ Firebase:", e.message); } } const db = admin.firestore(); // ── HELPERS ── async function verifyUser(req, res) { const auth = req.headers.authorization; if (!auth?.startsWith("Bearer ")) { res.status(401).json({ error:"Unauthorized" }); return null; } try { return await admin.auth().verifyIdToken(auth.split(" ")[1]); } catch(e) { res.status(401).json({ error:"Invalid token" }); return null; } } function genRefCode(uid, email, displayName) { // Use first name from display name (e.g. "Adeyemo" from "Adeyemo Oluwaseyi") if (displayName && displayName.trim()) { var first = displayName.trim().split(" ")[0].replace(/[^a-zA-Z]/g,"").toLowerCase(); if (first.length >= 2) return first; } // Fall back to email prefix var name = (email||"").split("@")[0].replace(/[^a-z]/gi,"").toLowerCase().slice(0,12); return name || uid.slice(0,8).toLowerCase(); } app.use((req,res,next) => { if(["/api/paystack-webhook","/api/crypto-webhook"].includes(req.path)) return next(); express.json()(req,res,next); }); // ── SETUP ACCOUNT ── app.post("/api/setup-account", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { uid, email, name } = { uid:user.uid, email:user.email, name:user.name||user.email.split("@")[0] }; try { const userDoc = await db.collection("users").doc(uid).get(); const refCode = req.body?.refCode || ""; console.log("setup-account called for:", email, "refCode:", refCode); // Process referral code first (before early return) let referredBy = null; if (refCode) { const refSnap = await db.collection("users").where("referralCode","==",refCode).limit(1).get(); if (!refSnap.empty && refSnap.docs[0].id !== uid) { referredBy = refSnap.docs[0].id; console.log("Referral found - referredBy:", referredBy); } } if (userDoc.exists && userDoc.data().virtualAccount) { // Already has virtual account const existingData = userDoc.data(); // Save referredBy if not already set and refCode provided if (referredBy && !existingData.referredBy) { await db.collection("users").doc(uid).update({ referredBy }); console.log("Saved referredBy for existing user:", referredBy); } return res.json({ success:true, data:{...existingData, referredBy: referredBy||existingData.referredBy} }); } // User exists but no virtual account - will retry creating one below const myRefCode = genRefCode(uid, email, req.body?.displayName||name); let virtualAccount = null; // Create Paystack Dedicated Virtual Account try { console.log("Creating Paystack customer for:", email); // Step 1: Create or fetch customer const firstName = name.split(" ")[0] || name; const lastName = name.split(" ").slice(1).join(" ") || firstName; let customerCode; try { const custRes = await axios.post(`${PAYSTACK_BASE}/customer`, { email, first_name: firstName, last_name: lastName, phone: "+2348000000000" }, { headers: { Authorization:`Bearer ${PAYSTACK_SECRET}`, "Content-Type":"application/json" }}); console.log("Customer response:", JSON.stringify(custRes.data)); if (!custRes.data.status) throw new Error(custRes.data.message); customerCode = custRes.data.data.customer_code; console.log("Customer code:", customerCode); } catch(custErr) { // Customer might already exist - try to fetch by email console.log("Customer creation failed, trying to fetch:", custErr.response?.data || custErr.message); const fetchRes = await axios.get(`${PAYSTACK_BASE}/customer/${email}`, { headers: { Authorization:`Bearer ${PAYSTACK_SECRET}` }}); if (fetchRes.data.status && fetchRes.data.data?.customer_code) { customerCode = fetchRes.data.data.customer_code; console.log("Fetched existing customer:", customerCode); // Update phone on existing customer await axios.put(`${PAYSTACK_BASE}/customer/${customerCode}`, { phone: "+2348000000000" }, { headers: { Authorization:`Bearer ${PAYSTACK_SECRET}`, "Content-Type":"application/json" }}).catch(e => console.log("Phone update:", e.message)); } else { throw new Error("Could not create or fetch customer"); } } // Step 1.5: Update customer with phone number (required for dedicated accounts) await axios.put(`${PAYSTACK_BASE}/customer/${customerCode}`, { phone: "+2348000000000" }, { headers: { Authorization:`Bearer ${PAYSTACK_SECRET}`, "Content-Type":"application/json" }}).catch(e => console.log("Phone update:", e.response?.data?.message)); // Step 2: Create dedicated virtual account console.log("Creating virtual account for customer:", customerCode); const vaRes = await axios.post(`${PAYSTACK_BASE}/dedicated_account`, { customer: customerCode, preferred_bank: "titan-paystack", }, { headers: { Authorization:`Bearer ${PAYSTACK_SECRET}`, "Content-Type":"application/json" }}); console.log("VA Response:", JSON.stringify(vaRes.data)); if (vaRes.data.status && vaRes.data.data) { const va = vaRes.data.data; virtualAccount = { bankName: va.bank?.name || "Titan Trust Bank", accountNumber: va.account_number, accountName: va.account_name, }; console.log("Virtual account created:", virtualAccount.accountNumber); } else { // Try wema-bank as fallback console.log("Titan failed, trying wema-bank..."); const vaRes2 = await axios.post(`${PAYSTACK_BASE}/dedicated_account`, { customer: customerCode, preferred_bank: "wema-bank", }, { headers: { Authorization:`Bearer ${PAYSTACK_SECRET}`, "Content-Type":"application/json" }}); console.log("Wema VA Response:", JSON.stringify(vaRes2.data)); if (vaRes2.data.status && vaRes2.data.data) { const va = vaRes2.data.data; virtualAccount = { bankName: va.bank?.name || "Wema Bank", accountNumber: va.account_number, accountName: va.account_name, }; console.log("Wema virtual account created:", virtualAccount.accountNumber); } else { throw new Error(vaRes2.data.message || "Virtual account creation failed"); } } } catch(paErr) { console.error("Paystack VA error:", paErr.response?.data || paErr.message); } const userData = { uid, email, name, credits: FREE_CREDITS, virtualAccount: virtualAccount || null, referralCode: myRefCode, referredBy: referredBy || null, referralEarningsNGN: 0, referralCount: 0, createdAt: admin.firestore.FieldValue.serverTimestamp(), }; console.log("Creating user:", email, "referralCode:", myRefCode, "referredBy:", referredBy); await db.collection("users").doc(uid).set(userData, { merge:true }); await db.collection("users").doc(uid).collection("transactions").add({ type:"credit", amount:FREE_CREDITS, note:"Welcome Bonus — 5,000 free credits to get you started! 🎙", createdAt: admin.firestore.FieldValue.serverTimestamp(), }); try { await audlabsTransporter.sendMail({ from: `"AudLabs" <${process.env.AUDLABS_SMTP_USER}>`, to: email, subject: "Welcome to AudLabs — Your 5000 Free Credits Are Ready! 🎙", html: `

Welcome to AudLabs

AI-Powered Voice Generation Platform.

Hi ${name},

Welcome to AudLabs! We are excited to have you on board. Your account has been created successfully and we have added 5000 free credits to get you started.

5,000
Free Credits Added to Your Account.

With AudLabs you can:

Join Our Telegram Channel

Follow us for updates: X · YouTube · Telegram

© 2026 AudLabs. All rights reserved.

` }); console.log("✅ Welcome email sent to:", email); } catch(emailErr){ console.warn("Welcome email failed:", emailErr.message); } if (referredBy) { console.log("Incrementing referral count for:", referredBy); await db.collection("users").doc(referredBy).update({ referralCount: admin.firestore.FieldValue.increment(1), }); } return res.json({ success:true, data:userData }); } catch(e) { return res.status(500).json({ error:e.message }); } }); // ── BALANCE ── app.get("/api/balance", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; try { const doc = await db.collection("users").doc(user.uid).get(); if (!doc.exists) return res.json({ credits:0, virtualAccount:null }); const d = doc.data(); return res.json({ credits: d.credits||0, virtualAccount: d.virtualAccount||null, virtualAccounts: d.virtualAccount ? [d.virtualAccount] : [], referralCode: d.referralCode||"", referralEarningsNGN: d.referralEarningsNGN||0, referralCount: d.referralCount||0, totalCharacters: d.totalCharacters||0, totalGenerations: d.totalGenerations||0, favVoice: d.voiceCount ? Object.keys(d.voiceCount).reduce(function(a,b){ return d.voiceCount[a]>d.voiceCount[b]?a:b; }) : "—", }); } catch(e) { return res.status(500).json({ error:e.message }); } }); // ── DEDUCT CREDITS ── app.post("/api/deduct-credits", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { characters, voiceName } = req.body; const cost = parseInt(characters); if (!cost||cost<1) return res.status(400).json({ error:"Invalid" }); try { const ref = db.collection("users").doc(user.uid); const doc = await ref.get(); const current = doc.exists?(doc.data().credits||0):0; if (current { try { const rawBody = req.body.toString("utf8"); const sig = req.headers["x-paystack-signature"]; const hash = crypto.createHmac("sha512", PAYSTACK_SECRET).update(rawBody).digest("hex"); if (sig && hash !== sig) { console.error("Invalid Paystack signature"); return res.status(400).json({ error:"Invalid signature" }); } const payload = JSON.parse(rawBody); console.log("Paystack webhook:", payload.event); console.log("Webhook event:", payload.event, "channel:", payload.data?.channel); // Handle both charge.success and dedicated account transfers if (!["charge.success", "transfer.success"].includes(payload.event)) { return res.json({ received:true, event:payload.event }); } const data = payload.data; const amountPaid = data.amount / 100; // Paystack sends in kobo const customerEmail = data.customer?.email || data.recipient?.email; const reference = data.reference || data.transfer_code; const accountNumber = data.authorization?.receiver_bank_account_number || data.metadata?.receiver_bank_account_number || data.dedicated_nuban?.account_number || data.paid_to?.nuban; console.log("Payment received:", amountPaid, "NGN for", customerEmail, "account:", accountNumber); // Find user by account number first, then fall back to email let snap = null; if (accountNumber) { snap = await db.collection("users").where("virtualAccount.accountNumber","==",accountNumber).limit(1).get(); if (!snap.empty) console.log("Found user by account number:", accountNumber); } if (!snap || snap.empty) { snap = await db.collection("users").where("email","==",customerEmail).limit(1).get(); if (!snap.empty) console.log("Found user by email:", customerEmail); } if (!snap || snap.empty) { console.error("No user found for email:", customerEmail, "account:", accountNumber); return res.json({ received:true }); } const uid = snap.docs[0].id; // Check duplicate const existing = await db.collection("users").doc(uid).collection("transactions") .where("paymentRef","==",reference).get(); if (!existing.empty) return res.json({ received:true, duplicate:true }); // Match payment to package using live rate // $1 = 100,000 credits // Fetch live rate to convert NGN amount to credits let creditsToAdd = 0; try { const rateRes = await axios.get("https://open.er-api.com/v6/latest/USD"); const ngnRate = rateRes.data?.rates?.NGN || 1600; const usdAmount = amountPaid / ngnRate; // Match to nearest package const packages = [ {usd:6, credits:500000}, {usd:12, credits:1000000}, {usd:18, credits:1500000}, {usd:24, credits:2000000} ]; // Find closest package let matched = packages[0]; let minDiff = Math.abs(usdAmount - packages[0].usd); packages.forEach(function(pkg){ const diff = Math.abs(usdAmount - pkg.usd); if(diff < minDiff){ minDiff = diff; matched = pkg; } }); // Only match if within 10% of package price if(minDiff / matched.usd <= 0.10){ creditsToAdd = matched.credits; console.log("Matched package:", matched.usd, "USD =", matched.credits, "credits, paid:", usdAmount.toFixed(2), "USD"); } else { // Fallback: calculate proportionally creditsToAdd = Math.floor(usdAmount * 100000); console.log("No exact package match, proportional credits:", creditsToAdd, "for", usdAmount.toFixed(2), "USD"); } } catch(rateErr) { // Fallback if rate fetch fails creditsToAdd = Math.floor(amountPaid / 1600 * 100000); console.log("Rate fetch failed, fallback credits:", creditsToAdd); } await db.collection("users").doc(uid).update({ credits: admin.firestore.FieldValue.increment(creditsToAdd), }); await db.collection("users").doc(uid).collection("transactions").add({ type:"credit", amount:creditsToAdd, amountNGN:amountPaid, paymentRef:reference, note:`Top-up — ₦${amountPaid.toLocaleString()} — ${creditsToAdd.toLocaleString()} credits`, createdAt: admin.firestore.FieldValue.serverTimestamp(), }); // Referral commission const userDoc = await db.collection("users").doc(uid).get(); const referredBy = userDoc.data()?.referredBy; console.log("Checking referral for uid:", uid, "referredBy:", referredBy); if (referredBy) { const commissionNGN = Math.floor(amountPaid * REFERRAL_PCT); console.log("Paying commission:", commissionNGN, "NGN to:", referredBy); await db.collection("users").doc(referredBy).update({ referralEarningsNGN: admin.firestore.FieldValue.increment(commissionNGN), }); await db.collection("users").doc(referredBy).collection("referralEarnings").add({ fromUid:uid, fromEmail:customerEmail, amountNGN:commissionNGN, note:`10% referral from ₦${amountPaid.toLocaleString()}`, createdAt: admin.firestore.FieldValue.serverTimestamp(), }); console.log("✅ Commission paid:", commissionNGN, "NGN to referrer:", referredBy); } else { console.log("No referredBy found for uid:", uid, "- no commission paid"); } console.log(`✅ Credited ${creditsToAdd} to ${uid}`); return res.json({ success:true }); } catch(e) { console.error("Webhook error:", e.message); return res.status(500).json({ error:e.message }); } }); // ── CRYPTO WEBHOOK ── app.post("/api/crypto-webhook", express.raw({ type:"*/*" }), async (req,res) => { try { const rawBody = req.body.toString("utf8"); const sig = req.headers["x-nowpayments-sig"]; const data = JSON.parse(rawBody); const sortedStr = JSON.stringify(data, Object.keys(data).sort()); const hash = crypto.createHmac("sha512", NOW_IPN_SECRET).update(sortedStr).digest("hex"); if (sig && hash !== sig) return res.status(400).json({ error:"Invalid signature" }); const { payment_status, order_id, actually_paid, pay_amount } = data; if (!["finished","confirmed"].includes(payment_status)) return res.json({ received:true }); const payDoc = await db.collection("cryptoPayments").doc(order_id).get(); if (!payDoc.exists) return res.json({ received:true }); const payData = payDoc.data(); if (payData.status === "completed") return res.json({ received:true, duplicate:true }); const creditsToAdd = payData.creditsAmount; await db.collection("users").doc(payData.uid).update({ credits: admin.firestore.FieldValue.increment(creditsToAdd), }); await db.collection("users").doc(payData.uid).collection("transactions").add({ type:"credit", amount:creditsToAdd, note:`Crypto top-up — $${payData.amountUSD} USDT — ${creditsToAdd.toLocaleString()} credits`, createdAt: admin.firestore.FieldValue.serverTimestamp(), }); await db.collection("cryptoPayments").doc(order_id).update({ status:"completed" }); console.log(`✅ Crypto credited ${creditsToAdd} to ${payData.uid}`); return res.json({ success:true }); } catch(e) { return res.status(500).json({ error:e.message }); } }); // ── VERIFY ACCOUNT ── app.post("/api/verify-account", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { bankCode, accountNumber } = req.body; if (!bankCode || !accountNumber) { return res.status(400).json({ error:"Bank code and account number required" }); } try { const response = await axios.get( `${PAYSTACK_BASE}/bank/resolve?account_number=${accountNumber}&bank_code=${bankCode}`, { headers: { Authorization:`Bearer ${PAYSTACK_SECRET}` }} ); if (response.data.status && response.data.data?.account_name) { console.log(`✅ Account verified: ${response.data.data.account_name}`); return res.json({ success:true, accountName: response.data.data.account_name }); } return res.status(400).json({ error:"Account not found" }); } catch(e) { const errData = e.response?.data; const msg = errData?.message || e.message || "Verification failed"; console.error("Verify account error:", msg, errData); return res.status(400).json({ error: msg }); } }); // ── WITHDRAWAL ── app.post("/api/request-withdrawal", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { amount, currency, bankName, bankCode, accountNumber, accountName, walletAddress, usdAmount, rateUsed } = req.body; if (!amount || amount < MIN_WITHDRAWAL) return res.status(400).json({ error:`Minimum withdrawal is ₦${MIN_WITHDRAWAL.toLocaleString()}` }); try { const ref = db.collection("users").doc(user.uid); const doc = await ref.get(); const balance = doc.data()?.referralEarningsNGN || 0; if (balance < amount) return res.status(400).json({ error:"Insufficient balance" }); // Deduct balance first await ref.update({ referralEarningsNGN: admin.firestore.FieldValue.increment(-amount) }); let note = ""; let withdrawalData = { uid:user.uid, email:user.email, amountNGN:amount, currency:currency||"NGN", status:"pending", createdAt:admin.firestore.FieldValue.serverTimestamp() }; if (currency === "USD") { // USDT - manual processing with email alert withdrawalData.walletAddress = walletAddress; withdrawalData.usdAmount = usdAmount; withdrawalData.rateUsed = rateUsed; note = `Withdrawal ₦${amount.toLocaleString()} → $${usdAmount} USDT`; await db.collection("withdrawalRequests").add(withdrawalData); await ref.collection("transactions").add({ type:"withdrawal", amount:-amount, note, createdAt:admin.firestore.FieldValue.serverTimestamp() }); // Send email alert await sendWithdrawalAlert({ email:user.email, amountNGN:amount, usdAmount, walletAddress, rateUsed }); return res.json({ success:true, message:"USDT withdrawal submitted. You will receive your USDT within 20 hours." }); } // NGN - process instantly via Paystack Transfer try { // Step 1: Create transfer recipient console.log("Creating Paystack transfer recipient for:", accountNumber, bankCode); const recipientRes = await axios.post(`${PAYSTACK_BASE}/transferrecipient`, { type: "nuban", name: accountName, account_number: accountNumber, bank_code: bankCode, currency: "NGN" }, { headers: { Authorization:`Bearer ${PAYSTACK_SECRET}`, "Content-Type":"application/json" }}); if (!recipientRes.data.status) throw new Error(recipientRes.data.message || "Failed to create recipient"); const recipientCode = recipientRes.data.data.recipient_code; console.log("Recipient code:", recipientCode); // Step 2: Initiate transfer const transferRef = `VG-WD-${user.uid.slice(0,8)}-${Date.now()}`; const transferRes = await axios.post(`${PAYSTACK_BASE}/transfer`, { source: "balance", amount: amount * 100, // Paystack uses kobo recipient: recipientCode, reason: `VoiceGen withdrawal for ${user.email}`, reference: transferRef }, { headers: { Authorization:`Bearer ${PAYSTACK_SECRET}`, "Content-Type":"application/json" }}); console.log("Transfer response:", JSON.stringify(transferRes.data)); if (!transferRes.data.status) throw new Error(transferRes.data.message || "Transfer failed"); const transferStatus = transferRes.data.data.status; note = `Withdrawal ₦${amount.toLocaleString()} to ${bankName} — ${accountNumber}`; withdrawalData.bankName = bankName; withdrawalData.bankCode = bankCode; withdrawalData.accountNumber = accountNumber; withdrawalData.accountName = accountName; withdrawalData.transferRef = transferRef; withdrawalData.recipientCode = recipientCode; withdrawalData.status = transferStatus; await db.collection("withdrawalRequests").add(withdrawalData); await ref.collection("transactions").add({ type:"withdrawal", amount:-amount, note, createdAt:admin.firestore.FieldValue.serverTimestamp() }); console.log("✅ Transfer initiated:", transferRef, "status:", transferStatus); return res.json({ success:true, message:`Transfer of ₦${amount.toLocaleString()} initiated successfully! You will receive it shortly.`, status: transferStatus }); } catch(transferErr) { // If Paystack transfer fails, refund the balance and return error console.error("Transfer error:", transferErr.response?.data || transferErr.message); await ref.update({ referralEarningsNGN: admin.firestore.FieldValue.increment(amount) }); const errMsg = transferErr.response?.data?.message || transferErr.message || "Transfer failed"; return res.status(400).json({ error:`Transfer failed: ${errMsg}. Your balance has been restored.` }); } } catch(e) { return res.status(500).json({ error:e.message }); } }); // ── REFERRAL EARNINGS ── app.get("/api/referral-earnings", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; try { const snap = await db.collection("users").doc(user.uid).collection("referralEarnings") .orderBy("createdAt","desc").limit(50).get(); return res.json({ earnings: snap.docs.map(d=>({ id:d.id,...d.data(),createdAt:d.data().createdAt?.toDate() })) }); } catch(e) { return res.status(500).json({ error:e.message }); } }); // ── CREATE CRYPTO PAYMENT ── app.get("/api/check-crypto-payment", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { paymentId } = req.query; if (!paymentId) return res.status(400).json({ error:"Payment ID required" }); try { const result = await axios.get( `https://api.nowpayments.io/v1/payment/${paymentId}`, { headers: { "x-api-key": NOW_API_KEY } } ); const status = result.data.payment_status; if (status === "finished" || status === "confirmed") { const paySnap = await db.collection("cryptoPayments") .where("paymentId","==",paymentId).limit(1).get(); if (!paySnap.empty) { const payData = paySnap.docs[0].data(); const orderId = paySnap.docs[0].id; if (payData.status !== "completed") { const creditsToAdd = payData.creditsAmount; await db.collection("users").doc(user.uid).update({ credits: admin.firestore.FieldValue.increment(creditsToAdd) }); await db.collection("users").doc(user.uid).collection("transactions").add({ type:"credit", amount:creditsToAdd, note:`Crypto top-up — $${payData.amountUSD} USDT — ${creditsToAdd.toLocaleString()} credits`, createdAt: admin.firestore.FieldValue.serverTimestamp() }); await db.collection("cryptoPayments").doc(orderId).update({ status:"completed" }); console.log("✅ Manual check credited:", creditsToAdd, "to", user.uid); } } } return res.json({ status, data: result.data }); } catch(e) { return res.status(500).json({ error: e.response?.data?.message || e.message }); } }); app.post("/api/create-crypto-payment", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { amountUSD, creditsAmount } = req.body; try { const orderId = `VG-CRYPTO-${user.uid.slice(0,8)}-${Date.now()}`; const response = await axios.post("https://api.nowpayments.io/v1/payment", { price_amount: amountUSD, price_currency:"usd", pay_currency:"usdttrc20", order_id: orderId, order_description:`VoiceGen ${creditsAmount} credits`, ipn_callback_url:`${'https://app.audlabs.io'}/api/crypto-webhook`, is_fixed_rate: false, is_fee_paid_by_user: true }, { headers:{ "x-api-key":NOW_API_KEY }}); await db.collection("cryptoPayments").doc(orderId).set({ uid:user.uid, email:user.email, orderId, paymentId:response.data.payment_id, amountUSD, creditsAmount:parseInt(creditsAmount), status:"pending", createdAt:admin.firestore.FieldValue.serverTimestamp(), }); return res.json({ success:true, payAddress:response.data.pay_address, payAmount:parseFloat(response.data.pay_amount).toFixed(4), payCurrency:response.data.pay_currency, orderId, paymentId:response.data.payment_id }); } catch(e) { return res.status(500).json({ error:e.response?.data?.message||e.message }); } }); // ── TRANSACTIONS ── app.get("/api/transactions", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; try { const snap = await db.collection("users").doc(user.uid).collection("transactions") .orderBy("createdAt","desc").limit(50).get(); return res.json({ transactions: snap.docs.map(d=>({ id:d.id,...d.data(),createdAt:d.data().createdAt?.toDate() })) }); } catch(e) { return res.status(500).json({ error:e.message }); } }); // ── CLONE VOICE ── app.post("/api/clone-voice", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; try { const { IncomingForm } = require("formidable"); const fs = require("fs"); const FormData = require("form-data"); const form = new IncomingForm({ maxFileSize: 20 * 1024 * 1024 }); form.parse(req, async (err, fields, files) => { if (err) return res.status(400).json({ error:"File upload failed" }); const file = files.file?.[0] || files.file; const name = fields.name?.[0] || fields.name || "My Voice"; if (!file) return res.status(400).json({ error:"No file provided" }); try { const MK = process.env.MINIMAX_API_KEY; const filePath = file.filepath || file.path; const fileData = fs.readFileSync(filePath); const originalName = file.originalFilename || file.name || "audio.mp3"; // Deduct 10,000 credits for voice cloning const userDoc = await db.collection("users").doc(user.uid).get(); const currentCredits = userDoc.data()?.credits || 0; if(currentCredits < 10000){ return res.status(400).json({ error:"Insufficient credits. Voice cloning costs 10,000 credits." }); } await db.collection("users").doc(user.uid).update({ credits: admin.firestore.FieldValue.increment(-10000) }); await db.collection("users").doc(user.uid).collection("transactions").add({ type:"debit", amount:10000, note:"Voice clone — 10,000 credits", createdAt: admin.firestore.FieldValue.serverTimestamp() }); // Step 1: Upload source audio console.log("Step 1: Uploading source audio..."); const uploadForm = new FormData(); uploadForm.append("purpose", "voice_clone"); uploadForm.append("file", fileData, { filename: originalName, contentType: file.mimetype || "audio/mpeg" }); const uploadRes = await axios.post( "https://api.minimax.io/v1/files/upload", uploadForm, { headers: { Authorization:`Bearer ${MK}`, ...uploadForm.getHeaders() }} ); console.log("Upload response:", JSON.stringify(uploadRes.data)); const fileId = uploadRes.data?.file?.file_id; if (!fileId) throw new Error("File upload failed: " + JSON.stringify(uploadRes.data)); // Step 2: Clone the voice with a custom voice_id // voice_id must be unique - use uid + timestamp const customVoiceId = "voice_"+user.uid.slice(0,8)+"_"+Date.now(); console.log("Step 2: Cloning voice, custom voice_id:", customVoiceId); const cloneRes = await axios.post( "https://api.minimax.io/v1/voice_clone", { file_id: fileId, voice_id: customVoiceId, text: "Hello, this is a preview of my cloned voice. How does it sound?", model: "speech-2.8-hd" }, { headers: { Authorization:`Bearer ${MK}`, "Content-Type":"application/json" }} ); console.log("Clone response:", JSON.stringify(cloneRes.data).slice(0,200)); // Check for success - MiniMax returns status_code 0 for success const statusCode = cloneRes.data?.base_resp?.status_code; const statusMsg = cloneRes.data?.base_resp?.status_msg; if (statusCode !== 0) throw new Error(statusMsg || "Clone failed"); // Get preview audio if returned const previewAudio = cloneRes.data?.data?.audio || cloneRes.data?.audio || null; // Save to Firestore with the custom voice_id await db.collection("users").doc(user.uid).collection("clonedVoices").add({ name, voiceId: customVoiceId, createdAt: admin.firestore.FieldValue.serverTimestamp() }); console.log("✅ Voice cloned successfully:", customVoiceId); return res.json({ success:true, voiceId: customVoiceId, name, previewAudio }); } catch(e) { console.error("Clone error:", e.response?.data || e.message); return res.status(500).json({ error: e.response?.data?.base_resp?.status_msg || e.response?.data?.message || e.message }); } }); } catch(e) { return res.status(500).json({ error:e.message }); } }); // ── GET CLONED VOICES ── app.get("/api/cloned-voices", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; try { const snap = await db.collection("users").doc(user.uid).collection("clonedVoices") .orderBy("createdAt","desc").get(); return res.json({ voices: snap.docs.map(d=>({ id:d.id, ...d.data() })) }); } catch(e) { return res.status(500).json({ error:e.message }); } }); // ── PREVIEW VOICE ── app.post("/api/preview-voice", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { voiceId, text } = req.body; if (!voiceId || !text) return res.status(400).json({ error:"voiceId and text required" }); try { const MK = process.env.MINIMAX_API_KEY; const response = await axios.post( "https://api.minimax.io/v1/t2a_v2", { model: "speech-2.8-hd", text: text, stream: false, voice_setting: { voice_id: voiceId, speed: 1.0, vol: 1.0, pitch: 0 }, audio_setting: { sample_rate: 32000, bitrate: 128000, format: "mp3", channel: 1 }, output_format: "hex" }, { headers: { Authorization:`Bearer ${MK}`, "Content-Type":"application/json" }} ); console.log("Preview response status:", response.data?.base_resp?.status_code, response.data?.base_resp?.status_msg); if (response.data?.data?.audio) { const hexAudio = response.data.data.audio; const audioBuffer = Buffer.from(hexAudio, 'hex'); res.set('Content-Type', 'audio/mpeg'); res.set('Content-Length', audioBuffer.length); res.set('Content-Type', 'audio/mpeg'); res.set('Content-Length', audioBuffer.length); return res.send(audioBuffer); } const errMsg = response.data?.base_resp?.status_msg || "Preview failed"; return res.status(400).json({ error: errMsg, raw: response.data?.base_resp }); } catch(e) { console.error("Preview error:", e.response?.data || e.message); return res.status(500).json({ error: e.response?.data?.base_resp?.status_msg || e.message }); } }); // ── ELEVENLABS GENERATE VOICE ── app.post("/api/generate-voice-el", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { voiceId, text, speed } = req.body; if (!voiceId || !text) return res.status(400).json({ error:"voiceId and text required" }); try { const response = await axios.post( `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, { text: text, model_id: "eleven_multilingual_v2", voice_settings: { stability: 0.5, similarity_boost: 0.75, speed: parseFloat(speed)||1.0 } }, { headers: { "xi-api-key": ELEVENLABS_API_KEY, "Content-Type": "application/json", "Accept": "audio/mpeg" }, responseType: "arraybuffer" } ); const audioBuffer = Buffer.from(response.data); res.set("Content-Type", "audio/mpeg"); res.set("Content-Length", audioBuffer.length); return res.send(audioBuffer); } catch(e) { console.error("ElevenLabs error:", e.response?.data || e.message); return res.status(500).json({ error: e.message }); } }); // ── GENERATE VOICE ── app.post("/api/generate-voice", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { voiceId, text, speed, vol, pitch } = req.body; if (!voiceId || !text) return res.status(400).json({ error:"voiceId and text required" }); try { const MK = process.env.MINIMAX_API_KEY; const response = await axios.post( "https://api.minimax.io/v1/t2a_v2", { model: "speech-2.8-hd", text: text, stream: false, voice_setting: { voice_id: voiceId, speed: parseFloat(speed)||1.0, vol: parseFloat(vol)||1.0, pitch: parseInt(pitch)||0 }, audio_setting: { sample_rate: 32000, bitrate: 128000, format: "mp3", channel: 1 }, output_format: "hex" }, { headers: { Authorization:`Bearer ${MK}`, "Content-Type":"application/json" }} ); console.log("Generate response status:", response.data?.base_resp?.status_code, response.data?.base_resp?.status_msg); if (response.data?.data?.audio) { const hexAudio = response.data.data.audio; const audioBuffer = Buffer.from(hexAudio, 'hex'); res.set('Content-Type', 'audio/mpeg'); res.set('Content-Length', audioBuffer.length); return res.send(audioBuffer); } var errMsg400 = response.data?.base_resp?.status_msg || "Generation failed"; var friendlyMsg = "⚠️ High traffic alert! Too many creators are generating at the same time. Please wait 2-3 hours and try again — we appreciate your patience."; if(errMsg400.includes("Token Plan") || errMsg400.includes("usage limit") || errMsg400.includes("Credits") || errMsg400.includes("limit") || errMsg400.includes("quota") || errMsg400.includes("exceeded") || errMsg400.includes("upgrade") || errMsg400.includes("Upgrade")){ errMsg400 = friendlyMsg; } return res.status(400).json({ error: errMsg400 }); } catch(e) { console.error("Generate error:", e.response?.data || e.message); var errMsg = e.response?.data?.base_resp?.status_msg || e.message || ""; var friendlyMsg = "⚠️ High traffic alert! Too many creators are generating at the same time. Please wait 2-3 hours and try again — we appreciate your patience."; if(errMsg.includes("Token Plan") || errMsg.includes("usage limit") || errMsg.includes("Credits") || errMsg.includes("limit") || errMsg.includes("quota") || errMsg.includes("exceeded") || errMsg.includes("upgrade") || errMsg.includes("Upgrade")){ errMsg = friendlyMsg; } // Also check HTTP status codes for rate limiting if(e.response?.status === 429 || e.response?.status === 402){ errMsg = friendlyMsg; } return res.status(500).json({ error: errMsg }); } }); // ── CARD PAYMENT (FLUTTERWAVE) ── app.post("/api/create-card-payment", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { amountUSD, creditsAmount } = req.body; if (!amountUSD || amountUSD < 5) return res.status(400).json({ error:"Minimum payment is $5" }); try { const txRef = `VG-CARD-${user.uid.slice(0,8)}-${Date.now()}`; // Save pending payment to Firestore await db.collection("cardPayments").doc(txRef).set({ uid: user.uid, email: user.email, amountUSD, creditsAmount: parseInt(creditsAmount), txRef, status: "pending", createdAt: admin.firestore.FieldValue.serverTimestamp() }); return res.json({ success:true, txRef, amountUSD }); } catch(e) { return res.status(500).json({ error:e.message }); } }); // ── FLUTTERWAVE WEBHOOK ── app.post("/api/flutterwave-webhook", express.raw({ type:"*/*" }), async (req,res) => { try { const sig = req.headers["verif-hash"]; if (sig !== FLW_WEBHOOK_SECRET) { console.error("Invalid Flutterwave webhook signature"); return res.status(400).json({ error:"Invalid signature" }); } const payload = JSON.parse(req.body.toString("utf8")); console.log("Flutterwave webhook:", payload.event, payload.data?.tx_ref); if (payload.event !== "charge.completed") return res.json({ received:true }); if (payload.data?.status !== "successful") return res.json({ received:true }); const txRef = payload.data?.tx_ref; if (!txRef) return res.json({ received:true }); // Find payment record const payDoc = await db.collection("cardPayments").doc(txRef).get(); if (!payDoc.exists) return res.json({ received:true }); const payData = payDoc.data(); if (payData.status === "completed") return res.json({ received:true, duplicate:true }); // Credit user const creditsToAdd = payData.creditsAmount; await db.collection("users").doc(payData.uid).update({ credits: admin.firestore.FieldValue.increment(creditsToAdd) }); await db.collection("users").doc(payData.uid).collection("transactions").add({ type:"credit", amount:creditsToAdd, note:`Card top-up — $${payData.amountUSD} — ${creditsToAdd.toLocaleString()} credits`, createdAt: admin.firestore.FieldValue.serverTimestamp() }); await db.collection("cardPayments").doc(txRef).update({ status:"completed" }); // Referral commission const userDoc = await db.collection("users").doc(payData.uid).get(); const referredBy = userDoc.data()?.referredBy; if (referredBy) { const commissionNGN = Math.floor(payData.amountNGN * REFERRAL_PCT); await db.collection("users").doc(referredBy).update({ referralEarningsNGN: admin.firestore.FieldValue.increment(commissionNGN), }); await db.collection("users").doc(referredBy).collection("referralEarnings").add({ fromUid:payData.uid, fromEmail:payData.email, amountNGN:commissionNGN, note:`10% referral from $${payData.amountUSD} card payment`, createdAt: admin.firestore.FieldValue.serverTimestamp() }); } console.log("✅ Card payment credited:", creditsToAdd, "to", payData.uid); return res.json({ success:true }); } catch(e) { console.error("FLW webhook error:", e.message); return res.status(500).json({ error:e.message }); } }); // ── VERIFY CARD PAYMENT ── app.post("/api/verify-card-payment", async (req,res) => { const user = await verifyUser(req,res); if (!user) return; const { txRef } = req.body; if (!txRef) return res.status(400).json({ error:"txRef required" }); try { const FLW_SECRET = FLW_SECRET_KEY; const verifyRes = await axios.get( `https://api.flutterwave.com/v3/transactions/verify_by_reference?tx_ref=${txRef}`, { headers: { Authorization:`Bearer ${FLW_SECRET}` }} ); const data = verifyRes.data?.data; if (data?.status === "successful") { const payDoc = await db.collection("cardPayments").doc(txRef).get(); if (!payDoc.exists) return res.status(400).json({ error:"Payment not found" }); const payData = payDoc.data(); if (payData.status === "completed") return res.json({ success:true, alreadyCredited:true }); const creditsToAdd = payData.creditsAmount; await db.collection("users").doc(user.uid).update({ credits: admin.firestore.FieldValue.increment(creditsToAdd) }); await db.collection("users").doc(user.uid).collection("transactions").add({ type:"credit", amount:creditsToAdd, note:`Card top-up — $${payData.amountUSD} — ${creditsToAdd.toLocaleString()} credits`, createdAt: admin.firestore.FieldValue.serverTimestamp() }); await db.collection("cardPayments").doc(txRef).update({ status:"completed" }); return res.json({ success:true, credits:creditsToAdd }); } return res.json({ success:false, status:data?.status }); } catch(e) { return res.status(500).json({ error:e.message }); } }); // Serve frontend app.get("/.env", (req,res) => { res.status(404).send("Not found"); }); app.get("/.env*", (req,res) => { res.status(404).send("Not found"); }); app.get("/secrets*", (req,res) => { res.status(404).send("Not found"); }); app.get("/config.json", (req,res) => { res.status(404).send("Not found"); }); app.get("/firebase-config.json", (req,res) => { res.status(404).send("Not found"); }); app.get("/.aws*", (req,res) => { res.status(404).send("Not found"); }); app.get("/api/config", (req,res) => { res.status(404).send("Not found"); }); app.get("/api/env", (req,res) => { res.status(404).send("Not found"); }); app.get("/api/settings", (req,res) => { res.status(404).send("Not found"); }); app.get("/google-services.json", (req,res) => { res.status(404).send("Not found"); }); app.get("/xmlrpc.php", (req,res) => { res.status(404).send("Not found"); }); app.get("/blog/xmlrpc.php", (req,res) => { res.status(404).send("Not found"); }); app.get("/wordpress/xmlrpc.php", (req,res) => { res.status(404).send("Not found"); }); app.get("/landing.html", (req,res) => { res.redirect("/"); }); app.get("/", (req,res) => { var host = req.headers.host || ""; if(host.startsWith("app.")){ res.redirect("https://app.audlabs.io/login"); } else { res.sendFile(path.join(__dirname, "landing.html")); } }); app.get("/login", (req,res) => { var host = req.headers.host || ""; if(host.startsWith("app.")){ res.sendFile(path.join(__dirname, "public", "app.html")); } else { res.redirect("https://app.audlabs.io/login"); } }); app.get("/privacy-policy", (req,res) => { res.sendFile(path.join(__dirname, "public", "privacy.html")); }); app.get("*", (req,res) => { res.sendFile(path.join(__dirname, "public", "app.html")); }); // ── FOLLOW-UP EMAILS ── app.post("/api/send-followup-emails", async (req,res) => { const secret = req.headers["x-cron-secret"] || ""; if(secret !== "audlabs-monthly-2026"){ return res.status(401).json({ error:"Unauthorized" }); } try { const now = new Date(); const usersSnap = await db.collection("users").get(); let sent = 0; for(const doc of usersSnap.docs){ const data = doc.data(); if(!data.email || !data.createdAt) continue; const createdAt = data.createdAt.toDate(); const daysSince = Math.floor((now - createdAt) / (1000 * 60 * 60 * 24)); const emailsSent = data.followupEmailsSent || []; // Day 3 email if(daysSince >= 3 && !emailsSent.includes("day3")){ await audlabsTransporter.sendMail({ from: `"AudLabs" <${process.env.AUDLABS_SMTP_USER}>`, to: data.email, subject: "Have you tried AudLabs yet? 🎙", html: `

Have you tried AudLabs yet?

Dear ${data.displayName||"Creator"},

We noticed you recently joined AudLabs but have not yet generated your first voiceover. You still have 5,000 free credits waiting for you.

With AudLabs, you can generate professional voiceovers for your YouTube videos, podcasts and content in seconds — no recording equipment needed.

Generate Your First Voiceover

Follow us: X · YouTube · Telegram

© 2026 AudLabs. All rights reserved.

` }); await db.collection("users").doc(doc.id).update({ followupEmailsSent: admin.firestore.FieldValue.arrayUnion("day3") }); sent++; } // Day 7 email if(daysSince >= 7 && !emailsSent.includes("day7")){ await audlabsTransporter.sendMail({ from: `"AudLabs" <${process.env.AUDLABS_SMTP_USER}>`, to: data.email, subject: "Your free credits are still waiting for you 🎙", html: `

Your Free Credits Are Still Waiting

Dear ${data.displayName||"Creator"},

It has been a week since you joined AudLabs. Your 5,000 free credits are still available and waiting for you to use.

Here is what AudLabs can do for you:

Start Generating Now

Follow us: X · YouTube · Telegram

© 2026 AudLabs. All rights reserved.

` }); await db.collection("users").doc(doc.id).update({ followupEmailsSent: admin.firestore.FieldValue.arrayUnion("day7") }); sent++; } // Day 30 email if(daysSince >= 30 && !emailsSent.includes("day30")){ await audlabsTransporter.sendMail({ from: `"AudLabs" <${process.env.AUDLABS_SMTP_USER}>`, to: data.email, subject: "A message from AudLabs 🎙", html: `

Your Fellow Creators Are Already Creating

Dear ${data.displayName||"Creator"},

It has been 30 days since you joined AudLabs. While you have been away, thousands of creators are already using AudLabs to generate professional voiceovers for their YouTube videos, podcasts and content.

Do not be left behind. Come back today and start creating with your free credits.

5,000
Free Credits Still Available
Come Back and Create

Follow us: X · YouTube · Telegram

© 2026 AudLabs. All rights reserved.

` }); await db.collection("users").doc(doc.id).update({ followupEmailsSent: admin.firestore.FieldValue.arrayUnion("day30") }); sent++; } } console.log("Follow-up emails sent:", sent); return res.json({ success:true, emailsSent:sent }); } catch(e){ console.error("Follow-up email error:", e.message); return res.status(500).json({ error:e.message }); } }); // ── MONTHLY CREDITS ── app.post("/api/monthly-credits", async (req,res) => { const secret = req.headers["x-cron-secret"] || ""; if(secret !== "audlabs-monthly-2026"){ return res.status(401).json({ error:"Unauthorized" }); } try { const usersSnap = await db.collection("users").get(); let count = 0; for(const doc of usersSnap.docs){ await db.collection("users").doc(doc.id).update({ credits: admin.firestore.FieldValue.increment(5000) }); await db.collection("users").doc(doc.id).collection("transactions").add({ type:"credit", amount:5000, note:"🎁 Monthly Credits — 5,000 free credits for being an AudLabs User.", createdAt: admin.firestore.FieldValue.serverTimestamp() }); count++; } console.log("Monthly credits distributed to", count, "users"); return res.json({ success:true, usersCredited:count }); } catch(e){ return res.status(500).json({ error:e.message }); } }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => console.log(`VoiceGen on port ${PORT}`));