Compare commits

..

4 Commits

4 changed files with 44 additions and 5 deletions

View File

@@ -247,8 +247,11 @@ Design: **Authentik authenticates once for the whole domain; Caddy authorizes pe
the matching access on next login; removing it removes it. the matching access on next login; removing it removes it.
copyparty additionally requires a header with a secret name that only Caddy adds (`idp-h-key`, copyparty additionally requires a header with a secret name that only Caddy adds (`idp-h-key`,
`COPYPARTY_IDP_KEY` in `.env`), so identity headers cannot be forged even if copyparty were reached directly. `COPYPARTY_IDP_KEY` in `.env`), so identity headers cannot be forged even if copyparty were reached directly.
Logout: copyparty's logout link and the live portal's "Sign out" go to `/outpost.goauthentik.io/sign_out`, Logout: copyparty's logout link, the live portal's "Sign out" and the dock's "sign out" go through
which ends the Authentik session (a local app logout alone would be undone on the next request). `/outpost.goauthentik.io/sign_out`. Set the provider's **Invalidation flow** to
`default-provider-invalidation-flow` so that also ends the Authentik login: every logout then forces a full
Discord re-login on the next visit (and picks up group changes). Discord itself may approve silently if
you are still logged into Discord in that browser.
5. Debug: `https://HOST/whoami` prints the username/groups headers exactly as the apps receive them. 5. Debug: `https://HOST/whoami` prints the username/groups headers exactly as the apps receive them.
Group changes in Authentik apply on the next login — log out at `https://auth.HOST/` and back in. Group changes in Authentik apply on the next login — log out at `https://auth.HOST/` and back in.
6. Test in a private window: `https://live.HOST/` → Discord login → portal; `https://bot.HOST/` → 403 unless 6. Test in a private window: `https://live.HOST/` → Discord login → portal; `https://bot.HOST/` → 403 unless

View File

@@ -154,7 +154,8 @@ AUTHENTIK FORWARD-AUTH (gates https://$LIVE/ and https://$BOT/; later the main h
1. Applications -> Outposts -> edit "authentik Embedded Outpost" -> config: authentik_host: https://$AUTH 1. Applications -> Outposts -> edit "authentik Embedded Outpost" -> config: authentik_host: https://$AUTH
2. Applications -> Create with provider -> Application "Vanguard sites"; Proxy Provider, authorization 2. Applications -> Create with provider -> Application "Vanguard sites"; Proxy Provider, authorization
flow implicit-consent, mode "Forward auth (domain level)", authentication URL https://$AUTH, flow implicit-consent, mode "Forward auth (domain level)", authentication URL https://$AUTH,
cookie domain ${BASE} -> then edit the Embedded Outpost -> Applications: add it -> Update cookie domain ${BASE}, Invalidation flow default-provider-invalidation-flow (logout = full re-login)
-> then edit the Embedded Outpost -> Applications: add it -> Update
3. Per-host access is enforced by Caddy from X-Authentik-Groups: .env BOT_REQUIRE_GROUP=bot-users 3. Per-host access is enforced by Caddy from X-Authentik-Groups: .env BOT_REQUIRE_GROUP=bot-users
(default), LIVE_REQUIRE_GROUP= (empty = any logged-in member). ./configure.py apply regenerates. (default), LIVE_REQUIRE_GROUP= (empty = any logged-in member). ./configure.py apply regenerates.
4. Discord roles -> Authentik groups: Directory -> Groups: create bot-users (keep streamers/editors/admins); 4. Discord roles -> Authentik groups: Directory -> Groups: create bot-users (keep streamers/editors/admins);

33
check.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Static checks for everything in the bundle. Run before committing / deploying.
set -uo pipefail
cd "$(dirname "$0")"
fail=0
say() { printf '%-28s %s\n' "$1" "$2"; }
for f in install.sh addCommunityMediaServer.sh installJellyfinPlugin.sh registerGiteaRunner.sh createGiteaDb.sh deployLive.sh firewall/firewall.sh bin/*.sh home/*.sh bot/entrypoint.sh mariadb/init/*.sh; do
[[ -f $f ]] || continue
bash -n "$f" && say "$f" ok || { say "$f" "BASH SYNTAX"; fail=1; }
done
for f in configure.py bin/dumpd.py; do
python3 -m py_compile "$f" && say "$f" ok || { say "$f" "PYTHON SYNTAX"; fail=1; }
done
rm -rf __pycache__ bin/__pycache__
python3 -c "import yaml,sys; [yaml.safe_load(open(f)) for f in sys.argv[1:]]" docker-compose.yml mediamtx.example.yml && say "yaml" ok || { say "yaml" "INVALID"; fail=1; }
node -e "
const fs=require('fs');const html=fs.readFileSync('dock/index.html','utf8');
for (const m of html.matchAll(/<script(?:[^>]*)>([\s\S]*?)<\/script>/g)) new Function(m[1]);" && say "dock/index.html JS" ok || { say "dock/index.html JS" "SYNTAX ERROR"; fail=1; }
# generated Caddyfile / mediamtx / copyparty from a dry configure run must at least be produced + parse
T=$(mktemp -d); cp configure.py "$T/"; ( cd "$T" && VG_DRY=1 python3 -c "
import configure as c, yaml
c.write_caddy('ovh1.example.com','auth.example.com','watch.example.com','git.example.com','bot.example.com','live.example.com')
c.write_mediamtx('ovh1.example.com'); yaml.safe_load(open('mediamtx.yml')); c.write_copyparty()
assert 'authMethod: http' in open('mediamtx.yml').read(); assert 'idp-h-usr' in open('copyparty.conf').read()
" ) && say "configure.py generators" ok || { say "configure.py generators" "FAILED"; fail=1; }
rm -rf "$T"
if command -v caddy >/dev/null; then
T=$(mktemp -d); cp configure.py "$T/"; ( cd "$T" && VG_DRY=1 python3 -c "import configure as c; c.write_caddy('ovh1.example.com','a.example.com','w.example.com','g.example.com','b.example.com','l.example.com')" && caddy validate --adapter caddyfile --config Caddyfile >/dev/null 2>&1 ) && say "Caddyfile (caddy validate)" ok || { say "Caddyfile (caddy validate)" "INVALID"; fail=1; }
rm -rf "$T"
fi
[[ -d live/node_modules ]] && ( cd live && npx --no-install vite build --logLevel error >/dev/null 2>&1 ) && say "live portal build" ok || say "live portal build" "(skipped or failed — run npm run build in live/)"
exit $fail

View File

@@ -127,7 +127,7 @@ async function mint(){
} }
let showTok = false; let showTok = false;
function renderManual(t){ function renderManual(t){
const c = cfg(); const title = t ?? buildTitle($("title").value) || "TITLE"; const c = cfg(); const title = (t ?? buildTitle($("title").value)) || "TITLE";
const user = me ? me.user : "<you>", tok = me ? me.token : "<token>"; const user = me ? me.user : "<you>", tok = me ? me.token : "<token>";
$("m_rtmp").textContent = `rtmp://${c.host||location.hostname}:1935/`; $("m_rtmp").textContent = `rtmp://${c.host||location.hostname}:1935/`;
$("m_key").textContent = `${user}/${title}?user=${user}&pass=${showTok ? tok : "••••••••"}`; $("m_key").textContent = `${user}/${title}?user=${user}&pass=${showTok ? tok : "••••••••"}`;
@@ -212,7 +212,9 @@ $("signout").onclick = async e => {
// Authentik's sign-out redirects to the site root (copyparty); do it in the background, then reload the dock, // Authentik's sign-out redirects to the site root (copyparty); do it in the background, then reload the dock,
// which sends us through the Discord login and back here. // which sends us through the Discord login and back here.
try { await fetch("/outpost.goauthentik.io/sign_out", {credentials:"same-origin", mode:"no-cors", redirect:"follow"}); } catch(e){} try { await fetch("/outpost.goauthentik.io/sign_out", {credentials:"same-origin", mode:"no-cors", redirect:"follow"}); } catch(e){}
setTimeout(() => location.replace("/dock/"), 500); // also end the Authentik login itself (otherwise the next visit re-authorises silently); the flow returns to this host
const auth = "https://" + location.hostname.replace(/^[^.]+\./, "auth.") + "/if/flow/default-invalidation-flow/?next=" + encodeURIComponent(location.origin + "/dock/");
setTimeout(() => location.replace(auth), 400);
}; };
whoami(); renderPreview(); whoami(); renderPreview();
</script> </script>