From 7d9ff9ff2bad432f8879b803a56cb97c82dda263 Mon Sep 17 00:00:00 2001 From: schreifuchs Date: Fri, 3 Apr 2026 13:01:30 +0200 Subject: [PATCH 1/4] refactor: move server-only code (resolves #2) --- src/lib/{auth.ts => server/session.ts} | 4 ++-- src/routes/+layout.server.ts | 2 +- src/routes/akti/+page.server.ts | 2 +- src/routes/akti/[aktiId]/+page.server.ts | 2 +- src/routes/akti/[aktiId]/comment/+page.server.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename src/lib/{auth.ts => server/session.ts} (94%) diff --git a/src/lib/auth.ts b/src/lib/server/session.ts similarity index 94% rename from src/lib/auth.ts rename to src/lib/server/session.ts index ea7bbfe..101aff6 100644 --- a/src/lib/auth.ts +++ b/src/lib/server/session.ts @@ -1,7 +1,7 @@ import type { Session, User } from '@auth/sveltekit'; import { error } from '@sveltejs/kit'; -import { db } from './server/db'; -import { users } from './server/db/schema'; +import { db } from './db'; +import { users } from './db/schema'; import { eq } from 'drizzle-orm'; interface Event { locals: { diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index f7dc423..ff80a36 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,4 +1,4 @@ -import { getSession as getSession } from '$lib/auth'; +import { getSession as getSession } from '$lib/server/session'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async (event) => { diff --git a/src/routes/akti/+page.server.ts b/src/routes/akti/+page.server.ts index 747749d..3157446 100644 --- a/src/routes/akti/+page.server.ts +++ b/src/routes/akti/+page.server.ts @@ -4,7 +4,7 @@ import { extractFormData } from '$lib/extractFormData'; import { resolve } from '$app/paths'; import * as v from 'valibot'; -import { ensureAuth } from '$lib/auth'; +import { ensureAuth } from '$lib/server/session'; import { db } from '$lib/server/db'; import { aktis } from '$lib/server/db/schema'; export const load: PageServerLoad = async (event) => { diff --git a/src/routes/akti/[aktiId]/+page.server.ts b/src/routes/akti/[aktiId]/+page.server.ts index 0dba094..3feae32 100644 --- a/src/routes/akti/[aktiId]/+page.server.ts +++ b/src/routes/akti/[aktiId]/+page.server.ts @@ -3,7 +3,7 @@ import { aktis, ratings } from '$lib/server/db/schema'; import { error, redirect, type Actions } from '@sveltejs/kit'; import { and, eq } from 'drizzle-orm'; import type { PageServerLoad } from './$types'; -import { ensureAuth } from '$lib/auth'; +import { ensureAuth } from '$lib/server/session'; import { extractFormData } from '$lib/extractFormData'; import * as v from 'valibot'; import { resolve } from '$app/paths'; diff --git a/src/routes/akti/[aktiId]/comment/+page.server.ts b/src/routes/akti/[aktiId]/comment/+page.server.ts index ddb75d2..a1a4d8d 100644 --- a/src/routes/akti/[aktiId]/comment/+page.server.ts +++ b/src/routes/akti/[aktiId]/comment/+page.server.ts @@ -1,5 +1,5 @@ import type { PageServerLoad } from './$types'; -import { ensureAuth } from '$lib/auth'; +import { ensureAuth } from '$lib/server/session'; import { error, redirect, type Actions } from '@sveltejs/kit'; import { extractFormData } from '$lib/extractFormData'; import { aktis, ratings } from '$lib/server/db/schema'; From 16248416e7daaaedf30bb7b22e79d9578862c935 Mon Sep 17 00:00:00 2001 From: schreifuchs Date: Fri, 3 Apr 2026 13:06:33 +0200 Subject: [PATCH 2/4] perf: parallelize database queries (resolves #5) --- src/routes/akti/[aktiId]/+page.server.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/routes/akti/[aktiId]/+page.server.ts b/src/routes/akti/[aktiId]/+page.server.ts index 0dba094..b23ce60 100644 --- a/src/routes/akti/[aktiId]/+page.server.ts +++ b/src/routes/akti/[aktiId]/+page.server.ts @@ -9,15 +9,16 @@ import * as v from 'valibot'; import { resolve } from '$app/paths'; export const load: PageServerLoad = async (event) => { - const akti = await db.query.aktis.findFirst({ - where: eq(aktis.id, event.params.aktiId), - with: { author: true } - }); - - const r = await db.query.ratings.findMany({ - with: { user: true }, - where: eq(ratings.aktiId, event.params.aktiId) - }); + const [akti, r] = await Promise.all([ + db.query.aktis.findFirst({ + where: eq(aktis.id, event.params.aktiId), + with: { author: true } + }), + db.query.ratings.findMany({ + with: { user: true }, + where: eq(ratings.aktiId, event.params.aktiId) + }) + ]); if (!akti) { error(404, { message: 'Die Akti gits garnid, sorry...' }); From 239bf163e8956f5c7e818e9e4e833c3475f7710e Mon Sep 17 00:00:00 2001 From: schreifuchs Date: Fri, 3 Apr 2026 13:09:45 +0200 Subject: [PATCH 3/4] fix: XSS Vulnerability (#17) Resolves #1 Reviewed-on: https://git.schreifuchs.ch/schreifuchs/aktiteil/pulls/17 --- package.json | 1 + pnpm-lock.yaml | 45 ++++++++++++++++++++++++ src/routes/akti/+page.server.ts | 3 ++ src/routes/akti/[aktiId]/+page.server.ts | 3 ++ 4 files changed, 52 insertions(+) diff --git a/package.json b/package.json index 7a9e44b..0e6b320 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@tailwindcss/vite": "^4.1.17", "@tiptap/core": "3.7.2", "@types/node": "^20.19.25", + "@types/sanitize-html": "^2.16.1", "drizzle-kit": "^0.31.7", "drizzle-orm": "^0.44.7", "eslint": "^9.39.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc60548..a702006 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@types/node': specifier: ^20.19.25 version: 20.19.25 + '@types/sanitize-html': + specifier: ^2.16.1 + version: 2.16.1 drizzle-kit: specifier: ^0.31.7 version: 0.31.7 @@ -638,56 +641,67 @@ packages: resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.53.3': resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.53.3': resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.53.3': resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.53.3': resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.53.3': resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.53.3': resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.53.3': resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.53.3': resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.3': resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.53.3': resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.53.3': resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} @@ -823,24 +837,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.17': resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.17': resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.17': resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.17': resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} @@ -1232,6 +1250,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/sanitize-html@2.16.1': + resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1551,6 +1572,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -1734,6 +1759,9 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} @@ -1870,24 +1898,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -3372,6 +3404,10 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/sanitize-html@2.16.1': + dependencies: + htmlparser2: 10.1.0 + '@types/unist@3.0.3': {} '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': @@ -3616,6 +3652,8 @@ snapshots: entities@4.5.0: {} + entities@7.0.1: {} + esbuild-register@3.6.0(esbuild@0.25.12): dependencies: debug: 4.4.3 @@ -3897,6 +3935,13 @@ snapshots: highlight.js@11.11.1: {} + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 diff --git a/src/routes/akti/+page.server.ts b/src/routes/akti/+page.server.ts index 747749d..07bdf85 100644 --- a/src/routes/akti/+page.server.ts +++ b/src/routes/akti/+page.server.ts @@ -7,6 +7,7 @@ import * as v from 'valibot'; import { ensureAuth } from '$lib/auth'; import { db } from '$lib/server/db'; import { aktis } from '$lib/server/db/schema'; +import sanitizeHtml from 'sanitize-html'; export const load: PageServerLoad = async (event) => { await ensureAuth(event); return {}; @@ -28,6 +29,8 @@ export const actions = { if (!akti) return {}; + akti.body = sanitizeHtml(akti.body); + const res = await db .insert(aktis) .values({ ...akti, author: user.id! }) diff --git a/src/routes/akti/[aktiId]/+page.server.ts b/src/routes/akti/[aktiId]/+page.server.ts index 0dba094..3ec492f 100644 --- a/src/routes/akti/[aktiId]/+page.server.ts +++ b/src/routes/akti/[aktiId]/+page.server.ts @@ -7,6 +7,7 @@ import { ensureAuth } from '$lib/auth'; import { extractFormData } from '$lib/extractFormData'; import * as v from 'valibot'; import { resolve } from '$app/paths'; +import sanitizeHtml from 'sanitize-html'; export const load: PageServerLoad = async (event) => { const akti = await db.query.aktis.findFirst({ @@ -56,6 +57,8 @@ export const actions = { if (!changeRequest) return error(400); + changeRequest.body = sanitizeHtml(changeRequest.body); + await db .update(aktis) .set({ ...changeRequest, version: akti[0].version + 1 }) From c459d58a28ac5ac974cc3d5ce001f0f9e74d99c2 Mon Sep 17 00:00:00 2001 From: schreifuchs Date: Fri, 3 Apr 2026 13:26:55 +0200 Subject: [PATCH 4/4] refactor: abstract heavy database queries (resolves #10) --- src/lib/server/db/queries.ts | 16 ++++++++++++++++ src/routes/+page.server.ts | 15 ++------------- 2 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 src/lib/server/db/queries.ts diff --git a/src/lib/server/db/queries.ts b/src/lib/server/db/queries.ts new file mode 100644 index 0000000..448d495 --- /dev/null +++ b/src/lib/server/db/queries.ts @@ -0,0 +1,16 @@ +import { db } from '$lib/server/db'; +import { aktis, ratings } from '$lib/server/db/schema'; +import { avg, eq } from 'drizzle-orm'; + +export async function getAktisWithAvgRating() { + return await db + .select({ + id: aktis.id, + title: aktis.title, + summary: aktis.summary, + rating: avg(ratings.rating) + }) + .from(aktis) + .leftJoin(ratings, eq(aktis.id, ratings.aktiId)) + .groupBy(aktis.id, aktis.title, aktis.summary); +} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 4510bd3..194192e 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,19 +1,8 @@ -import { db } from '$lib/server/db'; -import { aktis, ratings } from '$lib/server/db/schema'; -import { avg, eq } from 'drizzle-orm'; +import { getAktisWithAvgRating } from '$lib/server/db/queries'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async () => { - const a = await db - .select({ - id: aktis.id, - title: aktis.title, - summary: aktis.summary, - rating: avg(ratings.rating) - }) - .from(aktis) - .leftJoin(ratings, eq(aktis.id, ratings.aktiId)) - .groupBy(aktis.id, aktis.title, aktis.summary); + const a = await getAktisWithAvgRating(); return { aktis: a.map((a) => ({ ...a, rating: a.rating ? parseFloat(a.rating) : undefined }))