diff --git a/CHANGELOG.md b/CHANGELOG.md index a861ec00c9..3f3c7839dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ You can also check the component for smaller breakpoints - Maintenance - Improved code organization around Browse page +- Security + - Improved security of color palette API routes ### 6.0.0 - 2025-09-05 diff --git a/app/db/palettes.ts b/app/db/palettes.ts index 57e5c22519..780dab5058 100644 --- a/app/db/palettes.ts +++ b/app/db/palettes.ts @@ -10,7 +10,7 @@ import { } from "@/utils/chart-config/api"; export const createPaletteForUser = async ( - data: CreateCustomColorPalette & { user_id?: number } + data: CreateCustomColorPalette & { user_id: number } ): Promise => { const palette = await prisma.palette.create({ data: { @@ -28,9 +28,11 @@ export const createPaletteForUser = async ( }; }; -export const getPalettesForUser = async ( - user_id?: number -): Promise => { +export const getPalettesForUser = async ({ + user_id, +}: { + user_id: number; +}): Promise => { const palettes = await prisma.palette.findMany({ where: { user_id, @@ -47,7 +49,23 @@ export const getPalettesForUser = async ( }); }; -export const deletePaletteForUser = async (paletteId: string) => { +export const deletePaletteForUser = async ({ + paletteId, + user_id, +}: { + paletteId: string; + user_id: number; +}) => { + const palette = await prisma.palette.findUnique({ + where: { + paletteId, + }, + }); + + if (!palette || palette.user_id !== user_id) { + throw new Error("Palette not found"); + } + await prisma.palette.delete({ where: { paletteId, @@ -55,17 +73,31 @@ export const deletePaletteForUser = async (paletteId: string) => { }); }; -export const updatePaletteForUser = async (data: UpdateCustomColorPalette) => { - const type = data.type && convertPaletteTypeToDBType(data.type); +export const updatePaletteForUser = async ({ + type, + paletteId, + name, + colors, + user_id, +}: UpdateCustomColorPalette & { user_id: number }) => { + const palette = await prisma.palette.findUnique({ + where: { + paletteId, + }, + }); + + if (!palette || palette.user_id !== user_id) { + throw new Error("Palette not found"); + } await prisma.palette.update({ where: { - paletteId: data.paletteId, + paletteId, }, data: { - name: data.name, - colors: data.colors, - type, + name, + colors, + type: type && convertPaletteTypeToDBType(type), updated_at: new Date(), }, }); diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index be1f7764e3..853f8e544c 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -75,7 +75,7 @@ model Palette { created_at DateTime @default(now()) @db.Timestamp(6) updated_at DateTime @default(now()) @db.Timestamp(6) - user User? @relation(fields: [user_id], references: [id]) + user User? @relation(fields: [user_id], references: [id], onDelete: Cascade) user_id Int? @@map("palettes") diff --git a/app/server/user-controller.ts b/app/server/user-controller.ts index 62962cddbb..9062afb52d 100644 --- a/app/server/user-controller.ts +++ b/app/server/user-controller.ts @@ -18,6 +18,13 @@ export const UserController = controller({ createPalette: async ({ req, res }) => { const session = await getServerSession(req, res, nextAuthOptions); const userId = session?.user?.id; + + if (!userId) { + throw new Error( + "You must be logged in to create a custom color palette!" + ); + } + const data: CreateCustomColorPalette = req.body; return await createPaletteForUser({ @@ -29,18 +36,41 @@ export const UserController = controller({ const session = await getServerSession(req, res, nextAuthOptions); const userId = session?.user?.id; - return await getPalettesForUser(userId); + if (!userId) { + throw new Error( + "You must be logged in to view your custom color palettes!" + ); + } + + return await getPalettesForUser({ user_id: userId }); }, - deletePalette: async ({ req }) => { + deletePalette: async ({ req, res }) => { + const session = await getServerSession(req, res, nextAuthOptions); + const userId = session?.user?.id; + + if (!userId) { + throw new Error( + "You must be logged in to delete a custom color palette!" + ); + } + const data: DeleteCustomColorPalette = req.body; - const paletteId = data.paletteId; - await deletePaletteForUser(paletteId); + await deletePaletteForUser({ ...data, user_id: userId }); }, - updatePalette: async ({ req }) => { + updatePalette: async ({ req, res }) => { + const session = await getServerSession(req, res, nextAuthOptions); + const userId = session?.user?.id; + + if (!userId) { + throw new Error( + "You must be logged in to update a custom color palette!" + ); + } + const data: UpdateCustomColorPalette = req.body; - await updatePaletteForUser(data); + await updatePaletteForUser({ ...data, user_id: userId }); }, }); diff --git a/app/utils/chart-config/api.ts b/app/utils/chart-config/api.ts index a3694d58ca..d724b9ffee 100644 --- a/app/utils/chart-config/api.ts +++ b/app/utils/chart-config/api.ts @@ -13,6 +13,7 @@ import { createId } from "../create-id"; import type apiConfig from "../../pages/api/config/[key]"; import type apiConfigCreate from "../../pages/api/config-create"; import type apiConfigUpdate from "../../pages/api/config-update"; +import type apiUserColorPalette from "../../pages/api/user/color-palette"; export type CreateCustomColorPalette = Omit; export type UpdateCustomColorPalette = Partial< @@ -116,7 +117,7 @@ export const fetchChartViewCount = async (id: string) => { export const createCustomColorPalette = async ( options: CreateCustomColorPalette ) => { - return (await apiFetch>( + return (await apiFetch>( "/api/user/color-palette", { method: "POST", @@ -128,7 +129,7 @@ export const createCustomColorPalette = async ( export const getCustomColorPalettes = async (): Promise< CustomPaletteType[] > => { - return (await apiFetch>( + return (await apiFetch>( "/api/user/color-palette", { method: "GET", @@ -139,7 +140,7 @@ export const getCustomColorPalettes = async (): Promise< export const deleteCustomColorPalette = async ( options: DeleteCustomColorPalette ) => { - await apiFetch>( + await apiFetch>( "/api/user/color-palette", { method: "DELETE", @@ -151,7 +152,7 @@ export const deleteCustomColorPalette = async ( export const updateCustomColorPalette = async ( options: UpdateCustomColorPalette ) => { - await apiFetch>( + await apiFetch>( "/api/user/color-palette", { method: "PUT",