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
|
# Redirect URI instellen op: https://slaap.jouwdomein.be/api/auth/callback/discord
|
||||||
DISCORD_CLIENT_ID=
|
DISCORD_CLIENT_ID=
|
||||||
DISCORD_CLIENT_SECRET=
|
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
|
email String? @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
|
// Credentials login (optioneel — Discord users hebben dit niet)
|
||||||
|
username String? @unique
|
||||||
|
password String?
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
sleepEntries SleepEntry[]
|
sleepEntries SleepEntry[]
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, Suspense } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function LoginPage() {
|
const DISCORD_ERRORS: Record<string, string> = {
|
||||||
const { data: session, status } = useSession();
|
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 router = useRouter();
|
||||||
|
const params = useSearchParams();
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (status === "authenticated") router.replace("/");
|
if (status === "authenticated") router.replace("/");
|
||||||
}, [status, router]);
|
}, [status, router]);
|
||||||
@@ -30,11 +41,7 @@ export default function LoginPage() {
|
|||||||
async function handleCredentials() {
|
async function handleCredentials() {
|
||||||
setError("");
|
setError("");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await signIn("credentials", {
|
const res = await signIn("credentials", { username, password, redirect: false });
|
||||||
username,
|
|
||||||
password,
|
|
||||||
redirect: false,
|
|
||||||
});
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (res?.error) {
|
if (res?.error) {
|
||||||
setError("Gebruikersnaam of wachtwoord klopt niet.");
|
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>
|
<p className="text-slate-500 text-sm mt-1">Log je slaap, win het klassement.</p>
|
||||||
</div>
|
</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 */}
|
{/* Discord */}
|
||||||
<button
|
<button
|
||||||
onClick={() => signIn("discord", { callbackUrl: "/" })}
|
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"
|
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" />
|
<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>
|
</svg>
|
||||||
Inloggen met Discord
|
Inloggen met Discord
|
||||||
@@ -106,7 +120,17 @@ export default function LoginPage() {
|
|||||||
Registreer hier
|
Registreer hier
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</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({
|
DiscordProvider({
|
||||||
clientId: process.env.DISCORD_CLIENT_ID!,
|
clientId: process.env.DISCORD_CLIENT_ID!,
|
||||||
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
||||||
|
authorization: {
|
||||||
|
params: {
|
||||||
|
// guilds scope zodat we kunnen controleren of de user lid is
|
||||||
|
scope: "identify email guilds",
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
name: "credentials",
|
name: "credentials",
|
||||||
credentials: {
|
credentials: {
|
||||||
username: { label: "Gebruikersnaam", type: "text" },
|
username: { label: "Gebruikersnaam", type: "text" },
|
||||||
password: { label: "Wachtwoord", type: "password" },
|
password: { label: "Wachtwoord", type: "password" },
|
||||||
},
|
},
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
if (!credentials?.username || !credentials?.password) return null;
|
if (!credentials?.username || !credentials?.password) return null;
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { username: credentials.username },
|
where: { username: credentials.username },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user?.password) return null;
|
if (!user?.password) return null;
|
||||||
|
|
||||||
const valid = await bcrypt.compare(credentials.password, user.password);
|
const valid = await bcrypt.compare(credentials.password, user.password);
|
||||||
if (!valid) return null;
|
if (!valid) return null;
|
||||||
|
|
||||||
return { id: user.id, name: user.name, email: user.email, image: user.image };
|
return { id: user.id, name: user.name, email: user.email, image: user.image };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
callbacks: {
|
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 }) {
|
jwt({ token, user }) {
|
||||||
if (user) token.id = user.id;
|
if (user) token.id = user.id;
|
||||||
return token;
|
return token;
|
||||||
|
|||||||
Reference in New Issue
Block a user