diff --git a/backend/server.js b/backend/server.js index 8880cb1..523546e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,6 +10,22 @@ const app = express() app.use(cors()) app.use(express.json()) +// Helper: combine ISO date (YYYY-MM-DD) and time (HH:mm) into local Date +function parseLocalDateTime(dateStr, timeStr, end = false) { + if (!dateStr) return null + const [y, m, d] = String(dateStr).split('-').map(Number) + let hh = 0, mm = 0 + if (typeof timeStr === 'string' && timeStr.includes(':')) { + const [h, mi] = timeStr.split(':').map(Number) + hh = Number.isFinite(h) ? h : 0 + mm = Number.isFinite(mi) ? mi : 0 + } else if (end) { + hh = 23; mm = 59 + } + if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return null + return new Date(y, m - 1, d, hh, mm, 0, 0) +} + // Helper: normalize booking row fields to stable strings (avoid timezone surprises) function sanitizeBooking(row) { const normDate = (v) => { @@ -87,6 +103,30 @@ app.post('/api/bookings', async (req, res) => { return res.status(400).json({ error: 'Missing fields' }) } + // Validate temporal constraints: require start_time/end_time and ensure start >= now, end > start + if (!start_time || !end_time) { + return res.status(400).json({ error: 'Start and end times are required' }) + } + const s = parseLocalDateTime(start_date, start_time) + const e = parseLocalDateTime(end_date, end_time, true) + if (!s || !e) { + return res.status(400).json({ error: 'Invalid dates' }) + } + if (e <= s) { + return res.status(400).json({ error: 'End must be after start' }) + } + const now = new Date() + if (s < now) { + return res.status(400).json({ error: 'Start cannot be in the past' }) + } + // Enforce max 31 days in advance for start and end + const max = new Date() + max.setDate(max.getDate() + 31) + max.setHours(23, 59, 59, 999) + if (s > max || e > max) { + return res.status(400).json({ error: 'Bookings cannot be made more than 31 days in advance' }) + } + // Explicitly set status to 'pending' to avoid relying solely on DB defaults const text = `INSERT INTO bookings(bike_type, bike_types, start_date, start_time, end_date, end_time, name, email, status) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *` @@ -244,6 +284,43 @@ app.patch('/api/bookings/:id', async (req, res) => { if (entries.length === 0) return res.status(400).json({ error: 'No valid fields to update' }) + // Get current booking to validate temporal changes + const current = await pool.query('SELECT * FROM bookings WHERE id = $1', [id]) + if (current.rowCount === 0) return res.status(404).json({ error: 'Booking not found' }) + const existing = current.rows[0] + + // Validate temporal constraints if dates/times are being updated + const bodyObj = req.body || {} + const startDate = bodyObj.start_date || existing.start_date + const startTime = bodyObj.start_time || existing.start_time + const endDate = bodyObj.end_date || existing.end_date + const endTime = bodyObj.end_time || existing.end_time + + if (startDate && endDate && startTime && endTime) { + const s = parseLocalDateTime(startDate, startTime) + const e = parseLocalDateTime(endDate, endTime, true) + if (!s || !e) { + return res.status(400).json({ error: 'Invalid dates' }) + } + if (e <= s) { + return res.status(400).json({ error: 'End must be after start' }) + } + // Allow editing up to 1 day in the past + const oneDayAgo = new Date() + oneDayAgo.setDate(oneDayAgo.getDate() - 1) + oneDayAgo.setHours(0, 0, 0, 0) + if (s < oneDayAgo) { + return res.status(400).json({ error: 'Start cannot be more than 1 day in the past' }) + } + // Enforce max 31 days in advance for start and end + const max = new Date() + max.setDate(max.getDate() + 31) + max.setHours(23, 59, 59, 999) + if (s > max || e > max) { + return res.status(400).json({ error: 'Bookings cannot be made more than 31 days in advance' }) + } + } + // Build dynamic update const sets = [] const values = [] diff --git a/index.html b/index.html index b330a06..80a7bc5 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ -
{{ errors.association }}
+ +{{ errors.association }}
+{{ errors.bikeTypes }}
+{{ errors.bikeTypes }}
+{{ errors.start }}
{{ errors.start }}
{{ errors.end }}
{{ errors.end }}
{{ errors.emails }}
+{{ errors.emails }}
+