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 verbundenGOG 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;
+}