feat: credentials login, guild restrictie, schema username/password
All checks were successful
Build & Deploy / build (push) Successful in 2m1s
All checks were successful
Build & Deploy / build (push) Successful in 2m1s
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user