Add private health tracker (owner-only)

- health schema: vaccinations, body_measurements, workouts, nutrition_daily,
  sleep (generated hours), medications, hair_progress, appointments
- migration with updated_at triggers; grants to personal_app only
- /trackers/health overview with add/edit/delete, owner-gated (404 otherwise),
  noindex + private no-store, hidden from listings for non-owners
- owner-only JSON/CSV import endpoint and scripts/health-import.ts
This commit is contained in:
2026-10-24 00:14:07 +00:00
parent 88837af624
commit 8275f1fb22
24 changed files with 13622 additions and 14 deletions

27
src/lib/server/owner.ts Normal file
View File

@@ -0,0 +1,27 @@
import { error } from '@sveltejs/kit';
// The site has one owner (Stan). Owner-only data, such as the health tracker, needs both
// the admin role and the owner's account email, so granting someone else `admin` (e.g. to
// moderate Animaldex) never exposes it. Override with OWNER_EMAIL if the address changes.
const OWNER_EMAIL = (process.env.OWNER_EMAIL || 'stan@stanrunge.dev').toLowerCase();
type OwnerEvent = { locals: App.Locals };
export function isOwner(event: OwnerEvent): boolean {
const user = event.locals.user;
return !!user && user.role === 'admin' && user.email?.toLowerCase() === OWNER_EMAIL;
}
/** Owner-only routes answer 404 to everyone else, so they don't reveal that they exist. */
export function requireOwner(event: OwnerEvent) {
if (!isOwner(event)) error(404, 'Not Found');
return event.locals.user!;
}
/** Paths that only the owner may see (checked in hooks.server.ts before any route runs). */
export const ownerOnlyPrefixes = ['/trackers/health'] as const;
export function isOwnerOnlyPath(pathname: string): boolean {
const path = pathname.toLowerCase().replace(/\/+$/, '');
return ownerOnlyPrefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
}