feat: credentials login, guild restrictie, schema username/password
All checks were successful
Build & Deploy / build (push) Successful in 2m1s

This commit is contained in:
2026-05-15 20:19:16 +02:00
parent a094e67f69
commit e1847106fe
4 changed files with 79 additions and 16 deletions

View File

@@ -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=

View File

@@ -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[]

View File

@@ -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<string, string> = {
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() {
<p className="text-slate-500 text-sm mt-1">Log je slaap, win het klassement.</p>
</div>
{/* OAuth fout (bv. niet lid van server) */}
{oauthError && (
<div className="bg-red-500/10 border border-red-500/30 text-red-400 rounded-xl px-4 py-3 text-sm text-center">
{oauthError}
</div>
)}
{/* Discord */}
<button
onClick={() => signIn("discord", { callbackUrl: "/" })}
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-indigo-600 hover:bg-indigo-500 rounded-xl font-semibold text-white transition-colors"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<svg className="w-5 h-5 shrink-0" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057c.002.022.015.043.033.057a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z" />
</svg>
Inloggen met Discord
@@ -106,7 +120,17 @@ export default function LoginPage() {
Registreer hier
</Link>
</p>
</div>
</div>
);
}
// useSearchParams vereist Suspense boundary in Next.js App Router
export default function LoginPage() {
return (
<Suspense>
<LoginForm />
</Suspense>
);
}

View File

@@ -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;