diff --git a/src/actions/artworks/generateArtworkColors.ts b/src/actions/artworks/generateArtworkColors.ts index eb1108b..45aa184 100644 --- a/src/actions/artworks/generateArtworkColors.ts +++ b/src/actions/artworks/generateArtworkColors.ts @@ -54,12 +54,12 @@ function centroidFromPaletteHexes( }; const fallbackHex = - hexByType["Vibrant"] || - hexByType["Muted"] || - hexByType["DarkVibrant"] || - hexByType["DarkMuted"] || - hexByType["LightVibrant"] || - hexByType["LightMuted"]; + hexByType.Vibrant || + hexByType.Muted || + hexByType.DarkVibrant || + hexByType.DarkMuted || + hexByType.LightVibrant || + hexByType.LightMuted; let L = 0, A = 0, @@ -123,7 +123,9 @@ export async function generateArtworkColorsForArtwork(artworkId: string) { for (const { type, hex } of vibrantHexes) { if (!hex) continue; - const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16)); + const match = hex.match(/\w\w/g); + if (!match) continue; + const [r, g, b] = match.map((h) => parseInt(h, 16)); const name = generateColorName(hex); const color = await prisma.color.upsert({ diff --git a/src/actions/artworks/updateArtwork.ts b/src/actions/artworks/updateArtwork.ts index 877aefa..7827023 100644 --- a/src/actions/artworks/updateArtwork.ts +++ b/src/actions/artworks/updateArtwork.ts @@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma"; import { artworkSchema } from "@/schemas/artworks/imageSchema"; import { normalizeNames, slugify } from "@/utils/artworkHelpers"; -import { z } from "zod/v4"; +import type { z } from "zod/v4"; // Updates an artwork and its tag/category relationships. export async function updateArtwork( diff --git a/src/actions/auth/registerFirstUser.ts b/src/actions/auth/registerFirstUser.ts index 77f6e2d..6075858 100644 --- a/src/actions/auth/registerFirstUser.ts +++ b/src/actions/auth/registerFirstUser.ts @@ -2,9 +2,9 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; -import type { SignUpResponse } from "@/types/auth"; -import { registerFirstUserSchema } from "@/schemas/auth"; import type { RegisterFirstUserInput } from "@/schemas/auth"; +import { registerFirstUserSchema } from "@/schemas/auth"; +import type { SignUpResponse } from "@/types/auth"; // Registers the very first user and upgrades them to admin. export async function registerFirstUser(input: RegisterFirstUserInput) { diff --git a/src/actions/users/getUsers.ts b/src/actions/users/getUsers.ts index c88c29e..ded158a 100644 --- a/src/actions/users/getUsers.ts +++ b/src/actions/users/getUsers.ts @@ -2,9 +2,9 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; -import { headers } from "next/headers"; import type { SessionWithRole } from "@/types/auth"; -import type { UsersListRow } from "@/types/users"; +import type { UserRole, UsersListRow } from "@/types/users"; +import { headers } from "next/headers"; // Returns all users for the admin users table. export async function getUsers(): Promise { @@ -28,9 +28,16 @@ export async function getUsers(): Promise { }, }); - return rows.map((r) => ({ - ...r, - createdAt: r.createdAt.toISOString(), - updatedAt: r.updatedAt.toISOString(), - })); + return rows.map((r) => { + if (r.role !== "admin" && r.role !== "user") { + throw new Error(`Unexpected user role: ${r.role}`); + } + + return { + ...r, + role: r.role as UserRole, + createdAt: r.createdAt.toISOString(), + updatedAt: r.updatedAt.toISOString(), + }; + }); } diff --git a/src/app/api/image/[...key]/route.ts b/src/app/api/image/[...key]/route.ts index 3c29f32..9b7df88 100644 --- a/src/app/api/image/[...key]/route.ts +++ b/src/app/api/image/[...key]/route.ts @@ -2,7 +2,7 @@ import { s3 } from "@/lib/s3"; import type { S3Body } from "@/types/s3"; import { GetObjectCommand } from "@aws-sdk/client-s3"; import type { NextRequest } from "next/server"; -import { Readable } from "stream"; +import { Readable } from "node:stream"; function isWebReadableStream(value: unknown): value is ReadableStream { return !!value && typeof (value as ReadableStream).getReader === "function"; diff --git a/src/app/api/requests/image/route.ts b/src/app/api/requests/image/route.ts index 92db10f..98c2073 100644 --- a/src/app/api/requests/image/route.ts +++ b/src/app/api/requests/image/route.ts @@ -4,7 +4,7 @@ import type { S3Body } from "@/types/s3"; import { GetObjectCommand } from "@aws-sdk/client-s3"; import archiver from "archiver"; import type { NextRequest } from "next/server"; -import { Readable } from "stream"; +import { Readable } from "node:stream"; // Streams commission request files (single or zip) from S3. type Mode = "display" | "download" | "bulk"; @@ -17,13 +17,30 @@ function contentDisposition(filename: string, mode: Mode) { } function sanitizeZipEntryName(name: string) { - return name.replace(/[^\w.\- ()\[\]]+/g, "_").slice(0, 180); + return name.replace(/[^\w.\- ()[\]]+/g, "_").slice(0, 180); } function isWebReadableStream(value: unknown): value is ReadableStream { return !!value && typeof (value as ReadableStream).getReader === "function"; } +function webStreamToAsyncIterable(stream: ReadableStream) { + const reader = stream.getReader(); + return { + async *[Symbol.asyncIterator]() { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (value) yield value; + } + } finally { + reader.releaseLock(); + } + }, + }; +} + function toBodyInit(body: S3Body): BodyInit { if (body instanceof Readable) { return Readable.toWeb(body) as ReadableStream; @@ -141,9 +158,16 @@ export async function GET(req: NextRequest) { if (body instanceof Readable) { archive.append(body, { name: entryName }); } else if (isWebReadableStream(body)) { - archive.append(Readable.from(body as AsyncIterable), { name: entryName }); + archive.append(Readable.from(webStreamToAsyncIterable(body)), { name: entryName }); + } else if (body instanceof Blob) { + const stream = body.stream(); + archive.append(Readable.from(webStreamToAsyncIterable(stream)), { name: entryName }); + } else if (Buffer.isBuffer(body)) { + archive.append(body, { name: entryName }); + } else if (body instanceof Uint8Array) { + archive.append(Buffer.from(body), { name: entryName }); } else { - archive.append(body as Buffer, { name: entryName }); + throw new Error("Unsupported S3 body type for zip entry"); } } diff --git a/src/app/error.tsx b/src/app/error.tsx index 0ca1e96..740fbe6 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -3,7 +3,7 @@ import { useEffect } from "react"; // Global error UI for the app router segment. -export default function Error({ +export default function ErrorPage({ error, reset, }: { @@ -27,6 +27,7 @@ export default function Error({
diff --git a/src/components/artworks/FilterBar.tsx b/src/components/artworks/FilterBar.tsx index d10e51b..f8d8082 100644 --- a/src/components/artworks/FilterBar.tsx +++ b/src/components/artworks/FilterBar.tsx @@ -157,6 +157,7 @@ function FilterButton({ }) { return (
{src ? ( -