<script lang="ts">
  import { onMount } from 'svelte';
  let hasSteam: boolean = false;
  let steamId: string | null = null;
  let oauthConnected: boolean = false;
  let hasCreds: boolean = false;
  let hasSession: boolean = false;
  async function loadStatus() {
    try {
      const res = await fetch('/api/beatleader/oauth/status');
      if (res.ok) {
        const data = await res.json();
        oauthConnected = Boolean(data?.connected);
        hasCreds = Boolean(data?.hasCreds);
        hasSession = Boolean(data?.hasSession);
        hasSteam = Boolean(data?.hasSteam);
        steamId = (data?.steamId ?? null) as string | null;
      }
    } catch {}
  }
  onMount(loadStatus);
</script>

<section class="py-8 prose prose-invert max-w-none">
  <h1 class="font-display tracking-widest">BeatLeader Authentication</h1>
  <div class="not-prose rounded border border-yellow-500/40 bg-yellow-500/10 text-yellow-200 px-3 py-2 mt-2 text-sm">
    Educational use only: The information and resources on this page are for learning purposes. Do not use them for real authentication or accessing accounts.
  </div>
  <p>
    For this app, I explored three ways to access your BeatLeader data: Steam, OAuth, or a website‑style session.
  </p>
  <p>
    I wanted tools like <a href="/tools/compare-histories">Compare Players</a> to show unranked star ratings when your BeatLeader account is a supporter and <code>ShowAllRatings</code> is enabled. That turns out to not be possible without implementing Steam ticket handling using the Steamworks SDK. <strong>The rest of the notes here were written before I realized that.</strong>
  </p>


  <!-- Top navigation -->
  <nav class="not-prose mt-4 flex flex-wrap gap-2">
    <a class="rounded bg-white/10 px-3 py-1 text-sm hover:bg-white/20" href="#steam">Steam</a>
    <a class="rounded bg-white/10 px-3 py-1 text-sm hover:bg-white/20" href="#oauth">OAuth</a>
    <a class="rounded bg-white/10 px-3 py-1 text-sm hover:bg-white/20" href="#session">Website Session</a>
    <a class="rounded bg-white/10 px-3 py-1 text-sm hover:bg-white/20" href="#setup">OAuth App Setup</a>
    <a class="rounded bg-white/10 px-3 py-1 text-sm hover:bg-white/20" href="#unranked">Unranked Stars</a>
    <a class="rounded bg-white/10 px-3 py-1 text-sm hover:bg-white/20" href="#usage">How It’s Used</a>
    <a class="rounded bg-white/10 px-3 py-1 text-sm hover:bg-white/20" href="#sign-out">Sign out</a>
    <a class="rounded bg-white/10 px-3 py-1 text-sm hover:bg-white/20" href="#troubleshooting">Troubleshooting</a>
  </nav>

  <!-- Quick actions moved to tool page; link instead -->
  <div class="not-prose mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
    <a class="btn-neon" href="/tools/beatleader-auth">Open BeatLeader Auth Tool</a>
  </div>

  <!-- Current status summary -->
  <div class="not-prose mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
    <div class="rounded border border-white/10 p-3">
      <div class="text-sm">Steam</div>
      <div class="text-xs text-muted mt-1">{hasSteam ? `Linked (${steamId})` : 'Not linked'}</div>
      {#if hasSteam}
        <form method="POST" action="/auth/steam/logout" class="mt-2">
          <button class="rounded bg-white/10 px-2 py-1 text-xs hover:bg-white/20">Sign out of Steam</button>
        </form>
      {/if}
    </div>
    <div class="rounded border border-white/10 p-3">
      <div class="text-sm">OAuth</div>
      <div class="text-xs text-muted mt-1">{oauthConnected ? 'Connected' : 'Not connected'}</div>
      {#if oauthConnected}
        <form method="POST" action="/auth/beatleader/logout" class="mt-2">
          <button class="rounded bg-white/10 px-2 py-1 text-xs hover:bg-white/20">Sign out of OAuth</button>
        </form>
      {/if}
    </div>
    <div class="rounded border border-white/10 p-3">
      <div class="text-sm">Website Session</div>
      <div class="text-xs text-muted mt-1">{hasSession ? 'Active (captured via Steam)' : 'Not active'}</div>
      {#if hasSession}
        <form on:submit|preventDefault={async () => { await fetch('/auth/beatleader/session/logout', { method: 'POST' }); await loadStatus(); }} class="mt-2">
          <button class="rounded bg-white/10 px-2 py-1 text-xs hover:bg-white/20">Clear Session</button>
        </form>
      {/if}
    </div>
  </div>

  <p class="mt-3 text-sm text-muted">
    Default API auth is <strong>Steam</strong>. You can override per request using <code>?auth=steam|oauth|session|auto|none</code>.
  </p>

  <h2 id="steam">Steam Login</h2>
  <p>
    Authenticate via Steam OpenID to link your Steam account. Then use the <a href="/tools/beatleader-auth">BeatLeader Auth Tool</a> with your Steam session ticket to capture a website session.
  </p>

  <h2 id="oauth">OAuth Login</h2>
  <ul>
    <li><code>scp:profile</code> to read your user profile.</li>
    <li><code>scp:offline_access</code> to refresh your session without re‑prompting (optional).</li>
  </ul>
  <p>
    You can still login with OAuth when needed: <a class="btn-neon" href="/auth/beatleader/login?redirect_uri=%2Ftools%2Fcompare-histories">Login with BeatLeader (OAuth)</a>
  </p>
  <p>
    After OAuth login, we attempt to enable <code>ShowAllRatings</code> automatically. If your BL account is not a supporter, BL will ignore it.
  </p>

  <h2 id="session">Session Login (Website‑style)</h2>
  <p>
    Most users authenticate on the BeatLeader website using Steam. Use the <a href="/tools/beatleader-auth">BeatLeader Auth Tool</a> to submit a Steam session ticket; we’ll create a BL website session for server‑side calls where OAuth is not honored.
  </p>
  <p>
    The current session status is available via <code>GET /api/beatleader/oauth/status</code> as <code>hasSession</code>.
  </p>

  <h2 id="unranked">Requirements to see unranked star ratings</h2>
  <ul>
    <li>You must authenticate with a user identity that BeatLeader recognizes for profile flags like <code>ShowAllRatings</code>. For the endpoints listed below, this currently means a website session cookie, not an OAuth bearer.</li>
    <li>Your BeatLeader account must be a supporter (tipper/supporter/sponsor).</li>
    <li>Your BL profile setting <code>ShowAllRatings</code> must be enabled. We auto‑enable it after login; if you're not a supporter, BL will ignore it.</li>
  </ul>

  <h3 id="oauth-vs-session">OAuth tokens vs. website session for scores/stars</h3>
  <p>
    Some read endpoints on BeatLeader currently do <strong>not</strong> honor OAuth bearer tokens for determining the current user, and instead rely on the cookie‑authenticated website session:
  </p>
  <ul>
    <li><code>GET /player/&#123;id&#125;/scores</code></li>
    <li><code>GET /leaderboards/hash/&#123;hash&#125;</code></li>
  </ul>
  <p>
    For these endpoints, an OAuth <code>Authorization: Bearer ...</code> header will not affect user‑specific visibility like <code>ShowAllRatings</code>. They read the user from the website session cookie. In our debug panel this appears as <em>Using auth: oauth</em> with <em>Last Cookie: absent</em> — and unranked stars may be hidden even if your account is eligible.
  </p>
  <p><strong>Workarounds</strong></p>
  <ul>
    <li><strong>Use a website session</strong>: Use the <a href="/tools/beatleader-auth">BeatLeader Auth Tool</a> with your Steam session ticket. We store the BL session cookie and attach it to server calls. Debug will show <em>Using auth: session</em> and <em>Last Cookie: present</em>.</li>
    <li><strong>Force session for a request</strong>: Append <code>?auth=session</code> to our API URLs to prefer website session over OAuth for that call. Example: <code>/api/beatleader?path=/leaderboards/hash/&#123;hash&#125;&auth=session</code>.</li>
    <li><strong>Prefer session when both exist</strong>: Our tools default to <code>?auth=auto</code> (use OAuth if present, then session). If you have both and need session for these endpoints, explicitly pass <code>?auth=session</code> or temporarily sign out OAuth at <a href="/auth/beatleader/logout">/auth/beatleader/logout</a>.</li>
  </ul>
  <p>
    Other endpoints (e.g., <code>/oauth2/identity</code>, some write operations, and endpoints that explicitly check OAuth scopes) do honor OAuth bearer tokens. This limitation is specific to a few read endpoints listed above.
  </p>

  <h2 id="setup">OAuth App Setup (for developers)</h2>
  <ol>
    <li>
      Configure BeatLeader OAuth credentials here: <strike>BL OAuth Setup</strike>.
      If you already have credentials, you can paste them there.
    </li>
    <li>
      Use the OAuth Login above; we request <code>scp:offline_access</code> to allow refresh without re‑prompting.
    </li>
  </ol>
  <p>
    <strong>Multi‑environment setup:</strong> If you deploy to multiple environments (dev, staging, production), you must register <strong>all redirect URIs</strong> in your BeatLeader OAuth application. For example: <code>http://localhost:5173/auth/beatleader/callback</code>, <code>https://staging.example.com/auth/beatleader/callback</code>, and <code>https://example.com/auth/beatleader/callback</code>. The app automatically uses the correct redirect URI based on the current environment's origin.
  </p>

  <h2 id="usage">How this app uses your auth</h2>
  <ul>
    <li>Default auth mode is <strong>Steam</strong>. You can override per‑request with <code>?auth=steam|oauth|session|auto|none</code>.</li>
    <li>Server routes use your OAuth token when available; otherwise some endpoints fall back to your BL session cookie:</li>
    <ul>
      <li><code>/api/beatleader?path=/user</code> supports OAuth token or session cookie.</li>
      <li><code>/api/beatleader/user/profile</code> (PATCH) uses OAuth token or session cookie to toggle flags like <code>ShowAllRatings</code>.</li>
      <li>Other public GETs are proxied unauthenticated or with your token when relevant.</li>
    </ul>
    <li>Other tools call these endpoints and will include unranked stars if your user auth allows it and <code>ShowAllRatings</code> is enabled.</li>
  </ul>

  <h2 id="desktop">Desktop app (Tauri) implementation plan</h2>
  <p>
    The desktop build of this app will automatically obtain a Steam session ticket using the Steamworks SDK and create your BeatLeader website session without manual input. High‑level plan:
  </p>
  <ol>
    <li><strong>Integrate Steamworks in Tauri</strong>: add Steamworks SDK and implement a Tauri command (e.g., <code>getSteamSessionTicket</code>) that calls <code>ISteamUser.GetAuthSessionTicket</code> and returns the ticket string.</li>
    <li><strong>Wire the ticket endpoint</strong>: make <code>GET /api/steam/ticket</code> call the Tauri command. Until Tauri is in place, it supports a local helper via <code>BL_STEAM_TICKET_CMD</code> that outputs a ticket to stdout.</li>
    <li><strong>Create BL website session</strong>: the tool at <a href="/tools/beatleader-auth">BeatLeader Auth Tool</a> fetches the Steam ticket and then POSTs it to <code>/auth/beatleader/steam-ticket</code> (forwards to <code>api.beatleader.com/signin</code> with <code>provider=steamTicket</code>). We capture BL cookies server‑side.</li>
    <li><strong>Verify</strong>: <code>GET /api/beatleader/oauth/status</code> should report <code>hasSession: true</code>.</li>
    <li><strong>Use session for specific endpoints</strong>: For <code>/player/&#123;id&#125;/scores</code> and <code>/leaderboards/hash/&#123;hash&#125;</code> (where OAuth is ignored), call our proxy with <code>?auth=session</code> or keep <code>?auth=auto</code> when no OAuth token is present.</li>
  </ol>
  <p class="text-xs text-muted">
    Security note: the Steam session ticket is used only to establish a BeatLeader website session and is never stored. We store only the BL cookies (httpOnly) necessary for server‑side requests.
  </p>

  <h2>Why connect?</h2>
  <ul>
    <li>Compare Players can show BeatLeader star ratings alongside map tiles.</li>
    <li>If your BL account grants visibility into unranked stars, those will appear too.</li>
  </ul>

  <h2>Technical Details</h2>
  <p>
    For developers curious about the implementation, here's how BeatLeader's OAuth2.0 system works:
  </p>

  <h3>BeatLeader Server (Backend)</h3>
  <p>
    BeatLeader uses <strong>OpenIddict</strong> as its OAuth2.0/OpenID Connect server implementation. The token creation is handled automatically by OpenIddict rather than custom code.
  </p>

  <h4>Key Components:</h4>
  <ul>
    <li><strong>Token Endpoint:</strong> <code>/oauth2/token</code> - Handled automatically by OpenIddict</li>
    <li><strong>Authorization Endpoint:</strong> <code>/oauth2/authorize</code> - Custom controller in <code>AuthenticationController.cs</code></li>
    <li><strong>User Info Endpoint:</strong> <code>/oauth2/identity</code> - Returns user profile data</li>
    <li><strong>Client Management:</strong> <code>/developer/app*</code> - OAuth application registration and management</li>
  </ul>

  <h4>Authorization Flow:</h4>
  <ol>
    <li><strong>GET /oauth2/authorize:</strong> Renders consent screen and validates user authentication</li>
    <li><strong>POST /oauth2/authorize:</strong> Processes user consent and creates authorization code via <code>SignIn()</code></li>
    <li><strong>POST /oauth2/token:</strong> OpenIddict automatically exchanges authorization code for access/refresh tokens</li>
  </ol>

  <h4>Configuration (Startup.cs):</h4>
  <pre><code>options.SetAuthorizationEndpointUris("oauth2/authorize")
       .SetTokenEndpointUris("oauth2/token")
       .SetUserinfoEndpointUris("oauth2/identity");

options.AllowAuthorizationCodeFlow()
       .AllowRefreshTokenFlow();</code></pre>

  <h3>BeatLeader Website (Frontend)</h3>
  <p>
    The website provides the OAuth consent UI and developer portal, but delegates all token operations to the API.
  </p>

  <h4>Key Components:</h4>
  <ul>
    <li><strong>Consent Screen:</strong> <code>OauthSignIn.svelte</code> - Renders the authorization approval UI</li>
    <li><strong>Developer Portal:</strong> <code>DeveloperPortal.svelte</code> - OAuth application management</li>
    <li><strong>App Management:</strong> <code>OauthApp.svelte</code> - Create/edit OAuth applications</li>
  </ul>

  <h4>Authorization Flow:</h4>
  <ol>
    <li><strong>Consent UI:</strong> Posts to <code>BL_API_URL + 'oauth2/authorize'</code> with user approval</li>
    <li><strong>Client Info:</strong> Fetches app details from <code>BL_API_URL + 'oauthclient/info'</code></li>
    <li><strong>CSRF Protection:</strong> Uses antiforgery tokens from <code>BL_API_URL + 'oauthclient/antiforgery'</code></li>
  </ol>

  <h4>Developer Portal Features:</h4>
  <ul>
    <li>OAuth application registration with custom client IDs</li>
    <li>Scope management (profile, clan, offline_access)</li>
    <li>Redirect URL configuration</li>
    <li>Client secret generation and reset</li>
    <li>Test authorization URL generation</li>
  </ul>

  <h3>OAuth Flow Summary</h3>
  <ol>
    <li>Client redirects user to <code>/oauth2/authorize</code> with client_id, scope, response_type=code</li>
    <li>BeatLeader validates user authentication and renders consent screen</li>
    <li>User approves → authorization code generated via OpenIddict</li>
    <li>Client exchanges code for tokens at <code>/oauth2/token</code> (handled by OpenIddict)</li>
    <li>Client uses access token for API calls, refresh token for renewal</li>
  </ol>

  <h3>Security Features</h3>
  <ul>
    <li><strong>CSRF Protection:</strong> Antiforgery tokens on consent forms</li>
    <li><strong>Scope Validation:</strong> Only requested scopes are granted</li>
    <li><strong>Client Validation:</strong> Registered applications only</li>
    <li><strong>Token Security:</strong> Signed/encrypted tokens with X.509 certificates</li>
    <li><strong>Authorization Storage:</strong> Permanent authorizations to avoid repeated consent</li>
  </ul>

  <h2 id="sign-out">Sign out</h2>
  <ul>
    <li><strong>Everything:</strong> <code>POST /auth/beatleader/logout-all</code> (clears OAuth tokens and session)</li>
    <li><strong>Only OAuth tokens:</strong> visit <code>/auth/beatleader/logout</code></li>
    <li><strong>Only session cookie:</strong> <code>POST /auth/beatleader/session/logout</code></li>
  </ul>
  <pre><code>curl -X POST 'https://&#123;your-host&#125;/auth/beatleader/logout-all'</code></pre>

  <h2 id="troubleshooting">Troubleshooting</h2>
  <ul>
    <li>Unranked stars missing: confirm you’re logged in with a supporter BL account and that <code>ShowAllRatings</code> is enabled.</li>
    <li>Check connection: <code>GET /api/beatleader/oauth/status</code> shows <code>connected</code>, <code>hasCreds</code>, and <code>hasSession</code>.</li>
    <li>Re‑login: use OAuth at <code>/auth/beatleader/login</code> or submit a fresh Steam session ticket in the <a href="/tools/beatleader-auth">BeatLeader Auth Tool</a>.</li>
    <li>If the debug panel shows <em>Using auth: oauth</em> and <em>Last Cookie: absent</em> on <code>/player/&#123;id&#125;/scores</code> or <code>/leaderboards/hash/&#123;hash&#125;</code>, force session with <code>?auth=session</code> and ensure your website session is active via the tool.</li>
    <li>Redirect mismatch: ensure your app’s redirect in BL developer portal matches <code>&#123;origin&#125;/auth/beatleader/callback</code>.</li>
  </ul>
</section>


