Refactor OAuth to use env vars
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user