All checks were successful
Deploy production / deploy (push) Successful in 2m44s
- scripts/house-scrape.ts: polite nightly scrape (honest UA, 6-10 s apart, stops a source on 403/challenge), ingest + price history + removals, ntfy alerts on a separate topic (max 5 + summary, first run seeds)
- address_key + duplicate_of: copies on another source link to one listing, manual wins
- /projects/house-tracker -> /trackers/houses (308), owner only (404 otherwise); filters for min m2, rooms, source, Gouda region default; notes, add-by-hand form, total rent, also-on links
- scripts/house-db.sh: run the house-listing CLI as the limited house_cli role
- deploy/systemd/house-scrape.{service,timer}
64 lines
2.3 KiB
TypeScript
64 lines
2.3 KiB
TypeScript
import { error } from '@sveltejs/kit';
|
|
|
|
// 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 || 'stan@stanrunge.dev').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 = [
|
|
'/trackers/health',
|
|
'/trackers/tasks',
|
|
'/trackers/groceries',
|
|
'/trackers/houses',
|
|
'/trackers/books/files',
|
|
'/lists/db'
|
|
] 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 = [
|
|
'/trackers/health',
|
|
'/trackers/tasks',
|
|
'/trackers/groceries',
|
|
'/trackers/houses',
|
|
'/trackers/books'
|
|
] 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));
|
|
}
|