diff --git a/.env.example b/.env.example
index 60d1df2..5c1ed4b 100644
--- a/.env.example
+++ b/.env.example
@@ -20,3 +20,11 @@ NEXTAUTH_SECRET=
# Redirect URI instellen op: https://slaap.jouwdomein.be/api/auth/callback/discord
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
+
+# --- Discord server restrictie (optioneel) ---
+# Vul in om login te beperken tot leden van jouw Discord server.
+# Leeg laten = iedereen met een Discord account kan inloggen.
+#
+# Server ID vinden: Discord → rechtsklik op je server → "Server-ID kopiëren"
+# (Zet Ontwikkelaarsmodus aan via Instellingen → Geavanceerd)
+DISCORD_GUILD_ID=
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 5bbc62a..c0eac89 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -54,6 +54,9 @@ model User {
email String? @unique
emailVerified DateTime?
image String?
+ // Credentials login (optioneel — Discord users hebben dit niet)
+ username String? @unique
+ password String?
accounts Account[]
sessions Session[]
sleepEntries SleepEntry[]
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index 9a513db..ea08ca9 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -1,20 +1,31 @@
"use client";
import { signIn, useSession } from "next-auth/react";
-import { useRouter } from "next/navigation";
-import { useState, useEffect } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { useState, useEffect, Suspense } from "react";
import Link from "next/link";
-export default function LoginPage() {
- const { data: session, status } = useSession();
+const DISCORD_ERRORS: Record = {
+ not_in_server: "Je moet lid zijn van onze Discord server om in te loggen.",
+ OAuthAccountNotLinked: "Dit e-mailadres is al gekoppeld aan een ander account.",
+ default: "Inloggen mislukt, probeer opnieuw.",
+};
+
+function LoginForm() {
+ const { status } = useSession();
const router = useRouter();
+ const params = useSearchParams();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
- // Al ingelogd → meteen door naar klassement
+ // Foutmelding uit URL (bv. ?error=not_in_server)
+ const urlError = params.get("error");
+ const oauthError = urlError ? (DISCORD_ERRORS[urlError] ?? DISCORD_ERRORS.default) : null;
+
+ // Al ingelogd → meteen door
useEffect(() => {
if (status === "authenticated") router.replace("/");
}, [status, router]);
@@ -30,11 +41,7 @@ export default function LoginPage() {
async function handleCredentials() {
setError("");
setLoading(true);
- const res = await signIn("credentials", {
- username,
- password,
- redirect: false,
- });
+ const res = await signIn("credentials", { username, password, redirect: false });
setLoading(false);
if (res?.error) {
setError("Gebruikersnaam of wachtwoord klopt niet.");
@@ -54,12 +61,19 @@ export default function LoginPage() {
Log je slaap, win het klassement.
+ {/* OAuth fout (bv. niet lid van server) */}
+ {oauthError && (
+
+ ⚠️ {oauthError}
+
+ )}
+
{/* Discord */}
+
);
+}
+
+// useSearchParams vereist Suspense boundary in Next.js App Router
+export default function LoginPage() {
+ return (
+
+
+
+ );
}
\ No newline at end of file
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 2f611ca..5e32f9d 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -12,30 +12,58 @@ export const authOptions: NextAuthOptions = {
DiscordProvider({
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
+ authorization: {
+ params: {
+ // guilds scope zodat we kunnen controleren of de user lid is
+ scope: "identify email guilds",
+ },
+ },
}),
CredentialsProvider({
name: "credentials",
credentials: {
username: { label: "Gebruikersnaam", type: "text" },
- password: { label: "Wachtwoord", type: "password" },
+ password: { label: "Wachtwoord", type: "password" },
},
async authorize(credentials) {
if (!credentials?.username || !credentials?.password) return null;
-
const user = await prisma.user.findUnique({
where: { username: credentials.username },
});
-
if (!user?.password) return null;
-
const valid = await bcrypt.compare(credentials.password, user.password);
if (!valid) return null;
-
return { id: user.id, name: user.name, email: user.email, image: user.image };
},
}),
],
callbacks: {
+ async signIn({ account, profile }) {
+ // Credentials login: altijd toegestaan
+ if (account?.provider !== "discord") return true;
+
+ const guildId = process.env.DISCORD_GUILD_ID;
+
+ // Geen DISCORD_GUILD_ID ingesteld → geen beperking
+ if (!guildId) return true;
+
+ // Haal de guilds op van de ingelogde Discord gebruiker
+ const res = await fetch("https://discord.com/api/users/@me/guilds", {
+ headers: { Authorization: `Bearer ${account.access_token}` },
+ });
+
+ if (!res.ok) return false;
+
+ const guilds: { id: string }[] = await res.json();
+ const isMember = guilds.some((g) => g.id === guildId);
+
+ if (!isMember) {
+ // Stuur door naar login met foutmelding
+ return "/login?error=not_in_server";
+ }
+
+ return true;
+ },
jwt({ token, user }) {
if (user) token.id = user.id;
return token;