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

@@ -38,8 +38,14 @@ import {
AdminConfigUpdate
} from "@/types/api";
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1";
const API_KEY = process.env.NEXT_PUBLIC_DASHBOARD_API_KEY;
const isServer = typeof window === "undefined";
/** Server: direct bot API with secret key. Browser: same-origin proxy route. */
const BASE_URL = isServer
? (process.env.DASHBOARD_API_URL ||
process.env.NEXT_PUBLIC_API_URL ||
"http://127.0.0.1:8000/api/v1")
: "/api/proxy";
class ApiError extends Error {
constructor(public status: number, message: string) {
@@ -55,8 +61,20 @@ async function request<T>(
const url = `${BASE_URL}${endpoint}`;
const headers = new Headers(options.headers);
if (API_KEY) {
headers.set("Authorization", `Bearer ${API_KEY}`);
if (isServer) {
const apiKey = process.env.DASHBOARD_API_KEY;
if (apiKey) {
headers.set("Authorization", `Bearer ${apiKey}`);
}
const { getServerSession } = await import("next-auth/next");
const { authOptions } = await import("@/lib/auth");
const session = await getServerSession(authOptions);
if (session?.accessToken) {
headers.set("X-Discord-Access-Token", session.accessToken);
}
if (session?.user?.id) {
headers.set("X-Discord-User-Id", session.user.id);
}
}
headers.set("Content-Type", "application/json");
try {

View File

@@ -0,0 +1,45 @@
/**
* Discord guild permission helpers for dashboard access control.
*/
export interface DiscordGuild {
id: string;
permissions: string;
owner?: boolean;
}
const MANAGE_GUILD = BigInt(0x20);
const ADMINISTRATOR = BigInt(0x8);
export function canManageGuild(guild: DiscordGuild): boolean {
if (guild.owner) return true;
try {
const perms = BigInt(guild.permissions);
return (
(perms & ADMINISTRATOR) === ADMINISTRATOR ||
(perms & MANAGE_GUILD) === MANAGE_GUILD
);
} catch {
return false;
}
}
export async function fetchUserGuilds(
accessToken: string
): Promise<DiscordGuild[]> {
const res = await fetch("https://discord.com/api/users/@me/guilds", {
headers: { Authorization: `Bearer ${accessToken}` },
cache: "no-store",
});
if (!res.ok) return [];
return res.json();
}
export async function userCanManageGuild(
accessToken: string,
guildId: string
): Promise<boolean> {
const guilds = await fetchUserGuilds(accessToken);
const guild = guilds.find((g) => String(g.id) === String(guildId));
return guild ? canManageGuild(guild) : false;
}

View File

@@ -23,6 +23,11 @@ export function cn(...inputs: ClassValue[]) {
export function isAdmin(userId?: string | null) {
if (!userId) return false;
const adminIds = (process.env.NEXT_PUBLIC_ADMIN_IDS || "").split(",");
const adminIds = [
...(process.env.ADMIN_IDS || "").split(","),
...(process.env.NEXT_PUBLIC_ADMIN_IDS || "").split(","),
]
.map((id) => id.trim())
.filter(Boolean);
return adminIds.includes(userId);
}