121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
/**
|
|
* ╔══════════════════════════════════════════════════════════════════╗
|
|
* ║ ║
|
|
* ║ +-+-+-+-+-+-+-+-+ ║
|
|
* ║ |H|e|x|a|H|o|s|t| ║
|
|
* ║ +-+-+-+-+-+-+-+-+ ║
|
|
* ║ ║
|
|
* ║ © 2026 HexaHost — All Rights Reserved ║
|
|
* ║ ║
|
|
* ║ discord ── https://discord.gg/hexahost ║
|
|
* ║ github ── https://github.com/theoneandonlymace ║
|
|
* ║ ║
|
|
* ╚══════════════════════════════════════════════════════════════════╝
|
|
*/
|
|
import DiscordProvider from "next-auth/providers/discord";
|
|
import { AuthOptions } from "next-auth";
|
|
|
|
async function refreshDiscordAccessToken(token: {
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
expiresAt?: number;
|
|
error?: string;
|
|
[key: string]: unknown;
|
|
}) {
|
|
if (!token.refreshToken) {
|
|
return { ...token, error: "RefreshTokenMissing" as const };
|
|
}
|
|
|
|
try {
|
|
const body = new URLSearchParams({
|
|
client_id: process.env.DISCORD_CLIENT_ID || "",
|
|
client_secret: process.env.DISCORD_CLIENT_SECRET || "",
|
|
grant_type: "refresh_token",
|
|
refresh_token: String(token.refreshToken),
|
|
});
|
|
|
|
const res = await fetch("https://discord.com/api/oauth2/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body,
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
throw data;
|
|
}
|
|
|
|
return {
|
|
...token,
|
|
accessToken: data.access_token as string,
|
|
refreshToken: (data.refresh_token as string) || token.refreshToken,
|
|
expiresAt: Math.floor(Date.now() / 1000) + Number(data.expires_in || 604800),
|
|
error: undefined,
|
|
};
|
|
} catch (error) {
|
|
console.error("Discord token refresh failed:", error);
|
|
return { ...token, error: "RefreshAccessTokenError" as const };
|
|
}
|
|
}
|
|
|
|
export const authOptions: AuthOptions = {
|
|
providers: [
|
|
DiscordProvider({
|
|
clientId: process.env.DISCORD_CLIENT_ID || "",
|
|
clientSecret: process.env.DISCORD_CLIENT_SECRET || "",
|
|
authorization: { params: { scope: "identify guilds" } },
|
|
}),
|
|
],
|
|
callbacks: {
|
|
async jwt({ token, account }) {
|
|
// Initial sign-in
|
|
if (account) {
|
|
return {
|
|
...token,
|
|
accessToken: account.access_token,
|
|
refreshToken: account.refresh_token,
|
|
expiresAt: account.expires_at,
|
|
};
|
|
}
|
|
|
|
const expiresAt = typeof token.expiresAt === "number" ? token.expiresAt : 0;
|
|
// Refresh when expired, missing expiry, or previous refresh failed
|
|
const stillValid =
|
|
Boolean(token.accessToken) &&
|
|
expiresAt > 0 &&
|
|
Date.now() < expiresAt * 1000 - 60_000 &&
|
|
!token.error;
|
|
|
|
if (stillValid) {
|
|
return token;
|
|
}
|
|
|
|
return refreshDiscordAccessToken(token as {
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
expiresAt?: number;
|
|
error?: string;
|
|
});
|
|
},
|
|
async session({ session, token }) {
|
|
if (session.user) {
|
|
// @ts-ignore
|
|
session.user.id = token.sub;
|
|
// @ts-ignore
|
|
session.accessToken = token.accessToken;
|
|
// @ts-ignore
|
|
session.error = token.error;
|
|
}
|
|
// Surface refresh failures so pages can redirect to sign-in
|
|
if (token.error) {
|
|
// @ts-ignore
|
|
session.error = token.error;
|
|
}
|
|
return session;
|
|
},
|
|
},
|
|
pages: {
|
|
signIn: "/",
|
|
},
|
|
};
|