Amogh
Amogh

Reputation: 1

Routing with Spotify API

I am developing a spotify clone with the ability to play a preview of the songs and display user's different top tracks and artists. I have already made standalone pages for the website after authorizing with the help spotify-web-api-node package, but i am kinda facing a problem connecting the routers, after i login with spotify i reach my profile page where i have links to other pages, but when i try to go to another page i get an error on the server that it is an invalid authorization code and on the web console, the package throws an error that no access token was provided. I have tried every possible way to correct this but i am not able to do anything. Please help me out. The relevant code as well the whole GitHub repository is linked below: The Github repository for this project is https://github.com/amoghkapoor/Spotify-Clone

App.js

const code = new URLSearchParams(window.location.search).get("code")

const App = () => {

    return (
        <>
            {code ?
                <Router>
                    <Link to="/tracks">
                        <div style={{ marginBottom: "3rem" }}>
                            <p>Tracks</p>
                        </div>
                    </Link>
                    <Link to="/">
                        <div style={{ marginBottom: "3rem" }}>
                            <p>Home</p>
                        </div>
                    </Link>
                    <Switch>
                        <Route exact path="/">
                            <Profile code={code} />
                        </Route>
                        <Route path="/tracks">

                            <TopTracks code={code} />
                        </Route>
                    </Switch>

                </Router> : <Login />}
        </>
    )
}

TopTracks.js

const spotifyApi = new SpotifyWebApi({
    client_id: "some client id"
})

const TopTracks = ({ code }) => {
    const accessToken = useAuth(code)

    console.log(accessToken) // undefined in console

    console.log(code) // the correct code as provided by spotify 

    useEffect(() => {
        if (accessToken) {
            spotifyApi.setAccessToken(accessToken)
            return
        }

    }, [accessToken])

'useAuth' custom Hook

export default function useAuth(code) {
    const [accessToken, setAccessToken] = useState()
    const [refreshToken, setRefreshToken] = useState()
    const [expiresIn, setExpiresIn] = useState()

    useEffect(() => {
        axios
            .post("http://localhost:3001/login", {
                code
            })
            .then(res => {
                setAccessToken(res.data.accessToken)
                setRefreshToken(res.data.refreshToken)
                setExpiresIn(res.data.expiresIn)
                window.history.pushState({}, null, "/")
            })
            .catch((err) => {
                // window.location = "/"
                console.log("login error", err)
            })

    }, [code])

Upvotes: -1

Views: 702

Answers (1)

samthecodingman
samthecodingman

Reputation: 26171

You don't appear to be persisting your access/refresh tokens anywhere. As soon as the component is unloaded, the data would be discarded. In addition, a sign in code is only usable once. If you use it more than once, any OAuth-compliant service will invalidate all tokens related to that code.

You can persist these tokens using localStorage, IndexedDB or another database mechanism.

For the purposes of an example (i.e. use something more secure & permanent than this), I'll use localStorage.

To help manage state across multiple views and components, you should make use of a React Context. This allows you to lift common logic higher in your component tree so that it can be reused.

Furthermore, instead of using setInterval to refresh the token periodically, you should only perform refresh operations on-demand - that is, refresh it when it expires.

// SpotifyAuthContext.js

import SpotifyWebApi from 'spotify-web-api-node';

const spotifyApi = new SpotifyWebApi({
  clientId: 'fcecfc72172e4cd267473117a17cbd4d',
});

export const SpotifyAuthContext = React.createContext({
    exchangeCode: () => throw new Error("context not loaded"),
    refreshAccessToken: () => throw new Error("context not loaded"),
    get hasToken: spotifyApi.getAccessToken() !== undefined,
    api: spotifyApi
});

export const useSpotify = () => useContext(SpotifyAuthContext);

function setStoredJSON(id, obj) {
    localStorage.setItem(id, JSON.stringify(obj));
}

function getStoredJSON(id, fallbackValue = null) {
    const storedValue = localStorage.getItem(id);
    return storedValue === null
        ? fallbackValue
        : JSON.parse(storedValue);
}

export function SpotifyAuthContextProvider({children}) {
    const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))

    const hasToken = tokenInfo !== null

    useEffect(() => {
        if (tokenInfo === null) return; // do nothing, no tokens available

        // attach tokens to `SpotifyWebApi` instance
        spotifyApi.setCredentials({
            accessToken: tokenInfo.accessToken,
            refreshToken: tokenInfo.refreshToken,
        })

        // persist tokens
        setStoredJSON('myApp:spotify', tokenInfo)
    }, [tokenInfo])

    function exchangeCode(code) {
        return axios
            .post("http://localhost:3001/login", {
                code
            })
            .then(res => {
                // TODO: Confirm whether response contains `accessToken` or `access_token`
                const { accessToken, refreshToken, expiresIn } = res.data;
                // store expiry time instead of expires in
                setTokenInfo({
                    accessToken,
                    refreshToken,
                    expiresAt: Date.now() + (expiresIn * 1000)
                });
            })
    }

    function refreshAccessToken() {
        return axios
            .post("http://localhost:3001/refresh", {
                refreshToken
            })
            .then(res => {
                const refreshedTokenInfo = {
                     accessToken: res.data.accessToken,
                     // some refreshes may include a new refresh token!
                     refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
                     // store expiry time instead of expires in
                     expiresAt: Date.now() + (res.data.expiresIn * 1000)
                }

                setTokenInfo(refreshedTokenInfo)

                // attach tokens to `SpotifyWebApi` instance
                spotifyApi.setCredentials({
                    accessToken: refreshedTokenInfo.accessToken,
                    refreshToken: refreshedTokenInfo.refreshToken,
                })

                return refreshedTokenInfo
            })
    }

    async function refreshableCall(callApiFunc) {
         if (Date.now() > tokenInfo.expiresAt)
             await refreshAccessToken();

         try {
             return await callApiFunc()
         } catch (err) {
             if (err.name !== "WebapiAuthenticationError")
                 throw err; // rethrow irrelevant errors
         }

         // if here, has an authentication error, try refreshing now
         return refreshAccessToken()
             .then(callApiFunc)
    }

    return (
        <SpotifyAuthContext.Provider value={{
            api: spotifyApi,
            exchangeCode,
            hasToken,
            refreshableCall,
            refreshAccessToken
        }}>
            {children}
        </SpotifyAuthContext.Provider>
    )
}

Usage:

// TopTracks.js
import useSpotify from '...'

const TopTracks = () => {
    const { api, refreshableCall } = useSpotify()
    const [ tracks, setTracks ] = useState([])
    const [ error, setError ] = useState(null)

    useEffect(() => {
        let disposed = false
        refreshableCall(() => api.getMyTopTracks()) // <- calls getMyTopTracks, but retry if the token has expired
            .then((res) => {
                if (disposed) return
                setTracks(res.body.items)
                setError(null)
            })
            .catch((err) => {
                if (disposed) return
                setTracks([])
                setError(err)
            });

        return () => disposed = true
    });

    if (error != null) {
       return <span class="error">{error.message}</span>
    }

    if (tracks.length === 0) {
       return <span class="warning">No tracks found.</span>
    }

    return (<ul>
       {tracks.map((track) => {
           const artists = track.artists
               .map(artist => artist.name)
               .join(', ')

           return (
               <li key={track.id}>
                   <a href={track.preview_url}>
                       {track.name} - {artists}
                   </a>
               </li>
           )
       }
    </ul>)
}
// Login.js
import useSpotify from '...'

const Login = () => {
    const { exchangeCode } = useSpotify()
    const [ error, setError ] = useState(null)

    const code = new URLSearchParams(window.location.search).get("code")

    useEffect(() => {
       if (!code) return // no code. do nothing.

       // if here, code available for login
       
       let disposed = false
       exchangeCode(code)
           .then(() => {
               if (disposed) return
               setError(null)
               window.history.pushState({}, null, "/")
           })
           .catch(error => {
               if (disposed) return
               console.error(error)
               setError(error)
           })

        return () => disposed = true
    }, [code])

    if (error !== null) {
        return <span class="error">{error.message}</span>
    }

    if (code) {
        // TODO: Render progress bar/spinner/throbber for "Signing in..."
        return /* ... */
    }

    // if here, no code & no error. Show login button
    // TODO: Render login button
    return /* ... */
}
// MyRouter.js (rename it however you like)
import useSpotify from '...'
import Login from '...'

const MyRouter = () => {
    const { hasToken } = useSpotify()

    if (!hasToken) {
        // No access token available, show login screen
        return <Login />
    }
   
    // Access token available, show main content
    return (
        <Router>
            // ...
        </Router>
    )
}
// App.js
import SpotifyAuthContextProvider from '...'
import MyRouter from '...'

const App = () => {
    return (
        <SpotifyAuthContextProvider>
             <MyRouter />
        </SpotifyAuthContextProvider>
    );
}

Upvotes: 0

Related Questions