diff --git a/env.example b/env.example
index e676a10..2808150 100644
--- a/env.example
+++ b/env.example
@@ -1,3 +1,6 @@
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
-NEXTAUTH_SECRET=
\ No newline at end of file
+NEXTAUTH_SECRET=
+DISCORD_GUILD_ID=
+DISCORD_REQUIRED_ROLE_ID=
+DISCORD_BOT_TOKEN=
\ No newline at end of file
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
index 99d4a81..447a9b8 100644
--- a/src/app/api/auth/[...nextauth]/route.ts
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -1,3 +1,4 @@
+import { verifyUserRole } from "@/lib/discord";
import type { NextAuthOptions } from "next-auth";
import NextAuth from "next-auth";
import DiscordProvider, { type DiscordProfile } from "next-auth/providers/discord";
@@ -24,6 +25,20 @@ export const authOptions: NextAuthOptions = {
},
}),
],
+ callbacks: {
+ async signIn({ user }) {
+ const hasRequiredRole = await verifyUserRole(user.id);
+
+ if (!hasRequiredRole) {
+ return false;
+ }
+
+ return true;
+ },
+ },
+ pages: {
+ error: "/auth/error",
+ },
secret: process.env.NEXTAUTH_SECRET,
};
diff --git a/src/app/auth/error/AuthErrorClient.tsx b/src/app/auth/error/AuthErrorClient.tsx
new file mode 100644
index 0000000..e010689
--- /dev/null
+++ b/src/app/auth/error/AuthErrorClient.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import LanguageMenu from "@/components/LanguageMenu";
+import { useLanguage } from "@/LanguageContext";
+import { translations } from "@/translations";
+import { signIn } from "next-auth/react";
+import { FaDiscord } from "react-icons/fa";
+
+export default function AuthErrorClient() {
+ const { language } = useLanguage();
+ const t = translations[language];
+
+ return (
+ <>
+
+
+
+
+
+
{t.accessDenied}
+
{t.accessDeniedMessage}
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/auth/error/page.tsx b/src/app/auth/error/page.tsx
new file mode 100644
index 0000000..74bcf01
--- /dev/null
+++ b/src/app/auth/error/page.tsx
@@ -0,0 +1,20 @@
+import { authOptions } from "@/api/auth/[...nextauth]/route";
+import { LanguageProvider } from "@/LanguageContext";
+import { getServerSession } from "next-auth";
+import { redirect } from "next/navigation";
+import AuthErrorClient from "./AuthErrorClient";
+
+export default async function AuthErrorPage() {
+ const session = await getServerSession(authOptions);
+
+ // If user is already authenticated, redirect to home
+ if (session) {
+ redirect("/");
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/lib/discord.ts b/src/app/lib/discord.ts
new file mode 100644
index 0000000..3d2a49b
--- /dev/null
+++ b/src/app/lib/discord.ts
@@ -0,0 +1,61 @@
+interface DiscordGuildMember {
+ user: {
+ id: string;
+ username: string;
+ discriminator: string;
+ avatar: string | null;
+ };
+ roles: string[];
+ joined_at: string;
+}
+
+export async function verifyUserRole(userId: string): Promise {
+ const guildId = process.env.DISCORD_GUILD_ID;
+ const requiredRoleId = process.env.DISCORD_REQUIRED_ROLE_ID;
+
+ if (!guildId || !requiredRoleId) {
+ console.error("Missing DISCORD_GUILD_ID or DISCORD_REQUIRED_ROLE_ID environment variables");
+ return false;
+ }
+
+ try {
+ // Get the bot token from the Discord application
+ const botToken = process.env.DISCORD_BOT_TOKEN;
+ if (!botToken) {
+ console.error("Missing DISCORD_BOT_TOKEN environment variable");
+ return false;
+ }
+
+ // Fetch the user's guild member information
+ const response = await fetch(`https://discord.com/api/v10/guilds/${guildId}/members/${userId}`, {
+ headers: {
+ Authorization: `Bot ${botToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ // User is not a member of the guild
+ console.log(`User ${userId} is not a member of guild ${guildId}`);
+ return false;
+ }
+ if (response.status === 403) {
+ console.error("Bot does not have permission to view guild members");
+ return false;
+ }
+ throw new Error(`Discord API error: ${response.status} ${response.statusText}`);
+ }
+
+ const member: DiscordGuildMember = await response.json();
+
+ // Check if the user has the required role
+ const hasRole = member.roles.includes(requiredRoleId);
+ console.log(`User ${userId} has role ${requiredRoleId}: ${hasRole}`);
+
+ return hasRole;
+ } catch (error) {
+ console.error("Error verifying user role:", error);
+ return false;
+ }
+}
diff --git a/src/app/translations.ts b/src/app/translations.ts
index 80807e6..7df4979 100644
--- a/src/app/translations.ts
+++ b/src/app/translations.ts
@@ -15,6 +15,9 @@ export const translations = {
subject: "UiO Gamers",
loginPrompt: "Please log in to play",
loginWithDiscord: "Login with Discord",
+ accessDenied: "Access Denied",
+ accessDeniedMessage: "You don't have the paying member role in the UiO Gaming Discord server.",
+ tryAgain: "Try again logging in again",
},
no: {
submittedAnswers: "Dine svar",
@@ -32,5 +35,8 @@ export const translations = {
subject: "UiO-Gamere",
loginPrompt: "Vennligst logg inn for å spille",
loginWithDiscord: "Logg inn med Discord",
+ accessDenied: "Tilgang nektet",
+ accessDeniedMessage: 'Du har ikke rollen "Betalende medlem" i UiO Gaming sin Discord-server.',
+ tryAgain: "Logg på igjen",
},
};