Refactor OAuth to use env vars

This commit is contained in:
2025-10-29 13:05:43 -07:00
parent 0a031469cc
commit e04c6206db
10 changed files with 256 additions and 298 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

+126 -3
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import beatleaderLogo from '$lib/assets/beatleader-logo.png';
const links = [
{ href: '/', label: 'Home' },
{ href: '/tools', label: 'Tools' },
@@ -7,7 +10,81 @@
let open = false;
const toggle = () => (open = !open);
const close = () => (open = false);
const year = new Date().getFullYear();
type BeatLeaderIdentity = {
id?: string;
name?: string;
};
type BeatLeaderPlayer = {
id?: string;
name?: string;
avatar?: string | null;
};
type BeatLeaderProfile = {
id?: string;
name?: string;
avatar?: string;
};
let user: BeatLeaderProfile | null = null;
let loginHref = '/auth/beatleader/login';
let checkingSession = true;
const getProfileUrl = (id?: string) => (id ? `https://beatleader.com/u/${encodeURIComponent(id)}` : 'https://beatleader.com');
function extractPlayer(payload: unknown): BeatLeaderProfile | null {
if (!payload || typeof payload !== 'object') return null;
const { identity, player } = payload as { identity?: BeatLeaderIdentity | null; player?: BeatLeaderPlayer | null };
const sourcePlayer = player ?? null;
const sourceIdentity = identity ?? null;
const id = sourcePlayer?.id ?? sourceIdentity?.id;
const name = sourcePlayer?.name ?? sourceIdentity?.name;
const avatar = sourcePlayer?.avatar ?? null;
if (!id && !name) return null;
return {
id: typeof id === 'string' ? id : undefined,
name: typeof name === 'string' ? name : undefined,
avatar: typeof avatar === 'string' ? avatar : undefined
};
}
onMount(() => {
const redirectTarget = `${window.location.pathname}${window.location.search}${window.location.hash}` || '/';
loginHref = `/auth/beatleader/login?redirect_uri=${encodeURIComponent(redirectTarget)}`;
(async () => {
try {
const res = await fetch('/api/beatleader/me');
if (res.ok) {
const json = (await res.json()) as unknown;
const profile = extractPlayer(json);
if (profile) {
user = profile;
}
} else if (res.status === 401) {
try {
const body = (await res.json()) as Record<string, unknown>;
const suggested = body?.login;
if (typeof suggested === 'string') {
loginHref = suggested;
}
} catch {
// ignore JSON parsing errors for 401 responses
}
}
} catch (err) {
console.error('Failed to determine BeatLeader session state', err);
} finally {
checkingSession = false;
}
})();
});
</script>
<header class="sticky top-0 z-40 backdrop-blur supports-[backdrop-filter]:bg-surface/50 border-b border-white/10">
@@ -24,7 +101,30 @@
{#each links as link}
<a href={link.href} class="text-muted hover:text-white transition">{link.label}</a>
{/each}
<a href="/tools/beatleader-compare" class="btn-neon">Compare Players</a>
{#if checkingSession}
<span class="text-sm text-muted">Connecting…</span>
{:else if user}
<a
href={getProfileUrl(user.id)}
class="flex items-center gap-3 rounded-md border border-white/10 px-3 py-1.5 text-sm transition hover:bg-white/10"
target="_blank"
rel="noreferrer noopener"
title="View your BeatLeader profile"
>
<img
src={user.avatar ?? beatleaderLogo}
alt="BeatLeader avatar"
class="h-8 w-8 rounded-full object-cover shadow-sm"
loading="lazy"
/>
<span class="font-medium text-white">{user.name ?? 'BeatLeader user'}</span>
</a>
{:else}
<a href={loginHref} class="btn-neon inline-flex items-center gap-2 px-3 py-1.5">
<img src={beatleaderLogo} alt="BeatLeader" class="h-6 w-6" />
<span>Login</span>
</a>
{/if}
</nav>
<button class="md:hidden btn-neon px-3 py-1.5" on:click={toggle} aria-expanded={open} aria-controls="mobile-nav">
@@ -39,7 +139,30 @@
{#each links as link}
<a href={link.href} on:click={close} class="text-muted hover:text-white transition">{link.label}</a>
{/each}
<a href="/tools/beatleader-compare" on:click={close} class="btn-neon w-max">Compare Players</a>
{#if checkingSession}
<span class="text-sm text-muted">Connecting…</span>
{:else if user}
<a
href={getProfileUrl(user.id)}
target="_blank"
rel="noreferrer noopener"
on:click={close}
class="flex items-center gap-3 rounded-md border border-white/10 px-3 py-2 text-sm transition hover:bg-white/10"
>
<img
src={user.avatar ?? beatleaderLogo}
alt="BeatLeader avatar"
class="h-10 w-10 rounded-full object-cover shadow-sm"
loading="lazy"
/>
<span class="font-medium text-white">{user.name ?? 'BeatLeader user'}</span>
</a>
{:else}
<a href={loginHref} on:click={close} class="btn-neon inline-flex items-center gap-2 w-max px-3 py-2">
<img src={beatleaderLogo} alt="BeatLeader" class="h-6 w-6" />
<span>Login</span>
</a>
{/if}
</div>
</div>
{/if}
+22 -8
View File
@@ -2,6 +2,7 @@ import type { Cookies } from '@sveltejs/kit';
import { dev } from '$app/environment';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { env } from '$env/dynamic/private';
const TOKEN_URL = 'https://api.beatleader.com/oauth2/token';
@@ -206,10 +207,21 @@ export async function getValidAccessToken(cookies: Cookies): Promise<string | nu
return null;
}
export function buildAuthorizeUrl(origin: string, scopes: string[]): URL {
function getClientCredentials(): { client_id: string; client_secret: string } {
const envClientId = env.BL_CLIENT_ID;
const envClientSecret = env.BL_CLIENT_SECRET;
if (envClientId && envClientSecret) {
return { client_id: envClientId, client_secret: envClientSecret };
}
const creds = readJsonPersistent<{ client_id?: string; client_secret?: string }>(CREDS_FILE);
const clientId = creds?.client_id;
if (!clientId) throw new Error('BeatLeader OAuth is not configured. Visit /tools/beatleader-oauth to set it up.');
if (creds?.client_id && creds?.client_secret) {
return { client_id: creds.client_id, client_secret: creds.client_secret };
}
throw new Error('BeatLeader OAuth is not configured. Set BL_CLIENT_ID and BL_CLIENT_SECRET.');
}
export function buildAuthorizeUrl(origin: string, scopes: string[]): URL {
const { client_id: clientId } = getClientCredentials();
const redirectUri = `${origin}/auth/beatleader/callback`;
const url = new URL('https://api.beatleader.com/oauth2/authorize');
url.searchParams.set('client_id', clientId);
@@ -222,10 +234,7 @@ export function buildAuthorizeUrl(origin: string, scopes: string[]): URL {
}
export async function exchangeCodeForTokens(origin: string, code: string): Promise<{ access_token: string; refresh_token?: string; expires_in?: number } | null> {
const creds = readJsonPersistent<{ client_id?: string; client_secret?: string }>(CREDS_FILE);
const clientId = creds?.client_id;
const clientSecret = creds?.client_secret;
if (!clientId || !clientSecret) return null;
const { client_id: clientId, client_secret: clientSecret } = getClientCredentials();
const redirectUri = `${origin}/auth/beatleader/callback`;
const res = await fetch(TOKEN_URL, {
@@ -287,7 +296,12 @@ export function storeOAuthCredentials(input: { client_id: string; client_secret:
}
export function readOAuthCredentials(): { client_id: string; client_secret: string; scopes?: string[]; redirect_urls?: string[] } | null {
return readJsonPersistent(CREDS_FILE);
try {
const { client_id: clientId, client_secret: clientSecret } = getClientCredentials();
return { client_id: clientId, client_secret: clientSecret };
} catch {
return null;
}
}