GOG Login: Nahtloser Auth-Flow wie Steam (kein Code-Paste noetig)
- Electron: Fängt GOG Redirect automatisch ab (will-redirect Interceptor) - Code-Exchange passiert im Hintergrund, User sieht nur Erfolgs-Popup - GOG Auth-URLs (auth.gog.com, login.gog.com, embed.gog.com) in Popup erlaubt - Server: GET /gog/login Redirect + POST /gog/exchange Endpoint - Browser-Fallback: Code-Paste Dialog falls nicht in Electron - gog.ts: Feste redirect_uri (embed.gog.com/on_login_success) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
af2448a5bd
commit
3e8febb851
5 changed files with 281 additions and 34 deletions
|
|
@ -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 && (
|
||||
<div className="gl-dialog-overlay" onClick={() => setGogDialogOpen(false)}>
|
||||
<div className="gl-dialog" onClick={e => e.stopPropagation()}>
|
||||
<h3>🟣 GOG verbinden</h3>
|
||||
<p className="gl-dialog-hint">
|
||||
Nach dem GOG-Login wirst du auf eine Seite weitergeleitet.
|
||||
Kopiere die <strong>komplette URL</strong> aus der Adressleiste und füge sie hier ein:
|
||||
</p>
|
||||
<input
|
||||
className="gl-dialog-input"
|
||||
type="text"
|
||||
placeholder="https://embed.gog.com/on_login_success?code=..."
|
||||
value={gogCode}
|
||||
onChange={e => setGogCode(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') submitGogCode(); }}
|
||||
disabled={gogStatus === 'loading' || gogStatus === 'success'}
|
||||
autoFocus
|
||||
/>
|
||||
{gogStatusMsg && (
|
||||
<p className={`gl-dialog-status ${gogStatus}`}>{gogStatusMsg}</p>
|
||||
)}
|
||||
<div className="gl-dialog-actions">
|
||||
<button onClick={() => setGogDialogOpen(false)} className="gl-dialog-cancel">Abbrechen</button>
|
||||
<button
|
||||
onClick={submitGogCode}
|
||||
className="gl-dialog-submit"
|
||||
disabled={!gogCode.trim() || gogStatus === 'loading' || gogStatus === 'success'}
|
||||
>
|
||||
{gogStatus === 'loading' ? 'Verbinde...' : 'Verbinden'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue