fix: critical perf & security — rate limiting, DB indexes, N+1 query, image lazy loading

- Add in-memory rate limiter (src/lib/rateLimit.ts) to public registration endpoints
- Add DB migration #9 with 8 performance indexes on booking/registration tables
- Fix N+1 query in getUpcomingReminders() — single IN() query instead of per-title
- Add loading="lazy" to all non-hero images (MasterClasses, News, Classes, Team)
- Add sizes attribute to Classes images for better responsive loading

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 13:55:49 +03:00
parent 4e766d6957
commit 127990e532
9 changed files with 127 additions and 5 deletions

View File

@@ -184,6 +184,22 @@ const migrations: Migration[] = [
}
},
},
{
version: 9,
name: "add_performance_indexes",
up: (db) => {
db.exec(`
CREATE INDEX IF NOT EXISTS idx_mc_registrations_title ON mc_registrations(master_class_title);
CREATE INDEX IF NOT EXISTS idx_mc_registrations_created ON mc_registrations(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_group_bookings_created ON group_bookings(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_open_day_bookings_event ON open_day_bookings(event_id);
CREATE INDEX IF NOT EXISTS idx_open_day_bookings_class ON open_day_bookings(class_id);
CREATE INDEX IF NOT EXISTS idx_open_day_bookings_phone ON open_day_bookings(event_id, phone);
CREATE INDEX IF NOT EXISTS idx_open_day_bookings_class_phone ON open_day_bookings(class_id, phone);
CREATE INDEX IF NOT EXISTS idx_open_day_classes_event ON open_day_classes(event_id);
`);
},
},
];
function runMigrations(db: Database.Database) {
@@ -703,11 +719,22 @@ export function getUpcomingReminders(): ReminderItem[] {
}
}
}
for (const { title, date, time } of upcomingTitles) {
if (upcomingTitles.length > 0) {
const uniqueTitles = [...new Set(upcomingTitles.map((t) => t.title))];
const placeholders = uniqueTitles.map(() => "?").join(", ");
const rows = db.prepare(
"SELECT * FROM mc_registrations WHERE master_class_title = ?"
).all(title) as McRegistrationRow[];
`SELECT * FROM mc_registrations WHERE master_class_title IN (${placeholders})`
).all(...uniqueTitles) as McRegistrationRow[];
// Build a lookup: title → { date, time }
const titleInfo = new Map<string, { date: string; time?: string }>();
for (const t of upcomingTitles) {
titleInfo.set(t.title, { date: t.date, time: t.time });
}
for (const r of rows) {
const info = titleInfo.get(r.master_class_title);
if (!info) continue;
items.push({
id: r.id,
type: "master-class",
@@ -717,8 +744,8 @@ export function getUpcomingReminders(): ReminderItem[] {
instagram: r.instagram ?? undefined,
telegram: r.telegram ?? undefined,
reminderStatus: r.reminder_status ?? undefined,
eventLabel: `${title}${time ? ` · ${time}` : ""}`,
eventDate: date,
eventLabel: `${r.master_class_title}${info.time ? ` · ${info.time}` : ""}`,
eventDate: info.date,
});
}
}