security updates
This commit is contained in:
@@ -45,4 +45,4 @@ pm2 reload etc-prs
|
|||||||
echo ""
|
echo ""
|
||||||
success "Redeploy complete"
|
success "Redeploy complete"
|
||||||
divider
|
divider
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
39
src/hooks.server.js
Normal file
39
src/hooks.server.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/** @type {import('@sveltejs/kit').Handle} */
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
const response = await resolve(event);
|
||||||
|
|
||||||
|
// ── Security headers ────────────────────────────────────────────────────────
|
||||||
|
// Prevent clickjacking
|
||||||
|
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
|
||||||
|
|
||||||
|
// Prevent MIME-type sniffing
|
||||||
|
response.headers.set('X-Content-Type-Options', 'nosniff');
|
||||||
|
|
||||||
|
// Control referrer information
|
||||||
|
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||||
|
|
||||||
|
// Restrict browser features
|
||||||
|
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||||
|
|
||||||
|
// Content Security Policy
|
||||||
|
// - default-src 'self': only load resources from our own origin
|
||||||
|
// - script-src 'self' 'unsafe-inline': SvelteKit needs inline scripts for hydration
|
||||||
|
// - style-src 'self' 'unsafe-inline' fonts.googleapis.com: inline styles + Google Fonts CSS
|
||||||
|
// - font-src 'self' fonts.gstatic.com: Google Fonts files
|
||||||
|
// - img-src 'self' data:: allow data URIs for any inline images
|
||||||
|
// - connect-src 'self': API calls only to our own origin
|
||||||
|
// - frame-ancestors 'none': belt-and-suspenders against clickjacking
|
||||||
|
response.headers.set('Content-Security-Policy', [
|
||||||
|
"default-src 'self'",
|
||||||
|
"script-src 'self' 'unsafe-inline'",
|
||||||
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||||
|
"font-src 'self' https://fonts.gstatic.com",
|
||||||
|
"img-src 'self' data:",
|
||||||
|
"connect-src 'self'",
|
||||||
|
"frame-ancestors 'none'",
|
||||||
|
"base-uri 'self'",
|
||||||
|
"form-action 'self'",
|
||||||
|
].join('; '));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { getAdminStats, listReports, listRecentPersonalitiesAdmin, listContactMessages } from '$lib/server/db.js';
|
import { getAdminStats, listReports, listRecentPersonalitiesAdmin, listContactMessages } from '$lib/server/db.js';
|
||||||
|
|
||||||
export async function load({ url }) {
|
export async function load({ url }) {
|
||||||
const reportFilter = url.searchParams.get('reports') ?? 'open';
|
const VALID_REPORT_FILTERS = ['open', 'dismissed', 'all'];
|
||||||
const messageFilter = url.searchParams.get('messages') ?? 'unread';
|
const VALID_MESSAGE_FILTERS = ['unread', 'all'];
|
||||||
const adminQ = url.searchParams.get('q') ?? '';
|
|
||||||
|
const reportFilter = VALID_REPORT_FILTERS.includes(url.searchParams.get('reports') ?? '')
|
||||||
|
? url.searchParams.get('reports') : 'open';
|
||||||
|
const messageFilter = VALID_MESSAGE_FILTERS.includes(url.searchParams.get('messages') ?? '')
|
||||||
|
? url.searchParams.get('messages') : 'unread';
|
||||||
|
const adminQ = (url.searchParams.get('q') ?? '').slice(0, 200);
|
||||||
const adminPage = Math.max(1, parseInt(url.searchParams.get('page') ?? '1'));
|
const adminPage = Math.max(1, parseInt(url.searchParams.get('page') ?? '1'));
|
||||||
|
|
||||||
const stats = getAdminStats();
|
const stats = getAdminStats();
|
||||||
|
|||||||
@@ -276,8 +276,7 @@
|
|||||||
{msg.name || 'Anonymous'}
|
{msg.name || 'Anonymous'}
|
||||||
</span>
|
</span>
|
||||||
{#if msg.email}
|
{#if msg.email}
|
||||||
<a href="mailto:{msg.email}"
|
<a href="/cdn-cgi/l/email-protection#205b4d53470e454d41494c5d" style="font-family:'DM Mono',monospace; font-size:11px; color:var(--cyan);
|
||||||
style="font-family:'DM Mono',monospace; font-size:11px; color:var(--cyan);
|
|
||||||
text-decoration:none;">
|
text-decoration:none;">
|
||||||
{msg.email}
|
{msg.email}
|
||||||
</a>
|
</a>
|
||||||
@@ -643,5 +642,3 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import { listPersonalities, getDistinctManufacturers, getManufacturerCounts } fr
|
|||||||
import { MANUFACTURER_SEEDS } from '$lib/server/manufacturers.js';
|
import { MANUFACTURER_SEEDS } from '$lib/server/manufacturers.js';
|
||||||
|
|
||||||
const VALID_LIMITS = [12, 24, 48, 96];
|
const VALID_LIMITS = [12, 24, 48, 96];
|
||||||
|
const VALID_SORTS = ['newest', 'popular'];
|
||||||
|
const MAX_Q_LEN = 200;
|
||||||
|
|
||||||
export async function load({ url }) {
|
export async function load({ url }) {
|
||||||
const q = url.searchParams.get('q') ?? '';
|
const q = (url.searchParams.get('q') ?? '').slice(0, MAX_Q_LEN);
|
||||||
const manufacturer = url.searchParams.get('manufacturer') ?? '';
|
const manufacturer = (url.searchParams.get('manufacturer') ?? '').slice(0, 128);
|
||||||
const sort = url.searchParams.get('sort') ?? 'newest';
|
const sortParam = url.searchParams.get('sort') ?? 'newest';
|
||||||
|
const sort = VALID_SORTS.includes(sortParam) ? sortParam : 'newest';
|
||||||
const page = Math.max(1, parseInt(url.searchParams.get('page') ?? '1'));
|
const page = Math.max(1, parseInt(url.searchParams.get('page') ?? '1'));
|
||||||
const view = url.searchParams.get('view') ?? 'cards';
|
const view = url.searchParams.get('view') === 'table' ? 'table' : 'cards';
|
||||||
const limitParam = parseInt(url.searchParams.get('limit') ?? '24');
|
const limitParam = parseInt(url.searchParams.get('limit') ?? '24');
|
||||||
const limit = VALID_LIMITS.includes(limitParam) ? limitParam : 24;
|
const limit = VALID_LIMITS.includes(limitParam) ? limitParam : 24;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user