Serve Spotify client ID from /api/config instead of baking it in at build
All checks were successful
Docker / build-and-push (push) Successful in 46s
All checks were successful
Docker / build-and-push (push) Successful in 46s
Was a Vite build-time env var, requiring a second copy of a non-secret value as a separate Gitea CI secret alongside the one already in .env for the api service. Client id isn't sensitive (public in every Spotify authorize URL), so it doesn't need build-time baking at all — now fetched once from a new GET /api/config on the already-running api server, cached client-side after the first call. Collapses config back down to the one place it already lived (the deploy host's .env / docker-compose environment), removes the CI build-arg and its secret entirely. Verified end-to-end: /api/config returns the real client id, and a live guest-login click correctly carries it through into the actual Spotify authorize URL (decoded and checked, not just assumed from the code).
This commit is contained in:
@@ -3,7 +3,22 @@
|
||||
// involvement, tokens never leave the browser. Same flow shape as the
|
||||
// original bingo-bango (https://github.com/f8al/bingo-bango, MIT) uses
|
||||
// for its only mode; this is a fresh implementation, not copied code.
|
||||
const CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID as string;
|
||||
// Fetched from the API at runtime instead of baked in at build time —
|
||||
// not sensitive data (Spotify's client id is public in every authorize
|
||||
// URL), but this keeps it configured in exactly one place (the server's
|
||||
// .env), not a second copy as a separate CI build-arg. Cached after the
|
||||
// first call.
|
||||
let cachedClientId: string | null = null;
|
||||
async function getClientId(): Promise<string> {
|
||||
if (cachedClientId) return cachedClientId;
|
||||
const res = await fetch('/api/config');
|
||||
if (!res.ok) throw new Error('could not load Spotify config from the server');
|
||||
const data = await res.json();
|
||||
if (!data.clientId) throw new Error('server has no SPOTIFY_CLIENT_ID configured');
|
||||
cachedClientId = data.clientId as string;
|
||||
return cachedClientId;
|
||||
}
|
||||
|
||||
// Root URL, not a dedicated path — a single static index.html handles
|
||||
// the callback via a query param (?code=...) instead of needing a
|
||||
// second HTML entry point or server-side rewrite rules for a nested
|
||||
@@ -33,13 +48,14 @@ function randomVerifier(): string {
|
||||
}
|
||||
|
||||
export async function startGuestLogin(): Promise<void> {
|
||||
const clientId = await getClientId();
|
||||
const verifier = randomVerifier();
|
||||
sessionStorage.setItem(VERIFIER_KEY, verifier);
|
||||
const challenge = base64url(await sha256(verifier));
|
||||
|
||||
const url = new URL('https://accounts.spotify.com/authorize');
|
||||
url.search = new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
client_id: clientId,
|
||||
response_type: 'code',
|
||||
redirect_uri: REDIRECT_URI,
|
||||
code_challenge_method: 'S256',
|
||||
@@ -52,6 +68,7 @@ export async function startGuestLogin(): Promise<void> {
|
||||
export type GuestToken = { access_token: string; expires_at: number };
|
||||
|
||||
export async function completeGuestLogin(code: string): Promise<GuestToken> {
|
||||
const clientId = await getClientId();
|
||||
const verifier = sessionStorage.getItem(VERIFIER_KEY);
|
||||
if (!verifier) throw new Error('missing PKCE verifier — start login again');
|
||||
|
||||
@@ -59,7 +76,7 @@ export async function completeGuestLogin(code: string): Promise<GuestToken> {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
client_id: clientId,
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
|
||||
Reference in New Issue
Block a user