Refactor OAuth to use env vars
This commit is contained in:
parent
0a031469cc
commit
e04c6206db
2
.cursorignore
Normal file
2
.cursorignore
Normal file
@ -0,0 +1,2 @@
|
||||
.env
|
||||
archive
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -22,3 +22,6 @@ Thumbs.db
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.data
|
||||
|
||||
.env
|
||||
archive
|
||||
BIN
src/lib/assets/beatleader-logo.png
Normal file
BIN
src/lib/assets/beatleader-logo.png
Normal file
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
65
src/routes/api/beatleader/me/+server.ts
Normal file
65
src/routes/api/beatleader/me/+server.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { createBeatLeaderAPI } from '$lib/server/beatleader';
|
||||
import { clearTokens, getValidAccessToken } from '$lib/server/beatleaderAuth';
|
||||
|
||||
type BeatLeaderIdentity = { id?: string; name?: string };
|
||||
type BeatLeaderPlayer = { id?: string; name?: string; avatar?: string | null };
|
||||
|
||||
const IDENTITY_ENDPOINT = 'https://api.beatleader.com/oauth2/identity';
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies, fetch }) => {
|
||||
const token = await getValidAccessToken(cookies);
|
||||
if (!token) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Unauthorized', login: '/auth/beatleader/login' }),
|
||||
{ status: 401, headers: { 'content-type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const identityRes = await fetch(IDENTITY_ENDPOINT, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (identityRes.status === 401) {
|
||||
clearTokens(cookies);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Unauthorized', login: '/auth/beatleader/login' }),
|
||||
{ status: 401, headers: { 'content-type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
if (!identityRes.ok) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Identity lookup failed: ${identityRes.status}` }),
|
||||
{ status: 502, headers: { 'content-type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const identityJson = (await identityRes.json()) as BeatLeaderIdentity;
|
||||
const identity: BeatLeaderIdentity = {
|
||||
id: typeof identityJson.id === 'string' ? identityJson.id : undefined,
|
||||
name: typeof identityJson.name === 'string' ? identityJson.name : undefined
|
||||
};
|
||||
const playerId = identity.id ?? null;
|
||||
|
||||
let player: BeatLeaderPlayer | null = null;
|
||||
if (playerId) {
|
||||
try {
|
||||
const api = createBeatLeaderAPI(fetch, token, undefined);
|
||||
const raw = (await api.getPlayer(playerId)) as Record<string, unknown>;
|
||||
const candidate: BeatLeaderPlayer = {
|
||||
id: typeof raw?.id === 'string' ? (raw.id as string) : playerId,
|
||||
name: typeof raw?.name === 'string' ? (raw.name as string) : identity.name,
|
||||
avatar: typeof raw?.avatar === 'string' ? (raw.avatar as string) : null
|
||||
};
|
||||
player = candidate;
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch BeatLeader player profile', err);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ identity, player }), {
|
||||
headers: { 'content-type': 'application/json' }
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { dev } from '$app/environment';
|
||||
</script>
|
||||
|
||||
<section class="py-8 prose prose-invert max-w-none">
|
||||
<h1 class="font-display tracking-widest">Guides</h1>
|
||||
<p>Community-written tips and guides for improving your Beat Saber game. Contributions welcome.</p>
|
||||
|
||||
<div class="not-prose grid gap-4 sm:grid-cols-2 lg:grid-cols-3 mt-6">
|
||||
{#if dev}
|
||||
<a href="/guides/beatleader-auth" class="card-surface p-5 block">
|
||||
<h3 class="font-semibold">BeatLeader Authentication</h3>
|
||||
<p class="mt-1 text-sm text-muted">Connect BeatLeader to enhance tools like Compare Players.</p>
|
||||
</a>
|
||||
{/if}
|
||||
<a href="/guides/finding-new-songs" class="card-surface p-5 block">
|
||||
<h3 class="font-semibold">Finding New Songs (BeatLeader)</h3>
|
||||
<p class="mt-1 text-sm text-muted">Month-by-month search using unranked stars, tech rating, and friend filters.</p>
|
||||
|
||||
12
src/routes/guides/beatleader-auth/+page.server.ts
Normal file
12
src/routes/guides/beatleader-auth/+page.server.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
if (!dev) {
|
||||
throw error(404, 'Not found');
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
16
src/routes/tools/+layout.server.ts
Normal file
16
src/routes/tools/+layout.server.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { getValidAccessToken } from '$lib/server/beatleaderAuth';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const load: LayoutServerLoad = async ({ cookies, url }) => {
|
||||
const token = await getValidAccessToken(cookies);
|
||||
if (!token) {
|
||||
const pathWithQuery = `${url.pathname}${url.search}` || '/tools';
|
||||
throw redirect(302, `/auth/beatleader/login?redirect_uri=${encodeURIComponent(pathWithQuery)}`);
|
||||
}
|
||||
|
||||
return { hasBeatLeaderOAuth: true };
|
||||
};
|
||||
|
||||
@ -1,283 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let name = '';
|
||||
let clientId = '';
|
||||
let origin = '';
|
||||
let redirectUrls = '';
|
||||
let scopes: string[] = ['scp:profile', 'scp:offline_access'];
|
||||
let iconFile: File | null = null;
|
||||
let message: string | null = null;
|
||||
let created: any = null;
|
||||
let existing: { client_id: string; client_secret: string } | null = null;
|
||||
let manualClientId = '';
|
||||
let manualClientSecret = '';
|
||||
let recommendedClientId = generateDefaultClientId();
|
||||
let copying: string | null = null;
|
||||
let returnTo: string | null = null;
|
||||
let blCredsStatus: 'unknown' | 'configured' | 'missing' = 'unknown';
|
||||
let blManageUrl: string | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
if (browser) {
|
||||
try { origin = location.origin; } catch {}
|
||||
try { redirectUrls = origin + '/auth/beatleader/callback'; } catch {}
|
||||
}
|
||||
try {
|
||||
const sp = new URLSearchParams(location.search);
|
||||
const ret = sp.get('return');
|
||||
if (ret) returnTo = ret;
|
||||
} catch {}
|
||||
try {
|
||||
const redirect = location.pathname + (location.search || '');
|
||||
blManageUrl = `/tools/beatleader-oauth?return=${encodeURIComponent(redirect)}`;
|
||||
const statusRes = await fetch('/api/beatleader/oauth/status');
|
||||
if (statusRes.ok) {
|
||||
const st: any = await statusRes.json();
|
||||
blCredsStatus = st?.hasCreds ? 'configured' : 'missing';
|
||||
} else {
|
||||
blCredsStatus = 'unknown';
|
||||
}
|
||||
} catch {
|
||||
blCredsStatus = 'unknown';
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/beatleader/oauth/creds');
|
||||
if (res.ok) {
|
||||
const data: any = await res.json();
|
||||
const hasId = typeof data?.client_id === 'string' && data.client_id.length > 0;
|
||||
const hasSecret = typeof data?.client_secret === 'string' && data.client_secret.length > 0;
|
||||
existing = hasId && hasSecret ? { client_id: data.client_id, client_secret: data.client_secret } : null;
|
||||
} else {
|
||||
existing = null;
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
|
||||
function onIconChange(ev: Event) {
|
||||
const input = ev.target as HTMLInputElement;
|
||||
const f = input.files?.[0] ?? null;
|
||||
if (!f) {
|
||||
iconFile = null;
|
||||
return;
|
||||
}
|
||||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
|
||||
if (!validTypes.includes(f.type)) {
|
||||
message = 'Icon must be JPG, PNG, or GIF';
|
||||
iconFile = null;
|
||||
return;
|
||||
}
|
||||
if (f.size > 5 * 1024 * 1024) {
|
||||
message = 'Icon must be under 5MB';
|
||||
iconFile = null;
|
||||
return;
|
||||
}
|
||||
message = null;
|
||||
iconFile = f;
|
||||
}
|
||||
|
||||
async function saveManual(ev: SubmitEvent) {
|
||||
ev.preventDefault();
|
||||
message = null;
|
||||
try {
|
||||
const res = await fetch('/api/beatleader/oauth/creds', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ client_id: manualClientId, client_secret: manualClientSecret })
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
existing = { client_id: manualClientId, client_secret: manualClientSecret };
|
||||
manualClientId = '';
|
||||
manualClientSecret = '';
|
||||
message = 'Saved credentials.';
|
||||
} catch (err) {
|
||||
message = err instanceof Error ? err.message : 'Failed to save credentials';
|
||||
}
|
||||
}
|
||||
|
||||
async function register(ev: SubmitEvent) {
|
||||
ev.preventDefault();
|
||||
message = null;
|
||||
created = null;
|
||||
try {
|
||||
// Perform registration directly against BeatLeader using browser cookies (must be logged in on beatleader.com)
|
||||
const url = new URL('https://api.beatleader.com/developer/app');
|
||||
url.searchParams.set('name', name);
|
||||
url.searchParams.set('clientId', clientId || generateClientId());
|
||||
url.searchParams.set('scopes', scopes.join(','));
|
||||
url.searchParams.set('redirectUrls', redirectUrls);
|
||||
|
||||
let blRes: Response;
|
||||
if (iconFile) {
|
||||
const buf = await iconFile.arrayBuffer();
|
||||
blRes = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
body: buf,
|
||||
credentials: 'include'
|
||||
});
|
||||
} else {
|
||||
blRes = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
}
|
||||
|
||||
if (!blRes.ok) throw new Error(await blRes.text());
|
||||
const data = await blRes.json();
|
||||
created = data;
|
||||
|
||||
const savedClientId = data?.clientId ?? data?.client_id;
|
||||
const savedClientSecret = data?.clientSecret ?? data?.client_secret;
|
||||
if (!savedClientId || !savedClientSecret) throw new Error('BeatLeader response missing credentials');
|
||||
|
||||
// Persist credentials locally
|
||||
const saveRes = await fetch('/api/beatleader/oauth/creds', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ client_id: savedClientId, client_secret: savedClientSecret })
|
||||
});
|
||||
if (!saveRes.ok) throw new Error(await saveRes.text());
|
||||
|
||||
existing = { client_id: savedClientId, client_secret: savedClientSecret };
|
||||
message = 'Registered successfully. Credentials saved.';
|
||||
} catch (err) {
|
||||
const base = err instanceof Error ? err.message : 'Registration failed';
|
||||
message = base || 'Registration failed. Ensure you are logged into BeatLeader in this browser.';
|
||||
}
|
||||
}
|
||||
|
||||
function generateClientId(): string {
|
||||
return 'bl_' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
function generateDefaultClientId(): string {
|
||||
try {
|
||||
// Prefer uuid for readability; strip dashes
|
||||
const uuid = (crypto?.randomUUID?.() ?? '') as string;
|
||||
if (uuid) return 'bl_' + uuid.replace(/-/g, '');
|
||||
} catch {}
|
||||
return generateClientId();
|
||||
}
|
||||
|
||||
async function copy(text: string, kind: string) {
|
||||
try {
|
||||
copying = kind;
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setTimeout(() => (copying = null), 800);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="py-8">
|
||||
<h1 class="font-display text-3xl sm:text-4xl">BeatLeader OAuth Setup</h1>
|
||||
<p class="mt-2 text-muted">Register an OAuth application and store credentials for this site.</p>
|
||||
|
||||
<div class="mt-2 text-xs text-muted flex flex-wrap items-center gap-2">
|
||||
{#if blCredsStatus === 'unknown'}
|
||||
<span class="rounded bg-white/10 px-2 py-1">Checking BeatLeader access…</span>
|
||||
{:else if blCredsStatus === 'configured'}
|
||||
<span class="rounded bg-emerald-500/20 text-emerald-300 px-2 py-1">BeatLeader access: Connected</span>
|
||||
{#if blManageUrl}
|
||||
<a class="underline hover:text-white" href={blManageUrl}>Manage</a>
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="rounded bg-rose-500/20 text-rose-300 px-2 py-1">BeatLeader access: Not connected</span>
|
||||
{#if blManageUrl}
|
||||
<a class="underline hover:text-white" href={blManageUrl}>Configure</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if returnTo}
|
||||
<div class="mt-4">
|
||||
<a class="rounded-md border border-white/10 px-3 py-1.5 text-sm hover:border-white/20" href={returnTo}>Return to previous page</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-6 grid gap-3 max-w-xl text-sm">
|
||||
<div class="text-muted">Manual setup (recommended):</div>
|
||||
<ol class="list-decimal pl-5 grid gap-2">
|
||||
<li>
|
||||
Open the BeatLeader developer portal:
|
||||
<a class="underline" href="https://beatleader.com/developer" target="_blank" rel="noreferrer">beatleader.com/developer</a>
|
||||
</li>
|
||||
<li>
|
||||
Create a new application using the following values:
|
||||
<div class="mt-2 grid gap-2">
|
||||
<div>
|
||||
<div class="text-muted">Suggested Client ID</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code>{recommendedClientId}</code>
|
||||
<button type="button" class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20" on:click={() => copy(recommendedClientId, 'clientId')}>
|
||||
{copying === 'clientId' ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted">Redirect URL</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code>{origin}/auth/beatleader/callback</code>
|
||||
<button type="button" class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20" on:click={() => copy(`${origin}/auth/beatleader/callback`, 'redirect')}>
|
||||
{copying === 'redirect' ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted">Scopes</div>
|
||||
<code>scp:profile{scopes.includes('scp:offline_access') ? ',scp:offline_access' : ''}</code>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
After creation, copy the shown client secret and paste both values below to save locally.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{#if existing}
|
||||
<div class="mt-4 text-sm">
|
||||
<div class="text-muted">Existing credentials found.</div>
|
||||
<div class="mt-1">Client ID: <code>{existing.client_id}</code></div>
|
||||
<div>Client Secret: <code>{existing.client_secret}</code></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<details class="mt-4">
|
||||
<summary class="cursor-pointer text-sm text-muted">Enter credentials manually</summary>
|
||||
<form class="mt-3 grid gap-3 max-w-xl" on:submit|preventDefault={saveManual}>
|
||||
<div>
|
||||
<label class="block text-sm text-muted" for="mClientId">Client ID</label>
|
||||
<input id="mClientId" class="w-full rounded-md border border-white/10 bg-transparent px-3 py-2 text-sm outline-none" bind:value={manualClientId} required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-muted" for="mClientSecret">Client Secret</label>
|
||||
<input id="mClientSecret" class="w-full rounded-md border border-white/10 bg-transparent px-3 py-2 text-sm outline-none" bind:value={manualClientSecret} required />
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn-neon">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
{#if message}
|
||||
<div class="mt-4 text-sm">{message}</div>
|
||||
{/if}
|
||||
|
||||
{#if created}
|
||||
<div class="mt-6 text-sm">
|
||||
<div><strong>Client ID:</strong> <code>{created.clientId}</code></div>
|
||||
<div><strong>Client Secret:</strong> <code>{created.clientSecret}</code></div>
|
||||
<div class="mt-2">
|
||||
<a class="rounded-md border border-white/10 px-3 py-1.5 text-sm hover:border-white/20" href="/auth/beatleader/login">Proceed to Login</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user