Fixed validation
This commit is contained in:
@ -30,6 +30,46 @@ const objectsToCSV = (data: RSVPItem[]): string => {
|
|||||||
return csvRows.join("\n");
|
return csvRows.join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sanitization helpers
|
||||||
|
const sanitizeString = (value: unknown): string => {
|
||||||
|
if (typeof value !== 'string') return '';
|
||||||
|
// Remove any control characters and normalize whitespace
|
||||||
|
return value
|
||||||
|
.replace(/[\x00-\x1F\x7F-\x9F]/g, '') // Remove control characters
|
||||||
|
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||||
|
.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeBoolean = (value: unknown): boolean => {
|
||||||
|
if (typeof value === 'boolean') return value;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const lowered = value.toLowerCase().trim();
|
||||||
|
return !(lowered === 'false' || lowered === '0' || lowered === 'no');
|
||||||
|
}
|
||||||
|
return Boolean(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeTimestamp = (value: unknown): string => {
|
||||||
|
if (typeof value !== 'string') return new Date().toISOString();
|
||||||
|
try {
|
||||||
|
const date = new Date(value);
|
||||||
|
if (isNaN(date.getTime())) throw new Error('Invalid date');
|
||||||
|
return date.toISOString();
|
||||||
|
} catch {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeRSVP = (data: Record<string, unknown>): RSVPItem => {
|
||||||
|
return {
|
||||||
|
name: sanitizeString(data.name),
|
||||||
|
attending: sanitizeBoolean(data.attending),
|
||||||
|
dietaryRestrictions: sanitizeString(data.dietaryRestrictions),
|
||||||
|
notes: sanitizeString(data.notes),
|
||||||
|
timestamp: sanitizeTimestamp(data.timestamp),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const csvToObjects = (csv: string): RSVPItem[] => {
|
const csvToObjects = (csv: string): RSVPItem[] => {
|
||||||
const lines = csv.split("\n");
|
const lines = csv.split("\n");
|
||||||
const headers = lines[0].split(",").map((h) => h.trim());
|
const headers = lines[0].split(",").map((h) => h.trim());
|
||||||
@ -42,6 +82,7 @@ const csvToObjects = (csv: string): RSVPItem[] => {
|
|||||||
const values: string[] = [];
|
const values: string[] = [];
|
||||||
let inQuote = false;
|
let inQuote = false;
|
||||||
let currentValue = '';
|
let currentValue = '';
|
||||||
|
let isEscaped = false;
|
||||||
|
|
||||||
for (let i = 0; i < line.length; i++) {
|
for (let i = 0; i < line.length; i++) {
|
||||||
const char = line[i];
|
const char = line[i];
|
||||||
@ -51,41 +92,37 @@ const csvToObjects = (csv: string): RSVPItem[] => {
|
|||||||
// Handle escaped quotes
|
// Handle escaped quotes
|
||||||
currentValue += '"';
|
currentValue += '"';
|
||||||
i++;
|
i++;
|
||||||
} else {
|
isEscaped = true;
|
||||||
|
} else if (!isEscaped || (isEscaped && inQuote)) {
|
||||||
inQuote = !inQuote;
|
inQuote = !inQuote;
|
||||||
|
isEscaped = false;
|
||||||
|
} else {
|
||||||
|
currentValue += char;
|
||||||
}
|
}
|
||||||
} else if (char === ',' && !inQuote) {
|
} else if (char === ',' && !inQuote) {
|
||||||
values.push(currentValue.trim());
|
values.push(currentValue.trim());
|
||||||
currentValue = '';
|
currentValue = '';
|
||||||
|
isEscaped = false;
|
||||||
} else {
|
} else {
|
||||||
currentValue += char;
|
currentValue += char;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
values.push(currentValue.trim());
|
values.push(currentValue.trim());
|
||||||
|
|
||||||
// Create the entry object
|
// Create a data object from the CSV values
|
||||||
const entry: Partial<RSVPItem> = {};
|
const data: Record<string, unknown> = {};
|
||||||
headers.forEach((header, index) => {
|
headers.forEach((header, index) => {
|
||||||
let value = values[index] || "";
|
if (index < values.length) {
|
||||||
value = value.replace(/^"(.*)"$/, "$1").replace(/""/g, '"');
|
let value = values[index];
|
||||||
|
value = value.replace(/^"(.*)"$/, "$1").replace(/""/g, '"');
|
||||||
if (header === "attending") {
|
data[header] = value;
|
||||||
// Explicitly convert to boolean
|
|
||||||
entry.attending = value.toLowerCase() === "true";
|
|
||||||
} else {
|
|
||||||
entry[header as keyof RSVPItem] = value;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return entry as RSVPItem;
|
// Sanitize the data
|
||||||
|
return sanitizeRSVP(data);
|
||||||
})
|
})
|
||||||
.filter(entry =>
|
.filter(entry => entry.name.length > 0); // Only keep entries with non-empty names
|
||||||
// Filter out malformed entries
|
|
||||||
entry.name &&
|
|
||||||
typeof entry.attending === 'boolean' &&
|
|
||||||
typeof entry.timestamp === 'string' &&
|
|
||||||
entry.timestamp.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// GET: Retrieve all RSVPs (requires admin role)
|
// GET: Retrieve all RSVPs (requires admin role)
|
||||||
@ -133,7 +170,27 @@ const handlePost: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const FILE_KEY = "rsvp.csv";
|
const FILE_KEY = "rsvp.csv";
|
||||||
const newRsvp = await request.json();
|
const rawData = await request.json();
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!rawData.name) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Name is required",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize the input data
|
||||||
|
const newRsvp = sanitizeRSVP({
|
||||||
|
...rawData,
|
||||||
|
timestamp: rawData.timestamp || new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
let existingRsvps: RSVPItem[] = [];
|
let existingRsvps: RSVPItem[] = [];
|
||||||
const fileContent = await getS3Data<string>(FILE_KEY);
|
const fileContent = await getS3Data<string>(FILE_KEY);
|
||||||
@ -141,13 +198,7 @@ const handlePost: APIRoute = async ({ request }) => {
|
|||||||
existingRsvps = csvToObjects(fileContent);
|
existingRsvps = csvToObjects(fileContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
existingRsvps.push({
|
existingRsvps.push(newRsvp);
|
||||||
...newRsvp,
|
|
||||||
attending: Boolean(newRsvp.attending),
|
|
||||||
notes: newRsvp.notes || "",
|
|
||||||
dietaryRestrictions: newRsvp.dietaryRestrictions || "",
|
|
||||||
timestamp: newRsvp.timestamp || new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const csvContent = objectsToCSV(existingRsvps);
|
const csvContent = objectsToCSV(existingRsvps);
|
||||||
await putS3Data(FILE_KEY, csvContent, "text/csv");
|
await putS3Data(FILE_KEY, csvContent, "text/csv");
|
||||||
|
Reference in New Issue
Block a user