feat(podcast): fælles familielink uden login
CD / deploy (push) Successful in 15s
CI / check (push) Successful in 23s

Et valgfrit fælles link, /podcast/f/<nøgle>/, til fx familiens Messenger-
gruppe: samme afspillerside som den personlige, med abonnér-knapper og feed
under samme sti, men uden login og uden admin-ting. Kladder vises aldrig der.
I /admin/podcast laves linket, nøglen kan skiftes (det gamle dør med det
samme), det kan slås fra, og hentningerne tælles.

Siden er noindex (header og meta) og sender ingen referrer. Forkerte nøgler
giver 404 ligesom ukendte stier, og en klient der rammer 20 forkerte nøgler
på ti minutter, får 404 på alt under /podcast/<nøgle> i et stykke tid.
Det gælder også de personlige nøgler.

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
This commit is contained in:
agw
2026-10-04 20:11:28 +02:00
parent 4274047158
commit 94e5a0ca01
5 changed files with 208 additions and 28 deletions
+29
View File
@@ -34,6 +34,35 @@
</table>
{% endfor %}
<h2 style="font-size:1.15rem;margin-top:2rem">Fælles familielink</h2>
<p class="wt-muted" style="margin:.3rem 0 .8rem">Ét link uden login, fx til familiens Messenger-gruppe: afspiller og feed i ét. Kladder vises aldrig her. Skifter du nøglen, holder det gamle link op med at virke med det samme, også for dem der abonnerer via det.</p>
<div class="wt-card" style="padding:1rem 1.2rem;max-width:48rem">
{% if faelles.key %}
<p style="margin:0 0 .6rem"><strong>{{ 'Slået til' if faelles.aktiv else 'Slået fra' }}</strong>
· {{ faelles.hentninger or 0 }} hentninger{% if faelles.sidst %} · sidst <span data-ts="{{ faelles.sidst }}"></span>{% endif %}</p>
{% if faelles.aktiv %}
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.8rem">
<input class="wt-input" readonly id="faelles-link" value="{{ base }}/podcast/f/{{ faelles.key }}/"
style="flex:1 1 22rem;font-family:ui-monospace,monospace;font-size:.85rem" onclick="this.select()">
<button class="wt-btn" type="button" onclick="var i=document.getElementById('faelles-link');i.select();navigator.clipboard&&navigator.clipboard.writeText(i.value);this.textContent='Kopieret'">Kopiér</button>
</div>
{% endif %}
<form method="post" action="/admin/podcast/faelles" style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0">
<button class="wt-btn" type="submit" name="handling" value="skift"
onclick="return confirm('Det gamle fælles link holder op med at virke. Fortsæt?')">Skift nøgle</button>
{% if faelles.aktiv %}
<button class="wt-btn" type="submit" name="handling" value="fra">Slå fra</button>
{% else %}
<button class="wt-btn wt-btn--primary" type="submit" name="handling" value="til">Slå til</button>
{% endif %}
</form>
{% else %}
<form method="post" action="/admin/podcast/faelles" style="margin:0">
<button class="wt-btn wt-btn--primary" type="submit" name="handling" value="skift">Lav fælles link</button>
</form>
{% endif %}
</div>
<h2 style="font-size:1.15rem;margin-top:2rem">Lytternes adresser</h2>
<table class="wt-table">
<thead><tr><th>Bruger</th><th>Oprettet</th><th>Sidst hentet</th><th>Hentninger</th><th>App</th><th></th></tr></thead>
+1
View File
@@ -13,6 +13,7 @@
<link rel="stylesheet" href="https://assets.weircon.dk/css/weirsoe-theme.css" />
<link rel="stylesheet" href="/static/portal.css" />
<script defer src="https://assets.weircon.dk/js/weirsoe-theme.js"></script>
{% block head %}{% endblock %}
</head>
<body>
<header class="wt-header">
+8 -6
View File
@@ -1,5 +1,7 @@
{% extends "base.html" %}
{% block title %}Podcast{% endblock %}
{% block title %}{% if delt and udsendelser %}{{ udsendelser[0].titel }}{% else %}Podcast{% endif %}{% endblock %}
{% block head %}{% if delt %}<meta name="robots" content="noindex, nofollow, noarchive">
<meta name="referrer" content="no-referrer">{% endif %}{% endblock %}
{% block body %}
{% if saved %}<div class="wt-flash">{{ saved }}</div>{% endif %}
{% if not udsendelser %}
@@ -10,7 +12,7 @@
<section style="margin:1rem 0 2.5rem">
<div style="display:flex;gap:1.2rem;align-items:flex-start;flex-wrap:wrap">
{% if u.har_cover %}
<img src="/podcast/{{ key }}/{{ u.slug }}/cover.jpg" alt="" width="160" height="160"
<img src="{{ rod }}/{{ u.slug }}/cover.jpg" alt="" width="160" height="160"
style="border-radius:14px;flex:0 0 auto;box-shadow:0 4px 18px rgba(0,0,0,.25)">
{% endif %}
<div style="flex:1 1 18rem;min-width:0">
@@ -21,7 +23,7 @@
<div class="wt-card" style="margin:1.4rem 0;padding:1rem 1.2rem">
<h2 style="font-size:1.05rem;margin:0 0 .5rem">Abonnér i din podcast-app</h2>
<p class="wt-muted" style="margin:.2rem 0 .8rem">Tryk på din app. Nye episoder kommer så af sig selv. Adressen er din egen, så del den ikke; familien får hver sin.</p>
<p class="wt-muted" style="margin:.2rem 0 .8rem">{% if delt %}Tryk på din app, så kommer nye episoder af sig selv. Linket er familiens fælles, så del det kun inden for familien.{% else %}Tryk på din app. Nye episoder kommer så af sig selv. Adressen er din egen, så del den ikke; familien får hver sin.{% endif %}</p>
<div style="display:flex;flex-wrap:wrap;gap:.5rem">
<a class="wt-btn wt-btn--primary" href="podcast://{{ u.uden_skema }}">Apple Podcasts</a>
<a class="wt-btn" href="overcast://x-callback-url/add?url={{ u.feed_enc }}">Overcast</a>
@@ -48,9 +50,9 @@
{% if e.kladde %}<strong>Kladde, kun du kan se den</strong> · {% endif %}{{ (e.varighed // 60) }} min
</span>
</div>
{% if e.beskrivelse %}<p style="margin:.5rem 0">{{ e.beskrivelse }}</p>{% endif %}
{% if e.beskrivelse %}<p style="margin:.5rem 0;white-space:pre-line">{{ e.beskrivelse }}</p>{% endif %}
<audio controls preload="none" style="width:100%;margin-top:.4rem"
src="/podcast/{{ key }}/{{ u.slug }}/{{ e.slug }}.mp3"></audio>
src="{{ rod }}/{{ u.slug }}/{{ e.slug }}.mp3"></audio>
</article>
{% else %}
<p class="wt-muted">Ingen episoder endnu.</p>
@@ -58,7 +60,7 @@
</section>
{% endfor %}
{% if udsendelser %}
{% if udsendelser and not delt %}
<form method="post" action="/podcast/ny-noegle" style="margin:2rem 0"
onsubmit="return confirm('Din gamle adresse holder op med at virke, og du skal abonnere igen i din app. Fortsæt?')">
<button class="wt-btn" type="submit">Lav en ny adresse</button>