fix: security hardening, UI fixes, and validation improvements
- Fix header nav overflow by switching to lg: breakpoint with tighter gaps - Fix file upload path traversal by whitelisting allowed folders and extensions - Fix BookingModal using hardcoded content instead of DB-backed data - Add input length validation on public master-class registration API - Add ID validation on team member and reorder API routes - Fix BookingModal useCallback missing groupInfo/contact dependencies - Improve admin news date field to use native date picker - Add missing Мастер-классы and Новости cards to admin dashboard Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,18 @@ import { revalidatePath } from "next/cache";
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
function parseId(raw: string): number | null {
|
||||
const n = Number(raw);
|
||||
return Number.isInteger(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const member = getTeamMember(Number(id));
|
||||
const numId = parseId(id);
|
||||
if (!numId) {
|
||||
return NextResponse.json({ error: "Invalid ID" }, { status: 400 });
|
||||
}
|
||||
const member = getTeamMember(numId);
|
||||
if (!member) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
@@ -15,15 +24,23 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const numId = parseId(id);
|
||||
if (!numId) {
|
||||
return NextResponse.json({ error: "Invalid ID" }, { status: 400 });
|
||||
}
|
||||
const data = await request.json();
|
||||
updateTeamMember(Number(id), data);
|
||||
updateTeamMember(numId, data);
|
||||
revalidatePath("/");
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
const { id } = await params;
|
||||
deleteTeamMember(Number(id));
|
||||
const numId = parseId(id);
|
||||
if (!numId) {
|
||||
return NextResponse.json({ error: "Invalid ID" }, { status: 400 });
|
||||
}
|
||||
deleteTeamMember(numId);
|
||||
revalidatePath("/");
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { revalidatePath } from "next/cache";
|
||||
export async function PUT(request: NextRequest) {
|
||||
const { ids } = await request.json() as { ids: number[] };
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return NextResponse.json({ error: "ids array required" }, { status: 400 });
|
||||
if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => Number.isInteger(id) && id > 0)) {
|
||||
return NextResponse.json({ error: "ids must be a non-empty array of positive integers" }, { status: 400 });
|
||||
}
|
||||
|
||||
reorderTeamMembers(ids);
|
||||
|
||||
@@ -3,12 +3,15 @@ import { writeFile, mkdir } from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/avif"];
|
||||
const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif"];
|
||||
const ALLOWED_FOLDERS = ["team", "master-classes", "news", "classes"];
|
||||
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
const folder = (formData.get("folder") as string) || "team";
|
||||
const rawFolder = (formData.get("folder") as string) || "team";
|
||||
const folder = ALLOWED_FOLDERS.includes(rawFolder) ? rawFolder : "team";
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file provided" }, { status: 400 });
|
||||
@@ -28,8 +31,14 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Sanitize filename
|
||||
const ext = path.extname(file.name) || ".webp";
|
||||
// Validate and sanitize filename
|
||||
const ext = path.extname(file.name).toLowerCase() || ".webp";
|
||||
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid file extension" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const baseName = file.name
|
||||
.replace(ext, "")
|
||||
.toLowerCase()
|
||||
|
||||
@@ -6,21 +6,24 @@ export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const { masterClassTitle, name, instagram, telegram } = body;
|
||||
|
||||
if (!masterClassTitle || typeof masterClassTitle !== "string") {
|
||||
if (!masterClassTitle || typeof masterClassTitle !== "string" || masterClassTitle.length > 200) {
|
||||
return NextResponse.json({ error: "masterClassTitle is required" }, { status: 400 });
|
||||
}
|
||||
if (!name || typeof name !== "string" || !name.trim()) {
|
||||
return NextResponse.json({ error: "name is required" }, { status: 400 });
|
||||
if (!name || typeof name !== "string" || !name.trim() || name.length > 100) {
|
||||
return NextResponse.json({ error: "name is required (max 100 chars)" }, { status: 400 });
|
||||
}
|
||||
if (!instagram || typeof instagram !== "string" || !instagram.trim()) {
|
||||
if (!instagram || typeof instagram !== "string" || !instagram.trim() || instagram.length > 100) {
|
||||
return NextResponse.json({ error: "Instagram аккаунт обязателен" }, { status: 400 });
|
||||
}
|
||||
if (telegram && (typeof telegram !== "string" || telegram.length > 100)) {
|
||||
return NextResponse.json({ error: "Telegram too long" }, { status: 400 });
|
||||
}
|
||||
|
||||
const id = addMcRegistration(
|
||||
masterClassTitle.trim(),
|
||||
name.trim(),
|
||||
instagram.trim(),
|
||||
telegram && typeof telegram === "string" ? telegram.trim() : undefined
|
||||
masterClassTitle.trim().slice(0, 200),
|
||||
name.trim().slice(0, 100),
|
||||
instagram.trim().slice(0, 100),
|
||||
telegram && typeof telegram === "string" ? telegram.trim().slice(0, 100) : undefined
|
||||
);
|
||||
|
||||
return NextResponse.json({ ok: true, id });
|
||||
|
||||
Reference in New Issue
Block a user