diff --git a/README.md b/README.md index e870517..0ec1153 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ To Be Played is split into three repositories: game still in the backlog, and one to set the status to 100 % when every achievement is unlocked. - **Wishlist** with store price and discount. +- **Steam account** linked in the settings with "Sign in through Steam": the user signs in on Steam and Auth gets + the proved Steam ID, never a typed-in one. A Steam ID typed in before shows as "not verified" with a button to + verify it; unlinking is possible. - **Public profile** at `/u/`, opt-in in the settings, with perfect games and average completion. - **Appearance**: accent color, corner style, wide layout, page background and banner from your own game art. Saved in Auth, so it follows you to every device. @@ -68,6 +71,8 @@ flowchart LR Core -->|"OAuth token"| Twitch["Twitch"] Core --> Speedrun["speedrun.com API"] Browser -.->|"game art"| Images["Steam and IGDB image hosts"] + Browser -.->|"Sign in through Steam"| SteamLogin["steamcommunity.com (OpenID)"] + Auth -->|"check_authentication"| SteamLogin ``` - nginx serves the built app. It has no backend of its own: the browser calls Auth and Core directly diff --git a/public/i18n/de.json b/public/i18n/de.json index a9456ed..9ce340d 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -23,14 +23,12 @@ "PASSWORD": "Passwort", "PASSWORD_CONFIRMATION": "Passwort bestätigen", "INVITE_CODE": "Einladungscode", - "STEAM_ID": "Steam-ID", "SUBMIT": "Konto erstellen", "SUCCESS": "Konto erstellt. Weiterleitung zum Login …", "HAVE_ACCOUNT": "Bereits ein Konto?", "ERROR_GENERIC": "Registrierung fehlgeschlagen. Bitte erneut versuchen.", "ERROR_EMAIL_IN_USE": "Diese Email-Adresse ist bereits registriert.", "ERROR_INVALID_INVITE_CODE": "Der Einladungscode ist ungültig oder wurde bereits verwendet.", - "ERROR_STEAM_ID_IN_USE": "Diese Steam-ID ist bereits mit einem anderen Konto verknüpft.", "DISPLAY_NAME": "Anzeigename (optional)", "ERROR_DISPLAY_NAME_IN_USE": "Dieser Anzeigename ist schon vergeben.", "ERROR_TOO_MANY_ATTEMPTS": "Zu viele fehlgeschlagene Versuche. Bitte in {{minutes}} Minuten erneut versuchen." @@ -51,8 +49,6 @@ "EN": "Englisch", "DE": "Deutsch" }, - "STEAM_ID": "Steam-ID", - "SAVE_STEAM_ID": "Steam-ID speichern", "SAVED": "Einstellungen gespeichert.", "ERROR_SAVE": "Einstellungen konnten nicht gespeichert werden.", "PROFILE": "Profil", @@ -89,7 +85,23 @@ "BACKGROUND_REMOVE": "Hintergrund entfernen", "BANNER": "Dashboard-Banner", "BANNER_HINT": "Zeigt dein meistgespieltes Spiel der letzten zwei Wochen. Öffne ein Spiel und wähle „Als Banner“, um eins festzulegen.", - "BANNER_AUTO": "Wieder automatisch" + "BANNER_AUTO": "Wieder automatisch", + "STEAM_LINK": { + "LINK": "Steam-Konto verknüpfen", + "VERIFY": "Mit Steam bestätigen", + "CHANGE": "Anderes Steam-Konto verknüpfen", + "UNLINK": "Verknüpfung lösen", + "VERIFIED": "Bestätigt", + "NOT_VERIFIED": "Nicht bestätigt", + "NONE": "Noch kein Steam-Konto verknüpft. Du meldest dich bei Steam an, und Steam nennt uns deine Steam-ID; dein Steam-Passwort sehen wir nie.", + "VERIFY_HINT": "Diese Steam-ID wurde eingetippt und ist noch nicht bestätigt. Bestätige sie mit Steam, sonst übernimmt sie ein anderes Konto, das sie bestätigt.", + "LINKED": "Steam-Konto verknüpft.", + "IN_USE": "Dieses Steam-Konto ist schon mit einem anderen Konto verknüpft.", + "CANCELLED": "Steam-Verknüpfung abgebrochen.", + "FAILED": "Steam konnte die Verknüpfung nicht bestätigen. Bitte versuch es noch einmal.", + "ERROR": "Die Steam-Verknüpfung konnte nicht starten.", + "UNLINKED": "Steam-Verknüpfung gelöst." + } }, "VALIDATION": { "EMAIL_REQUIRED": "Email ist erforderlich.", @@ -98,8 +110,6 @@ "PASSWORD_CONFIRMATION_REQUIRED": "Bitte Passwort bestätigen.", "PASSWORD_MISMATCH": "Die Passwörter stimmen nicht überein.", "INVITE_CODE_REQUIRED": "Einladungscode ist erforderlich.", - "STEAM_ID_REQUIRED": "Steam-ID ist erforderlich.", - "STEAM_ID_INVALID": "Steam-ID muss eine positive Zahl sein.", "LANGUAGE_INVALID": "Nicht unterstützte Sprache.", "DISPLAY_NAME_INVALID": "3–24 Zeichen: Buchstaben, Zahlen, Leerzeichen, _ . - (kein Leerzeichen am Anfang oder Ende)." }, @@ -113,7 +123,7 @@ "LOADING": "Lade Dashboard ...", "HELLO": "Hallo", "STEAM_GAMES": "Steam Spiele", - "NO_STEAM_ID": "Keine Steam-ID hinterlegt – jetzt in den Einstellungen eintragen", + "NO_STEAM_ID": "Noch kein Steam-Konto verknüpft – verknüpfe es in den Einstellungen", "MOST_PLAYED": "Meistgespielt", "SHOW_ALL": "Alle anzeigen", "TO_LIBRARY": "Zur Bibliothek", diff --git a/public/i18n/en.json b/public/i18n/en.json index 3f8e879..787d8d8 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -23,14 +23,12 @@ "PASSWORD": "Password", "PASSWORD_CONFIRMATION": "Confirm password", "INVITE_CODE": "Invite code", - "STEAM_ID": "Steam ID", "SUBMIT": "Create account", "SUCCESS": "Account created. Redirecting to login …", "HAVE_ACCOUNT": "Already have an account?", "ERROR_GENERIC": "Registration failed. Please try again.", "ERROR_EMAIL_IN_USE": "This email address is already registered.", "ERROR_INVALID_INVITE_CODE": "The invite code is invalid or has already been used.", - "ERROR_STEAM_ID_IN_USE": "This Steam ID is already linked to another account.", "DISPLAY_NAME": "Display name (optional)", "ERROR_DISPLAY_NAME_IN_USE": "This display name is already taken.", "ERROR_TOO_MANY_ATTEMPTS": "Too many failed attempts. Please try again in {{minutes}} minutes." @@ -51,8 +49,6 @@ "EN": "English", "DE": "German" }, - "STEAM_ID": "Steam ID", - "SAVE_STEAM_ID": "Save Steam ID", "SAVED": "Settings saved.", "ERROR_SAVE": "Could not save settings.", "PROFILE": "Profile", @@ -89,7 +85,23 @@ "BACKGROUND_REMOVE": "Remove background", "BANNER": "Dashboard banner", "BANNER_HINT": "Shows your most played game of the last two weeks. Open a game and choose \"Use as banner\" to pick one.", - "BANNER_AUTO": "Back to automatic" + "BANNER_AUTO": "Back to automatic", + "STEAM_LINK": { + "LINK": "Link Steam account", + "VERIFY": "Verify with Steam", + "CHANGE": "Link another Steam account", + "UNLINK": "Unlink", + "VERIFIED": "Verified", + "NOT_VERIFIED": "Not verified", + "NONE": "No Steam account linked yet. You sign in on Steam, and Steam tells us your Steam ID; we never see your Steam password.", + "VERIFY_HINT": "This Steam ID was typed in and is not proved yet. Verify it with Steam, or another account that proves it takes it over.", + "LINKED": "Steam account linked.", + "IN_USE": "This Steam account is already linked to another account.", + "CANCELLED": "Steam link cancelled.", + "FAILED": "Steam could not confirm the link. Please try again.", + "ERROR": "Could not start the Steam link.", + "UNLINKED": "Steam account unlinked." + } }, "VALIDATION": { "EMAIL_REQUIRED": "Email is required.", @@ -98,8 +110,6 @@ "PASSWORD_CONFIRMATION_REQUIRED": "Please confirm your password.", "PASSWORD_MISMATCH": "Passwords do not match.", "INVITE_CODE_REQUIRED": "Invite code is required.", - "STEAM_ID_REQUIRED": "Steam ID is required.", - "STEAM_ID_INVALID": "Steam ID must be a positive number.", "LANGUAGE_INVALID": "Unsupported language.", "DISPLAY_NAME_INVALID": "3–24 characters: letters, digits, spaces, _ . - (no space at the start or end)." }, @@ -113,7 +123,7 @@ "LOADING": "Loading dashboard ...", "HELLO": "Hello", "STEAM_GAMES": "Steam Games", - "NO_STEAM_ID": "No Steam ID saved yet – add it in the settings", + "NO_STEAM_ID": "No Steam account linked yet – link it in the settings", "MOST_PLAYED": "Most played", "SHOW_ALL": "Show all", "TO_LIBRARY": "Go to library", diff --git a/src/components/SteamAccountCard.jsx b/src/components/SteamAccountCard.jsx new file mode 100644 index 0000000..8b4c31e --- /dev/null +++ b/src/components/SteamAccountCard.jsx @@ -0,0 +1,98 @@ +import {useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import {BadgeCheck, CircleAlert, Gamepad2, Link2, Loader2, Unlink} from 'lucide-react'; +import {toast} from 'sonner'; + +import {Badge} from '@/components/ui/badge'; +import {Button} from '@/components/ui/button'; +import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; +import useSessionExpired from '../hooks/useSessionExpired.js'; +import {markSession} from '../utils/auth.js'; +import {startSteamLink, unlinkSteam} from '../utils/steamLink.js'; + +/** + * The linked Steam account. A Steam ID comes only from "Sign in through Steam"; one typed in before that is shown + * as not verified. + */ +export default function SteamAccountCard({steamId, verified, onUnlinked}) { + const {t} = useTranslation(); + const expireSession = useSessionExpired(); + const [busy, setBusy] = useState(false); + + const link = async () => { + setBusy(true); + try { + const response = await startSteamLink(); + if (response.ok) return; // on the way to Steam: the button stays busy + if (response.status === 401) { + expireSession(); + } else { + toast.error(t('SETTINGS.STEAM_LINK.ERROR')); + } + } catch (err) { + console.error('Steam link error:', err); + toast.error(t('SETTINGS.STEAM_LINK.ERROR')); + } + setBusy(false); + }; + + const unlink = async () => { + setBusy(true); + try { + const response = await unlinkSteam(); + if (response.status === 401) { + expireSession(); + return; + } + if (!response.ok) { + toast.error(t('SETTINGS.STEAM_LINK.ERROR')); + return; + } + // The new access cookie has no Steam ID; the new stamp makes the header load again. + markSession(); + toast.success(t('SETTINGS.STEAM_LINK.UNLINKED')); + onUnlinked(); + } catch (err) { + console.error('Steam unlink error:', err); + toast.error(t('INFO.ERROR.NETWORK_FAILURE')); + } finally { + setBusy(false); + } + }; + + return ( + + + {t('SETTINGS.STEAM')} + + + {steamId ? ( +
+ {steamId} + {verified ? ( + {t('SETTINGS.STEAM_LINK.VERIFIED')} + ) : ( + {t('SETTINGS.STEAM_LINK.NOT_VERIFIED')} + )} +
+ ) : ( +

{t('SETTINGS.STEAM_LINK.NONE')}

+ )} + {steamId && !verified && ( +

{t('SETTINGS.STEAM_LINK.VERIFY_HINT')}

+ )} +
+ + {steamId && ( + + )} +
+
+
+ ); +} diff --git a/src/hooks/useSteamProfile.js b/src/hooks/useSteamProfile.js index d9157e4..a81e62f 100644 --- a/src/hooks/useSteamProfile.js +++ b/src/hooks/useSteamProfile.js @@ -5,7 +5,7 @@ import {sessionStamp} from '../utils/auth.js'; /** * The user's Steam name and avatar from Core, or null while loading, without a Steam ID or on any error. Only * decoration, so a failed call shows the default icon and never ends the session. Loads again after a new login - * or a settings save (see sessionStamp) on the next render, e.g. the next navigation. + * or a Steam link or unlink (see sessionStamp) on the next render, e.g. the next navigation. */ export default function useSteamProfile(enabled) { const [profile, setProfile] = useState(null); diff --git a/src/pages/RegistrationPage.jsx b/src/pages/RegistrationPage.jsx index 70e8f89..ead87e3 100644 --- a/src/pages/RegistrationPage.jsx +++ b/src/pages/RegistrationPage.jsx @@ -17,7 +17,6 @@ import {clientFetch} from '../utils/api.js'; const BACKEND_ERROR_KEY = { EMAIL_ALREADY_IN_USE: 'REGISTRATION.ERROR_EMAIL_IN_USE', INVALID_INVITATION_CODE: 'REGISTRATION.ERROR_INVALID_INVITE_CODE', - STEAM_ID_ALREADY_IN_USE: 'REGISTRATION.ERROR_STEAM_ID_IN_USE', DISPLAY_NAME_ALREADY_IN_USE: 'REGISTRATION.ERROR_DISPLAY_NAME_IN_USE', TOO_MANY_ATTEMPTS: 'REGISTRATION.ERROR_TOO_MANY_ATTEMPTS', }; @@ -38,7 +37,6 @@ export default function RegistrationPage() { password: '', passwordConfirmation: '', inviteCode: '', - steamId: '', displayName: '', }, }); @@ -54,7 +52,6 @@ export default function RegistrationPage() { email: values.email, password: values.password, inviteCode: values.inviteCode, - steamId: values.steamId, displayName: values.displayName, }), }); @@ -140,16 +137,6 @@ export default function RegistrationPage() { /> - - - -