Files
website/src/lib/server/owner.ts
T
stan 079399635d
Deploy production / deploy (push) Successful in 1m22s
trackers: media hub with webtoons, podcasts and videos; shows and anime filters
- /trackers/media: one card per child (reading, movies, shows, webtoons, music, podcasts,
  videos) with quick stats, a combined currently-consuming feed and a mixed watchlist
- webtoons, podcasts, videos: new owner-editable trackers on one generic media.items table,
  JSON import (owner form or scripts/media-run.sh)
- shows: /trackers/shows -> /trackers/movies?type=show; anime flag on movies.titles
  (backfilled, set from TMDB, owner toggle) and ?type=anime filter
- nav: media replaces the top-level reading, movies and music entries; their URLs are kept
2026-10-26 23:34:22 +00:00

102 lines
3.6 KiB
TypeScript

import { error } from '@sveltejs/kit';
import { MATH_PRIVATE } from '#lib/math.js';
// The site has exactly one owner: Stan's account (prod auth.user id tfn6OZDOpiQvMARIH65HQ2XJuaVvmGEe).
// Owner-only data, such as the health tracker, is gated on that account's email, not on
// a role, so other users (now or later, e.g. on a public tracking project) never see it.
// Override with OWNER_EMAIL if the address ever changes.
const OWNER_EMAIL = (process.env.OWNER_EMAIL || '[email protected]').trim().toLowerCase();
type OwnerEvent = { locals: App.Locals };
export function isOwner(event: OwnerEvent): boolean {
return event.locals.user?.email?.trim().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 = [
'/start',
'/trackers/health',
'/trackers/tasks',
'/trackers/groceries',
'/trackers/houses',
'/trackers/data-holders',
'/trackers/books/files',
// Notes (the Obsidian vault): pages, API, attachments. The /ws/notes socket checks the owner itself.
'/trackers/notes',
// AI tracker: the news tab (/trackers/ai) is public, bots, usage and setup are mine only.
'/trackers/ai/bots',
'/trackers/ai/usage',
'/trackers/ai/setup',
'/lists/db',
// Spotify OAuth for the listening tracker (connect + callback 404 for everyone else).
'/api/spotify',
// Worldwide todo list: the list is public, the review queue and its API are mine only.
'/projects/worldwide-todo/queue',
'/projects/worldwide-todo/api/queue',
// Cargill plant tracker: what the factory I work at does. Mine only.
'/trackers/cargill',
// Radio tracker: Opa's scanner frequencies, banks, nets, ISS passes and my log. Mine only.
'/trackers/radio',
// Math tracker (the Napkin): owner-only while MATH_PRIVATE (#lib/math) is on.
...(MATH_PRIVATE ? (['/trackers/math'] as const) : [])
] as const;
/**
* Owner-only paths inside otherwise public areas: the book reader and its progress/notes
* APIs. /trackers/books and /trackers/books/<id> themselves are public catalogue pages.
*/
export const ownerOnlyPatterns = [/^\/trackers\/books\/\d+\/(read|progress|notes)(\/|$)/] as const;
/**
* Areas where only the owner may write: any non-GET request from anyone else is refused
* (404) in hooks.server.ts, which also covers form actions on the public book pages.
*/
export const ownerWritePrefixes = [
'/start',
'/trackers/health',
'/trackers/tasks',
'/trackers/groceries',
'/trackers/houses',
'/trackers/data-holders',
'/trackers/books',
'/trackers/cars',
'/trackers/planes',
'/trackers/trains',
'/trackers/wildlife',
'/trackers/music',
'/trackers/pc',
'/trackers/notes',
'/trackers/ai',
'/trackers/movies',
'/trackers/media',
'/trackers/webtoons',
'/trackers/podcasts',
'/trackers/videos',
'/trackers/math',
'/trackers/cargill',
'/trackers/radio'
] as const;
const normalise = (pathname: string) => pathname.toLowerCase().replace(/\/+$/, '');
const under = (path: string, prefix: string) => path === prefix || path.startsWith(`${prefix}/`);
export function isOwnerOnlyPath(pathname: string): boolean {
const path = normalise(pathname);
return (
ownerOnlyPrefixes.some((prefix) => under(path, prefix)) ||
ownerOnlyPatterns.some((pattern) => pattern.test(path))
);
}
export function isOwnerWritePath(pathname: string): boolean {
const path = normalise(pathname);
return ownerWritePrefixes.some((prefix) => under(path, prefix));
}