Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/hooks/ratelimit.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import (
func RateLimitMiddleware(rl *services.RateLimitService, tier string) func(func(*core.RequestEvent) error) func(*core.RequestEvent) error {
return func(handler func(*core.RequestEvent) error) func(*core.RequestEvent) error {
return func(e *core.RequestEvent) error {
// Bypass rate limiting for SSR internal requests (same-container fetch)
if e.Request.Header.Get("X-Internal") == "true" {
return handler(e)
}

info := rl.AllowWithInfo(e.Request, tier)

// Always set rate limit headers (allows clients to monitor quota)
Expand Down
34 changes: 31 additions & 3 deletions docker/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,40 @@
# Disable browser features not used by Facet
Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"

# HSTS - safe because all traffic goes through Cloudflare (TLS-terminated at edge)
Strict-Transport-Security "max-age=31536000; includeSubDomains"

# Remove server identification
-Server

# Content Security Policy (enforcing)
# Restricts script/style/frame sources to mitigate XSS.
# 'unsafe-inline' kept for scripts/styles due to Svelte's inline injection pattern.
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; frame-src https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com https://www.loom.com https://w.soundcloud.com https://open.spotify.com https://codepen.io https://www.figma.com; connect-src 'self'; media-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'"

# INTENTIONALLY OMITTED:
# - X-XSS-Protection: Deprecated since 2023, can cause vulnerabilities
# - HSTS: TLS terminates at edge proxy (Cloudflare), not here
# - COOP/COEP/CORP: Phase 5B - requires testing with frontend
# - CSP: Phase 5B - requires report-only rollout first
# - CSP nonce-based: Would allow removing 'unsafe-inline'; requires SSR nonce injection
}

# Request body size limit
request_body {
max_size 25MB
}

# Uploaded files (images, etc.) — cacheable, served by PocketBase
handle /api/files/* {
header Cache-Control "public, max-age=86400"
reverse_proxy localhost:8090
}

# API and PocketBase routes go to backend
# API and PocketBase routes go to backend (no caching)
handle /api/* {
header Cache-Control "no-store, no-cache"
# Strip X-Internal header from external requests to prevent spoofing.
# SSR bypasses Caddy (goes direct to :8090), so legitimate internal requests are unaffected.
request_header -X-Internal
reverse_proxy localhost:8090
}

Expand Down Expand Up @@ -64,6 +86,12 @@
reverse_proxy localhost:8090
}

# SvelteKit immutable assets (content-hashed filenames, safe to cache forever)
handle /_app/immutable/* {
header Cache-Control "public, max-age=31536000, immutable"
reverse_proxy localhost:3000
}

# Everything else goes to SvelteKit frontend
handle {
reverse_proxy localhost:3000
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/app.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
<!doctype html>
<html lang="en" class="%sveltekit.theme%">
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- favicon is set dynamically in +layout.svelte -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<script>
// Apply dark mode before paint to prevent FOUC
(function() {
var t = localStorage.getItem('theme');
if (t === 'dark' || (!t && matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100" style="margin: 0; padding: 0; width: 100%; max-width: 100%;">
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/public/ATSContent.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
<p>Location: {profile.location}</p>
{/if}
{#if emailContact}
<p>Email: {emailContact.value}</p>
<p><!--email_off-->Email: {emailContact.value}<!--/email_off--></p>
{/if}
{#if phoneContact}
<p>Phone: {phoneContact.value}</p>
Expand All @@ -99,7 +99,7 @@
<p>Website: {websiteContact.value}</p>
{/if}
{#if !hasAnyContact && profile?.contact_email}
<p>Email: {profile.contact_email}</p>
<p><!--email_off-->Email: {profile.contact_email}<!--/email_off--></p>
{/if}
</section>

Expand Down
Loading
Loading