Update configuration and README for improved setup and security; disable default API and emoji sync, and enhance Discord permission checks in API routes.

This commit is contained in:
TheOnlyMace
2026-07-21 16:00:52 +02:00
parent 1fdaf4dd6c
commit 127f8a032f
25 changed files with 780 additions and 434 deletions

View File

@@ -0,0 +1,146 @@
/**
* Authenticated server-side proxy to the bot FastAPI backend.
* - Requires Discord OAuth session
* - Guild routes: user must have Manage Server / Admin on that guild
* - Admin routes: user must be in ADMIN_IDS
*/
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth";
import { isAdmin } from "@/lib/utils";
import { userCanManageGuild } from "@/lib/guild-auth";
const BOT_API_URL =
process.env.DASHBOARD_API_URL ||
process.env.NEXT_PUBLIC_API_URL ||
"http://127.0.0.1:8000/api/v1";
const API_KEY = process.env.DASHBOARD_API_KEY;
type RouteContext = { params: { path: string[] } };
async function authorizeProxyRequest(
path: string,
session: { user?: { id?: string }; accessToken?: string }
): Promise<NextResponse | null> {
if (!session.user) {
return NextResponse.json({ detail: "Unauthorized — sign in required." }, { status: 401 });
}
const accessToken = session.accessToken;
if (!accessToken) {
return NextResponse.json({ detail: "Missing Discord access token." }, { status: 401 });
}
const normalized = path.replace(/\/$/, "");
if (normalized.startsWith("admin")) {
if (!isAdmin(session.user.id)) {
return NextResponse.json({ detail: "Forbidden — admin access required." }, { status: 403 });
}
return null;
}
const guildMatch = normalized.match(/^guilds\/(\d+)/);
if (guildMatch) {
const guildId = guildMatch[1];
const allowed = await userCanManageGuild(accessToken, guildId);
if (!allowed) {
return NextResponse.json(
{ detail: "Forbidden — you cannot manage this server." },
{ status: 403 }
);
}
}
return null;
}
async function proxyRequest(request: NextRequest, { params }: RouteContext) {
if (!API_KEY) {
return NextResponse.json(
{ detail: "DASHBOARD_API_KEY is not configured on the dashboard server." },
{ status: 500 }
);
}
const session = await getServerSession(authOptions);
const path = params.path.join("/");
const authError = await authorizeProxyRequest(path, session ?? {});
if (authError) return authError;
const targetUrl = `${BOT_API_URL.replace(/\/$/, "")}/${path}${request.nextUrl.search}`;
const headers = new Headers();
headers.set("Authorization", `Bearer ${API_KEY}`);
if (session?.accessToken) {
headers.set("X-Discord-Access-Token", session.accessToken);
}
if (session?.user?.id) {
headers.set("X-Discord-User-Id", session.user.id);
}
const contentType = request.headers.get("content-type");
if (contentType) {
headers.set("Content-Type", contentType);
}
const init: RequestInit = {
method: request.method,
headers,
cache: "no-store",
};
if (request.method !== "GET" && request.method !== "HEAD") {
init.body = await request.text();
}
try {
const response = await fetch(targetUrl, init);
let body = await response.text();
// Filter guild list to servers the user can manage (bot guild list ∩ user admin guilds)
const normalized = path.replace(/\/$/, "");
if (
request.method === "GET" &&
normalized === "guilds" &&
response.ok &&
session?.accessToken
) {
try {
const botGuilds = JSON.parse(body) as Array<{ id: string }>;
const { fetchUserGuilds, canManageGuild } = await import("@/lib/guild-auth");
const userGuilds = await fetchUserGuilds(session.accessToken);
const manageableIds = new Set(
userGuilds.filter(canManageGuild).map((g) => String(g.id))
);
const filtered = botGuilds.filter((g) => manageableIds.has(String(g.id)));
body = JSON.stringify(filtered);
} catch {
// If filtering fails, return original response rather than breaking the dashboard
}
}
return new NextResponse(body, {
status: response.status,
headers: {
"Content-Type": response.headers.get("Content-Type") || "application/json",
},
});
} catch (error) {
console.error(`[API Proxy] Failed to reach bot API at ${targetUrl}:`, error);
return NextResponse.json(
{ detail: "Could not connect to the bot API backend." },
{ status: 502 }
);
}
}
export const GET = proxyRequest;
export const POST = proxyRequest;
export const PATCH = proxyRequest;
export const DELETE = proxyRequest;
export const PUT = proxyRequest;

View File

@@ -32,6 +32,10 @@ import {
} from "lucide-react";
import { api } from "@/lib/api";
import { cn } from "@/lib/utils";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth";
import { userCanManageGuild } from "@/lib/guild-auth";
import { redirect } from "next/navigation";
export const revalidate = 0; // Never cache any guild dashboard page
@@ -51,6 +55,27 @@ export default async function GuildLayout({
let guild;
let error = null;
const session = await getServerSession(authOptions);
if (!session?.accessToken) {
redirect("/");
}
const hasGuildAccess = await userCanManageGuild(session.accessToken, guildId);
if (!hasGuildAccess) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] border-2 border-dashed border-red-500/20 rounded-3xl bg-red-500/5 p-12 text-center">
<ShieldAlert className="h-16 w-16 text-red-500 mb-6 opacity-50" />
<h2 className="text-2xl font-bold text-white">Access Denied</h2>
<p className="text-slate-400 mt-2 max-w-md">
You do not have permission to manage this server.
</p>
<Link href="/dashboard/guilds" className="mt-8">
<Button variant="outline">Back to Servers</Button>
</Link>
</div>
);
}
try {
guild = await api.getGuildDetails(guildId);
} catch (err: any) {