From 478e47684a3a242e4ed4c60e395b599c523ed57d Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Fri, 31 Jul 2026 15:29:31 +0200 Subject: [PATCH] =?UTF-8?q?Login=20=C3=BCber=20beide=20Domains=20repariere?= =?UTF-8?q?n,=20Hub-Link=20auf=20der=20Bot-Seite=20korrigieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Fehler aus dem ersten Praxistest: 1. "Ungültiger OAuth-State" beim Anmelden auf dem Hub. Der Login startete auf hub.d4rkst3r.de, aber die Rücksprung-Adresse für Discord war fest auf die öffentliche URL gesetzt — Discord schickte also zur Bot-Domain zurück. Das Schutz-Cookie gegen Sitzungsübernahme gilt aber nur für die Domain, auf der es gesetzt wurde, und war dort nicht lesbar. Jetzt bleibt der ganze Ablauf auf der Domain, von der er gestartet ist. Damit entfällt auch der Umweg über ein zusätzliches Cookie, das sich die Ursprungsseite merken sollte. 2. Der Link "zum Community-Hub" auf der Bot-Seite zeigte auf die Bot-Seite selbst — er benutzte die alte öffentliche URL statt der Hub-Adresse. /api/legal liefert jetzt beide Adressen getrennt; die Verweise zwischen den Seiten erscheinen nur, wenn die Domains wirklich verschieden sind. Co-Authored-By: Claude Fable 5 --- frontend/src/BotApp.jsx | 2 +- frontend/src/pages/BotLanding.jsx | 7 +++-- src/web/api.js | 6 +++- src/web/auth.js | 49 ++++++++++++++----------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/frontend/src/BotApp.jsx b/frontend/src/BotApp.jsx index 9a1bc22..a55b613 100644 --- a/frontend/src/BotApp.jsx +++ b/frontend/src/BotApp.jsx @@ -23,7 +23,7 @@ export default function BotApp() { apiGet('/api/me') .then((data) => setMe({ scopes: [], ...data, loading: false })) .catch(() => setMe({ user: null, admin: false, scopes: [], loading: false })); - apiGet('/api/legal').then((d) => setHub(d.publicUrl)).catch(() => {}); + apiGet('/api/legal').then((d) => setHub(d.split ? d.hubUrl : null)).catch(() => {}); }, []); useEffect(() => { diff --git a/frontend/src/pages/BotLanding.jsx b/frontend/src/pages/BotLanding.jsx index 3039493..c578eaa 100644 --- a/frontend/src/pages/BotLanding.jsx +++ b/frontend/src/pages/BotLanding.jsx @@ -25,7 +25,8 @@ export default function BotLanding() { const [hub, setHub] = useState(null); useEffect(() => { - apiGet('/api/legal').then((d) => setHub(d)).catch(() => {}); + // Nur verlinken, wenn der Hub wirklich woanders liegt + apiGet('/api/legal').then((d) => setHub(d.split ? d.hubUrl : null)).catch(() => {}); apiGet('/api/serverstats').then(setStatus).catch(() => {}); window.scrollTo(0, 0); }, []); @@ -106,11 +107,11 @@ export default function BotLanding() { - {hub?.publicUrl && ( + {hub && (

Der Bot läuft für die D4RKST3R-Community {status?.members ? ` mit ${status.members} Mitgliedern` : ''} — - {' '}zum Community-Hub. + {' '}zum Community-Hub.

)} diff --git a/src/web/api.js b/src/web/api.js index a4383f5..d11bb65 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -27,7 +27,7 @@ import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-men import { computeNextRun } from './scheduled-posts.js'; import { config } from '../config.js'; import { removeDevlog } from '../bot/devlog-archive.js'; -import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo, brandColor, brandFooter, brandName, giteaApiToken, votingChannelId, discordGuildId, discordInviteUrl, legalInfo } from '../runtime-settings.js'; +import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo, brandColor, brandFooter, brandName, giteaApiToken, votingChannelId, discordGuildId, discordInviteUrl, legalInfo, hubUrl, botUrl } from '../runtime-settings.js'; import { xpForLevel } from '../bot/levels.js'; import { getMilestones } from '../gitea-api.js'; import { getSessionUser, isAdmin, isGuildMember } from './auth.js'; @@ -1261,6 +1261,10 @@ ${rssItems} legal: legalInfo(), brand: brandName(), publicUrl: publicUrl(), + // Für Querverweise zwischen den beiden Seiten + hubUrl: hubUrl(), + botUrl: botUrl(), + split: hubUrl() !== botUrl(), guildConfigured: Boolean(discordGuildId()), })); diff --git a/src/web/auth.js b/src/web/auth.js index a1e9e60..d89c90e 100644 --- a/src/web/auth.js +++ b/src/web/auth.js @@ -6,10 +6,23 @@ import { publicUrl, discordGuildId, memberGateEnabled, cookieDomain, hubUrl, bot const DISCORD_API = 'https://discord.com/api/v10'; const SESSION_COOKIE = 'd4rkbot_session'; const STATE_COOKIE = 'd4rkbot_oauth_state'; -const ORIGIN_COOKIE = 'd4rkbot_origin'; -// Dynamisch, damit die Setup-Seite die URL ändern kann (Redirect auch im Dev-Portal eintragen!) -const redirectUri = () => `${publicUrl()}/auth/callback`; +/** + * Rücksprung-Adresse für Discord — bleibt auf der Domain, von der der Login + * gestartet wurde. Das ist wichtig, weil das Schutz-Cookie gegen + * Sitzungsübernahme nur für diese eine Domain gilt: Würde Discord auf eine + * andere Adresse zurückschicken, wäre es dort nicht lesbar und der Login + * schlüge mit „Ungültiger OAuth-State" fehl. + * + * Jede hier mögliche Adresse muss im Discord-Portal als Redirect eingetragen sein. + */ +function redirectUri(request) { + const host = String(request?.headers?.host ?? '').toLowerCase().split(':')[0]; + const known = [hubUrl(), botUrl(), publicUrl()].find((u) => { + try { return new URL(u).hostname === host; } catch { return false; } + }); + return `${known ?? publicUrl()}/auth/callback`; +} /** Eingeloggten User aus dem signierten Session-Cookie lesen (null wenn nicht eingeloggt) */ export function getSessionUser(request) { @@ -46,25 +59,16 @@ export function registerAuthRoutes(app, client) { const state = crypto.randomBytes(16).toString('hex'); const params = new URLSearchParams({ client_id: config.discordClientId, - redirect_uri: redirectUri(), + redirect_uri: redirectUri(request), response_type: 'code', scope: 'identify', state, }); - // Bei getrennten Domains merken, von welcher Seite der Login kam — - // Discord schickt immer zur selben Rücksprung-Adresse zurück. - const host = String(request.headers.host ?? '').toLowerCase().split(':')[0]; - const origin = [hubUrl(), botUrl()].find((u) => { - try { return new URL(u).hostname === host; } catch { return false; } - }); return reply .setCookie(STATE_COOKIE, state, { path: '/auth', httpOnly: true, sameSite: 'lax', maxAge: 600, signed: true, }) - .setCookie(ORIGIN_COOKIE, origin ?? '', { - path: '/', httpOnly: true, sameSite: 'lax', maxAge: 600, signed: true, - }) .redirect(`https://discord.com/oauth2/authorize?${params}`); }); @@ -86,7 +90,8 @@ export function registerAuthRoutes(app, client) { client_secret: config.discordClientSecret, grant_type: 'authorization_code', code, - redirect_uri: redirectUri(), + // Muss exakt dieselbe Adresse sein wie beim Weiterleiten + redirect_uri: redirectUri(request), }), }); if (!tokenRes.ok) { @@ -120,28 +125,18 @@ export function registerAuthRoutes(app, client) { const { takeReturnTo } = await import('./sso.js'); const returnTo = takeReturnTo(request, reply); - // Sonst zurück zu der Seite, von der der Login gestartet wurde - let origin = null; - const rawOrigin = request.cookies[ORIGIN_COOKIE]; - if (rawOrigin) { - const unsigned = request.unsignCookie(rawOrigin); - // Nur die beiden bekannten Adressen zulassen - if (unsigned.valid && [hubUrl(), botUrl()].includes(unsigned.value)) { - origin = unsigned.value; - } - } - // Mit gesetzter Cookie-Domain (z. B. .d4rkst3r.de) gilt die Anmeldung // auf Hub und Bot-Seite gleichzeitig const domain = cookieDomain(); return reply .clearCookie(STATE_COOKIE, { path: '/auth' }) - .clearCookie(ORIGIN_COOKIE, { path: '/' }) .setCookie(SESSION_COOKIE, JSON.stringify(session), { path: '/', httpOnly: true, sameSite: 'lax', maxAge: 7 * 24 * 3600, signed: true, ...(domain ? { domain } : {}), }) - .redirect(returnTo ?? (origin ? `${origin}/` : '/')); + // Der Callback läuft auf derselben Domain wie der Login — ein + // relativer Pfad führt also automatisch zurück zur richtigen Seite + .redirect(returnTo ?? '/'); }); app.get('/auth/logout', async (request, reply) => {