diff --git a/src/actions/admin-help.ts b/src/actions/admin-help.ts index f703f79a..8459fe71 100644 --- a/src/actions/admin-help.ts +++ b/src/actions/admin-help.ts @@ -5,6 +5,7 @@ import { redirect } from "next/navigation"; import { requirePermission } from "@/lib/admin/guard"; import { PERMS } from "@/lib/permissions"; import { formPositiveBigInt } from "@/lib/form-data"; +import { canonicalize, sanitizeField } from "@/lib/foundation/security"; import { prisma } from "@/lib/prisma"; import { logStaffActivity } from "@/lib/services/staff-activity"; @@ -18,37 +19,15 @@ function parsePosition(value: FormDataEntryValue | null): number { export async function createHelpQuestion(formData: FormData): Promise { const staff = await requirePermission(PERMS.PAGES_EDIT); - const name = String(formData.get("name") ?? "") - .normalize("NFC") - .trim() - .slice(0, 255); - const content = String(formData.get("content") ?? "") - .normalize("NFC") - .trim(); + const name = sanitizeField(formData.get("name")); + const content = canonicalize(String(formData.get("content") ?? "")); if (!name || !content) return; - const imageUrl = String(formData.get("imageUrl") ?? "") - .normalize("NFC") - .trim() - .slice(0, 255); - const buttonText = String(formData.get("buttonText") ?? "") - .normalize("NFC") - .trim() - .slice(0, 255); - const buttonUrl = String(formData.get("buttonUrl") ?? "") - .normalize("NFC") - .trim() - .slice(0, 255); - const buttonColor = - String(formData.get("buttonColor") ?? "") - .normalize("NFC") - .trim() - .slice(0, 16) || "#eeb425"; - const buttonBorderColor = - String(formData.get("buttonBorderColor") ?? "") - .normalize("NFC") - .trim() - .slice(0, 16) || "#facc15"; + const imageUrl = sanitizeField(formData.get("imageUrl")); + const buttonText = sanitizeField(formData.get("buttonText")); + const buttonUrl = sanitizeField(formData.get("buttonUrl")); + const buttonColor = sanitizeField(formData.get("buttonColor"), 16) || "#eeb425"; + const buttonBorderColor = sanitizeField(formData.get("buttonBorderColor"), 16) || "#facc15"; try { const entry = await prisma.websiteHelpCenterCategories.create({ @@ -87,37 +66,15 @@ export async function updateHelpQuestion(formData: FormData): Promise { const id = formPositiveBigInt(formData, "id"); if (!id) return; - const name = String(formData.get("name") ?? "") - .normalize("NFC") - .trim() - .slice(0, 255); - const content = String(formData.get("content") ?? "") - .normalize("NFC") - .trim(); + const name = sanitizeField(formData.get("name")); + const content = canonicalize(String(formData.get("content") ?? "")); if (!name || !content) return; - const imageUrl = String(formData.get("imageUrl") ?? "") - .normalize("NFC") - .trim() - .slice(0, 255); - const buttonText = String(formData.get("buttonText") ?? "") - .normalize("NFC") - .trim() - .slice(0, 255); - const buttonUrl = String(formData.get("buttonUrl") ?? "") - .normalize("NFC") - .trim() - .slice(0, 255); - const buttonColor = - String(formData.get("buttonColor") ?? "") - .normalize("NFC") - .trim() - .slice(0, 16) || "#eeb425"; - const buttonBorderColor = - String(formData.get("buttonBorderColor") ?? "") - .normalize("NFC") - .trim() - .slice(0, 16) || "#facc15"; + const imageUrl = sanitizeField(formData.get("imageUrl")); + const buttonText = sanitizeField(formData.get("buttonText")); + const buttonUrl = sanitizeField(formData.get("buttonUrl")); + const buttonColor = sanitizeField(formData.get("buttonColor"), 16) || "#eeb425"; + const buttonBorderColor = sanitizeField(formData.get("buttonBorderColor"), 16) || "#facc15"; try { await prisma.websiteHelpCenterCategories.update({ diff --git a/src/components/public/radio-player.tsx b/src/components/public/radio-player.tsx index b97ba4c0..297b5b11 100644 --- a/src/components/public/radio-player.tsx +++ b/src/components/public/radio-player.tsx @@ -2,6 +2,7 @@ import { motion } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; +import { useEventSource } from "@/lib/use-event-source"; /** * Fixed bottom-right radio player widget — the Next.js port of AtomCMS's @@ -135,8 +136,6 @@ function parseListeners(data: unknown): number | null { return null; } -import { useEventSource } from "@/lib/use-event-source"; - export default function RadioPlayer() { const audioRef = useRef(null); diff --git a/src/lib/admin/notice.ts b/src/lib/admin/notice.ts index 83504c4c..01cde5b5 100644 --- a/src/lib/admin/notice.ts +++ b/src/lib/admin/notice.ts @@ -1,3 +1,4 @@ +// TODO: Check of dit bestand weg kan — uitsluitend gebruikt door eigen test export interface AdminMutationNotice { tone: "ok" | "danger"; label: "Saved" | "Error"; diff --git a/src/lib/admin/rank-authority.ts b/src/lib/admin/rank-authority.ts index b97250a5..7b53aaa0 100644 --- a/src/lib/admin/rank-authority.ts +++ b/src/lib/admin/rank-authority.ts @@ -1,3 +1,4 @@ +// TODO: Check of dit bestand weg kan — uitsluitend gebruikt door eigen test import type { AuthorizationActor } from "@/lib/admin/authorization-policy"; export interface RankAuthorityDb { diff --git a/src/lib/admin/staff-user.ts b/src/lib/admin/staff-user.ts index 9264b44c..4eb578bf 100644 --- a/src/lib/admin/staff-user.ts +++ b/src/lib/admin/staff-user.ts @@ -1,3 +1,4 @@ +// TODO: Check of dit bestand weg kan — uitsluitend gebruikt door eigen test import { isStaff } from "@/lib/admin/is-staff"; export interface StaffUserRecord { diff --git a/src/lib/api-response.ts b/src/lib/api-response.ts index e13cb4fe..e46f2888 100644 --- a/src/lib/api-response.ts +++ b/src/lib/api-response.ts @@ -30,7 +30,7 @@ export function apiValidationError(zodError: z.ZodError) { ); } -/** Centralized error handler for API routes */ +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export function handleApiError(error: unknown): Response { if (error instanceof ApiError) { return apiError(error.message, error.status); diff --git a/src/lib/cache.ts b/src/lib/cache.ts index b2f807ab..fb73f24d 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -16,6 +16,7 @@ export function cached( }); } +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export function bustCache(key: string): void { store.delete(key); } diff --git a/src/lib/foundation/errors.ts b/src/lib/foundation/errors.ts index 73d286a5..a1656359 100644 --- a/src/lib/foundation/errors.ts +++ b/src/lib/foundation/errors.ts @@ -49,6 +49,7 @@ export class RateLimitError extends DomainError { } } +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export class ConflictError extends DomainError { constructor(message: string) { super(message, 409); diff --git a/src/lib/foundation/request-context.ts b/src/lib/foundation/request-context.ts index 1cc9acf2..3f1cf599 100644 --- a/src/lib/foundation/request-context.ts +++ b/src/lib/foundation/request-context.ts @@ -30,6 +30,7 @@ export function runWithStore(store: RequestStore, fn: () => T): T { return als.run(store, fn); } +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export function getRequestStore(): RequestStore | null { return als.getStore() ?? null; } @@ -38,6 +39,7 @@ export function getRequestId(): RequestId { return als.getStore()?.requestId ?? generateRequestId(); } +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export function getClientIp(): IpAddress { return als.getStore()?.ip ?? ("0.0.0.0" as IpAddress); } @@ -47,6 +49,7 @@ export function setContextUserId(userId: UserId): void { if (store) store.userId = userId; } +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export function elapsed(): number { const store = als.getStore(); return store ? Date.now() - store.startedAt : 0; diff --git a/src/lib/foundation/security.ts b/src/lib/foundation/security.ts index 3d04eadc..5631811d 100644 --- a/src/lib/foundation/security.ts +++ b/src/lib/foundation/security.ts @@ -182,6 +182,15 @@ export function canonicalize(input: string): string { return input.normalize("NFC").trim(); } +/** Normalize, trim and truncate a form field value to maxLen (default 255). */ +export function sanitizeField( + value: FormDataEntryValue | null, + maxLen = 255, +): string { + const s = canonicalize(String(value ?? "")); + return s.slice(0, maxLen); +} + const INVALID_FILENAME_CHARS = /[<>:"/\\|?*]/; function removeControlChars(s: string): string { @@ -193,6 +202,7 @@ function removeControlChars(s: string): string { return result; } +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export function sanitizeFilename(name: string): string { return removeControlChars( name @@ -212,6 +222,7 @@ export function canonicalizeFormValue( return maxLen ? s.slice(0, maxLen) : s; } +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export function canonicalizeFormData( formData: FormData, fields: Record, diff --git a/src/lib/local-imports.ts b/src/lib/local-imports.ts index f913da23..c591d594 100644 --- a/src/lib/local-imports.ts +++ b/src/lib/local-imports.ts @@ -1,3 +1,4 @@ +// TODO: Check of dit bestand weg kan — uitsluitend gebruikt door eigen test import { access, readdir, readFile } from "node:fs/promises"; import { dirname, extname, join, relative, resolve, sep } from "node:path"; diff --git a/src/lib/motion.ts b/src/lib/motion.ts index 85d08fd6..812aa5a2 100644 --- a/src/lib/motion.ts +++ b/src/lib/motion.ts @@ -1,35 +1,42 @@ import type { Variants } from "framer-motion"; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const fadeIn: Variants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 0.4 } }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const fadeInUp: Variants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const fadeInDown: Variants = { hidden: { opacity: 0, y: -12 }, visible: { opacity: 1, y: 0, transition: { duration: 0.3 } }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const fadeInLeft: Variants = { hidden: { opacity: 0, x: -20 }, visible: { opacity: 1, x: 0, transition: { duration: 0.35 } }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const fadeInRight: Variants = { hidden: { opacity: 0, x: 20 }, visible: { opacity: 1, x: 0, transition: { duration: 0.35 } }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const scaleIn: Variants = { hidden: { opacity: 0, scale: 0.95 }, visible: { opacity: 1, scale: 1, transition: { duration: 0.25 } }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const slideUp: Variants = { hidden: { opacity: 0, y: 30 }, visible: { @@ -39,6 +46,7 @@ export const slideUp: Variants = { }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const staggerContainer: Variants = { hidden: {}, visible: { @@ -49,6 +57,7 @@ export const staggerContainer: Variants = { }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const staggerItem: Variants = { hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0, transition: { duration: 0.35 } }, @@ -103,12 +112,14 @@ export const modalContentVariants: Variants = { exit: { opacity: 0, scale: 0.95, y: 10, transition: { duration: 0.15 } }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const hoverScale = { whileHover: { scale: 1.03 }, whileTap: { scale: 0.97 }, transition: { type: "spring" as const, stiffness: 400, damping: 17 }, }; +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export const hoverLift = { whileHover: { y: -3, transition: { duration: 0.2 } }, whileTap: { y: 0 }, diff --git a/src/lib/redis-cache.ts b/src/lib/redis-cache.ts index 84561bc6..fdf3bf7d 100644 --- a/src/lib/redis-cache.ts +++ b/src/lib/redis-cache.ts @@ -33,9 +33,7 @@ export async function redisCache( return fresh; } -/** - * Invalidate a cached key. No-op if Redis is unavailable. - */ +// TODO: Check of dit weg kan — niet geïmporteerd in de codebase export async function invalidateCache(key: string): Promise { if (!redis) return; try {