diff --git a/src/lib/server/domains/repository.ts b/src/lib/server/domains/repository.ts new file mode 100644 index 0000000..9dc9a18 --- /dev/null +++ b/src/lib/server/domains/repository.ts @@ -0,0 +1,33 @@ +import type Database from 'better-sqlite3'; +import type { AllowedDomain } from '$lib/types'; + +export function normalizeDomain(raw: string): string { + return raw.trim().toLowerCase().replace(/^www\./, ''); +} + +export function listDomains(db: Database.Database): AllowedDomain[] { + return db + .prepare('SELECT id, domain, display_name FROM allowed_domain ORDER BY domain') + .all() as AllowedDomain[]; +} + +export function addDomain( + db: Database.Database, + domain: string, + displayName: string | null = null, + addedByProfileId: number | null = null +): AllowedDomain { + const normalized = normalizeDomain(domain); + const row = db + .prepare( + `INSERT INTO allowed_domain(domain, display_name, added_by_profile_id) + VALUES (?, ?, ?) + RETURNING id, domain, display_name` + ) + .get(normalized, displayName, addedByProfileId) as AllowedDomain; + return row; +} + +export function removeDomain(db: Database.Database, id: number): void { + db.prepare('DELETE FROM allowed_domain WHERE id = ?').run(id); +} diff --git a/src/lib/server/domains/whitelist.ts b/src/lib/server/domains/whitelist.ts new file mode 100644 index 0000000..7db9746 --- /dev/null +++ b/src/lib/server/domains/whitelist.ts @@ -0,0 +1,16 @@ +import type Database from 'better-sqlite3'; +import { normalizeDomain } from './repository'; + +export function isDomainAllowed(db: Database.Database, urlString: string): boolean { + let host: string; + try { + host = new URL(urlString).hostname; + } catch { + return false; + } + const normalized = normalizeDomain(host); + const row = db + .prepare('SELECT 1 AS ok FROM allowed_domain WHERE domain = ? LIMIT 1') + .get(normalized); + return row !== undefined; +} diff --git a/tests/integration/whitelist.test.ts b/tests/integration/whitelist.test.ts new file mode 100644 index 0000000..e11e19a --- /dev/null +++ b/tests/integration/whitelist.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest'; +import { openInMemoryForTest } from '../../src/lib/server/db'; +import { addDomain, listDomains, removeDomain } from '../../src/lib/server/domains/repository'; +import { isDomainAllowed } from '../../src/lib/server/domains/whitelist'; + +describe('allowed domains', () => { + it('round-trips domains', () => { + const db = openInMemoryForTest(); + addDomain(db, 'chefkoch.de', 'Chefkoch'); + addDomain(db, 'emmikochteinfach.de', 'Emmi kocht einfach'); + const all = listDomains(db); + expect(all.map((d) => d.domain).sort()).toEqual(['chefkoch.de', 'emmikochteinfach.de']); + }); + + it('normalizes www. and case', () => { + const db = openInMemoryForTest(); + addDomain(db, 'WWW.Chefkoch.DE'); + expect(isDomainAllowed(db, 'https://chefkoch.de/abc')).toBe(true); + expect(isDomainAllowed(db, 'https://www.chefkoch.de/abc')).toBe(true); + expect(isDomainAllowed(db, 'https://fake.de/abc')).toBe(false); + }); + + it('rejects invalid urls', () => { + const db = openInMemoryForTest(); + addDomain(db, 'chefkoch.de'); + expect(isDomainAllowed(db, 'not a url')).toBe(false); + }); + + it('removes domains', () => { + const db = openInMemoryForTest(); + const d = addDomain(db, 'chefkoch.de'); + removeDomain(db, d.id); + expect(listDomains(db)).toEqual([]); + }); +});