add inital auth implementation

This commit is contained in:
Ayden Jahola 2024-10-29 13:00:28 +00:00
parent 3a2fe0ebc1
commit 43b8e55022
No known key found for this signature in database
GPG key ID: 71DD90AE4AE92742
10 changed files with 550 additions and 14 deletions

View file

@ -0,0 +1,255 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import axios from "axios";
interface ServerSetting {
_id: string;
guildId: string;
emailDomains: string[];
countingChannelId: string;
generalChannelId: string;
logChannelId: string;
verificationChannelId: string;
verifiedRoleName: string;
actionItemsChannelId: string;
actionItemsTargetChannelId: string;
}
interface Channel {
id: string;
name: string;
}
interface GuildData {
guildId: string;
guildName: string;
guildIcon: string | null;
settings: ServerSetting | null;
channels: Channel[];
}
const getChannelNames = async (
guildId: string,
channelIds: string[]
): Promise<Channel[]> => {
const channels: Channel[] = [];
try {
const response = await axios.get(
`/api/discord/channels?guildId=${guildId}`
);
const allChannels: Channel[] = response.data;
channelIds.forEach((channelId) => {
const channel = allChannels.find((c) => c.id === channelId);
if (channel) {
channels.push({ id: channel.id, name: channel.name });
}
});
} catch (error) {
console.error("Error fetching channels:", error);
}
return channels;
};
export default function ManageServerPage({
params,
}: {
params: Promise<{ guildId: string }>;
}) {
const router = useRouter();
const [guildData, setGuildData] = useState<GuildData | null>(null);
const [activeTab, setActiveTab] = useState<string>("settings");
useEffect(() => {
const fetchParams = async () => {
if (params) {
const resolvedParams = await params;
const guildId = resolvedParams.guildId;
const response = await fetch(`/api/discord/guilds`);
const serverSettings = await response.json();
const guildInfo = serverSettings.find(
(setting: ServerSetting) => setting.guildId === guildId
);
if (guildInfo) {
const channelIds = [
guildInfo.countingChannelId,
guildInfo.generalChannelId,
guildInfo.logChannelId,
guildInfo.verificationChannelId,
guildInfo.actionItemsChannelId,
guildInfo.actionItemsTargetChannelId,
];
const channels = await getChannelNames(guildId, channelIds);
setGuildData({
guildId,
guildName: guildInfo.guildName,
guildIcon: guildInfo.guildIcon,
settings: guildInfo,
channels,
});
}
}
};
fetchParams();
}, [params]);
const renderSettingsContent = () => {
if (!guildData?.settings) return <div>No settings found.</div>;
const {
emailDomains,
countingChannelId,
generalChannelId,
logChannelId,
verificationChannelId,
verifiedRoleName,
actionItemsChannelId,
actionItemsTargetChannelId,
} = guildData.settings;
const countingChannel =
guildData.channels?.find((c) => c.id === countingChannelId)?.name ||
countingChannelId;
const generalChannel =
guildData.channels?.find((c) => c.id === generalChannelId)?.name ||
generalChannelId;
const logChannel =
guildData.channels?.find((c) => c.id === logChannelId)?.name ||
logChannelId;
const verificationChannel =
guildData.channels?.find((c) => c.id === verificationChannelId)?.name ||
verificationChannelId;
const actionItemsChannel =
guildData.channels?.find((c) => c.id === actionItemsChannelId)?.name ||
actionItemsChannelId;
const actionItemsTargetChannel =
guildData.channels?.find((c) => c.id === actionItemsTargetChannelId)
?.name || actionItemsTargetChannelId;
return (
<div className="space-y-4">
<h2 className="text-2xl font-bold">Current Server Settings</h2>
<div>
<strong>Guild ID:</strong> {guildData.guildId}
</div>
<div>
<strong>Email Domains:</strong> {emailDomains.join(", ") || "None"}
</div>
<div>
<strong>Counting Channel:</strong> {countingChannel}
</div>
<div>
<strong>General Channel:</strong> {generalChannel}
</div>
<div>
<strong>Log Channel:</strong> {logChannel}
</div>
<div>
<strong>Verification Channel:</strong> {verificationChannel}
</div>
<div>
<strong>Verified Role Name:</strong> {verifiedRoleName}
</div>
<div>
<strong>Action Items Channel:</strong> {actionItemsChannel}
</div>
<div>
<strong>Action Items Target Channel:</strong>{" "}
{actionItemsTargetChannel}
</div>
</div>
);
};
const renderContent = () => {
switch (activeTab) {
case "settings":
return renderSettingsContent();
case "roles":
return <div>Roles management for {guildData?.guildName}</div>;
case "channels":
return <div>Channels management for {guildData?.guildName}</div>;
case "logs":
return <div>Logs for {guildData?.guildName}</div>;
default:
return <div>Select a tab to manage.</div>;
}
};
return (
<div className="flex min-h-screen bg-gray-900 text-gray-200">
<aside className="w-64 bg-gray-800 p-4">
<h2 className="text-lg font-bold mb-4">Admin Panel</h2>
<ul className="space-y-2">
<li>
<button
className={`w-full text-left p-2 rounded hover:bg-gray-700 ${
activeTab === "settings" ? "bg-gray-700" : ""
}`}
onClick={() => setActiveTab("settings")}
>
Server Settings
</button>
</li>
<li>
<button
className={`w-full text-left p-2 rounded hover:bg-gray-700 ${
activeTab === "roles" ? "bg-gray-700" : ""
}`}
onClick={() => setActiveTab("roles")}
>
Manage Roles
</button>
</li>
<li>
<button
className={`w-full text-left p-2 rounded hover:bg-gray-700 ${
activeTab === "channels" ? "bg-gray-700" : ""
}`}
onClick={() => setActiveTab("channels")}
>
Manage Channels
</button>
</li>
<li>
<button
className={`w-full text-left p-2 rounded hover:bg-gray-700 ${
activeTab === "logs" ? "bg-gray-700" : ""
}`}
onClick={() => setActiveTab("logs")}
>
View Logs
</button>
</li>
</ul>
</aside>
<main className="flex-1 p-8">
<h1 className="text-4xl font-bold mb-4">
Manage Server: {guildData ? guildData.guildName : "Loading..."}
</h1>
{guildData?.guildIcon && (
<img
src={`https://cdn.discordapp.com/icons/${guildData.guildId}/${guildData.guildIcon}.png`}
alt={`${guildData.guildName} icon`}
className="w-24 h-24 mb-4"
/>
)}
{renderContent()}
<button
onClick={() => router.back()}
className="mt-4 px-4 py-2 bg-blue-600 rounded hover:bg-blue-500 transition duration-200"
>
Back
</button>
</main>
</div>
);
}

View file

@ -0,0 +1,90 @@
"use client";
import { useEffect, useState } from "react";
import { useSession, signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
interface ServerSetting {
_id: string;
guildId: string;
guildName: string;
guildIcon: string | null;
}
export default function AdminPage() {
const { data: session } = useSession();
const [serverSettings, setServerSettings] = useState<ServerSetting[]>([]);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
if (!session) return;
const fetchServerSettings = async () => {
try {
const response = await fetch("/api/discord/guilds");
if (!response.ok) {
throw new Error("Failed to fetch server settings");
}
const data = await response.json();
console.log("Fetched Server Settings:", data);
setServerSettings(data);
} catch (error) {
console.error("Error fetching server settings:", error);
} finally {
setLoading(false);
}
};
fetchServerSettings();
}, [session]);
if (!session) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-gray-200">
<h1 className="text-2xl font-bold">Access Denied</h1>
<p>Please sign in to access this page.</p>
</div>
);
}
const handleServerClick = (guildId: string) => {
router.push(`/admin/manage/${guildId}`);
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-gray-200">
<h1 className="text-4xl font-bold mb-4">Welcome to the Admin Page!</h1>
<p className="mb-6">You are signed in as: {session.user?.name}</p>
<button
onClick={() => signOut({ callbackUrl: "/" })}
className="px-6 py-2 bg-red-600 rounded hover:bg-red-500 transition duration-200 mb-4"
>
Sign Out
</button>
{loading ? (
<p className="mt-4">Loading server settings...</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mt-4">
{serverSettings.map((setting) => (
<div
key={setting._id}
onClick={() => handleServerClick(setting.guildId)}
className="bg-gray-800 p-4 rounded shadow-md cursor-pointer hover:bg-gray-700 transition duration-200 flex flex-col items-center"
>
{setting.guildIcon && (
<img
src={`https://cdn.discordapp.com/icons/${setting.guildId}/${setting.guildIcon}.png`}
alt={`${setting.guildName} icon`}
className="w-16 h-16 rounded-full mb-2"
/>
)}
<h2 className="text-lg font-bold">{setting.guildName}</h2>
</div>
))}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,21 @@
import NextAuth from "next-auth";
import DiscordProvider from "next-auth/providers/discord";
export const authOptions = {
providers: [
DiscordProvider({
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
authorization: {
params: {
scope: "identify guilds",
},
},
}),
],
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View file

@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import axios from "axios";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../auth/[...nextauth]/route";
const DISCORD_API_BASE = "https://discord.com/api/v10";
const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
export async function GET(request: Request) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json(
{ error: "Unauthorized access." },
{ status: 401 }
);
}
const url = new URL(request.url);
const guildId = url.searchParams.get("guildId");
if (!guildId || !BOT_TOKEN) {
return NextResponse.json(
{ error: "Guild ID or Discord bot token not set." },
{ status: 400 }
);
}
try {
const response = await axios.get(
`${DISCORD_API_BASE}/guilds/${guildId}/channels`,
{
headers: {
Authorization: `Bot ${BOT_TOKEN}`,
},
}
);
return NextResponse.json(response.data);
} catch (error) {
console.error("Error fetching channels from Discord API:", error);
return NextResponse.error();
}
}

View file

@ -0,0 +1,74 @@
import { NextResponse } from "next/server";
import clientPromise from "@/lib/mongodb";
import axios from "axios";
interface ServerSetting {
_id: string;
guildId: string;
emailDomains: string[];
countingChannelId: string;
generalChannelId: string;
logChannelId: string;
verificationChannelId: string;
verifiedRoleName: string;
actionItemsChannelId: string;
actionItemsTargetChannelId: string;
}
const DISCORD_API_BASE = "https://discord.com/api/v10";
const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
export async function GET() {
if (!BOT_TOKEN) {
return NextResponse.json(
{ error: "Discord bot token not set." },
{ status: 500 }
);
}
try {
const client = await clientPromise;
const database = client.db("test");
const collection = database.collection<ServerSetting>("serversettings");
const serverSettings = await collection.find({}).toArray();
const enrichedServerSettings = await Promise.all(
serverSettings.map(async (setting) => {
try {
const guildResponse = await axios.get(
`${DISCORD_API_BASE}/guilds/${setting.guildId}`,
{
headers: {
Authorization: `Bot ${BOT_TOKEN}`,
},
}
);
const guildData = guildResponse.data;
return {
...setting,
guildName: guildData.name,
guildIcon: guildData.icon,
};
} catch (error) {
console.error(
`Error fetching guild details for ${setting.guildId}:`,
error
);
return {
...setting,
guildName: "Unknown Guild",
guildIcon: null,
};
}
})
);
return NextResponse.json(enrichedServerSettings);
} catch (error) {
console.error("Error fetching server settings:", error);
return NextResponse.error();
}
}

View file

@ -3,6 +3,7 @@ import localFont from "next/font/local";
import "./globals.css";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import ClientProvider from "@/components/ClientProvider";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
@ -26,14 +27,16 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Navbar />
{children}
<Footer />
</body>
</html>
<ClientProvider>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Navbar />
{children}
<Footer />
</body>
</html>
</ClientProvider>
);
}

View file

@ -0,0 +1,24 @@
"use client";
import { signIn } from "next-auth/react";
export default function SignIn() {
const handleSignIn = () => {
signIn("discord", { callbackUrl: "/admin" });
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-gray-200">
<h1 className="text-4xl font-bold mb-4">Sign In</h1>
<p className="mb-6">
Please sign in to access the features of our Discord bot.
</p>
<button
onClick={handleSignIn}
className="px-6 py-2 bg-blue-600 rounded hover:bg-blue-500 transition duration-200"
>
Sign in with Discord
</button>
</div>
);
}

View file

@ -0,0 +1,11 @@
"use client";
import { SessionProvider } from "next-auth/react";
interface ClientProviderProps {
children: React.ReactNode;
}
export default function ClientProvider({ children }: ClientProviderProps) {
return <SessionProvider>{children}</SessionProvider>;
}

10
dashboard/lib/mongodb.ts Normal file
View file

@ -0,0 +1,10 @@
import { MongoClient } from "mongodb";
if (!process.env.MONGODB_URI) {
throw new Error("MongoDB URI not found in .env.local");
}
const client = new MongoClient(process.env.MONGODB_URI);
const clientPromise = client.connect();
export default clientPromise;

View file

@ -9,18 +9,22 @@
"lint": "next lint"
},
"dependencies": {
"@types/next-auth": "^3.13.0",
"axios": "^1.7.7",
"mongodb": "^6.10.0",
"next": "15.0.1",
"next-auth": "^4.24.10",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"next": "15.0.1"
"react-dom": "19.0.0-rc-69d4b800-20241021"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.1",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "15.0.1"
"typescript": "^5"
}
}