feat(domains): add allowed-domain repository and whitelist check
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
33
src/lib/server/domains/repository.ts
Normal file
33
src/lib/server/domains/repository.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
16
src/lib/server/domains/whitelist.ts
Normal file
16
src/lib/server/domains/whitelist.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
35
tests/integration/whitelist.test.ts
Normal file
35
tests/integration/whitelist.test.ts
Normal file
@@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user