Update configuration and README for improved setup and security; disable default API and emoji sync, and enhance Discord permission checks in API routes.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
259
dashboard/lib/api.ts
Normal file
259
dashboard/lib/api.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* ╔══════════════════════════════════════════════════════════════════╗
|
||||
* ║ ║
|
||||
* ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
* ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
* ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
* ║ ║
|
||||
* ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
* ║ ║
|
||||
* ║ discord ── https://discord.gg/codexdev ║
|
||||
* ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
* ║ github ── https://github.com/RayExo ║
|
||||
* ║ ║
|
||||
* ╚══════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
import {
|
||||
BotInfo,
|
||||
BotStatus,
|
||||
GuildSummary,
|
||||
GuildDetails,
|
||||
PrefixConfig,
|
||||
AutomodConfig,
|
||||
TicketConfig,
|
||||
LevelingConfig,
|
||||
LoggingConfig,
|
||||
PrefixUpdate,
|
||||
AutomodUpdate,
|
||||
LevelingUpdate,
|
||||
LoggingUpdate,
|
||||
LeaderboardEntry,
|
||||
DiscordChannel,
|
||||
DiscordRole,
|
||||
AutoRoleConfig,
|
||||
AutoRoleUpdate,
|
||||
AdminStats,
|
||||
AdminConfig,
|
||||
AdminConfigUpdate
|
||||
} from "@/types/api";
|
||||
|
||||
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) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit & { next?: NextFetchRequestConfig } = {}
|
||||
): Promise<T> {
|
||||
const url = `${BASE_URL}${endpoint}`;
|
||||
|
||||
const headers = new Headers(options.headers);
|
||||
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 {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
// CRITICAL: Never cache mutation responses. For GETs, use revalidate: 0
|
||||
// to always fetch fresh data from the bot API. Caching was causing
|
||||
// saved data to "disappear" on reload because Next.js served stale cache.
|
||||
next: options.next || { revalidate: 0 },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch {
|
||||
errorData = { detail: "An unknown error occurred" };
|
||||
}
|
||||
console.error(`[API HTTP Error] Status ${response.status} for ${url}:`, errorData);
|
||||
throw new ApiError(response.status, errorData.detail || response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
console.error(`[API Network/Fetch Error] Failed to fetch ${url}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// Bot
|
||||
getBotStatus: () => request<BotStatus>("/bot/status"),
|
||||
getBotInfo: () => request<BotInfo>("/bot/info"),
|
||||
|
||||
// Guilds
|
||||
listGuilds: () => request<GuildSummary[]>("/guilds/"),
|
||||
getGuildDetails: (guildId: string) => request<any>(`/guilds/${guildId}`),
|
||||
getChannels: (guildId: string) => request<DiscordChannel[]>(`/guilds/${guildId}/channels`),
|
||||
getRoles: (guildId: string) => request<DiscordRole[]>(`/guilds/${guildId}/roles`),
|
||||
|
||||
// Module Configs
|
||||
getPrefix: (guildId: string) => request<PrefixConfig>(`/guilds/${guildId}/prefix`),
|
||||
updatePrefix: (guildId: string, prefix: string) =>
|
||||
request<{ status: string; new_prefix: string }>(`/guilds/${guildId}/prefix`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ prefix }),
|
||||
}),
|
||||
|
||||
getAutomod: (guildId: string) => request<AutomodConfig>(`/guilds/${guildId}/automod`),
|
||||
updateAutomod: (guildId: string, data: Partial<AutomodConfig>) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/automod`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getTickets: (guildId: string) => request<TicketConfig>(`/guilds/${guildId}/tickets`),
|
||||
updateTickets: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/tickets`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getLeveling: (guildId: string) => request<LevelingConfig>(`/guilds/${guildId}/leveling`),
|
||||
updateLeveling: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/leveling`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getLogging: (guildId: string) => request<LoggingConfig>(`/guilds/${guildId}/logging`),
|
||||
updateLogging: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/logging`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getLeaderboard: (guildId: string) => request<LeaderboardEntry[]>(`/guilds/${guildId}/leveling/leaderboard`),
|
||||
|
||||
getWelcome: (guildId: string) => request<any>(`/guilds/${guildId}/welcome`),
|
||||
updateWelcome: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/welcome`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getAntiNuke: (guildId: string) => request<any>(`/guilds/${guildId}/antinuke`),
|
||||
updateAntiNuke: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/antinuke`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getVerification: (guildId: string) => request<any>(`/guilds/${guildId}/verification`),
|
||||
updateVerification: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/verification`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getVanityRoles: (guildId: string) => request<any[]>(`/guilds/${guildId}/vanityroles`),
|
||||
addVanityRole: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/vanityroles`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
deleteVanityRole: (guildId: string, vanity: string) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/vanityroles/${vanity}`, {
|
||||
method: "DELETE",
|
||||
}),
|
||||
|
||||
getAutoRole: (guildId: string) => request<AutoRoleConfig>(`/guilds/${guildId}/autorole`),
|
||||
updateAutoRole: (guildId: string, data: AutoRoleUpdate) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/autorole`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getTracking: (guildId: string) => request<any>(`/guilds/${guildId}/tracking`),
|
||||
updateTracking: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/tracking`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getJ2C: (guildId: string) => request<any>(`/guilds/${guildId}/j2c`),
|
||||
updateJ2C: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/j2c`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getJoinDM: (guildId: string) => request<any>(`/guilds/${guildId}/joindm`),
|
||||
updateJoinDM: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/joindm`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getCustomRoles: (guildId: string) => request<any>(`/guilds/${guildId}/customroles`),
|
||||
updateCustomRoles: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/customroles`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getAutoReact: (guildId: string) => request<any>(`/guilds/${guildId}/autoreact`),
|
||||
updateAutoReact: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/autoreact`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getInvcRole: (guildId: string) => request<any>(`/guilds/${guildId}/invcrole`),
|
||||
updateInvcRole: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/invcrole`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getRR: (guildId: string) => request<any>(`/guilds/${guildId}/reactionroles`),
|
||||
updateRR: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/reactionroles`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
getInvites: (guildId: string) => request<any>(`/guilds/${guildId}/invites`),
|
||||
updateInvites: (guildId: string, data: any) =>
|
||||
request<{ status: string }>(`/guilds/${guildId}/invites`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
// Admin
|
||||
getAdminStats: () => request<AdminStats>("/admin/stats"),
|
||||
getAdminConfig: () => request<AdminConfig>("/admin/config"),
|
||||
updateAdminConfig: (data: AdminConfigUpdate) =>
|
||||
request<{ status: string }>("/admin/config", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
};
|
||||
48
dashboard/lib/auth.ts
Normal file
48
dashboard/lib/auth.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* ╔══════════════════════════════════════════════════════════════════╗
|
||||
* ║ ║
|
||||
* ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
* ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
* ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
* ║ ║
|
||||
* ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
* ║ ║
|
||||
* ║ discord ── https://discord.gg/codexdev ║
|
||||
* ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
* ║ github ── https://github.com/RayExo ║
|
||||
* ║ ║
|
||||
* ╚══════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
import DiscordProvider from "next-auth/providers/discord";
|
||||
import { AuthOptions } from "next-auth";
|
||||
|
||||
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 }) {
|
||||
if (account) {
|
||||
token.accessToken = account.access_token;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
// @ts-ignore
|
||||
session.user.id = token.sub;
|
||||
// @ts-ignore
|
||||
session.accessToken = token.accessToken;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/",
|
||||
},
|
||||
};
|
||||
45
dashboard/lib/guild-auth.ts
Normal file
45
dashboard/lib/guild-auth.ts
Normal 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;
|
||||
}
|
||||
33
dashboard/lib/utils.ts
Normal file
33
dashboard/lib/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* ╔══════════════════════════════════════════════════════════════════╗
|
||||
* ║ ║
|
||||
* ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
* ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
* ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
* ║ ║
|
||||
* ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
* ║ ║
|
||||
* ║ discord ── https://discord.gg/codexdev ║
|
||||
* ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
* ║ github ── https://github.com/RayExo ║
|
||||
* ║ ║
|
||||
* ╚══════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function isAdmin(userId?: string | null) {
|
||||
if (!userId) return false;
|
||||
const adminIds = [
|
||||
...(process.env.ADMIN_IDS || "").split(","),
|
||||
...(process.env.NEXT_PUBLIC_ADMIN_IDS || "").split(","),
|
||||
]
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean);
|
||||
return adminIds.includes(userId);
|
||||
}
|
||||
Reference in New Issue
Block a user