added frontend + securing beta server invites

This commit is contained in:
2026-01-02 22:50:02 +05:30
parent cb12b8ef75
commit 9b17a99456
52 changed files with 5409 additions and 5 deletions

View File

@@ -0,0 +1,85 @@
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { checkBetaServer } from "@/lib/discord";
export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get("code");
const state = searchParams.get("state");
const guildId = searchParams.get("guild_id");
const error = searchParams.get("error");
if (error) {
return NextResponse.redirect(new URL("/dashboard?error=access_denied", request.url));
}
if (!code || !state || !guildId) {
return NextResponse.redirect(new URL("/dashboard?error=invalid_request", request.url));
}
const cookieStore = await cookies();
const storedState = cookieStore.get("oauth_invite_state")?.value;
const storedGuildId = cookieStore.get("oauth_invite_guild")?.value;
// 1. Verify State
if (!storedState || state !== storedState) {
return NextResponse.redirect(new URL("/dashboard?error=state_mismatch", request.url));
}
// 2. Verify Guild ID Match (Optional but recommended extra layer)
if (storedGuildId && guildId !== storedGuildId) {
return NextResponse.redirect(new URL("/dashboard?error=guild_mismatch", request.url));
}
// 3. CRITICAL: Check Beta Server Eligibility
// We only exchange the code (and thus add the bot) if this check passes.
const [isBeta] = await checkBetaServer([guildId]);
if (!isBeta) {
console.warn(`Blocked attempt to add bot to non-beta server: ${guildId}`);
return NextResponse.redirect(new URL("/dashboard?error=not_beta_server", request.url));
}
// 4. Exchange Code for Token (Finalize Bot Join)
const appUrl = process.env.APP_URL;
if (!appUrl) {
console.error("APP_URL env var is not set");
return NextResponse.redirect(new URL("/dashboard?error=config_error", request.url));
}
const redirectUri = `${appUrl}/api/oauth/callback`;
const tokenResponse = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: process.env.AUTH_DISCORD_ID as string,
client_secret: process.env.AUTH_DISCORD_SECRET as string,
grant_type: "authorization_code",
code: code,
redirect_uri: redirectUri,
}),
});
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error("Failed to exchange token:", errorText);
return NextResponse.redirect(new URL("/dashboard?error=token_exchange_failed", request.url));
}
// Clean up cookies
cookieStore.delete("oauth_invite_state");
cookieStore.delete("oauth_invite_guild");
// Success!
return NextResponse.redirect(new URL(`/dashboard?success=bot_added&guild_id=${guildId}`, request.url));
} catch (error) {
console.error("Callback handler error:", error);
return NextResponse.redirect(new URL("/dashboard?error=internal_server_error", request.url));
}
}

View File

@@ -0,0 +1,66 @@
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import crypto from "crypto";
export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const guildId = searchParams.get("guild_id");
if (!guildId) {
return new NextResponse("Missing guild_id", { status: 400 });
}
// Generate a random state for security
const state = crypto.randomBytes(16).toString("hex");
// Set the state in a secure cookie (HttpOnly, Secure, SameSite)
// This cookie allows us to verify the callback originated from this flow
const cookieStore = await cookies();
cookieStore.set("oauth_invite_state", state, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 300, // 5 minutes to complete the flow
path: "/",
});
// Store the target guild ID in a cookie too, to verify against the callback's guild_id
// This prevents someone from starting a flow for Guild A and swapping the callback to Guild B (though state mismatch would likely catch it too)
cookieStore.set("oauth_invite_guild", guildId, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 300,
path: "/",
});
const appUrl = process.env.APP_URL;
if (!appUrl) {
console.error("APP_URL env var is not set");
return new NextResponse("Configuration Error", { status: 500 });
}
const redirectUri = `${appUrl}/api/oauth/callback`;
const clientId = process.env.AUTH_DISCORD_ID;
// Construct Discord OAuth2 Authorization URL
// We use response_type=code because we enabled "Requires OAuth2 Code Grant" for the bot
const params = new URLSearchParams({
client_id: clientId as string,
permissions: "8", // Administrator (as requested in original link)
scope: "bot", // Add 'applications.commands' if needed, but 'bot' is the primary one here
redirect_uri: redirectUri,
response_type: "code",
state: state,
guild_id: guildId,
disable_guild_select: "true",
});
return NextResponse.redirect(`https://discord.com/oauth2/authorize?${params.toString()}`);
} catch (error) {
console.error("Failed to initiate invite flow:", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}