Deploy production / deploy (push) Successful in 1m22s
- /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
102 lines
3.6 KiB
TypeScript
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));
|
|
}
|