XciD's picture
XciD HF Staff
feat: initial playlist-archiver app
7b589ec unverified
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 };