Ig0tU commited on
Commit
33644aa
·
1 Parent(s): ee831a5

Chore: environment variable support and security updates

Browse files
Files changed (5) hide show
  1. .gitignore +2 -0
  2. credentials.json +0 -13
  3. public/live-site-integration.js +23 -1
  4. scrape_forms.js +119 -0
  5. server.js +15 -79
.gitignore CHANGED
@@ -1 +1,3 @@
1
  node_modules/
 
 
 
1
  node_modules/
2
+ credentials.json
3
+ .env
credentials.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "type": "service_account",
3
- "project_id": "gen-lang-client-0963727327",
4
- "private_key_id": "9bd1ab7c0de460b7e1e99f7cbb9ee0423f0581e6",
5
- "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9vhCyLjXvka79\n4siXVml6tRMBjKu686XPjcZpj9DplKu4nuJbnkW9HbdKxz2qaA3K2/0Eb5+vV86U\ngK/QxArvJ3SwbF3Uoo8S6YOgtZ4/WZLKthIEN0o8t6emqifPbDydsvJ88hNmm7r0\n7Jy2+xCdQhnbkNnfPLBjEornTc86Pqq3lDh24XE8YK8bPB1gpsiOMKh6rzBV80qM\noPSYwkNttdvoEdMSVEr6bvWUtt7XJiXitR/ZZbs2oPl3jeVDZzxRPb4TTrPpvWRK\nXAXq8ktkj8ZiReapgahEcse3h49rAxU8Z93YBVdMKrNf3TxlL+VAuUafVknVBdg5\nCYd0vqgNAgMBAAECggEAAbYDFER1nrwvpyzp9SYj/YPvtBIOzGFHLwwVed7Kkz6J\n4cXdpDSl4BSDYyrlQHxC7rxx/+6wK8+8+BI3bBKyLIul4UfQCl+WronK5UDPseVx\nRyK7VH2c72u0Oqiy7tWPAMtBETYoddbOCAUcaFkN6toqFV60CzMc38JcLYIMZ2nN\nG/5OOCgopdX3pEjKLZqdp1pKCB1m4+Tkf+MXJigCnUqVtcOtaDMq4LjMxj1uwM4l\nQXd1fanQU4dEdPzCrgjNe0pFEfnCYsMSByMAw8ny99J4GTyYCRQaQxmf27rHWX31\n4XSDwQoUgI3KueqpAhchYujwGqy5nN10To3guNWccQKBgQDtBD889ltKvdx5vHts\n0Jfhhm1zzUYwKpsZqNJoR521MkdJZ5/scrrgFiOQ+TEmBkq2dKoO185kWf6saKeE\nuWdmbnEIuRC1hm1yS/NcDW9u66/Z02VS88ZOkPkzVxtfdLfrJzA5qQbji2kRKFoO\n0W56CrcdMUpNyFDdIp6txfmRBQKBgQDM8IQeYNFQAJqeS/NGQDa2TKbqohiqbivw\nNodXmeIAbYDyoZYuCbJq1kx0bJpwG+YXGmFJ0HPVi5C3wwKQcAzV4UQK5CAVk6pX\n+cpAk+/a6xdiER0a0pe9Iae+OCJaLsrWb2v9HGebRjKbffFI6EqrELSU6cWX17cs\nw18RpHUJaQKBgBCU8HuqXJ6xA8C8kAH/4fBUQEoOvW9XO7yi0/2ZrQ3lM5mOF2Eq\ncaqFwf09gdPAMu/q347kSDs7FJcpRzcA5ZwD9AKBOAsLGZMafy8cfYRMFuRtZrRT\n+7a5a8XMvUyDVO6tsjEGg0XeFf6uTQamXk1JfKAdN66TahzedC2CIUClAoGBAL0e\nnu1hrwbC5+x4bjFABL1KEyanG1f7fzSXPWJLsVFvu/UrxGkLrcgFplwx9HrMZBKh\n13HEmYBQ4OWTYgRkQpQE40OhrTH8KNAyxL+/RTKii9uFq3QbLsfsDN3u81SBpdEo\n1WCoG7wglYcEO+tp4a3nJp3c9OjgujrmuA6R0ycxAoGAU9KF9GfAJDXKMjXBQiH+\n6ch1awgNF8jQ+fmvp5Va62FgdK9aMSQTnJJDVMYg6nTg2cvhbTL1rXr/bYjWHglV\nJx0SIzUAJjMRoOLxeG9bbX3JAUcuxcRdN97f0JTTpG9y2FoxwwLuTPeG7GhAUi60\njNfEUUkqWEFAjnjK8eujifU=\n-----END PRIVATE KEY-----\n",
6
- "client_email": "wall-form-api@gen-lang-client-0963727327.iam.gserviceaccount.com",
7
- "client_id": "101446101542617249984",
8
- "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9
- "token_uri": "https://oauth2.googleapis.com/token",
10
- "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11
- "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/wall-form-api%40gen-lang-client-0963727327.iam.gserviceaccount.com",
12
- "universe_domain": "googleapis.com"
13
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
public/live-site-integration.js CHANGED
@@ -73,7 +73,7 @@
73
  }
74
  });
75
 
76
- // Read file inputs as base64 DataURLs — server handles saving
77
  const filePromises = [];
78
  Array.from(form.elements).forEach(el => {
79
  if (el.type !== 'file' || !el.files || !el.files.length) return;
@@ -88,6 +88,28 @@
88
  });
89
  await Promise.all(filePromises);
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  // Set fallbacks for strict checking on the backend if this was just a generic form
92
  if (!formData.name && !formData.Name && !formData.contactName && !formData.vetFirstName && !formData.submitterName) {
93
  formData._generic_name = "User";
 
73
  }
74
  });
75
 
76
+ // Read file inputs as base64 DataURLs
77
  const filePromises = [];
78
  Array.from(form.elements).forEach(el => {
79
  if (el.type !== 'file' || !el.files || !el.files.length) return;
 
88
  });
89
  await Promise.all(filePromises);
90
 
91
+ // Upload photo directly to live server PHP — avoids HF ephemeral storage
92
+ if (PHOTO_UPLOAD_URL && formData.vetPhoto && formData.vetPhoto.startsWith('data:image')) {
93
+ try {
94
+ const idRes = await fetch(RESERVE_ID_URL + '?tab=Honor');
95
+ const idData = await idRes.json();
96
+ const uploadRes = await fetch(PHOTO_UPLOAD_URL, {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ body: JSON.stringify({ image: formData.vetPhoto, id: idData.id })
100
+ });
101
+ if (uploadRes.ok) {
102
+ const uploadData = await uploadRes.json();
103
+ if (uploadData.url) {
104
+ formData.vetPhoto = uploadData.url;
105
+ formData._reservedPhotoId = idData.id;
106
+ }
107
+ }
108
+ } catch (e) {
109
+ console.warn('[WallAPI] Direct photo upload failed, API will handle it:', e.message);
110
+ }
111
+ }
112
+
113
  // Set fallbacks for strict checking on the backend if this was just a generic form
114
  if (!formData.name && !formData.Name && !formData.contactName && !formData.vetFirstName && !formData.submitterName) {
115
  formData._generic_name = "User";
scrape_forms.js ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const cheerio = require('cheerio');
4
+
5
+ const urls = [
6
+ { name: 'tours', url: 'https://www.thewallthathealsreynolds.org/tours.html' },
7
+ { name: 'escort', url: 'https://www.thewallthathealsreynolds.org/escort.html' },
8
+ { name: 'honor', url: 'https://www.thewallthathealsreynolds.org/honor.html' },
9
+ { name: 'contact', url: 'https://www.thewallthathealsreynolds.org/contact.html' }
10
+ ];
11
+
12
+ const { execSync } = require('child_process');
13
+
14
+ async function scrapeForms() {
15
+ for (const item of urls) {
16
+ console.log(`Fetching ${item.url}...`);
17
+ try {
18
+ const html = execSync(`curl -s -k "${item.url}"`).toString();
19
+ const $ = cheerio.load(html);
20
+
21
+ // Find the form container
22
+ let formHtml = '';
23
+
24
+ const formContainer = $('.contact-form');
25
+ if (formContainer.length > 0) {
26
+ formHtml = $.html(formContainer);
27
+ } else {
28
+ const justForm = $('form[data-validate]');
29
+ if (justForm.length > 0) {
30
+ formHtml = $.html(justForm);
31
+ } else {
32
+ const fallback = $('form');
33
+ if (fallback.length > 0) {
34
+ formHtml = $.html(fallback.first());
35
+ }
36
+ }
37
+ }
38
+
39
+ if (formHtml) {
40
+ const finalHtml = `<!DOCTYPE html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="UTF-8">
44
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
45
+ <title>${item.name.charAt(0).toUpperCase() + item.name.slice(1)} Form</title>
46
+ <style>
47
+ body {
48
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
49
+ background-color: transparent;
50
+ margin: 0;
51
+ padding: 20px;
52
+ color: #333;
53
+ }
54
+ .contact-form {
55
+ max-width: 600px;
56
+ margin: 0 auto;
57
+ background: #fff;
58
+ padding: 30px;
59
+ border-radius: 8px;
60
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
61
+ }
62
+ .form-group {
63
+ margin-bottom: 20px;
64
+ }
65
+ label {
66
+ display: block;
67
+ margin-bottom: 8px;
68
+ font-weight: 500;
69
+ color: #1a365d;
70
+ }
71
+ input[type="text"],
72
+ input[type="email"],
73
+ input[type="tel"],
74
+ select,
75
+ textarea {
76
+ width: 100%;
77
+ padding: 10px;
78
+ border: 1px solid #ccc;
79
+ border-radius: 4px;
80
+ font-size: 16px;
81
+ box-sizing: border-box;
82
+ }
83
+ button[type="submit"], input[type="submit"] {
84
+ background-color: #1a365d;
85
+ color: white;
86
+ padding: 12px 24px;
87
+ border: none;
88
+ border-radius: 4px;
89
+ font-size: 16px;
90
+ cursor: pointer;
91
+ width: 100%;
92
+ transition: background-color 0.2s;
93
+ }
94
+ button[type="submit"]:hover, input[type="submit"]:hover {
95
+ background-color: #2c5282;
96
+ }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ ${formHtml.replace(/action="[^"]*"/g, '').replace(/method="[^"]*"/g, '')}
101
+
102
+ <!-- Integration Script to automatically wire this form up to the Hugging Face API -->
103
+ <script src="/live-site-integration.js"></script>
104
+ </body>
105
+ </html>`;
106
+
107
+ const outputPath = path.join(__dirname, 'public', `${item.name}.html`);
108
+ fs.writeFileSync(outputPath, finalHtml);
109
+ console.log(`Saved form to ${outputPath}`);
110
+ } else {
111
+ console.log(`No form found on ${item.url}`);
112
+ }
113
+ } catch (e) {
114
+ console.error(`Failed to fetch ${item.url}:`, e);
115
+ }
116
+ }
117
+ }
118
+
119
+ scrapeForms();
server.js CHANGED
@@ -82,27 +82,6 @@ async function getAuth() {
82
  // Ensure the Google Sheet ID is configured
83
  const spreadsheetId = process.env.GOOGLE_SHEET_ID;
84
 
85
- // Reserve next ID for a tab (used by client before photo upload)
86
- app.get('/api/reserve-id', async (req, res) => {
87
- try {
88
- const tab = req.query.tab || 'Sheet1';
89
- const auth = await getAuth();
90
- const sheets = google.sheets({ version: 'v4', auth });
91
- const idCheck = await sheets.spreadsheets.values.get({
92
- spreadsheetId,
93
- range: `${tab}!A:A`,
94
- });
95
- const numericIds = (idCheck.data.values || [])
96
- .flat()
97
- .map(v => parseInt(v))
98
- .filter(v => !isNaN(v));
99
- const nextId = numericIds.length > 0 ? Math.max(...numericIds) + 1 : 1;
100
- res.json({ id: nextId });
101
- } catch (e) {
102
- res.status(500).json({ error: e.message });
103
- }
104
- });
105
-
106
  // API Endpoint to receive form submissions
107
  app.post('/api/submit', async (req, res) => {
108
  try {
@@ -127,31 +106,9 @@ app.post('/api/submit', async (req, res) => {
127
  // We are going to append to Columns A to G (Timestamp, Name, Email, Message, Phone, Subject, Origin).
128
  const TIMESTAMP = new Date().toISOString();
129
 
130
- // Pre-determine range so we can fetch the current row count for the ID
 
131
  let range = 'Sheet1';
132
- if (origin) {
133
- const lo = origin.toLowerCase();
134
- if (lo.includes('contact')) range = 'Contact';
135
- else if (lo.includes('tours')) range = 'Tours';
136
- else if (lo.includes('escort')) range = 'Escort';
137
- else if (lo.includes('honor')) range = 'Honor';
138
- }
139
-
140
- let nextId = 1;
141
- try {
142
- const idCheck = await sheets.spreadsheets.values.get({
143
- spreadsheetId,
144
- range: `${range}!A:A`,
145
- });
146
- const numericIds = (idCheck.data.values || [])
147
- .flat()
148
- .filter(v => /^\d+$/.test(String(v).trim()))
149
- .map(v => parseInt(v, 10));
150
- nextId = numericIds.length > 0 ? Math.max(...numericIds) + 1 : 1;
151
- } catch (e) {
152
- console.error('ID fetch failed:', e.message);
153
- }
154
-
155
  let rowData = [];
156
 
157
  // Fallbacks if generic integration script used
@@ -164,8 +121,8 @@ app.post('/api/submit', async (req, res) => {
164
  const lowerOrigin = origin.toLowerCase();
165
  if (lowerOrigin.includes('contact')) {
166
  range = 'Contact';
 
167
  rowData = [
168
- nextId,
169
  TIMESTAMP,
170
  safeName,
171
  safeEmail,
@@ -176,8 +133,8 @@ app.post('/api/submit', async (req, res) => {
176
  ];
177
  } else if (lowerOrigin.includes('tours')) {
178
  range = 'Tours';
 
179
  rowData = [
180
- nextId,
181
  TIMESTAMP,
182
  req.body.groupName || '',
183
  req.body.tourType || '',
@@ -194,13 +151,14 @@ app.post('/api/submit', async (req, res) => {
194
  ];
195
  } else if (lowerOrigin.includes('escort')) {
196
  range = 'Escort';
 
197
  rowData = [
198
- nextId,
199
  TIMESTAMP,
200
  req.body.name || '',
201
  req.body.email || '',
202
  req.body.phone || '',
203
  req.body.vehicle || '',
 
204
  origin
205
  ];
206
  } else if (lowerOrigin.includes('honor')) {
@@ -210,35 +168,13 @@ app.post('/api/submit', async (req, res) => {
210
  const photoData = req.body.vetPhoto;
211
  if (photoData && photoData.startsWith('data:image')) {
212
  try {
213
- const matches = photoData.match(/^data:image\/(jpeg|jpg|png|gif|webp);base64,(.+)$/si);
214
  if (matches) {
215
- const ext = matches[1].toLowerCase() === 'jpeg' ? 'jpg' : matches[1].toLowerCase();
216
-
217
- // Try permanent storage on live server first
218
- if (process.env.PHOTO_UPLOAD_ENDPOINT) {
219
- try {
220
- const phpRes = await fetch(process.env.PHOTO_UPLOAD_ENDPOINT, {
221
- method: 'POST',
222
- headers: { 'Content-Type': 'application/json' },
223
- body: JSON.stringify({ image: photoData, id: nextId })
224
- });
225
- if (phpRes.ok) {
226
- const phpData = await phpRes.json();
227
- if (phpData.url) vetPhotoUrl = phpData.url;
228
- }
229
- } catch (phpErr) {
230
- console.error('PHP upload failed, falling back to local:', phpErr.message);
231
- }
232
- }
233
-
234
- // Fallback: save on HF space, but store the permanent thewall URL
235
- if (!vetPhotoUrl) {
236
- const uploadDir = path.join(__dirname, 'public', 'uploads');
237
- if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
238
- const fileName = `${nextId}.${ext}`;
239
- fs.writeFileSync(path.join(uploadDir, fileName), Buffer.from(matches[2], 'base64'));
240
- vetPhotoUrl = `https://www.thewallthathealsreynolds.org/images/uploads/tributes/${fileName}`;
241
- }
242
  }
243
  } catch (photoErr) {
244
  console.error('Failed to save photo:', photoErr);
@@ -246,7 +182,6 @@ app.post('/api/submit', async (req, res) => {
246
  }
247
 
248
  rowData = [
249
- nextId,
250
  TIMESTAMP,
251
  req.body.vetFirstName || '',
252
  req.body.vetMiddleName || '',
@@ -268,10 +203,11 @@ app.post('/api/submit', async (req, res) => {
268
  vetPhotoUrl
269
  ];
270
  } else {
271
- rowData = [nextId, TIMESTAMP, safeName, safeEmail, safeMessage, safePhone, subject || '', origin];
 
272
  }
273
  } else {
274
- rowData = [nextId, TIMESTAMP, safeName, safeEmail, safeMessage, safePhone, subject || '', origin || 'Unknown'];
275
  }
276
 
277
  const response = await sheets.spreadsheets.values.append({
 
82
  // Ensure the Google Sheet ID is configured
83
  const spreadsheetId = process.env.GOOGLE_SHEET_ID;
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  // API Endpoint to receive form submissions
86
  app.post('/api/submit', async (req, res) => {
87
  try {
 
106
  // We are going to append to Columns A to G (Timestamp, Name, Email, Message, Phone, Subject, Origin).
107
  const TIMESTAMP = new Date().toISOString();
108
 
109
+ // The range specifies where to append.
110
+ // Dynamically sort into different tabs based on the origin URL!
111
  let range = 'Sheet1';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  let rowData = [];
113
 
114
  // Fallbacks if generic integration script used
 
121
  const lowerOrigin = origin.toLowerCase();
122
  if (lowerOrigin.includes('contact')) {
123
  range = 'Contact';
124
+ // Match Contact Form: Date, Name, Email, Phone, Subject, Message, Origin
125
  rowData = [
 
126
  TIMESTAMP,
127
  safeName,
128
  safeEmail,
 
133
  ];
134
  } else if (lowerOrigin.includes('tours')) {
135
  range = 'Tours';
136
+ // Map exactly to: Date, Group Name, Tour Type, Contact Name, Contact Title, Email, Phone, Group Size, Date, Time, Special Needs, Additional Info, Origin
137
  rowData = [
 
138
  TIMESTAMP,
139
  req.body.groupName || '',
140
  req.body.tourType || '',
 
151
  ];
152
  } else if (lowerOrigin.includes('escort')) {
153
  range = 'Escort';
154
+ // Map exactly to: Date, Name, Email, Phone, Vehicle, Group Affiliation, Origin
155
  rowData = [
 
156
  TIMESTAMP,
157
  req.body.name || '',
158
  req.body.email || '',
159
  req.body.phone || '',
160
  req.body.vehicle || '',
161
+ req.body.groupAffiliation || '',
162
  origin
163
  ];
164
  } else if (lowerOrigin.includes('honor')) {
 
168
  const photoData = req.body.vetPhoto;
169
  if (photoData && photoData.startsWith('data:image')) {
170
  try {
171
+ const matches = photoData.match(/^data:image\/(\w+);base64,(.+)$/s);
172
  if (matches) {
173
+ const uploadDir = path.join(__dirname, 'public', 'uploads');
174
+ if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
175
+ const fileName = `${Date.now()}.${matches[1]}`;
176
+ fs.writeFileSync(path.join(uploadDir, fileName), Buffer.from(matches[2], 'base64'));
177
+ vetPhotoUrl = `${req.protocol}://${req.get('host')}/uploads/${fileName}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  }
179
  } catch (photoErr) {
180
  console.error('Failed to save photo:', photoErr);
 
182
  }
183
 
184
  rowData = [
 
185
  TIMESTAMP,
186
  req.body.vetFirstName || '',
187
  req.body.vetMiddleName || '',
 
203
  vetPhotoUrl
204
  ];
205
  } else {
206
+ // Fallback catch-all
207
+ rowData = [TIMESTAMP, safeName, safeEmail, safeMessage, safePhone, subject || '', origin];
208
  }
209
  } else {
210
+ rowData = [TIMESTAMP, safeName, safeEmail, safeMessage, safePhone, subject || '', origin || 'Unknown'];
211
  }
212
 
213
  const response = await sheets.spreadsheets.values.append({