Spaces:
Running
Running
| 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<UserProfile>(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<UserAuth>(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<string | null> { | |
| const auth = await readJSON<UserAuth>(paths.userAuth(userId)); | |
| return auth?.refreshToken ?? null; | |
| } | |
| export { refreshAccessToken }; | |