sitepaige-mcp-server 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -155,6 +155,7 @@ interface FormData {
155
155
  export default function RForm({ name, custom_view_description, design }: RFormProps) {
156
156
  const [submitted, setSubmitted] = useState(false);
157
157
  const [isSubmitting, setIsSubmitting] = useState(false);
158
+ const [error, setError] = useState<string | null>(null);
158
159
 
159
160
  // Parse form configuration
160
161
  let formData: FormData = {
@@ -171,26 +172,60 @@ export default function RForm({ name, custom_view_description, design }: RFormPr
171
172
  console.error('Error parsing form data:', e);
172
173
  }
173
174
 
174
- // Don't render if no submission email
175
- if (!formData.submissionEmail) {
176
- return (
177
- <div className="p-8 text-center text-gray-500">
178
- <h1>{name}</h1>
179
- <p>Form configuration error: No submission email specified.</p>
180
- </div>
181
- );
182
- }
183
-
184
175
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
185
176
  e.preventDefault();
186
177
  setIsSubmitting(true);
178
+ setError(null);
179
+
180
+ // Get form data
181
+ const form = e.currentTarget;
182
+ const formDataObj: Record<string, any> = {};
183
+
184
+ // Collect all form fields
185
+ formData.fields.forEach((field) => {
186
+ const element = form.elements.namedItem(field.name) as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
187
+ if (element) {
188
+ formDataObj[field.name] = element.value;
189
+ }
190
+ });
191
+
192
+ // Add metadata
193
+ formDataObj._formName = name;
194
+ if (formData.submissionEmail) {
195
+ formDataObj._notificationEmail = formData.submissionEmail;
196
+ }
187
197
 
188
- // FormSubmit.co will handle the actual submission
189
- // We just need to show the success message after a delay
190
- setTimeout(() => {
191
- setSubmitted(true);
198
+ try {
199
+ // Get CSRF token if available
200
+ const csrfMeta = document.querySelector('meta[name="csrf-token"]');
201
+ const csrfToken = csrfMeta?.getAttribute('content');
202
+
203
+ const response = await fetch('/api/form', {
204
+ method: 'POST',
205
+ headers: {
206
+ 'Content-Type': 'application/json',
207
+ ...(csrfToken && { 'X-CSRF-Token': csrfToken })
208
+ },
209
+ body: JSON.stringify({
210
+ formName: name,
211
+ data: formDataObj
212
+ })
213
+ });
214
+
215
+ const result = await response.json();
216
+
217
+ if (response.ok) {
218
+ setSubmitted(true);
219
+ setIsSubmitting(false);
220
+ } else {
221
+ setError(result.error || 'Failed to submit form. Please try again.');
222
+ setIsSubmitting(false);
223
+ }
224
+ } catch (err) {
225
+ console.error('Form submission error:', err);
226
+ setError('Network error. Please check your connection and try again.');
192
227
  setIsSubmitting(false);
193
- }, 1000);
228
+ }
194
229
  };
195
230
 
196
231
  if (submitted) {
@@ -219,8 +254,7 @@ export default function RForm({ name, custom_view_description, design }: RFormPr
219
254
  <button
220
255
  onClick={() => {
221
256
  setSubmitted(false);
222
- // Reset form by reloading (formsubmit.co doesn't provide a JS API)
223
- window.location.reload();
257
+ setError(null);
224
258
  }}
225
259
  className="mt-6 px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
226
260
  >
@@ -252,19 +286,17 @@ export default function RForm({ name, custom_view_description, design }: RFormPr
252
286
  <div className="w-full max-w-2xl mx-auto p-6" style={formStyles}>
253
287
  <h1>{name}</h1>
254
288
  <form
255
- action={`https://formsubmit.co/${formData.submissionEmail}`}
256
- method="POST"
257
289
  onSubmit={handleSubmit}
258
290
  className="space-y-6"
259
291
  >
260
- {/* Hidden fields for FormSubmit.co configuration */}
261
- {!formData.useCaptcha && (
262
- <input type="hidden" name="_captcha" value="false" />
292
+ {/* Display error message if any */}
293
+ {error && (
294
+ <div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
295
+ <p className="font-medium">Error</p>
296
+ <p className="text-sm mt-1">{error}</p>
297
+ </div>
263
298
  )}
264
299
 
265
- {/* Thank you page - redirect back to same page with submitted state */}
266
- <input type="hidden" name="_next" value={typeof window !== 'undefined' ? window.location.href : ''} />
267
-
268
300
  {/* Render form fields */}
269
301
  {formData.fields.map((field, index) => (
270
302
  <div key={field.id || index} className="form-field">
@@ -384,11 +416,6 @@ export default function RForm({ name, custom_view_description, design }: RFormPr
384
416
  </button>
385
417
  </div>
386
418
  </form>
387
-
388
- {/* FormSubmit.co attribution (optional but nice to have) */}
389
- <div className="mt-6 text-center text-xs text-gray-400">
390
- <p>{getFormTranslation('Form secured by FormSubmit', design.websiteLanguage)}</p>
391
- </div>
392
419
  </div>
393
420
  );
394
421
  }
@@ -281,7 +281,6 @@ export default function Menu({ menu, onClick, pages = [] }: MenuProps) {
281
281
  key={item.name}
282
282
  href={linkUrl}
283
283
  onClick={(e) => {
284
- e.preventDefault();
285
284
  setSelectedPage(item.page);
286
285
  onClick?.();
287
286
  }}
@@ -0,0 +1,203 @@
1
+ /*
2
+ Form submission database functions
3
+ Handles storing form data in the database
4
+ */
5
+
6
+ import { db_init, db_query, DatabaseClient } from './db';
7
+ import * as crypto from 'crypto';
8
+
9
+ /**
10
+ * Insert a form submission into the database
11
+ * @param formName - Name/identifier of the form
12
+ * @param formData - JSON data from the form
13
+ * @returns The ID of the inserted submission
14
+ */
15
+ export async function insertFormSubmission(
16
+ formName: string,
17
+ formData: Record<string, any>
18
+ ): Promise<number> {
19
+ const client = await db_init();
20
+
21
+ const dbType = (process.env.DATABASE_TYPE || process.env.DB_TYPE || 'postgres').toLowerCase();
22
+
23
+ let query: string;
24
+ let params: any[];
25
+
26
+ switch (dbType) {
27
+ case 'postgres':
28
+ query = `
29
+ INSERT INTO form_submissions (form_name, form_data)
30
+ VALUES ($1, $2)
31
+ RETURNING id
32
+ `;
33
+ params = [formName, JSON.stringify(formData)];
34
+ break;
35
+
36
+ case 'mysql':
37
+ query = `
38
+ INSERT INTO form_submissions (form_name, form_data)
39
+ VALUES (?, ?)
40
+ `;
41
+ params = [formName, JSON.stringify(formData)];
42
+ break;
43
+
44
+ default: // sqlite
45
+ query = `
46
+ INSERT INTO form_submissions (form_name, form_data)
47
+ VALUES (?, ?)
48
+ `;
49
+ params = [formName, JSON.stringify(formData)];
50
+ break;
51
+ }
52
+
53
+ const result = await db_query(client, query, params);
54
+
55
+ if (dbType === 'postgres') {
56
+ return result[0].id;
57
+ } else if (dbType === 'mysql') {
58
+ return (result as any).insertId;
59
+ } else {
60
+ // SQLite - get the last inserted row id
61
+ const lastIdResult = await db_query(client, 'SELECT last_insert_rowid() as id', []);
62
+ return lastIdResult[0].id;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get form submissions by form name with pagination
68
+ * @param formName - Name of the form (optional, returns all if not provided)
69
+ * @param limit - Number of records to return
70
+ * @param offset - Number of records to skip
71
+ * @returns Array of form submissions
72
+ */
73
+ export async function getFormSubmissions(
74
+ formName?: string,
75
+ limit: number = 50,
76
+ offset: number = 0
77
+ ): Promise<Array<{
78
+ id: number;
79
+ timestamp: string;
80
+ form_name: string;
81
+ form_data: any;
82
+ }>> {
83
+ const client = await db_init();
84
+
85
+ let query: string;
86
+ let params: any[];
87
+
88
+ if (formName) {
89
+ query = `
90
+ SELECT id, timestamp, form_name, form_data
91
+ FROM form_submissions
92
+ WHERE form_name = ?
93
+ ORDER BY timestamp DESC
94
+ LIMIT ? OFFSET ?
95
+ `;
96
+ params = [formName, limit, offset];
97
+ } else {
98
+ query = `
99
+ SELECT id, timestamp, form_name, form_data
100
+ FROM form_submissions
101
+ ORDER BY timestamp DESC
102
+ LIMIT ? OFFSET ?
103
+ `;
104
+ params = [limit, offset];
105
+ }
106
+
107
+ const result = await db_query(client, query, params);
108
+
109
+ // Parse JSON data for each submission
110
+ return result.map(row => ({
111
+ ...row,
112
+ form_data: typeof row.form_data === 'string' ? JSON.parse(row.form_data) : row.form_data
113
+ }));
114
+ }
115
+
116
+ /**
117
+ * Get a single form submission by ID
118
+ * @param id - ID of the submission
119
+ * @returns Form submission or null if not found
120
+ */
121
+ export async function getFormSubmissionById(
122
+ id: number
123
+ ): Promise<{
124
+ id: number;
125
+ timestamp: string;
126
+ form_name: string;
127
+ form_data: any;
128
+ } | null> {
129
+ const client = await db_init();
130
+
131
+ const query = `
132
+ SELECT id, timestamp, form_name, form_data
133
+ FROM form_submissions
134
+ WHERE id = ?
135
+ `;
136
+
137
+ const result = await db_query(client, query, [id]);
138
+
139
+ if (result.length === 0) {
140
+ return null;
141
+ }
142
+
143
+ const row = result[0];
144
+ return {
145
+ ...row,
146
+ form_data: typeof row.form_data === 'string' ? JSON.parse(row.form_data) : row.form_data
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Delete a form submission by ID
152
+ * @param id - ID of the submission to delete
153
+ * @returns true if deleted, false if not found
154
+ */
155
+ export async function deleteFormSubmission(id: number): Promise<boolean> {
156
+ const client = await db_init();
157
+
158
+ const query = `
159
+ DELETE FROM form_submissions
160
+ WHERE id = ?
161
+ `;
162
+
163
+ const result = await db_query(client, query, [id]);
164
+
165
+ // Check if a row was deleted
166
+ if ((result as any).affectedRows !== undefined) {
167
+ return (result as any).affectedRows > 0;
168
+ } else if ((result as any).changes !== undefined) {
169
+ return (result as any).changes > 0;
170
+ }
171
+
172
+ return false;
173
+ }
174
+
175
+ /**
176
+ * Get count of form submissions by form name
177
+ * @param formName - Name of the form (optional, returns total if not provided)
178
+ * @returns Count of submissions
179
+ */
180
+ export async function getFormSubmissionCount(formName?: string): Promise<number> {
181
+ const client = await db_init();
182
+
183
+ let query: string;
184
+ let params: any[];
185
+
186
+ if (formName) {
187
+ query = `
188
+ SELECT COUNT(*) as count
189
+ FROM form_submissions
190
+ WHERE form_name = ?
191
+ `;
192
+ params = [formName];
193
+ } else {
194
+ query = `
195
+ SELECT COUNT(*) as count
196
+ FROM form_submissions
197
+ `;
198
+ params = [];
199
+ }
200
+
201
+ const result = await db_query(client, query, params);
202
+ return parseInt(result[0].count, 10);
203
+ }
@@ -0,0 +1,244 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getFormSubmissions, getFormSubmissionById } from '../db-forms';
3
+ import { validateSession } from '../db-users';
4
+ import { cookies } from 'next/headers';
5
+ import { send_email } from '../storage/email';
6
+
7
+ /**
8
+ * POST /api/form
9
+ * Submit a form with data
10
+ *
11
+ * Request body:
12
+ * {
13
+ * formName: string, // Required: Name/identifier of the form
14
+ * data: object // Required: Form data as key-value pairs
15
+ * }
16
+ *
17
+ * Response:
18
+ * {
19
+ * success: boolean,
20
+ * message: string
21
+ * }
22
+ */
23
+ export async function POST(request: NextRequest) {
24
+ try {
25
+
26
+
27
+ const body = await request.json();
28
+ const { formName, data } = body;
29
+
30
+ // Validate required fields
31
+ if (!formName || typeof formName !== 'string') {
32
+ return NextResponse.json(
33
+ { error: 'Form name is required' },
34
+ { status: 400 }
35
+ );
36
+ }
37
+
38
+ if (!data || typeof data !== 'object') {
39
+ return NextResponse.json(
40
+ { error: 'Form data is required and must be an object' },
41
+ { status: 400 }
42
+ );
43
+ }
44
+
45
+ // Add metadata to the form data
46
+ const enrichedData = {
47
+ ...data,
48
+ _metadata: {
49
+ submittedAt: new Date().toISOString(),
50
+ userAgent: request.headers.get('user-agent') || 'Unknown',
51
+ ip: request.headers.get('x-forwarded-for') ||
52
+ request.headers.get('x-real-ip') ||
53
+ 'Unknown',
54
+ referer: request.headers.get('referer') || 'Direct'
55
+ }
56
+ };
57
+
58
+ // Check if user is authenticated (optional - forms can work for anonymous users too)
59
+ let userId: string | null = null;
60
+ try {
61
+ const sessionCookie = await cookies();
62
+ const sessionToken = sessionCookie.get('session_id')?.value;
63
+ if (sessionToken) {
64
+ const sessionData = await validateSession(sessionToken);
65
+ if (sessionData.valid && sessionData.user) {
66
+ userId = sessionData.user.userid;
67
+ enrichedData._metadata.userId = userId;
68
+ enrichedData._metadata.userEmail = sessionData.user.email;
69
+ }
70
+ }
71
+ } catch (err) {
72
+ // Authentication is optional, continue without user data
73
+ }
74
+
75
+ // Send email using Resend
76
+ const siteDomain = process.env.NODE_ENV === 'production' ? process.env.DOMAIN : process.env.LOCAL_DOMAIN;
77
+
78
+ // Determine hostname for email addresses
79
+ let hostname = 'sitepaige.com'; // Fallback
80
+ try {
81
+ if (siteDomain) {
82
+ // Handle potential missing protocol
83
+ const urlStr = siteDomain.startsWith('http') ? siteDomain : `https://${siteDomain}`;
84
+ hostname = new URL(urlStr).hostname;
85
+ }
86
+ } catch (e) {
87
+ console.warn('Could not parse siteDomain for hostname', siteDomain);
88
+ }
89
+
90
+ const toAddress = `admin@${hostname}`;
91
+ const fromAddress = process.env.EMAIL_FROM || `noreply@sitepaige.com`;
92
+
93
+ // Construct email content
94
+ const dataRows = Object.entries(enrichedData)
95
+ .filter(([key]) => key !== '_metadata')
96
+ .map(([key, value]) => `
97
+ <tr>
98
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>${key}</strong></td>
99
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">${typeof value === 'object' ? JSON.stringify(value) : String(value)}</td>
100
+ </tr>
101
+ `).join('');
102
+
103
+ const emailHtml = `
104
+ <h2>New Form Submission: ${formName}</h2>
105
+ <table style="width: 100%; border-collapse: collapse;">
106
+ <tbody>
107
+ ${dataRows}
108
+ </tbody>
109
+ </table>
110
+ <br>
111
+ <h3>Metadata</h3>
112
+ <pre>${JSON.stringify(enrichedData._metadata, null, 2)}</pre>
113
+ `;
114
+
115
+ const emailText = `
116
+ New Form Submission: ${formName}
117
+ --------------------------------
118
+ ${Object.entries(enrichedData)
119
+ .filter(([key]) => key !== '_metadata')
120
+ .map(([key, value]) => `${key}: ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`)
121
+ .join('\n')}
122
+
123
+ Metadata:
124
+ ${JSON.stringify(enrichedData._metadata, null, 2)}
125
+ `;
126
+
127
+ await send_email({
128
+ to: toAddress,
129
+ from: fromAddress,
130
+ subject: `New Form Submission: ${formName}`,
131
+ html: emailHtml,
132
+ text: emailText
133
+ });
134
+
135
+ return NextResponse.json({
136
+ success: true,
137
+ message: 'Form submitted successfully'
138
+ }, { status: 201 });
139
+
140
+ } catch (error) {
141
+ console.error('Form submission error:', error);
142
+ return NextResponse.json(
143
+ { error: error instanceof Error ? error.message : 'Failed to submit form' },
144
+ { status: 500 }
145
+ );
146
+ }
147
+ }
148
+
149
+ /**
150
+ * GET /api/form
151
+ * Retrieve form submissions (requires admin authentication)
152
+ *
153
+ * Query parameters:
154
+ * - formName: string (optional) - Filter by form name
155
+ * - limit: number (optional, default 50) - Number of records to return
156
+ * - offset: number (optional, default 0) - Number of records to skip
157
+ * - id: number (optional) - Get a specific submission by ID
158
+ *
159
+ * Response:
160
+ * {
161
+ * submissions: Array<{
162
+ * id: number,
163
+ * timestamp: string,
164
+ * form_name: string,
165
+ * form_data: object
166
+ * }>,
167
+ * total: number
168
+ * }
169
+ */
170
+ export async function GET(request: NextRequest) {
171
+ try {
172
+ // Check authentication - only admins can view form submissions
173
+ const sessionCookie = await cookies();
174
+ const sessionToken = sessionCookie.get('session_id')?.value;
175
+
176
+ if (!sessionToken) {
177
+ return NextResponse.json(
178
+ { error: 'Authentication required' },
179
+ { status: 401 }
180
+ );
181
+ }
182
+
183
+ const sessionData = await validateSession(sessionToken);
184
+ if (!sessionData.valid || !sessionData.user) {
185
+ return NextResponse.json(
186
+ { error: 'Invalid session' },
187
+ { status: 401 }
188
+ );
189
+ }
190
+
191
+ // Check if user is admin (userlevel 2)
192
+ if (sessionData.user.userlevel !== 2) {
193
+ return NextResponse.json(
194
+ { error: 'Admin access required' },
195
+ { status: 403 }
196
+ );
197
+ }
198
+
199
+ // Parse query parameters
200
+ const { searchParams } = new URL(request.url);
201
+ const formName = searchParams.get('formName') || undefined;
202
+ const limit = parseInt(searchParams.get('limit') || '50', 10);
203
+ const offset = parseInt(searchParams.get('offset') || '0', 10);
204
+ const id = searchParams.get('id');
205
+
206
+ // If ID is provided, get a specific submission
207
+ if (id) {
208
+ const submission = await getFormSubmissionById(parseInt(id, 10));
209
+ if (!submission) {
210
+ return NextResponse.json(
211
+ { error: 'Submission not found' },
212
+ { status: 404 }
213
+ );
214
+ }
215
+ return NextResponse.json({ submission });
216
+ }
217
+
218
+ // Get form submissions with pagination
219
+ const [submissions, total] = await Promise.all([
220
+ getFormSubmissions(formName, limit, offset),
221
+ getFormSubmissionCount(formName)
222
+ ]);
223
+
224
+ return NextResponse.json({
225
+ submissions,
226
+ total,
227
+ pagination: {
228
+ limit,
229
+ offset,
230
+ hasMore: offset + submissions.length < total
231
+ }
232
+ });
233
+
234
+ } catch (error) {
235
+ console.error('Form retrieval error:', error);
236
+ return NextResponse.json(
237
+ { error: error instanceof Error ? error.message : 'Failed to retrieve forms' },
238
+ { status: 500 }
239
+ );
240
+ }
241
+ }
242
+
243
+ // Import the missing function
244
+ import { getFormSubmissionCount } from '../db-forms';
@@ -1,8 +1,8 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { cookies } from 'next/headers';
3
- import { validateSession } from '../../db-users';
4
- import { updatePassword, getPasswordAuthByEmail } from '../../db-password-auth';
5
- import { deleteUser } from '../../db-users';
3
+ import { validateSession } from '../db-users';
4
+ import { updatePassword } from '../db-password-auth';
5
+ import { deleteUser } from '../db-users';
6
6
 
7
7
  // Handle password change
8
8
  export async function PUT(request: NextRequest) {