require('dotenv').config(); const express = require('express'); const http = require('http'); const cors = require('cors'); const bodyParser = require('body-parser'); const { google } = require('googleapis'); const fs = require('fs'); const path = require('path'); const { Server } = require("socket.io"); const nodemailer = require('nodemailer'); const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*", // allow all domains to connect for real-time updates (you can restrict this later) methods: ["GET", "POST"] } }); const port = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(bodyParser.json({ limit: '10mb' })); app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' })); // Basic Auth for UI Protection app.use((req, res, next) => { // Always permit the API and integration scripts if (req.path.startsWith('/api/') || req.path === '/embed.js' || req.path === '/live-site-integration.js') { return next(); } // For HTML pages and other assets, require a password const b64auth = (req.headers.authorization || '').split(' ')[1] || ''; const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':'); // Let them use 'admin' as the username, and the SMTP password (or fallback to 'admin' if not deployed yet) const expectedPassword = process.env.UI_PASSWORD || process.env.SMTP_PASS || 'admin'; // We allow 'admin' user or ANY user as long as the password matches. Browsers usually prompt for both. if (password === expectedPassword) { return next(); } res.set('WWW-Authenticate', 'Basic realm="Restricted WallAPI Space"'); res.status(401).send('Authentication required. Username: admin. Password matches your SMTP_PASS Secret (or "admin" if not set).'); }); app.use(express.static('public')); // Serve the frontend assets // Google Sheets Setup const SCOPES = ['https://www.googleapis.com/auth/spreadsheets']; async function getAuth() { if (process.env.GOOGLE_CREDENTIALS) { // Hosted on Hugging Face: we read the JSON string directly from the secret try { const credentials = JSON.parse(process.env.GOOGLE_CREDENTIALS); const auth = new google.auth.GoogleAuth({ credentials, scopes: SCOPES, }); return auth; } catch (e) { console.error("Failed to parse GOOGLE_CREDENTIALS secret. Is it valid JSON?", e); throw new Error("Invalid Google Credentials Secret"); } } else if (fs.existsSync(process.env.GOOGLE_CREDENTIALS_PATH)) { // Local fallback: read from the file const auth = new google.auth.GoogleAuth({ keyFile: process.env.GOOGLE_CREDENTIALS_PATH, scopes: SCOPES, }); return auth; } else { throw new Error('Google Credentials not found. Please set GOOGLE_CREDENTIALS secret or provide a local credentials.json.'); } } // Ensure the Google Sheet ID is configured const spreadsheetId = process.env.GOOGLE_SHEET_ID; // API Endpoint to receive form submissions app.post('/api/submit', async (req, res) => { try { const { name, email, message, phone, subject, origin } = req.body; const hasIdentifier = name || email || phone || req.body.submitterEmail || req.body.submitterName || req.body.submitterPhone || req.body.vetFirstName || req.body.contactName || req.body.groupName; if (!hasIdentifier) { return res.status(400).json({ error: 'Please provide at least a name, email, or phone number.' }); } if (!spreadsheetId || spreadsheetId === 'your_google_sheet_id_here') { console.error('SERVER SETUP ERROR: GOOGLE_SHEET_ID is not properly configured in .env'); return res.status(500).json({ error: 'Server configuration error.' }); } const auth = await getAuth(); const sheets = google.sheets({ version: 'v4', auth }); // Assuming the first sheet is where data goes. // We are going to append to Columns A to G (Timestamp, Name, Email, Message, Phone, Subject, Origin). const TIMESTAMP = new Date().toISOString(); // The range specifies where to append. // Dynamically sort into different tabs based on the origin URL! let range = 'Sheet1'; let rowData = []; // Fallbacks if generic integration script used const safeName = req.body.name || req.body.Name || req.body._generic_name || ''; const safePhone = req.body.phone || req.body.Phone || ''; const safeEmail = req.body.email || req.body.Email || ''; const safeMessage = req.body.message || req.body.Message || ''; if (origin) { const lowerOrigin = origin.toLowerCase(); if (lowerOrigin.includes('contact')) { range = 'Contact'; // Match Contact Form: Date, Name, Email, Phone, Subject, Message, Origin rowData = [ TIMESTAMP, safeName, safeEmail, safePhone, req.body.subject || '', safeMessage, origin ]; } else if (lowerOrigin.includes('tours')) { range = 'Tours'; // Map exactly to: Date, Group Name, Tour Type, Contact Name, Contact Title, Email, Phone, Group Size, Date, Time, Special Needs, Additional Info, Origin rowData = [ TIMESTAMP, req.body.groupName || '', req.body.tourType || '', req.body.contactName || '', req.body.contactTitle || '', req.body.email || '', req.body.phone || '', req.body.groupSize || '', req.body.preferredDate || '', req.body.preferredTime || '', req.body.specialNeeds || '', req.body.additionalInfo || '', origin ]; } else if (lowerOrigin.includes('escort')) { range = 'Escort'; // Map exactly to: Date, Name, Email, Phone, Vehicle, Group Affiliation, Origin rowData = [ TIMESTAMP, req.body.name || '', req.body.email || '', req.body.phone || '', req.body.vehicle || '', req.body.groupAffiliation || '', origin ]; } else if (lowerOrigin.includes('honor')) { range = 'Honor'; let vetPhotoUrl = ''; const photoData = req.body.vetPhoto; if (photoData && photoData.startsWith('data:image')) { try { const matches = photoData.match(/^data:image\/(\w+);base64,(.+)$/s); if (matches) { const uploadDir = path.join(__dirname, 'public', 'uploads'); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); const fileName = `${Date.now()}.${matches[1]}`; fs.writeFileSync(path.join(uploadDir, fileName), Buffer.from(matches[2], 'base64')); vetPhotoUrl = `${req.protocol}://${req.get('host')}/uploads/${fileName}`; } } catch (photoErr) { console.error('Failed to save photo:', photoErr); } } rowData = [ TIMESTAMP, req.body.vetFirstName || '', req.body.vetMiddleName || '', req.body.vetLastName || '', req.body.vetRank || '', req.body.vetBranch || '', req.body.yearsServed || '', req.body.conflictEra || '', req.body.vetHometown || '', req.body.isDeceased || 'No', req.body.vetStory || '', req.body.specialRecognition || '', req.body.submitterName || '', req.body.relationship || '', req.body.submitterEmail || '', req.body.submitterPhone || '', req.body.displayPermission ? 'Yes' : 'No', origin, vetPhotoUrl ]; } else { // Fallback catch-all rowData = [TIMESTAMP, safeName, safeEmail, safeMessage, safePhone, subject || '', origin]; } } else { rowData = [TIMESTAMP, safeName, safeEmail, safeMessage, safePhone, subject || '', origin || 'Unknown']; } const response = await sheets.spreadsheets.values.append({ spreadsheetId, range, valueInputOption: 'USER_ENTERED', requestBody: { values: [rowData], }, }); console.log(`Successfully appended row to sheet: ${response.data.updates.updatedRange}`); // Broadcast the new submission to all connected clients io.emit('new_submission', { timestamp: TIMESTAMP, name, email, message, phone: phone || '', subject: subject || '', origin: origin || 'Hugging Face Widget' }); // Send Email Notification if credentials exist if (process.env.SMTP_USER && process.env.SMTP_PASS && process.env.NOTIFICATION_EMAIL) { try { const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'smtp.gmail.com', port: process.env.SMTP_PORT || 465, secure: process.env.SMTP_PORT == 465 || process.env.SMTP_PORT == undefined, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } }); const mailOptions = { from: `"WallAPI Form System" <${process.env.SMTP_USER}>`, to: process.env.NOTIFICATION_EMAIL, subject: `New Lead: ${name} via ${subject || 'Website'}`, html: `
You have received a new contact submission.
| Name | ${name} |
| ${email} | |
| Phone | ${phone || 'Not provided'} |
| Subject | ${subject || 'Not provided'} |
| Origin | ${origin || 'Hugging Face Widget'} |
| Timestamp | ${TIMESTAMP} |
| Message | |
| ${message} | |