Files
odin 1dfa2c0a04 Add the profile page, and publish it as an embeddable panel
Developed in a separate session; committed here alongside the calendar work
that shares this repository's migration journal.

A person can now see their own Plex link, their Watch Now slots and their watch
history at /profile. The same panel is published in two further forms so that
accounts.sticknife.com on charon can carry it as one section of a wider
sticknife profile, next to the other services' sections.

  - watch_history (0024) records what has been played, keyed on the Plex
    history id so a re-sync cannot duplicate a row. Partial unique index,
    because that id is null for anything entered by hand.
  - plex_accounts.is_server_owner (0025) marks the one account whose viewing
    the server files under local account 1 rather than under its plex.tv id.
  - The embed carries its own layout, origin allowlist and a frame-height
    reporter, so the host page can size it without guessing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 15:42:17 +02:00

60 lines
2.0 KiB
TypeScript

import type { NextConfig } from "next";
/**
* Who may put Ampelos in an iframe.
*
* Only /embed/* is framable, and only by the sticknife accounts page. The rest
* of the app is denied outright below -- framing the catalog or the admin views
* has no legitimate use and is how a clickjacked "Remove" button gets pressed.
*
* Configurable because the accounts host is charon's to name, not ours: a
* staging origin or a rename should not need a code change. Comma-separated,
* and note these are ORIGINS (scheme and host), which is what frame-ancestors
* takes -- a bare hostname is silently ignored by the CSP parser.
*/
const EMBED_ANCESTORS =
process.env.AMPELOS_EMBED_ANCESTORS ?? "https://accounts.sticknife.com";
const nextConfig: NextConfig = {
allowedDevOrigins: ["10.24.88.95", "ampelos.sticknife.com"],
images: {
remotePatterns: [
{
protocol: "https",
hostname: "image.tmdb.org",
pathname: "/t/p/**",
},
],
},
async headers() {
return [
{
// Everything EXCEPT the embed, framed by nobody.
//
// The exclusion is in the matcher rather than left to header override,
// because X-Frame-Options has no "allow these origins" form -- the
// multi-origin ALLOW-FROM was never implemented by any browser -- so a
// blanket rule could set CSP correctly for /embed and still stamp a
// DENY that some agent honours in preference. Not matching at all is
// the only version with no precedence question in it.
source: "/:path((?!embed/).*)",
headers: [
{ key: "Content-Security-Policy", value: "frame-ancestors 'none'" },
{ key: "X-Frame-Options", value: "DENY" },
],
},
{
source: "/embed/:path*",
headers: [
{
key: "Content-Security-Policy",
value: `frame-ancestors 'self' ${EMBED_ANCESTORS.split(",").map((o) => o.trim()).filter(Boolean).join(" ")}`,
},
],
},
];
},
};
export default nextConfig;