Compare commits

..

10 Commits

Author SHA1 Message Date
Falkan
2a4a1fb61d fix: merge hooks.ts into hooks.server.ts — root cause of K.resolve hydration crash
hooks.ts contained Node-only imports (fs, path) and was being included in
the client bundle by SvelteKit because it lacked the .server. designation.
This caused 'K.resolve is not a function' in the browser, which aborted
hydration and killed ALL client-side interactivity site-wide.

Fix: merge reroute export from hooks.ts into hooks.server.ts alongside the
existing CSRF handle. Delete hooks.ts. SvelteKit supports both handle and
reroute exports from hooks.server.ts.

This was the true root cause of today's outage. The CSS and  issues
were red herrings — the site was broken from commit 231eacc onward.
2026-03-20 15:10:14 -04:00
Falkan
4d2d7c77d3 feat: initialize selected unit to base unit of first group on load
On initial page load (before any user interaction), the converter now
selects the base unit of the first visible group instead of the first
unit in the alphabetical list. Falls back to units[0] if the group's
baseUnitId is not found in the visible unit list. URL param ?from=
override and shift+click behavior are unaffected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 11:41:29 -04:00
Falkan
231eacc27c fix: adminPath TTL cache in hooks.ts instead of read-once at startup
Previous approach read config.json once at module load time. If the PVC
wasn't fully mounted yet, it fell back to 'admin' and stayed there for
the lifetime of the process. New approach caches for 5 seconds and re-reads
on expiry, so it recovers from startup races and picks up changes without
requiring a restart.
2026-03-20 01:57:06 -04:00
Falkan
7239ca6d5a debug: log reroute calls in hooks.ts 2026-03-20 01:56:15 -04:00
Falkan
f10e4a6cbe debug: log adminPath at startup in hooks.ts 2026-03-20 01:53:25 -04:00
Falkan
ea08258ad4 debug: log hash/secret/password lengths in login route 2026-03-20 01:18:51 -04:00
Falkan
ae7ad0073b fix: trim whitespace from PASSWORD_HASH and SESSION_SECRET env vars 2026-03-20 01:17:04 -04:00
Falkan
258b0f5065 fix: default adminPath is boss, not admin 2026-03-20 00:59:32 -04:00
Falkan
d4b8a9a646 fix: brand name Dick Loads -> Dickloads 2026-03-20 00:55:57 -04:00
Falkan
5ec1222720 fix: remove debug logging from login route; fix .env.example formatting
- Remove console.log debug lines added during password troubleshooting
- .env.example: restore correct formatting, add note about $ escaping with
  single quotes + backslashes for bcrypt hashes
2026-03-20 00:23:16 -04:00
8 changed files with 80 additions and 52 deletions

View File

@@ -1,13 +1,18 @@
# Copy this to .env and fill in values for local development.
# In production, set these as environment variables / Kubernetes secrets.
#
# NOTE: Values containing $ must be quoted with single quotes + backslashes:
# PASSWORD_HASH='$2b$12$...'
# Comma-separated list of additional allowed CORS origins.
# Add your dev machine's IP here if accessing via IP address.
# e.g. ALLOWED_ORIGINS=http://192.168.0.94:5173
ALLOWED_ORIGINS=
# bcrypt hash of the admin password. Generate with:
# python3 -c "import bcrypt; print(bcrypt.hashpw(b'yourpassword', bcrypt.gensalt(rounds=12)).decode())"
PASSWORD_HASH=
# Random secret for signing session tokens. Generate with:
# openssl rand -hex 32
SESSION_SECRET=
# Comma-separated list of additional allowed origins for CSRF.
# Add your dev machine's IP here if accessing via IP address.
# e.g. ALLOWED_ORIGINS=http://192.168.0.94:5173
ALLOWED_ORIGINS=

View File

@@ -1,5 +1,5 @@
{
"adminPath": "admin",
"adminPath": "boss",
"yoloLabel": "YOLO mode",
"yoloDescription": "",
"yoloVisibility": "auto"

View File

@@ -1,9 +1,61 @@
import { type Handle } from '@sveltejs/kit';
import fs from 'fs';
import path from 'path';
import { type Handle, type Reroute } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
// ── Admin path rerouting ────────────────────────────────────────────────────
const CONFIG_PATH = path.resolve('data', 'config.json');
// Cache adminPath with a 5-second TTL so changes take effect quickly
// but we don't hit disk on every request.
let cachedAdminPath = 'admin';
let cacheExpiry = 0;
function getAdminPath(): string {
const now = Date.now();
if (now < cacheExpiry) return cachedAdminPath;
try {
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
cachedAdminPath = (config.adminPath ?? 'admin').replace(/^\/|\/$/g, '') || 'admin';
} catch {
// config.json missing or unreadable — keep current cached value
}
cacheExpiry = now + 5000;
return cachedAdminPath;
}
export { getAdminPath as adminPath };
/**
* Rewrites /<adminPath>/... to /admin/... before SvelteKit resolves the route.
* When adminPath is not 'admin', direct access to /admin is rewritten to /404
* so attackers cannot enumerate the admin URL.
*/
export const reroute: Reroute = ({ url }) => {
const adminPath = getAdminPath();
const prefix = `/${adminPath}`;
// Public path → internal /admin
if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
return '/admin' + url.pathname.slice(prefix.length);
}
// Block direct /admin access when a custom path is configured
if (adminPath !== 'admin') {
if (url.pathname === '/admin' || url.pathname.startsWith('/admin/')) {
return '/404';
}
}
return url.pathname;
};
// ── CSRF origin allowlist ───────────────────────────────────────────────────
/**
* CSRF origin allowlist.
*
* Production: set ALLOWED_ORIGINS to a comma-separated list of trusted origins.
* e.g. ALLOWED_ORIGINS=https://humorunits.com,https://dickloads.com
*

View File

@@ -1,37 +0,0 @@
import fs from 'fs';
import path from 'path';
import type { Reroute } from '@sveltejs/kit';
// Read adminPath once at startup. Changes require a server restart.
let adminPath = 'admin';
try {
const config = JSON.parse(fs.readFileSync(path.resolve('data', 'config.json'), 'utf8'));
adminPath = (config.adminPath ?? 'admin').replace(/^\/|\/$/g, '') || 'admin';
} catch {
// config.json missing or unreadable — fall back to 'admin'
}
export { adminPath };
/**
* Rewrites /<adminPath>/... to /admin/... before SvelteKit resolves the route.
* When adminPath is not 'admin', direct access to /admin is rewritten to /404
* so attackers cannot enumerate the admin URL.
*/
export const reroute: Reroute = ({ url }) => {
const prefix = `/${adminPath}`;
// Public path → internal /admin
if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
return '/admin' + url.pathname.slice(prefix.length);
}
// Block direct /admin access when a custom path is configured
if (adminPath !== 'admin') {
if (url.pathname === '/admin' || url.pathname.startsWith('/admin/')) {
return '/404';
}
}
return url.pathname;
};

View File

@@ -15,8 +15,8 @@ const BRANDS: Record<string, Brand> = {
footer: 'Humor Units — invented conversions for ridiculous people.',
},
'dickloads.com': {
name: 'Dick Loads',
footer: 'Dick Loads — serious units for serious people.',
name: 'Dickloads',
footer: 'Dickloads — serious units for serious people.',
},
};

View File

@@ -8,5 +8,5 @@ import { requireAuth } from '$lib/server/auth';
import type { Request } from '@sveltejs/kit';
export function authRequest(request: Request): boolean {
return requireAuth(request, env.SESSION_SECRET ?? '');
return requireAuth(request, (env.SESSION_SECRET ?? '').trim());
}

View File

@@ -31,10 +31,14 @@
let highlightedUnitId = $state<string | null>(null);
let yoloMode = $state<boolean>(false);
// Initialize fromUnitId to first unit once data arrives
// Initialize fromUnitId to the base unit of the first visible group.
// Falls back to units[0] if no groups or the base unit isn't in the visible list.
$effect(() => {
if (fromUnitId === '' && units.length > 0) {
fromUnitId = units[0].id;
const firstGroupBaseId = groups.length > 0 ? groups[0].baseUnitId : null;
fromUnitId = (firstGroupBaseId && units.some((u) => u.id === firstGroupBaseId))
? firstGroupBaseId
: units[0].id;
}
});

View File

@@ -11,8 +11,12 @@ export const POST: RequestHandler = async ({ request, url }) => {
return json({ error: 'Password required' }, { status: 400 });
}
const passwordHash = env.PASSWORD_HASH ?? '';
const sessionSecret = env.SESSION_SECRET ?? '';
const passwordHash = (env.PASSWORD_HASH ?? '').trim();
const sessionSecret = (env.SESSION_SECRET ?? '').trim();
console.log('[login] passwordHash length:', passwordHash.length, 'first4:', passwordHash.slice(0, 4));
console.log('[login] sessionSecret length:', sessionSecret.length);
console.log('[login] password length:', password.length);
if (!passwordHash || !sessionSecret) {
console.warn('[humor-units] WARNING: PASSWORD_HASH or SESSION_SECRET env var not set.');