fix: 4 bugs from regression testing
- BUG-1: Strip HTML tags in sanitizeName (prevent stored XSS) - BUG-2: Strip HTML tags in notes via sanitizeText across all 3 booking APIs - BUG-3: Dashboard excludes archived/past MCs and expired Open Day events from counts - BUG-4: Truncate long names in booking cards to prevent overflow
This commit is contained in:
@@ -86,7 +86,7 @@ export function GenericBookingsList<T extends BaseBooking>({
|
|||||||
<BookingCard key={item.id} status={item.status}>
|
<BookingCard key={item.id} status={item.status}>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex items-center gap-2 flex-wrap text-sm min-w-0">
|
<div className="flex items-center gap-2 flex-wrap text-sm min-w-0">
|
||||||
<span className="font-medium text-white">{item.name}</span>
|
<span className="font-medium text-white truncate max-w-[200px]">{item.name}</span>
|
||||||
<ContactLinks phone={item.phone} instagram={item.instagram} telegram={item.telegram} />
|
<ContactLinks phone={item.phone} instagram={item.instagram} telegram={item.telegram} />
|
||||||
{renderExtra?.(item)}
|
{renderExtra?.(item)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -568,17 +568,31 @@ function DashboardSummary({ onNavigate }: { onNavigate: (tab: Tab) => void }) {
|
|||||||
const [counts, setCounts] = useState<DashboardCounts | null>(null);
|
const [counts, setCounts] = useState<DashboardCounts | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
const tomorrow = new Date(Date.now() + 86400000).toISOString().split("T")[0];
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
adminFetch("/api/admin/group-bookings").then((r) => r.json()),
|
adminFetch("/api/admin/group-bookings").then((r) => r.json()),
|
||||||
adminFetch("/api/admin/mc-registrations").then((r) => r.json()),
|
// Fetch MC registrations + section data to filter out archived
|
||||||
adminFetch("/api/admin/open-day").then((r) => r.json()).then(async (events: { id: number }[]) => {
|
Promise.all([
|
||||||
if (events.length === 0) return [];
|
adminFetch("/api/admin/mc-registrations").then((r) => r.json()),
|
||||||
return adminFetch(`/api/admin/open-day/bookings?eventId=${events[0].id}`).then((r) => r.json());
|
adminFetch("/api/admin/sections/masterClasses").then((r) => r.json()),
|
||||||
|
]).then(([regs, mcData]: [{ status: string; masterClassTitle: string }[], { items?: { title: string; slots: { date: string }[] }[] }]) => {
|
||||||
|
// Build set of upcoming MC titles
|
||||||
|
const upcomingTitles = new Set<string>();
|
||||||
|
for (const mc of mcData.items || []) {
|
||||||
|
if (mc.slots?.some((s) => s.date >= today)) upcomingTitles.add(mc.title);
|
||||||
|
}
|
||||||
|
return regs.filter((r) => upcomingTitles.has(r.masterClassTitle));
|
||||||
|
}),
|
||||||
|
// Fetch Open Day — only upcoming events
|
||||||
|
adminFetch("/api/admin/open-day").then((r) => r.json()).then(async (events: { id: number; date: string }[]) => {
|
||||||
|
const active = events.find((e) => e.date >= today);
|
||||||
|
if (!active) return [];
|
||||||
|
return adminFetch(`/api/admin/open-day/bookings?eventId=${active.id}`).then((r) => r.json());
|
||||||
}),
|
}),
|
||||||
adminFetch("/api/admin/reminders").then((r) => r.json()).catch(() => []),
|
adminFetch("/api/admin/reminders").then((r) => r.json()).catch(() => []),
|
||||||
]).then(([gb, mc, od, rem]: [{ status: string }[], { status: string }[], { status: string }[], { eventDate: string }[]]) => {
|
]).then(([gb, mc, od, rem]: [{ status: string }[], { status: string }[], { status: string }[], { eventDate: string }[]]) => {
|
||||||
const today = new Date().toISOString().split("T")[0];
|
|
||||||
const tomorrow = new Date(Date.now() + 86400000).toISOString().split("T")[0];
|
|
||||||
setCounts({
|
setCounts({
|
||||||
classesNew: gb.filter((b) => b.status === "new").length,
|
classesNew: gb.filter((b) => b.status === "new").length,
|
||||||
classesContacted: gb.filter((b) => b.status === "contacted").length,
|
classesContacted: gb.filter((b) => b.status === "contacted").length,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getGroupBookings, addGroupBooking, toggleGroupBookingNotification, deleteGroupBooking, setGroupBookingStatus, updateBookingNotes } from "@/lib/db";
|
import { getGroupBookings, addGroupBooking, toggleGroupBookingNotification, deleteGroupBooking, setGroupBookingStatus, updateBookingNotes } from "@/lib/db";
|
||||||
import type { BookingStatus } from "@/lib/db";
|
import type { BookingStatus } from "@/lib/db";
|
||||||
|
import { sanitizeText } from "@/lib/validation";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const bookings = getGroupBookings();
|
const bookings = getGroupBookings();
|
||||||
@@ -35,7 +36,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
if (body.action === "set-notes") {
|
if (body.action === "set-notes") {
|
||||||
const { id, notes } = body;
|
const { id, notes } = body;
|
||||||
if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 });
|
if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 });
|
||||||
updateBookingNotes("group_bookings", id, notes ?? "");
|
updateBookingNotes("group_bookings", id, sanitizeText(notes, 1000) ?? "");
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true });
|
||||||
}
|
}
|
||||||
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
|
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getMcRegistrations, getAllMcRegistrations, addMcRegistration, updateMcRegistration, toggleMcNotification, deleteMcRegistration, setMcRegistrationStatus, updateBookingNotes } from "@/lib/db";
|
import { getMcRegistrations, getAllMcRegistrations, addMcRegistration, updateMcRegistration, toggleMcNotification, deleteMcRegistration, setMcRegistrationStatus, updateBookingNotes } from "@/lib/db";
|
||||||
|
import { sanitizeText } from "@/lib/validation";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const title = request.nextUrl.searchParams.get("title");
|
const title = request.nextUrl.searchParams.get("title");
|
||||||
@@ -44,7 +45,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
if (body.action === "set-notes") {
|
if (body.action === "set-notes") {
|
||||||
const { id, notes } = body;
|
const { id, notes } = body;
|
||||||
if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 });
|
if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 });
|
||||||
updateBookingNotes("mc_registrations", id, notes ?? "");
|
updateBookingNotes("mc_registrations", id, sanitizeText(notes, 1000) ?? "");
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
setOpenDayBookingStatus,
|
setOpenDayBookingStatus,
|
||||||
updateBookingNotes,
|
updateBookingNotes,
|
||||||
} from "@/lib/db";
|
} from "@/lib/db";
|
||||||
|
import { sanitizeText } from "@/lib/validation";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const eventIdStr = request.nextUrl.searchParams.get("eventId");
|
const eventIdStr = request.nextUrl.searchParams.get("eventId");
|
||||||
@@ -30,7 +31,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
if (body.action === "set-notes") {
|
if (body.action === "set-notes") {
|
||||||
const { id, notes } = body;
|
const { id, notes } = body;
|
||||||
if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 });
|
if (!id) return NextResponse.json({ error: "id is required" }, { status: 400 });
|
||||||
updateBookingNotes("open_day_bookings", id, notes ?? "");
|
updateBookingNotes("open_day_bookings", id, sanitizeText(notes, 1000) ?? "");
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true });
|
||||||
}
|
}
|
||||||
if (body.action === "toggle-notify") {
|
if (body.action === "toggle-notify") {
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
* Shared input sanitization for public registration endpoints.
|
* Shared input sanitization for public registration endpoints.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function stripHtml(str: string): string {
|
||||||
|
return str.replace(/<[^>]*>/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
export function sanitizeName(name: unknown): string | null {
|
export function sanitizeName(name: unknown): string | null {
|
||||||
if (!name || typeof name !== "string") return null;
|
if (!name || typeof name !== "string") return null;
|
||||||
const clean = name.trim().slice(0, 100);
|
const clean = stripHtml(name).trim().slice(0, 100);
|
||||||
return clean || null;
|
return clean || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +26,6 @@ export function sanitizeHandle(value: unknown): string | undefined {
|
|||||||
|
|
||||||
export function sanitizeText(value: unknown, maxLength: number = 200): string | undefined {
|
export function sanitizeText(value: unknown, maxLength: number = 200): string | undefined {
|
||||||
if (!value || typeof value !== "string") return undefined;
|
if (!value || typeof value !== "string") return undefined;
|
||||||
const clean = value.trim().slice(0, maxLength);
|
const clean = stripHtml(value).trim().slice(0, maxLength);
|
||||||
return clean || undefined;
|
return clean || undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user