fix(web): redirect HTTP to HTTPS to fix Safari CORS hang

When users type 'mana.how' (no scheme), Safari and other browsers default
to HTTP. Cloudflare/cloudflared serves the page over HTTP without
rewriting the scheme. The browser then sends 'Origin: http://mana.how'
on every fetch, but mana-auth CORS only allows 'https://mana.how'.

Result: every auth request fails, the SSO check throws, AuthGate hangs
on the loading spinner forever, and the page never finishes loading.

Fix: detect HTTP requests in hooks.server.ts via cf-visitor /
x-forwarded-proto / event.url.protocol and 301-redirect to HTTPS before
serving any content. Localhost is exempted for dev.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-11 20:38:21 +02:00
parent f3cc853e08
commit 7ba058c017
14 changed files with 174 additions and 2 deletions

View file

@ -76,10 +76,30 @@ const APP_SUBDOMAINS = new Set([
]);
export const handle: Handle = async ({ event, resolve }) => {
// Force HTTPS in production. Cloudflare forwards HTTP requests to the
// origin without rewriting the scheme, so without this redirect a user
// who types `mana.how` (no scheme → HTTP default) loads the page over
// HTTP and the browser then sends `Origin: http://mana.how` on every
// fetch. mana-auth's CORS only allows `https://mana.how`, so all auth
// requests fail and the loader hangs forever.
const host = event.request.headers.get('host') || '';
const cfVisitor = event.request.headers.get('cf-visitor'); // {"scheme":"http"|"https"}
const xfProto = event.request.headers.get('x-forwarded-proto');
const isHttp =
(cfVisitor && cfVisitor.includes('"scheme":"http"')) ||
xfProto === 'http' ||
(event.url.protocol === 'http:' && !cfVisitor && !xfProto);
const isLocal = host.startsWith('localhost') || host.startsWith('127.');
if (isHttp && !isLocal) {
return new Response(null, {
status: 301,
headers: { Location: `https://${host}${event.url.pathname}${event.url.search}` },
});
}
// Redirect app subdomains to their path equivalent
// e.g. todo.mana.how → mana.how/todo
const host = event.request.headers.get('host') || '';
const subdomain = host.split('.')[0];
const subdomain = host.split('.')[0]; // host already declared above
if (APP_SUBDOMAINS.has(subdomain) && event.url.pathname === '/') {
return new Response(null, {
status: 302,

View file

@ -250,6 +250,38 @@
animation: fadeIn 0.25s ease-out;
overflow: hidden;
position: relative;
/* Establish a blend-mode stacking context so the grain overlay
only blends within this card, not through to the workbench
background behind it. */
isolation: isolate;
}
/* Per-theme paper-grain overlay. CSS variables come from
applyThemeToDocument() in @mana/shared-theme — swap one line in
THEME_DEFINITIONS to change the texture for a whole theme. */
.page-shell::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
background-image: var(--paper-texture, none);
background-size: var(--paper-size, 240px 240px);
background-repeat: repeat;
mix-blend-mode: var(--paper-blend-mode, multiply);
opacity: var(--paper-opacity, 0);
transition: opacity 0.25s ease;
}
/* Make sure the header, body and resize handle sit above the grain */
.page-shell > * {
position: relative;
z-index: 1;
}
/* A11y: let users who asked for reduced transparency/contrast opt out */
@media (prefers-contrast: more) {
.page-shell::before {
opacity: 0;
}
}
.page-shell.resizing {
box-shadow:

View file

@ -0,0 +1,20 @@
Paper textures used for the per-theme workbench paper-grain overlay.
Source: ambientCG (https://ambientcg.com)
License: CC0 1.0 Universal (Public Domain)
https://creativecommons.org/publicdomain/zero/1.0/
No attribution required for commercial or private use. The original 1K
PBR material sets were downloaded from ambientCG, only the diffuse /
Color map was extracted, then downscaled to 512×512 via `sips` and
re-encoded as JPEG (quality 70) to keep web payloads tiny.
Original asset → local filename:
Paper001 → paper-001.jpg
Paper002 → paper-002.jpg
Paper003 → paper-003.jpg
Paper004 → paper-004.jpg
Paper005 → paper-005.jpg
Paper006 → paper-006.jpg
Cardboard001 → cardboard-001.jpg
Cardboard002 → cardboard-002.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB