diff --git a/.env.example b/.env.example index 9b3a581..919a6b5 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,9 @@ DEEPGRAM_API_KEY= OPENAI_API_KEY= SLACK_POSTMAN_WEBHOOK_URL= +SPOTIFY_CLIENT_ID= +SPOTIFY_CLIENT_SECRET= + STRIPE_SECRET_KEY= STRIPE_PUBLISHABLE_KEY= STRIPE_WEBHOOK_SECRET= diff --git a/env.mjs b/env.mjs index 78bf11b..99b556d 100644 --- a/env.mjs +++ b/env.mjs @@ -19,6 +19,8 @@ export const env = createEnv({ PODCAST_INDEX_API_KEY: process.env.PODCAST_INDEX_API_KEY, PODCAST_INDEX_SECRET: process.env.PODCAST_INDEX_SECRET, SLACK_POSTMAN_WEBHOOK_URL: process.env.SLACK_POSTMAN_WEBHOOK_URL, + SPOTIFY_CLIENT_ID: process.env.SPOTIFY_CLIENT_ID, + SPOTIFY_CLIENT_SECRET: process.env.SPOTIFY_CLIENT_SECRET, STRIPE_PUBLISHABLE_KEY: process.env.STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, @@ -32,6 +34,8 @@ export const env = createEnv({ PODCAST_INDEX_API_KEY: z.string().min(1), PODCAST_INDEX_SECRET: z.string().min(1), SLACK_POSTMAN_WEBHOOK_URL: z.string().min(1), + SPOTIFY_CLIENT_ID: z.string().min(1), + SPOTIFY_CLIENT_SECRET: z.string().min(1), STRIPE_PUBLISHABLE_KEY: z.string().min(1), STRIPE_SECRET_KEY: z.string().min(1), STRIPE_WEBHOOK_SECRET: z.string().min(1), diff --git a/lib/services/spotify/client.ts b/lib/services/spotify/client.ts index fae2e14..f230efe 100644 --- a/lib/services/spotify/client.ts +++ b/lib/services/spotify/client.ts @@ -1,8 +1,59 @@ +import { env } from '@/env.mjs'; +import { DatabaseError } from '@/lib/errors'; +import { cookies } from 'next/headers'; import { ofetch } from 'ofetch'; +import { fetchAccount } from '../account'; +import { createSupabaseServerClient } from '../supabase/server'; + +type RefreshTokenResponse = { + access_token: string; + expires_in: number; + scope: string; + token_type: string; +}; + const SPOTIFY_BASE_URL = 'https://api.spotify.com/v1'; export const spotifyFetchClient = ofetch.create({ baseURL: SPOTIFY_BASE_URL, params: { limit: 50, offset: 0 }, }); + +export const refreshToken = async (providerRefreshToken: string) => { + const supabase = createSupabaseServerClient(cookies()); + const account = await fetchAccount(); + + const bearer = Buffer.from( + `${env.SPOTIFY_CLIENT_ID}:${env.SPOTIFY_CLIENT_SECRET}`, + ).toString('base64'); + + try { + const response = await ofetch( + 'https://accounts.spotify.com/api/token', + { + body: `grant_type=refresh_token&refresh_token=${providerRefreshToken}`, + headers: { + Authorization: `Basic ${bearer}`, + 'content-type': 'application/x-www-form-urlencoded', + }, + method: 'post', + }, + ); + + const { data, error } = await supabase + .from('account') + .update({ provider_token: response.access_token }) + .eq('id', account.id) + .select() + .single(); + + if (error) { + throw new DatabaseError(error); + } + + return data.provider_token; + } catch (error) { + throw new Error('Could not get new access token'); + } +}; diff --git a/lib/services/spotify/get-user-shows.ts b/lib/services/spotify/get-user-shows.ts index 81e79b6..fefe805 100644 --- a/lib/services/spotify/get-user-shows.ts +++ b/lib/services/spotify/get-user-shows.ts @@ -1,4 +1,7 @@ -import { spotifyFetchClient } from './client'; +import { FetchError } from 'ofetch'; + +import { fetchAccount } from '../account'; +import { refreshToken, spotifyFetchClient } from './client'; type Image = { height: number; @@ -29,12 +32,27 @@ type SpotifyItems = { export const getUserShows = async ( spotifyToken: string, -): Promise => { - const response = await spotifyFetchClient('/me/shows', { - headers: { - Authorization: `Bearer ${spotifyToken}`, - }, - }); - - return response.items; +): Promise<[] | SpotifyShow[]> => { + try { + const response = await spotifyFetchClient('/me/shows', { + headers: { + Authorization: `Bearer ${spotifyToken}`, + }, + retry: 2, + retryDelay: 1000, + }); + return response.items; + } catch (error) { + if (error instanceof FetchError) { + if (error.status === 401) { + const account = await fetchAccount(); + if (account.provider_refresh_token) { + await refreshToken(account.provider_refresh_token); + return []; + } + throw new Error('Authorization error'); + } + } + throw new Error('Spotify server error'); + } };