spotify controls

This commit is contained in:
yohlo
2025-09-12 11:08:21 -05:00
parent 9d92a8a510
commit 0169468114
15 changed files with 1655 additions and 28 deletions

View File

@@ -0,0 +1,135 @@
import { createServerFileRoute } from '@tanstack/react-start/server'
import { SpotifyAuth } from '@/lib/spotify/auth'
const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!
const SPOTIFY_REDIRECT_URI = import.meta.env.VITE_SPOTIFY_REDIRECT_URI!
export const ServerRoute = createServerFileRoute('/api/spotify/callback').methods({
GET: async ({ request }: { request: Request }) => {
// Helper function to get return path from state parameter
const getReturnPath = (state: string | null): string => {
if (!state) return '/';
try {
const decodedState = JSON.parse(atob(state));
return decodedState.returnPath || '/';
} catch {
return '/';
}
};
try {
const url = new URL(request.url)
const code = url.searchParams.get('code')
const state = url.searchParams.get('state')
const error = url.searchParams.get('error')
const returnPath = getReturnPath(state);
// Check for OAuth errors
if (error) {
console.error('Spotify OAuth error:', error)
return new Response(null, {
status: 302,
headers: {
'Location': returnPath + '?spotify_error=' + encodeURIComponent(error),
},
})
}
if (!code || !state) {
console.error('Missing code or state:', { code: !!code, state: !!state })
return new Response(null, {
status: 302,
headers: {
'Location': returnPath + '?spotify_error=missing_code_or_state',
},
})
}
console.log('Token exchange attempt:', {
client_id: SPOTIFY_CLIENT_ID,
redirect_uri: SPOTIFY_REDIRECT_URI,
has_code: !!code,
has_state: !!state,
})
// Exchange code for tokens
const tokenResponse = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: SPOTIFY_REDIRECT_URI,
}),
})
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text()
console.error('Token exchange error:', {
status: tokenResponse.status,
statusText: tokenResponse.statusText,
body: errorText,
redirect_uri: SPOTIFY_REDIRECT_URI,
})
// Return more detailed error info
const errorParam = encodeURIComponent(`${tokenResponse.status}: ${errorText}`)
return new Response(null, {
status: 302,
headers: {
'Location': `${returnPath}?spotify_error=token_exchange_failed&details=${errorParam}`,
},
})
}
const tokens = await tokenResponse.json()
console.log('Token exchange successful:', {
has_access_token: !!tokens.access_token,
has_refresh_token: !!tokens.refresh_token,
expires_in: tokens.expires_in,
})
console.log('Decoded return path:', returnPath);
// Create response with redirect to original path
const response = new Response(null, {
status: 302,
headers: {
'Location': returnPath + '?spotify_auth=success',
},
})
// Set secure cookies for tokens
const isSecure = process.env.NODE_ENV === 'production'
const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}`
response.headers.append('Set-Cookie', `spotify_access_token=${tokens.access_token}; ${cookieOptions}`)
if (tokens.refresh_token) {
// Refresh token doesn't expire, set longer max age
const refreshCookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${60 * 60 * 24 * 30}` // 30 days
response.headers.append('Set-Cookie', `spotify_refresh_token=${tokens.refresh_token}; ${refreshCookieOptions}`)
}
return response
} catch (error) {
console.error('Spotify callback error:', error)
// Try to get return path from query params if available, otherwise use default
const url = new URL(request.url);
const state = url.searchParams.get('state');
const returnPath = getReturnPath(state);
return new Response(null, {
status: 302,
headers: {
'Location': returnPath + '?spotify_error=callback_failed',
},
})
}
},
})