diff --git a/electron/main.js b/electron/main.js index 4ed7433..25f7f04 100644 --- a/electron/main.js +++ b/electron/main.js @@ -222,6 +222,53 @@ document.getElementById('cancelBtn').addEventListener('click', () => { return { action: 'allow' }; }); + // ── GOG OAuth: intercept the redirect in child windows ── + mainWindow.webContents.on('did-create-window', (childWindow) => { + childWindow.webContents.on('will-redirect', async (event, url) => { + // GOG redirects to embed.gog.com/on_login_success?code=XXX after auth + if (url.includes('on_login_success') && url.includes('code=')) { + event.preventDefault(); + + try { + const parsed = new URL(url); + const code = parsed.searchParams.get('code') || ''; + const state = parsed.searchParams.get('state') || ''; + + if (code) { + // Exchange the code via our server + const resp = await fetch(`${HUB_URL}/api/game-library/gog/exchange`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code, linkTo: state }), + }); + const result = await resp.json(); + + if (resp.ok && result.ok) { + childWindow.loadURL(`data:text/html,${encodeURIComponent(`

GOG verbunden!

${result.profileName}: ${result.gameCount} Spiele geladen.

`)}`); + // Notify renderer to refresh profiles + mainWindow.webContents.executeJavaScript('window.dispatchEvent(new Event("gog-connected"))'); + setTimeout(() => { try { childWindow.close(); } catch {} }, 2500); + } else { + childWindow.loadURL(`data:text/html,${encodeURIComponent(`

Fehler

${result.error || 'Unbekannter Fehler'}

`)}`); + } + } + } catch (err) { + console.error('[GOG] Exchange error:', err); + childWindow.loadURL(`data:text/html,${encodeURIComponent('

Fehler

GOG-Verbindung fehlgeschlagen.

')}`); + } + } + }); + + // Allow child windows to navigate to GOG auth (don't open in external browser) + childWindow.webContents.on('will-navigate', (event, url) => { + if (url.startsWith('https://auth.gog.com') || url.startsWith('https://login.gog.com') || + url.startsWith('https://embed.gog.com') || url.startsWith(HUB_URL)) { + // Allow navigation within popup for GOG auth flow + return; + } + }); + }); + // Handle navigation to external URLs mainWindow.webContents.on('will-navigate', (event, url) => { if (!url.startsWith(HUB_URL)) { diff --git a/server/src/plugins/game-library/gog.ts b/server/src/plugins/game-library/gog.ts index 3caf794..11b8a7d 100644 --- a/server/src/plugins/game-library/gog.ts +++ b/server/src/plugins/game-library/gog.ts @@ -10,6 +10,7 @@ const GOG_CLIENT_SECRET = const GOG_AUTH_URL = 'https://auth.gog.com/auth'; const GOG_TOKEN_URL = 'https://auth.gog.com/token'; const GOG_EMBED_URL = 'https://embed.gog.com'; +const GOG_REDIRECT_URI = 'https://embed.gog.com/on_login_success?origin=client'; // ── Types ──────────────────────────────────────────────────────────────────── @@ -37,13 +38,13 @@ export interface GogGame { /** * Build the GOG OAuth authorization URL that the user should visit to grant - * access. After approval, GOG will redirect to `redirectUri` with a `code` - * query parameter. + * access. After approval, GOG redirects to `embed.gog.com/on_login_success` + * with a `code` query parameter that the user copies back into the app. */ -export function getGogAuthUrl(redirectUri: string): string { +export function getGogAuthUrl(): string { const params = new URLSearchParams({ client_id: GOG_CLIENT_ID, - redirect_uri: redirectUri, + redirect_uri: GOG_REDIRECT_URI, response_type: 'code', layout: 'client2', }); @@ -53,10 +54,10 @@ export function getGogAuthUrl(redirectUri: string): string { /** * Exchange an authorization code for GOG access + refresh tokens. + * Uses the fixed GOG_REDIRECT_URI that matches the OAuth registration. */ export async function exchangeGogCode( code: string, - redirectUri: string, ): Promise { console.log('[GOG] Exchanging authorization code for tokens...'); @@ -65,7 +66,7 @@ export async function exchangeGogCode( client_secret: GOG_CLIENT_SECRET, grant_type: 'authorization_code', code, - redirect_uri: redirectUri, + redirect_uri: GOG_REDIRECT_URI, }); const res = await fetch(`${GOG_TOKEN_URL}?${params.toString()}`); diff --git a/server/src/plugins/game-library/index.ts b/server/src/plugins/game-library/index.ts index 86968af..46ba1de 100644 --- a/server/src/plugins/game-library/index.ts +++ b/server/src/plugins/game-library/index.ts @@ -613,32 +613,29 @@ const gameLibraryPlugin: Plugin = { res.json({ profiles }); }); - // ── GOG Login ── + // ── GOG Login (redirect to GOG auth page) ── app.get('/api/game-library/gog/login', (req, res) => { - const proto = req.headers['x-forwarded-proto'] || req.protocol || 'http'; - const host = req.headers['x-forwarded-host'] || req.headers.host || 'localhost'; - const realm = `${proto}://${host}`; - const redirectUri = `${realm}/api/game-library/gog/callback`; - const linkTo = req.query.linkTo ? `&state=${req.query.linkTo}` : ''; - res.redirect(getGogAuthUrl(redirectUri) + linkTo); + const linkTo = req.query.linkTo ? String(req.query.linkTo) : ''; + // Pass linkTo as OAuth state param — GOG passes it back to redirect_uri + let authUrl = getGogAuthUrl(); + if (linkTo) authUrl += `&state=${encodeURIComponent(linkTo)}`; + res.redirect(authUrl); }); - // ── GOG Callback ── - app.get('/api/game-library/gog/callback', async (req, res) => { + // ── GOG Code Exchange ── + // User pastes the code from the GOG redirect URL. We exchange it for + // tokens and fetch the game library. + app.post('/api/game-library/gog/exchange', async (req, res) => { try { - const code = String(req.query.code || ''); - const linkToProfileId = String(req.query.state || ''); + const code = String(req.body?.code || ''); + const linkToProfileId = String(req.body?.linkTo || ''); if (!code) { - res.status(400).send(errorPage('GOG-Authentifizierung fehlgeschlagen', 'Kein Authorization-Code erhalten.')); + res.status(400).json({ error: 'Kein Authorization-Code angegeben.' }); return; } - const proto = req.headers['x-forwarded-proto'] || req.protocol || 'http'; - const host = req.headers['x-forwarded-host'] || req.headers.host || 'localhost'; - const redirectUri = `${proto}://${host}/api/game-library/gog/callback`; - // Exchange code for tokens - const tokens = await exchangeGogCode(code, redirectUri); + const tokens = await exchangeGogCode(code); // Fetch user info + games const [userInfo, games] = await Promise.all([ @@ -689,10 +686,10 @@ const gameLibraryPlugin: Plugin = { console.log(`[GameLibrary] GOG verknuepft: ${profileName} (${userInfo.userId}) - ${games.length} Spiele`); - res.send(`GOG verbunden

GOG verbunden!

${profileName}: ${games.length} Spiele geladen.

Du kannst dieses Fenster schliessen.

`); + res.json({ ok: true, username: userInfo.username, gameCount: games.length, profileName }); } catch (err) { - console.error('[GameLibrary] GOG Callback error:', err); - res.status(500).send(errorPage('GOG-Fehler', 'Ein unerwarteter Fehler ist aufgetreten.')); + console.error('[GameLibrary] GOG exchange error:', err); + res.status(500).json({ error: 'GOG-Authentifizierung fehlgeschlagen.' }); } }); diff --git a/web/src/plugins/game-library/GameLibraryTab.tsx b/web/src/plugins/game-library/GameLibraryTab.tsx index ea52312..03157e2 100644 --- a/web/src/plugins/game-library/GameLibraryTab.tsx +++ b/web/src/plugins/game-library/GameLibraryTab.tsx @@ -137,17 +137,73 @@ export default function GameLibraryTab({ data }: { data: any }) { }, [fetchProfiles]); // ── GOG login ── + const isElectron = navigator.userAgent.includes('GamingHubDesktop'); + + // Code-paste dialog state (browser fallback only) + const [gogDialogOpen, setGogDialogOpen] = useState(false); + const [gogCode, setGogCode] = useState(''); + const [gogStatus, setGogStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + const [gogStatusMsg, setGogStatusMsg] = useState(''); + const connectGog = useCallback(() => { - // If viewing a profile, pass linkTo param so GOG gets linked to it const linkParam = selectedProfile ? `?linkTo=${selectedProfile}` : ''; - const w = window.open(`/api/game-library/gog/login${linkParam}`, '_blank', 'width=800,height=600'); - const interval = setInterval(() => { - if (w && w.closed) { - clearInterval(interval); - setTimeout(fetchProfiles, 1500); + + if (isElectron) { + // Electron: Popup opens → GOG auth → Electron intercepts redirect → done + const w = window.open(`/api/game-library/gog/login${linkParam}`, '_blank', 'width=800,height=700'); + const interval = setInterval(() => { + if (w && w.closed) { + clearInterval(interval); + setTimeout(fetchProfiles, 1000); + } + }, 500); + } else { + // Browser: Open GOG auth in popup, show code-paste dialog + window.open(`/api/game-library/gog/login${linkParam}`, '_blank', 'width=800,height=700'); + setGogCode(''); + setGogStatus('idle'); + setGogStatusMsg(''); + setGogDialogOpen(true); + } + }, [fetchProfiles, selectedProfile, isElectron]); + + const submitGogCode = useCallback(async () => { + let code = gogCode.trim(); + // Accept full URL or just the code + const codeMatch = code.match(/[?&]code=([^&]+)/); + if (codeMatch) code = codeMatch[1]; + if (!code) return; + + setGogStatus('loading'); + setGogStatusMsg('Verbinde mit GOG...'); + try { + const resp = await fetch('/api/game-library/gog/exchange', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code, linkTo: selectedProfile || '' }), + }); + const result = await resp.json(); + if (resp.ok && result.ok) { + setGogStatus('success'); + setGogStatusMsg(`${result.profileName}: ${result.gameCount} Spiele geladen!`); + fetchProfiles(); + setTimeout(() => setGogDialogOpen(false), 2000); + } else { + setGogStatus('error'); + setGogStatusMsg(result.error || 'Unbekannter Fehler'); } - }, 500); - }, [fetchProfiles, selectedProfile]); + } catch { + setGogStatus('error'); + setGogStatusMsg('Verbindung fehlgeschlagen.'); + } + }, [gogCode, selectedProfile, fetchProfiles]); + + // ── Listen for GOG-connected event from Electron main process ── + useEffect(() => { + const onGogConnected = () => fetchProfiles(); + window.addEventListener('gog-connected', onGogConnected); + return () => window.removeEventListener('gog-connected', onGogConnected); + }, [fetchProfiles]); // ── Refetch on window focus (after login redirect) ── useEffect(() => { @@ -797,6 +853,42 @@ export default function GameLibraryTab({ data }: { data: any }) { ); })()} + + {/* ── GOG Code Dialog (browser fallback only) ── */} + {gogDialogOpen && ( +
setGogDialogOpen(false)}> +
e.stopPropagation()}> +

🟣 GOG verbinden

+

+ Nach dem GOG-Login wirst du auf eine Seite weitergeleitet. + Kopiere die komplette URL aus der Adressleiste und füge sie hier ein: +

+ setGogCode(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') submitGogCode(); }} + disabled={gogStatus === 'loading' || gogStatus === 'success'} + autoFocus + /> + {gogStatusMsg && ( +

{gogStatusMsg}

+ )} +
+ + +
+
+
+ )} ); } diff --git a/web/src/plugins/game-library/game-library.css b/web/src/plugins/game-library/game-library.css index 896c6bc..ce86c55 100644 --- a/web/src/plugins/game-library/game-library.css +++ b/web/src/plugins/game-library/game-library.css @@ -737,3 +737,113 @@ flex-direction: column; } } + +/* ── GOG Code Dialog (browser fallback) ── */ +.gl-dialog-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.gl-dialog { + background: #2a2a3e; + border-radius: 12px; + padding: 24px; + max-width: 500px; + width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); +} + +.gl-dialog h3 { + margin: 0 0 12px; + font-size: 1.2rem; + color: #fff; +} + +.gl-dialog-hint { + font-size: 0.85rem; + color: #aaa; + margin-bottom: 14px; + line-height: 1.5; +} + +.gl-dialog-input { + width: 100%; + padding: 10px 12px; + background: #1a1a2e; + border: 1px solid #444; + border-radius: 8px; + color: #fff; + font-size: 0.9rem; + outline: none; + transition: border-color 0.2s; +} + +.gl-dialog-input:focus { + border-color: #a855f7; +} + +.gl-dialog-status { + margin-top: 8px; + font-size: 0.85rem; + padding: 6px 10px; + border-radius: 6px; +} + +.gl-dialog-status.loading { + color: #a855f7; +} + +.gl-dialog-status.success { + color: #4caf50; +} + +.gl-dialog-status.error { + color: #e74c3c; +} + +.gl-dialog-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 16px; +} + +.gl-dialog-cancel { + padding: 8px 18px; + background: #3a3a4e; + color: #ccc; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; +} + +.gl-dialog-cancel:hover { + background: #4a4a5e; +} + +.gl-dialog-submit { + padding: 8px 18px; + background: #a855f7; + color: #fff; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; +} + +.gl-dialog-submit:hover:not(:disabled) { + background: #9333ea; +} + +.gl-dialog-submit:disabled { + opacity: 0.5; + cursor: not-allowed; +}