import type { NextAuthOptions } from "next-auth"; import SpotifyProvider from "next-auth/providers/spotify"; import { writeJSON, readJSON, paths } from "./storage"; import type { UserAuth, UserProfile } from "./types"; const SCOPES = [ "user-read-private", "user-read-email", "playlist-read-private", "playlist-read-collaborative", "playlist-modify-private", "playlist-modify-public" ].join(" "); async function refreshAccessToken(refreshToken: string) { const basic = Buffer.from( `${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}` ).toString("base64"); const res = await fetch("https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${basic}` }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }) }); if (!res.ok) throw new Error(`Spotify refresh failed: ${res.status}`); const data = (await res.json()) as { access_token: string; expires_in: number; refresh_token?: string; }; return { accessToken: data.access_token, expiresAt: Date.now() + data.expires_in * 1000, refreshToken: data.refresh_token ?? refreshToken }; } export const authOptions: NextAuthOptions = { providers: [ SpotifyProvider({ clientId: process.env.SPOTIFY_CLIENT_ID!, clientSecret: process.env.SPOTIFY_CLIENT_SECRET!, authorization: { params: { scope: SCOPES } } }) ], session: { strategy: "jwt" }, callbacks: { async jwt({ token, account, profile }) { // Initial sign-in: persist refresh token + profile to the HF bucket. if (account && profile) { const sp = profile as { id: string; display_name?: string; images?: { url: string }[] }; token.userId = sp.id; token.accessToken = account.access_token; token.refreshToken = account.refresh_token; token.expiresAt = (account.expires_at ?? 0) * 1000; await writeJSON(paths.userProfile(sp.id), { id: sp.id, displayName: sp.display_name ?? sp.id, avatar: sp.images?.[0]?.url ?? null, createdAt: new Date().toISOString() }); await writeJSON(paths.userAuth(sp.id), { refreshToken: account.refresh_token!, scope: SCOPES, updatedAt: new Date().toISOString() }); return token; } // Refresh when access token expires. if (token.expiresAt && Date.now() < (token.expiresAt as number) - 60_000) { return token; } if (token.refreshToken) { try { const r = await refreshAccessToken(token.refreshToken as string); token.accessToken = r.accessToken; token.expiresAt = r.expiresAt; token.refreshToken = r.refreshToken; } catch (err) { token.error = "RefreshAccessTokenError"; } } return token; }, async session({ session, token }) { session.userId = token.userId as string; session.accessToken = token.accessToken as string; session.error = token.error as string | undefined; return session; } }, pages: { signIn: "/login" } }; export async function getRefreshTokenForUser(userId: string): Promise { const auth = await readJSON(paths.userAuth(userId)); return auth?.refreshToken ?? null; } export { refreshAccessToken };