Enhance Discord integration by adding optional bot permissions in environment files and updating the dashboard to include an "Add Bot to your Server" button. Refactor authentication logic to handle token refresh and improve error handling for Discord sessions. Update placeholder text for better clarity in the search input field.
This commit is contained in:
@@ -26,6 +26,8 @@ JISHAKU_ENABLED=false
|
|||||||
NEXTAUTH_SECRET=generate_a_long_random_secret_here
|
NEXTAUTH_SECRET=generate_a_long_random_secret_here
|
||||||
DISCORD_CLIENT_ID=
|
DISCORD_CLIENT_ID=
|
||||||
DISCORD_CLIENT_SECRET=
|
DISCORD_CLIENT_SECRET=
|
||||||
|
# Optional: Discord permission bitfield for /api/invite (default: 8 = Administrator)
|
||||||
|
# DISCORD_BOT_PERMISSIONS=8
|
||||||
|
|
||||||
# Same Discord user IDs as owners/admins (comma-separated)
|
# Same Discord user IDs as owners/admins (comma-separated)
|
||||||
ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE
|
ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ NEXT_PUBLIC_API_URL=http://127.0.0.1:8000/api/v1
|
|||||||
NEXTAUTH_URL=http://localhost:3000/
|
NEXTAUTH_URL=http://localhost:3000/
|
||||||
NEXTAUTH_SECRET=generate_a_long_random_secret_here
|
NEXTAUTH_SECRET=generate_a_long_random_secret_here
|
||||||
|
|
||||||
# Discord OAuth configuration
|
# Discord OAuth configuration (also used by /api/invite for “Add to Server”)
|
||||||
DISCORD_CLIENT_ID=
|
DISCORD_CLIENT_ID=
|
||||||
DISCORD_CLIENT_SECRET=
|
DISCORD_CLIENT_SECRET=
|
||||||
|
# DISCORD_BOT_PERMISSIONS=8
|
||||||
|
|
||||||
# Dashboard admin user IDs (comma separated — your Discord user ID)
|
# Dashboard admin user IDs (comma separated — your Discord user ID)
|
||||||
ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE
|
ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE
|
||||||
|
|||||||
33
dashboard/app/api/invite/route.ts
Normal file
33
dashboard/app/api/invite/route.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
* ║ ║
|
||||||
|
* ║ +-+-+-+-+-+-+-+-+ ║
|
||||||
|
* ║ |H|e|x|a|H|o|s|t| ║
|
||||||
|
* ║ +-+-+-+-+-+-+-+-+ ║
|
||||||
|
* ║ ║
|
||||||
|
* ║ © 2026 HexaHost — All Rights Reserved ║
|
||||||
|
* ║ ║
|
||||||
|
* ║ discord ── https://discord.gg/hexahost ║
|
||||||
|
* ║ github ── https://github.com/theoneandonlymace ║
|
||||||
|
* ║ ║
|
||||||
|
* ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
*/
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getBotInviteUrl } from "@/lib/discord-invite";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
/** Redirects to the Discord bot invite (uses DISCORD_CLIENT_ID at runtime). */
|
||||||
|
export async function GET() {
|
||||||
|
const url = getBotInviteUrl();
|
||||||
|
if (!url) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
detail:
|
||||||
|
"DISCORD_CLIENT_ID is not configured. Set it in the dashboard environment.",
|
||||||
|
},
|
||||||
|
{ status: 503 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.redirect(url, 302);
|
||||||
|
}
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Users, ShieldCheck, ChevronRight, Hash } from "lucide-react";
|
import { Users, ShieldCheck, ChevronRight, Hash, Plus, ExternalLink } from "lucide-react";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { getBotInviteUrl } from "@/lib/discord-invite";
|
||||||
|
|
||||||
import { GuildSummary } from "@/types/api";
|
import { GuildSummary } from "@/types/api";
|
||||||
import { getServerSession } from "next-auth/next";
|
import { getServerSession } from "next-auth/next";
|
||||||
@@ -27,6 +28,18 @@ import { redirect } from "next/navigation";
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
||||||
|
function AddBotButton({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<Button className={className} asChild>
|
||||||
|
<a href="/api/invite" target="_blank" rel="noopener noreferrer">
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Bot to your Server
|
||||||
|
<ExternalLink className="h-3.5 w-3.5 ml-2 opacity-60" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default async function GuildsPage() {
|
export default async function GuildsPage() {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
@@ -34,6 +47,10 @@ export default async function GuildsPage() {
|
|||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (session.error === "RefreshAccessTokenError" || session.error === "RefreshTokenMissing") {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
let botGuilds: GuildSummary[] = [];
|
let botGuilds: GuildSummary[] = [];
|
||||||
let userGuilds: any[] = [];
|
let userGuilds: any[] = [];
|
||||||
let userDiscordError: string | null = null;
|
let userDiscordError: string | null = null;
|
||||||
@@ -51,88 +68,103 @@ export default async function GuildsPage() {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${session.accessToken}`,
|
Authorization: `Bearer ${session.accessToken}`,
|
||||||
},
|
},
|
||||||
next: { revalidate: 300 } // Cache for 5 mins
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
userGuilds = await res.json();
|
userGuilds = await res.json();
|
||||||
} else {
|
} else {
|
||||||
userDiscordError = "Failed to fetch your Discord servers.";
|
const body = await res.text().catch(() => "");
|
||||||
|
console.error("Discord guilds fetch failed:", res.status, body);
|
||||||
|
userDiscordError =
|
||||||
|
res.status === 401
|
||||||
|
? "Discord session expired — please sign in again."
|
||||||
|
: "Failed to fetch your Discord servers.";
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Discord API Error:", err);
|
console.error("Discord API Error:", err);
|
||||||
userDiscordError = "Error connecting to Discord.";
|
userDiscordError = "Error connecting to Discord.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out guilds that the user is an admin of (permission flag 0x8 or MANAGE_GUILD 0x20)
|
|
||||||
// Manage Server is 0x20, Admin is 0x8
|
|
||||||
const MANAGE_GUILD = BigInt(0x20);
|
const MANAGE_GUILD = BigInt(0x20);
|
||||||
const ADMINISTRATOR = BigInt(0x8);
|
const ADMINISTRATOR = BigInt(0x8);
|
||||||
const adminUserGuilds = userGuilds.filter(g => {
|
const adminUserGuilds = userGuilds.filter((g) => {
|
||||||
try {
|
try {
|
||||||
const perms = BigInt(g.permissions);
|
const perms = BigInt(g.permissions);
|
||||||
return (perms & ADMINISTRATOR) === ADMINISTRATOR ||
|
return (
|
||||||
|
(perms & ADMINISTRATOR) === ADMINISTRATOR ||
|
||||||
(perms & MANAGE_GUILD) === MANAGE_GUILD ||
|
(perms & MANAGE_GUILD) === MANAGE_GUILD ||
|
||||||
g.owner === true;
|
g.owner === true
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return g.owner === true;
|
return g.owner === true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const adminGuildIds = new Set(adminUserGuilds.map(g => String(g.id)));
|
const adminGuildIds = new Set(adminUserGuilds.map((g) => String(g.id)));
|
||||||
|
const guilds = botGuilds.filter((g) => adminGuildIds.has(String(g.id)));
|
||||||
// The intersecting guilds we can manage
|
|
||||||
const guilds = botGuilds.filter(g => adminGuildIds.has(String(g.id)));
|
|
||||||
const error = botError || userDiscordError;
|
|
||||||
|
|
||||||
|
const inviteConfigured = Boolean(getBotInviteUrl());
|
||||||
|
// Hard failure only when bot API is down — Discord sync issues still show invite CTA
|
||||||
|
const hardError = botError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="flex justify-between items-end">
|
<div className="flex flex-col gap-4 sm:flex-row sm:justify-between sm:items-end">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-white">Your Servers</h1>
|
<h1 className="text-3xl font-bold text-white">Your Servers</h1>
|
||||||
<p className="text-slate-400 mt-2">
|
<p className="text-slate-400 mt-2">
|
||||||
Select a server to manage its unique configuration and modules.
|
Select a server to manage its unique configuration and modules.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
{inviteConfigured && <AddBotButton className="shadow-lg shadow-primary/20" />}
|
||||||
<div className="text-sm font-medium px-4 py-2 bg-slate-800 rounded-xl border border-slate-700 text-slate-300">
|
<div className="text-sm font-medium px-4 py-2 bg-slate-800 rounded-xl border border-slate-700 text-slate-300">
|
||||||
Showing <span className="text-white">{guilds.length}</span> active guilds
|
Showing <span className="text-white">{guilds.length}</span> active guilds
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{hardError ? (
|
||||||
<div className="bg-red-500/10 border border-red-500/20 p-8 rounded-2xl text-center">
|
<div className="bg-red-500/10 border border-red-500/20 p-8 rounded-2xl text-center">
|
||||||
<ShieldCheck className="h-12 w-12 text-red-500 mx-auto mb-4 opacity-50" />
|
<ShieldCheck className="h-12 w-12 text-red-500 mx-auto mb-4 opacity-50" />
|
||||||
<h3 className="text-white font-bold text-lg">Connection Error</h3>
|
<h3 className="text-white font-bold text-lg">Connection Error</h3>
|
||||||
<p className="text-slate-400 mt-2">{error}</p>
|
<p className="text-slate-400 mt-2">{hardError}</p>
|
||||||
<Button variant="outline" className="mt-6">Retry Connection</Button>
|
<div className="mt-6 flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/dashboard/guilds">Retry Connection</Link>
|
||||||
|
</Button>
|
||||||
|
{inviteConfigured && <AddBotButton />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : guilds.length === 0 ? (
|
) : guilds.length === 0 ? (
|
||||||
<div className="bg-slate-800/30 border border-slate-800 border-dashed p-16 rounded-3xl text-center">
|
<div className="bg-slate-800/30 border border-slate-800 border-dashed p-12 md:p-16 rounded-3xl text-center">
|
||||||
<div className="h-16 w-16 bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-6">
|
<div className="h-16 w-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-6 border border-primary/20">
|
||||||
<Users className="h-8 w-8 text-slate-600" />
|
<Users className="h-8 w-8 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-white font-bold text-xl">No Servers Found</h3>
|
<h3 className="text-white font-bold text-xl">No Servers Found</h3>
|
||||||
<p className="text-slate-400 mt-2 max-w-sm mx-auto">
|
<p className="text-slate-400 mt-2 max-w-md mx-auto">
|
||||||
The bot hasn't joined any servers yet, or you don't have permission.
|
Invite Axiom to a Discord server where you have <span className="text-slate-200">Manage Server</span>{" "}
|
||||||
|
permissions, then come back here to configure it.
|
||||||
|
</p>
|
||||||
|
{userDiscordError && (
|
||||||
|
<p className="text-amber-400/90 text-sm mt-4 max-w-md mx-auto">
|
||||||
|
Note: {userDiscordError}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 bg-slate-900/50 p-4 rounded-xl text-left font-mono text-sm text-slate-300 max-w-2xl mx-auto overflow-auto max-h-48 whitespace-pre">
|
|
||||||
<p className="font-bold text-red-400 mb-2">Diagnostic Data:</p>
|
|
||||||
<p>1. Bot's Cache Total Servers: {botGuilds.length}</p>
|
|
||||||
<p>2. Your Discord Profile Total Servers: {userGuilds.length}</p>
|
|
||||||
<p>3. Your Discord Profile Admin/Manage Servers: {adminUserGuilds.length}</p>
|
|
||||||
{botGuilds.length > 0 && adminUserGuilds.length > 0 && (
|
|
||||||
<div className="mt-4 pt-4 border-t border-white/10">
|
|
||||||
<p className="text-emerald-400 mb-1">Bot First Server ID: {botGuilds[0].id} (Type: {typeof botGuilds[0].id})</p>
|
|
||||||
<p className="text-blue-400">Your First Admin Server ID: {adminUserGuilds[0].id} (Type: {typeof adminUserGuilds[0].id})</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<hr className="my-2 border-white/10" />
|
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center items-center">
|
||||||
<p>Intersection Mappings found: {guilds.length}</p>
|
{inviteConfigured ? (
|
||||||
{userDiscordError && <p className="text-red-400">User Error: {userDiscordError}</p>}
|
<AddBotButton className="px-8 h-12 text-base shadow-lg shadow-primary/25" />
|
||||||
{botError && <p className="text-red-400">Bot Error: {botError}</p>}
|
) : (
|
||||||
|
<p className="text-sm text-red-400">
|
||||||
|
Invite link not configured — set <code className="text-xs">DISCORD_CLIENT_ID</code> in the
|
||||||
|
dashboard environment.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/dashboard/guilds">Refresh list</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button className="mt-8">Invite to Discord</Button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
@@ -157,11 +189,16 @@ export default async function GuildsPage() {
|
|||||||
{guild.name.charAt(0)}
|
{guild.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute -bottom-1 -right-1 h-4 w-4 rounded-full bg-emerald-500 border-2 border-[#141B2D]" title="Bot Online" />
|
<div
|
||||||
|
className="absolute -bottom-1 -right-1 h-4 w-4 rounded-full bg-emerald-500 border-2 border-[#141B2D]"
|
||||||
|
title="Bot Online"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-end text-right">
|
<div className="flex flex-col items-end text-right">
|
||||||
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-widest mb-1">Guild ID</span>
|
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-widest mb-1">
|
||||||
|
Guild ID
|
||||||
|
</span>
|
||||||
<span className="text-xs font-mono text-slate-400 bg-black/20 px-2 py-1 rounded-lg border border-white/5 truncate max-w-[120px]">
|
<span className="text-xs font-mono text-slate-400 bg-black/20 px-2 py-1 rounded-lg border border-white/5 truncate max-w-[120px]">
|
||||||
{guild.id}
|
{guild.id}
|
||||||
</span>
|
</span>
|
||||||
@@ -175,7 +212,9 @@ export default async function GuildsPage() {
|
|||||||
<div className="flex items-center gap-4 mt-4 text-slate-400">
|
<div className="flex items-center gap-4 mt-4 text-slate-400">
|
||||||
<div className="flex items-center gap-1.5 bg-slate-800/50 px-3 py-1.5 rounded-xl border border-white/5">
|
<div className="flex items-center gap-1.5 bg-slate-800/50 px-3 py-1.5 rounded-xl border border-white/5">
|
||||||
<Users className="h-4 w-4 text-slate-500" />
|
<Users className="h-4 w-4 text-slate-500" />
|
||||||
<span className="text-sm font-semibold text-slate-300">{guild.member_count.toLocaleString()}</span>
|
<span className="text-sm font-semibold text-slate-300">
|
||||||
|
{guild.member_count.toLocaleString()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 bg-slate-800/50 px-3 py-1.5 rounded-xl border border-white/5">
|
<div className="flex items-center gap-1.5 bg-slate-800/50 px-3 py-1.5 rounded-xl border border-white/5">
|
||||||
<Hash className="h-4 w-4 text-slate-500" />
|
<Hash className="h-4 w-4 text-slate-500" />
|
||||||
@@ -193,7 +232,6 @@ export default async function GuildsPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ export default function DashboardLayout({
|
|||||||
<Search className="absolute left-4 h-4 w-4 text-slate-500 group-focus-within:text-primary transition-colors" />
|
<Search className="absolute left-4 h-4 w-4 text-slate-500 group-focus-within:text-primary transition-colors" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Query neural network..."
|
placeholder="Search Axiom..."
|
||||||
className="w-full bg-white/[0.03] border border-white/5 rounded-2xl py-2.5 pl-12 pr-4 text-xs font-bold text-slate-300 focus:outline-none focus:ring-1 focus:ring-primary/30 focus:bg-white/[0.05] transition-all placeholder:text-slate-600"
|
className="w-full bg-white/[0.03] border border-white/5 rounded-2xl py-2.5 pl-12 pr-4 text-xs font-bold text-slate-300 focus:outline-none focus:ring-1 focus:ring-primary/30 focus:bg-white/[0.05] transition-all placeholder:text-slate-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -114,9 +114,11 @@ export default function LandingPage() {
|
|||||||
<LayoutDashboard className="h-6 w-6 group-hover:rotate-12 transition-transform" />
|
<LayoutDashboard className="h-6 w-6 group-hover:rotate-12 transition-transform" />
|
||||||
Open Dashboard
|
Open Dashboard
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" className="w-full sm:w-auto rounded-2xl px-14 py-9 text-lg font-bold border-white/5 bg-white/[0.02] backdrop-blur-3xl hover:bg-white/[0.05] gap-3 text-white transition-all">
|
<Button variant="outline" className="w-full sm:w-auto rounded-2xl px-14 py-9 text-lg font-bold border-white/5 bg-white/[0.02] backdrop-blur-3xl hover:bg-white/[0.05] gap-3 text-white transition-all" asChild>
|
||||||
|
<a href="/api/invite" target="_blank" rel="noopener noreferrer">
|
||||||
Add to Server
|
Add to Server
|
||||||
<ChevronRight className="h-5 w-5 opacity-40 group-hover:translate-x-1 transition-transform" />
|
<ChevronRight className="h-5 w-5 opacity-40 group-hover:translate-x-1 transition-transform" />
|
||||||
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,49 @@
|
|||||||
import DiscordProvider from "next-auth/providers/discord";
|
import DiscordProvider from "next-auth/providers/discord";
|
||||||
import { AuthOptions } from "next-auth";
|
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 = {
|
export const authOptions: AuthOptions = {
|
||||||
providers: [
|
providers: [
|
||||||
DiscordProvider({
|
DiscordProvider({
|
||||||
@@ -25,10 +68,28 @@ export const authOptions: AuthOptions = {
|
|||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account }) {
|
async jwt({ token, account }) {
|
||||||
|
// Initial sign-in
|
||||||
if (account) {
|
if (account) {
|
||||||
token.accessToken = account.access_token;
|
return {
|
||||||
|
...token,
|
||||||
|
accessToken: account.access_token,
|
||||||
|
refreshToken: account.refresh_token,
|
||||||
|
expiresAt: account.expires_at,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expiresAt = typeof token.expiresAt === "number" ? token.expiresAt : 0;
|
||||||
|
// Refresh ~60s before expiry
|
||||||
|
if (Date.now() < expiresAt * 1000 - 60_000) {
|
||||||
return token;
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshDiscordAccessToken(token as {
|
||||||
|
accessToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
expiresAt?: number;
|
||||||
|
error?: string;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (session.user) {
|
if (session.user) {
|
||||||
@@ -36,6 +97,8 @@ export const authOptions: AuthOptions = {
|
|||||||
session.user.id = token.sub;
|
session.user.id = token.sub;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
session.accessToken = token.accessToken;
|
session.accessToken = token.accessToken;
|
||||||
|
// @ts-ignore
|
||||||
|
session.error = token.error;
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
|||||||
21
dashboard/lib/discord-invite.ts
Normal file
21
dashboard/lib/discord-invite.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Discord bot invite URL helpers.
|
||||||
|
*/
|
||||||
|
const DEFAULT_PERMISSIONS = "8"; // Administrator (Discord invite UI still shows the checklist)
|
||||||
|
|
||||||
|
export function getBotInviteUrl(clientId?: string | null): string | null {
|
||||||
|
const id =
|
||||||
|
(clientId || "").trim() ||
|
||||||
|
(process.env.DISCORD_CLIENT_ID || "").trim() ||
|
||||||
|
(process.env.NEXT_PUBLIC_DISCORD_CLIENT_ID || "").trim();
|
||||||
|
|
||||||
|
if (!id) return null;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: id,
|
||||||
|
permissions: process.env.DISCORD_BOT_PERMISSIONS?.trim() || DEFAULT_PERMISSIONS,
|
||||||
|
scope: "bot applications.commands",
|
||||||
|
});
|
||||||
|
|
||||||
|
return `https://discord.com/oauth2/authorize?${params.toString()}`;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
10
dashboard/types/next-auth.d.ts
vendored
10
dashboard/types/next-auth.d.ts
vendored
@@ -17,8 +17,18 @@ import NextAuth, { DefaultSession } from "next-auth";
|
|||||||
declare module "next-auth" {
|
declare module "next-auth" {
|
||||||
interface Session {
|
interface Session {
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
error?: string;
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
} & DefaultSession["user"];
|
} & DefaultSession["user"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "next-auth/jwt" {
|
||||||
|
interface JWT {
|
||||||
|
accessToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
expiresAt?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user