Skip to content

Commit 0ac1ce6

Browse files
committed
fix: auth not properly working because of a race condition
1 parent ec74784 commit 0ac1ce6

File tree

5 files changed

+128
-70
lines changed

5 files changed

+128
-70
lines changed

app/api/auth/refreshToken/route.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,25 @@ export async function POST(req: Request) {
1111
const refreshToken = res.refresh_token;
1212

1313
if (!refreshToken) {
14-
return Response.json({ error: 'Invalid request' }, { status: 400 });
14+
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
1515
}
1616

1717
try {
18+
const body = new URLSearchParams({
19+
grant_type: 'refresh_token',
20+
refresh_token: refreshToken,
21+
client_id: client_id || '',
22+
}).toString();
23+
1824
const response = await axios.post(
1925
'https://accounts.spotify.com/api/token',
20-
{
21-
grant_type: 'refresh_token',
22-
client_id,
23-
refresh_token: refreshToken,
24-
},
26+
body,
2527
{
2628
headers: {
27-
'content-Type': 'application/x-www-form-urlencoded',
29+
'Content-Type': 'application/x-www-form-urlencoded',
2830
Authorization:
2931
'Basic ' +
30-
Buffer.from(client_id + ':' + client_secret).toString('base64'),
32+
Buffer.from(`${client_id}:${client_secret}`).toString('base64'),
3133
},
3234
},
3335
);
@@ -39,8 +41,17 @@ export async function POST(req: Request) {
3941
{ status: 200 },
4042
);
4143
} catch (error) {
42-
error = error as AxiosError;
44+
console.error('Spotify refresh token exchange failed');
45+
const axiosErr = error as AxiosError | any;
46+
if (axiosErr?.response?.data) {
47+
console.error('Spotify response:', axiosErr.response.data);
48+
return NextResponse.json(
49+
{ spotify_error: axiosErr.response.data },
50+
{ status: 400 },
51+
);
52+
}
4353

44-
return NextResponse.json({ error }, { status: 400 });
54+
console.error(error);
55+
return NextResponse.json({ error: 'Unknown error' }, { status: 500 });
4556
}
4657
}

app/api/auth/route.ts

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import axios, { AxiosError } from "axios";
22

33
import { NextResponse } from "next/server";
4-
import { getUser } from "@/app/lib/spotify";
5-
import { sql } from "@vercel/postgres";
6-
import prisma from "@/app/lib/prisma";
4+
import { getUser } from '@/app/lib/spotify';
5+
import prisma from '@/app/lib/prisma';
76

87
const redirect_uri = process.env.REDIRECT_URL;
98
const client_id = process.env.SPOTIFY_CLIENT_ID;
@@ -12,29 +11,45 @@ const client_secret = process.env.SPOTIFY_CLIENT_SECRET;
1211
export async function POST(req: Request) {
1312
const res = await req.json();
1413
const code = res.code;
14+
15+
// Log the incoming authorization code to help debugging (non-secret, short lived)
16+
console.info('Received Spotify authorization code:', code);
17+
// Log some diagnostics about the code
18+
console.info('Auth code length:', code?.length);
19+
if (code && /\s/.test(code)) {
20+
console.warn(
21+
'Auth code contains whitespace characters which may indicate copying issues',
22+
);
23+
}
1524
if (!code) {
16-
return Response.json({ error: "Invalid request" }, { status: 400 });
25+
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
1726
}
1827

1928
try {
29+
const body = new URLSearchParams({
30+
grant_type: 'authorization_code',
31+
code,
32+
redirect_uri: redirect_uri || '',
33+
}).toString();
34+
35+
// Log the redirect and client id used for the token exchange to help
36+
// diagnose redirect_uri mismatches which commonly cause invalid_grant.
37+
console.info('Token exchange using redirect_uri:', redirect_uri);
38+
console.info('Token exchange using client_id:', client_id);
39+
2040
const response = await axios.post(
21-
"https://accounts.spotify.com/api/token",
22-
{
23-
grant_type: "authorization_code",
24-
code,
25-
redirect_uri,
26-
},
41+
'https://accounts.spotify.com/api/token',
42+
body,
2743
{
2844
headers: {
2945
Authorization:
30-
"Basic " +
31-
Buffer.from(client_id + ":" + client_secret).toString("base64"),
32-
"content-Type": "application/x-www-form-urlencoded",
46+
'Basic ' +
47+
Buffer.from(`${client_id}:${client_secret}`).toString('base64'),
48+
'Content-Type': 'application/x-www-form-urlencoded',
3349
},
34-
}
50+
},
3551
);
3652

37-
3853
const { access_token, refresh_token, expires_in } = response.data;
3954
const user = await getUser(access_token);
4055

@@ -50,14 +65,23 @@ export async function POST(req: Request) {
5065
});
5166
}
5267

53-
5468
return NextResponse.json(
5569
{ expires_in, refresh_token, access_token, user },
56-
{ status: 200 }
70+
{ status: 200 },
5771
);
5872
} catch (error) {
59-
console.log(error);
60-
error = error as AxiosError;
61-
return NextResponse.json({ error }, { status: 400 });
73+
console.error('Spotify token exchange failed');
74+
// If axios error, surface Spotify's error body for debugging
75+
const axiosErr = error as AxiosError | any;
76+
if (axiosErr?.response?.data) {
77+
console.error('Spotify response:', axiosErr.response.data);
78+
return NextResponse.json(
79+
{ spotify_error: axiosErr.response.data },
80+
{ status: 400 },
81+
);
82+
}
83+
84+
console.error(error);
85+
return NextResponse.json({ error: 'Unknown error' }, { status: 500 });
6286
}
6387
}

app/components/ConnectSpotify/index.tsx

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,75 @@
11
'use client';
22

3-
import { useAuth } from '@/app/context/authContext';
4-
import useRefreshToken from '@/app/hooks/useRefreshToken';
5-
import spotifyApi from '@/app/lib/spotifyApi';
6-
import { decrypt, encrypt } from '@/app/lib/utils';
73
import axios, { AxiosResponse } from 'axios';
4+
import { decrypt, encrypt } from '@/app/lib/utils';
5+
import { useEffect, useRef, useState } from 'react';
86
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
9-
import { useEffect, useState } from 'react';
7+
8+
import spotifyApi from '@/app/lib/spotifyApi';
9+
import { useAuth } from '@/app/context/authContext';
10+
import useRefreshToken from '@/app/hooks/useRefreshToken';
1011

1112
const ConnectSpotify = ({ authUrl }: { authUrl: string }) => {
1213
const { isAuthInProgress, isLoggedIn, authInProgress, logIn, setUserData } =
1314
useAuth();
1415

1516
const [expires, setExpires] = useState<number | null>(null);
17+
const isExchangingCode = useRef(false);
1618

1719
const currentPath = usePathname();
1820

1921
const searchParams = useSearchParams();
2022
const router = useRouter();
2123

2224
useEffect(() => {
23-
const current = new URLSearchParams(Array.from(searchParams.entries()));
24-
const extractedCode = searchParams.get('code');
25-
26-
if (extractedCode && extractedCode !== '') {
27-
if (isAuthInProgress) return;
28-
authInProgress(true);
29-
loginUser(extractedCode);
30-
current.delete('code');
31-
32-
const search = current.toString();
33-
const query = search ? `?${search}` : '';
34-
35-
router.push(`${currentPath}${query}`);
36-
return;
37-
}
38-
39-
refreshAccessToken();
25+
(async () => {
26+
const current = new URLSearchParams(Array.from(searchParams.entries()));
27+
const extractedCode = searchParams.get('code');
28+
29+
if (extractedCode && extractedCode !== '') {
30+
if (isAuthInProgress || isExchangingCode.current) return;
31+
isExchangingCode.current = true;
32+
authInProgress(true);
33+
34+
// Attempt to exchange the code. Only remove `code` from the URL
35+
// if the exchange succeeds. If it fails, keep the code visible
36+
// so the developer can copy it for debugging.
37+
const response = await loginUser(extractedCode);
38+
if (response?.status === 200) {
39+
current.delete('code');
40+
41+
const search = current.toString();
42+
const query = search ? `?${search}` : '';
43+
44+
router.push(`${currentPath}${query}`);
45+
} else {
46+
// leave the code in the URL for debugging; authInProgress
47+
// will be set to false in loginUser when a failure occurs
48+
isExchangingCode.current = false;
49+
}
50+
51+
return;
52+
}
53+
54+
refreshAccessToken();
55+
})();
4056
}, []);
4157

4258
async function loginUser(code: string) {
43-
if (!code) return;
44-
const response = await axios.post('/api/auth', { code });
45-
processResponse(response);
59+
if (!code) return null;
60+
try {
61+
const response = await axios.post('/api/auth', { code });
62+
processResponse(response);
63+
return response;
64+
} catch (err: any) {
65+
// If server returned spotify_error, surface it on client console
66+
console.error('Login exchange failed', err?.response?.data || err);
67+
authInProgress(false);
68+
// If the code expired, quickly redirect to start a fresh auth
69+
const isExpired = err?.response?.data?.spotify_error?.error_description === 'Authorization code expired';
70+
if (isExpired) window.location.href = authUrl;
71+
return err?.response || null;
72+
}
4673
}
4774

4875
async function refreshAccessToken() {

app/components/DiscoverTracks/SubmitButtion.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
'use client';
22

3+
import {
4+
addTracksToPlayList,
5+
createPlayList,
6+
getAllTracksInAPlaylist,
7+
} from '@/app/lib/spotify';
38
import {
49
extractPlaylistId,
510
getAllTracks,
611
getEveryAlbum,
712
isValidPlaylistLink,
813
} from '@/app/lib/utils';
9-
import {
10-
addTracksToPlayList,
11-
createPlayList,
12-
getAllTracksInAPlaylist,
13-
} from '@/app/lib/spotify';
1414

1515
import { GoogleGenerativeAI } from '@google/generative-ai';
1616
import React from 'react';
1717
import SubmitButtionContainer from '../SubmitButtonContainer';
18+
import { addToUrl } from '@/app/lib/clientUtils';
1819
import { addUserHistory } from '@/app/lib/db';
20+
import { toast } from 'react-toastify';
1921
import { useAuth } from '@/app/context/authContext';
2022
import { useGeneralState } from '@/app/context/generalStateContext';
2123
import { useHistory } from '@/app/context/HistoryContext';
2224
import { useInput } from '@/app/context/inputContext';
2325
import { useLoading } from '@/app/context/loadingContext';
2426
import { useOptions } from '@/app/context/optionsContext';
2527
import { useType } from '@/app/context/DiscoverTracks/typeContext';
26-
import { toast } from 'react-toastify';
27-
import { addToUrl } from '@/app/lib/clientUtils';
2828

2929
const API_KEY = process.env.NEXT_PUBLIC_API_KEY;
3030
const genAI = new GoogleGenerativeAI(API_KEY as string);
3131

32-
const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' });
32+
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' });
3333

3434
const SubmitButtion = () => {
3535
const { setLoading } = useLoading();
@@ -60,11 +60,7 @@ const SubmitButtion = () => {
6060
? 'completely different from'
6161
: 'similar to';
6262
const popularity = isNotPopularArtists ? 'not popular' : 'popular';
63-
const prompt = `Give me 20 musicians who are ${popularity} and ${type} the following artists provided: ${artists.join(
64-
', ',
65-
)}. Be sure none of the musicians listed overlap with those provided and that the result is not in list form and not more than 20 musicians. The results should also be separated by a comma.`;
66-
67-
// const newPrompt = `Please analyze the following list of musicians: '${artists.join(', ')}', and identify the sub-genre that is associated with 70 - 90% of them. Based on this analysis, please provide a list of 20 musicians who are ${popularity} and are ${type} as the sub-genres. Please ensure that the resulting list does not include any of the musicians from the original list provided. To help narrow down the results, please only provide the list of recommended musicians separated by commas.`
63+
const prompt = `Please analyze the following list of musicians: '${artists.join(', ')}', and identify the sub-genre that is associated with 70 - 90% of them. Based on this analysis, please provide a list of 20 musicians who are ${popularity} and are ${type} as the sub-genres. Please ensure that the resulting list does not include any of the musicians from the original list provided. To help narrow down the results, please only provide the list of recommended musicians separated by commas.`
6864

6965
setLoadingMessage(`Getting the list of new artists`);
7066
const result = await model.generateContent(prompt);

app/components/History/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import React, { useEffect, useState } from 'react';
44

55
import HistoryCard from './HistoryCard';
66
import axios from 'axios';
7+
import { getUser } from '@/app/lib/spotify';
78
import { useAuth } from '@/app/context/authContext';
89
import { useHistory } from '@/app/context/HistoryContext';
9-
import { getUser } from '@/app/lib/spotify';
1010

1111
const History = () => {
1212
const { user, logOut } = useAuth();
@@ -22,7 +22,7 @@ const History = () => {
2222
}
2323

2424
const response = await axios.get(`api/users/${user.user_id}/history`);
25-
const data = response.data.message.map(
25+
const data = response?.data?.message?.map(
2626
({ text, lastUsed }: { text: string; lastUsed: string }) => ({
2727
text,
2828
lastUsed: new Date(lastUsed),

0 commit comments

Comments
 (0)