12 Commits

Author SHA1 Message Date
schreifuchs b064ccf5d6 Merge pull request 'Add Vitest + Svelte Testing Library' (#23) from issue-13 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #23
2026-04-03 14:09:29 +02:00
schreifuchs d6f6125204 ci: run tests in pipeline and fix docker rate limits
Commit / ci (push) Successful in 10m32s
PullRequest / publish (pull_request) Failing after 5m21s
2026-04-03 13:53:21 +02:00
schreifuchs beb790bed8 chore: resolve merge conflicts 2026-04-03 13:52:58 +02:00
schreifuchs c1a0a5de6c Merge pull request 'Abstract Heavy Database Queries' (#24) from issue-10 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #24
2026-04-03 13:48:53 +02:00
schreifuchs 2140d06fb5 Merge pull request 'Parallelize Database Queries' (#19) from issue-5 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #19
2026-04-03 13:42:19 +02:00
schreifuchs 6e24b68a08 Merge pull request 'Move Server-Only Code to $lib/server' (#16) from issue-2 into main
Commit / ci (push) Has been cancelled
Reviewed-on: #16
2026-04-03 13:40:06 +02:00
schreifuchs b3087aa9d4 test: add vitest and svelte testing library (resolves #13)
Commit / ci (push) Has been cancelled
PullRequest / publish (pull_request) Failing after 1m56s
2026-04-03 13:26:59 +02:00
schreifuchs c459d58a28 refactor: abstract heavy database queries (resolves #10)
Commit / ci (push) Successful in 10m34s
PullRequest / publish (pull_request) Failing after 2m17s
2026-04-03 13:26:55 +02:00
schreifuchs 239bf163e8 fix: XSS Vulnerability (#17)
Commit / ci (push) Has been cancelled
Resolves #1

Reviewed-on: #17
2026-04-03 13:09:45 +02:00
schreifuchs 16248416e7 perf: parallelize database queries (resolves #5)
Commit / ci (push) Successful in 10m40s
PullRequest / publish (pull_request) Failing after 2m23s
2026-04-03 13:06:33 +02:00
schreifuchs 7d9ff9ff2b refactor: move server-only code (resolves #2)
PullRequest / publish (pull_request) Failing after 2m22s
Commit / ci (push) Successful in 10m38s
2026-04-03 13:01:30 +02:00
schreifuchs 2e16cf9d51 docs: add TODO.md with project review and refactoring tasks
Commit / ci (push) Successful in 10m32s
2026-04-03 12:31:11 +02:00
15 changed files with 963 additions and 30 deletions
+3
View File
@@ -34,3 +34,6 @@ jobs:
- name: Type Check (Svelte Check) - name: Type Check (Svelte Check)
# Based on your package.json "check" script # Based on your package.json "check" script
run: pnpm check run: pnpm check
- name: Run Tests (Vitest)
run: pnpm run test
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:25-trixie AS base FROM public.ecr.aws/docker/library/node:25-trixie AS base
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
# RUN corepack enable # RUN corepack enable
+65
View File
@@ -0,0 +1,65 @@
# Project Review & Refactoring TODOs
This document contains the prioritized list of refactoring tasks, architectural improvements, and testing strategies for the Aktiteil project.
## 🚨 Must do (Security & Critical Best Practices)
- [ ] **Fix Critical XSS Vulnerability (`{@html}` without sanitization)**
- **Where:** `src/routes/akti/[aktiId]/+page.svelte`
- **Why:** Rendering user input via `{@html data.akti.body}` without sanitization allows malicious scripts to be injected.
- **Fix:** Use the already installed `sanitize-html` library on the server to sanitize `changeRequest.body` before updating/inserting into the database.
- [ ] **Move Server-Only Code to `$lib/server`**
- **Where:** `src/lib/auth.ts`
- **Why:** It imports from `./server/db`. Keeping server-side dependencies in the general `$lib` folder risks accidental imports by client components, breaking the Vite build and potentially leaking server logic.
- **Fix:** Move and rename it to `src/lib/server/session.ts` (or `authUtils.ts`) and update imports in `.server.ts` files.
- [ ] **Fix Action Validation Error Handling**
- **Where:** `src/routes/akti/[aktiId]/+page.server.ts` and `src/routes/akti/[aktiId]/comment/+page.server.ts`
- **Why:** Currently returning `error(400)` on validation failure, which wipes form data and shows a generic error page.
- **Fix:** Use SvelteKit's `fail(400, { message: 'Invalid data' })` to keep the user on the page and preserve their input.
- [ ] **Fix Hacky Fallback in Auth Query**
- **Where:** `src/lib/auth.ts` -> `getSession()`
- **Why:** Querying the DB with a fallback UUID (`eaf930...`) when email is missing is an anti-pattern.
- **Fix:** Implement an early return (`if (!session?.user?.email) return null;`) before hitting the database.
## 🛠️ Should do (Performance & Architecture)
- [ ] **Parallelize Database Queries**
- **Where:** `src/routes/akti/[aktiId]/+page.server.ts` (load function)
- **Why:** Queries are running sequentially.
- **Fix:** Use `Promise.all([ db.query.aktis.findFirst(...), db.query.ratings.findMany(...) ])` to run concurrently.
- [ ] **Implement Pagination / Limit for the Dashboard**
- **Where:** `src/routes/+page.server.ts`
- **Why:** Querying all records joined with ratings will scale poorly.
- **Fix:** Add a `.limit()` clause and consider basic pagination or infinite scrolling.
- [ ] **Extend Auth.js Types Globally**
- **Where:** `src/app.d.ts`
- **Why:** TypeScript doesn't inherently know `session.user.id` exists, leading to hacky workarounds.
- **Fix:** Override `@auth/sveltekit` Session types in `app.d.ts` to include `id` and `email` strictly.
- [ ] **Consider Adopting a Form Library**
- **Where:** `src/lib/extractFormData.ts`
- **Why:** Custom form extractors lack instant client-side validation and seamless server-side error mapping.
- **Fix:** Consider switching to `sveltekit-superforms` which integrates well with Valibot.
## ✨ Nice to have (UX & Polish)
- [ ] **Clarify File Naming (`auth.ts` vs `auth.ts`)**
- Rename `src/lib/auth.ts` to `session.ts` or similar to distinguish from `src/auth.ts` (Auth.js setup).
- [ ] **Abstract Heavy Database Queries**
- Move complex aggregations (like computing averages in `src/routes/+page.server.ts`) into a dedicated `src/lib/server/db/queries.ts` file to keep routes clean.
- [ ] **Clean up Redundant Imports**
- In `src/routes/+layout.server.ts`, change `import { getSession as getSession }` to `import { getSession }`.
## 🧪 Testing Plan
- [ ] **Add Playwright (End-to-End Testing)**
- Install Playwright to test SvelteKit server actions, DB integration, and Flowbite forms holistically.
- [ ] **Add Vitest + Svelte Testing Library (Unit/Component Testing)**
- Set up Vitest to test UI components (`AktiCard`, `AktiEditor`) and utility functions (`extractFormData`) in isolation.
+8 -1
View File
@@ -10,6 +10,8 @@
"prepare": "husky", "prepare": "husky",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest run",
"test:watch": "vitest",
"format": "prettier --write .", "format": "prettier --write .",
"lint": "prettier --check . && eslint .", "lint": "prettier --check . && eslint .",
"db:start": "docker compose up", "db:start": "docker compose up",
@@ -28,8 +30,11 @@
"@sveltejs/kit": "^2.49.0", "@sveltejs/kit": "^2.49.0",
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1",
"@tiptap/core": "3.7.2", "@tiptap/core": "3.7.2",
"@types/node": "^20.19.25", "@types/node": "^20.19.25",
"@types/sanitize-html": "^2.16.1",
"drizzle-kit": "^0.31.7", "drizzle-kit": "^0.31.7",
"drizzle-orm": "^0.44.7", "drizzle-orm": "^0.44.7",
"eslint": "^9.39.1", "eslint": "^9.39.1",
@@ -39,6 +44,7 @@
"flowbite-svelte-icons": "^3.0.0", "flowbite-svelte-icons": "^3.0.0",
"globals": "^16.5.0", "globals": "^16.5.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^29.0.1",
"lowlight": "^3.3.0", "lowlight": "^3.3.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.0",
@@ -47,7 +53,8 @@
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.47.0", "typescript-eslint": "^8.47.0",
"vite": "^7.2.4" "vite": "^7.2.4",
"vitest": "^4.1.2"
}, },
"dependencies": { "dependencies": {
"@auth/drizzle-adapter": "^1.11.1", "@auth/drizzle-adapter": "^1.11.1",
+787
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest';
import { extractFormData } from './extractFormData';
import * as v from 'valibot';
describe('extractFormData', () => {
it('should successfully extract and validate correct form data', async () => {
const formData = new FormData();
formData.append('name', 'John Doe');
formData.append('age', '30');
const request = new Request('http://localhost', {
method: 'POST',
body: formData
});
const schema = v.object({
name: v.string(),
age: v.string()
});
const result = await extractFormData(request, schema);
expect(result.error).toBeNull();
expect(result.data).toEqual({ name: 'John Doe', age: '30' });
});
it('should fail validation with missing required fields', async () => {
const formData = new FormData();
formData.append('age', '30');
const request = new Request('http://localhost', {
method: 'POST',
body: formData
});
const schema = v.object({
name: v.string(),
age: v.string()
});
const result = await extractFormData(request, schema);
expect(result.data).toBeUndefined();
expect(result.error).toBeTypeOf('string');
});
});
+16
View File
@@ -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);
}
@@ -1,7 +1,7 @@
import type { Session, User } from '@auth/sveltekit'; import type { Session, User } from '@auth/sveltekit';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { db } from './server/db'; import { db } from './db';
import { users } from './server/db/schema'; import { users } from './db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
interface Event { interface Event {
locals: { locals: {
+1 -1
View File
@@ -1,4 +1,4 @@
import { getSession as getSession } from '$lib/auth'; import { getSession as getSession } from '$lib/server/session';
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async (event) => { export const load: LayoutServerLoad = async (event) => {
+2 -13
View File
@@ -1,19 +1,8 @@
import { db } from '$lib/server/db'; import { getAktisWithAvgRating } from '$lib/server/db/queries';
import { aktis, ratings } from '$lib/server/db/schema';
import { avg, eq } from 'drizzle-orm';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
const a = await db const a = await getAktisWithAvgRating();
.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);
return { return {
aktis: a.map((a) => ({ ...a, rating: a.rating ? parseFloat(a.rating) : undefined })) aktis: a.map((a) => ({ ...a, rating: a.rating ? parseFloat(a.rating) : undefined }))
+4 -1
View File
@@ -4,9 +4,10 @@ import { extractFormData } from '$lib/extractFormData';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import * as v from 'valibot'; import * as v from 'valibot';
import { ensureAuth } from '$lib/auth'; import { ensureAuth } from '$lib/server/session';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { aktis } from '$lib/server/db/schema'; import { aktis } from '$lib/server/db/schema';
import sanitizeHtml from 'sanitize-html';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
await ensureAuth(event); await ensureAuth(event);
return {}; return {};
@@ -28,6 +29,8 @@ export const actions = {
if (!akti) return {}; if (!akti) return {};
akti.body = sanitizeHtml(akti.body);
const res = await db const res = await db
.insert(aktis) .insert(aktis)
.values({ ...akti, author: user.id! }) .values({ ...akti, author: user.id! })
+14 -10
View File
@@ -3,21 +3,23 @@ import { aktis, ratings } from '$lib/server/db/schema';
import { error, redirect, type Actions } from '@sveltejs/kit'; import { error, redirect, type Actions } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { ensureAuth } from '$lib/auth'; import { ensureAuth } from '$lib/server/session';
import { extractFormData } from '$lib/extractFormData'; import { extractFormData } from '$lib/extractFormData';
import * as v from 'valibot'; import * as v from 'valibot';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import sanitizeHtml from 'sanitize-html';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const akti = await db.query.aktis.findFirst({ const [akti, r] = await Promise.all([
where: eq(aktis.id, event.params.aktiId), db.query.aktis.findFirst({
with: { author: true } where: eq(aktis.id, event.params.aktiId),
}); with: { author: true }
}),
const r = await db.query.ratings.findMany({ db.query.ratings.findMany({
with: { user: true }, with: { user: true },
where: eq(ratings.aktiId, event.params.aktiId) where: eq(ratings.aktiId, event.params.aktiId)
}); })
]);
if (!akti) { if (!akti) {
error(404, { message: 'Die Akti gits garnid, sorry...' }); error(404, { message: 'Die Akti gits garnid, sorry...' });
@@ -56,6 +58,8 @@ export const actions = {
if (!changeRequest) return error(400); if (!changeRequest) return error(400);
changeRequest.body = sanitizeHtml(changeRequest.body);
await db await db
.update(aktis) .update(aktis)
.set({ ...changeRequest, version: akti[0].version + 1 }) .set({ ...changeRequest, version: akti[0].version + 1 })
@@ -1,5 +1,5 @@
import type { PageServerLoad } from './$types'; 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 { error, redirect, type Actions } from '@sveltejs/kit';
import { extractFormData } from '$lib/extractFormData'; import { extractFormData } from '$lib/extractFormData';
import { aktis, ratings } from '$lib/server/db/schema'; import { aktis, ratings } from '$lib/server/db/schema';
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config';
import { sveltekit } from '@sveltejs/kit/vite';
import { svelteTesting } from '@testing-library/svelte/vite';
export default defineConfig({
plugins: [sveltekit(), svelteTesting()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
environment: 'jsdom',
setupFiles: ['./vitest-setup.ts']
}
});