Aktueller Stand
This commit is contained in:
@@ -3,7 +3,7 @@ import { getServerSession } from "next-auth";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { isAdminSession, requireSession } from "../../../lib/auth-helpers";
|
||||
import { authOptions } from "../../../lib/auth";
|
||||
import { getAccessSettings } from "../../../lib/system-settings";
|
||||
import { getAccessSettings, getEmailVerificationRequired } from "../../../lib/system-settings";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -13,7 +13,8 @@ export async function GET(request: Request) {
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
if (session?.user?.emailVerified === false) {
|
||||
const emailVerificationRequired = await getEmailVerificationRequired();
|
||||
if (emailVerificationRequired && session?.user?.emailVerified === false) {
|
||||
return NextResponse.json(
|
||||
{ error: "E-Mail nicht verifiziert." },
|
||||
{ status: 403 }
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { requireSession } from "../../../lib/auth-helpers";
|
||||
import { sendMail } from "../../../lib/mailer";
|
||||
import { getEmailVerificationRequired } from "../../../lib/system-settings";
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
const { session } = await requireSession();
|
||||
@@ -36,6 +37,7 @@ export async function PATCH(request: Request) {
|
||||
}
|
||||
|
||||
const data: { email?: string; passwordHash?: string; emailVerified?: boolean } = {};
|
||||
let emailVerificationRequired: boolean | null = null;
|
||||
|
||||
if (normalizedEmail && normalizedEmail !== user.email) {
|
||||
const existing = await prisma.user.findUnique({ where: { email: normalizedEmail } });
|
||||
@@ -46,7 +48,8 @@ export async function PATCH(request: Request) {
|
||||
);
|
||||
}
|
||||
data.email = normalizedEmail;
|
||||
data.emailVerified = false;
|
||||
emailVerificationRequired = await getEmailVerificationRequired();
|
||||
data.emailVerified = !emailVerificationRequired;
|
||||
}
|
||||
|
||||
if (newPassword) {
|
||||
@@ -63,6 +66,17 @@ export async function PATCH(request: Request) {
|
||||
});
|
||||
|
||||
if (data.email) {
|
||||
const verificationRequired =
|
||||
emailVerificationRequired ?? (await getEmailVerificationRequired());
|
||||
if (!verificationRequired) {
|
||||
return NextResponse.json({
|
||||
id: updated.id,
|
||||
email: updated.email,
|
||||
changedEmail: true,
|
||||
changedPassword: Boolean(data.passwordHash)
|
||||
});
|
||||
}
|
||||
|
||||
const token = crypto.randomUUID();
|
||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
await prisma.verificationToken.create({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { isAdminEmail, isSuperAdminEmail } from "../../../lib/auth";
|
||||
import { sendMail } from "../../../lib/mailer";
|
||||
import { checkRateLimit, getRateLimitConfig } from "../../../lib/rate-limit";
|
||||
import { getClientIp } from "../../../lib/request";
|
||||
import { getEmailVerificationRequired } from "../../../lib/system-settings";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const registrationSetting = await prisma.setting.findUnique({
|
||||
@@ -49,6 +50,9 @@ export async function POST(request: Request) {
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
const superAdmin = isSuperAdminEmail(normalizedEmail);
|
||||
const admin = isAdminEmail(normalizedEmail) || superAdmin;
|
||||
const emailVerificationRequired = await getEmailVerificationRequired();
|
||||
const shouldVerifyEmail = emailVerificationRequired && !admin;
|
||||
const emailVerified = admin || !emailVerificationRequired;
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
@@ -57,7 +61,7 @@ export async function POST(request: Request) {
|
||||
passwordHash,
|
||||
role: superAdmin ? "SUPERADMIN" : admin ? "ADMIN" : "USER",
|
||||
status: admin ? "ACTIVE" : "PENDING",
|
||||
emailVerified: admin
|
||||
emailVerified
|
||||
}
|
||||
});
|
||||
|
||||
@@ -82,7 +86,7 @@ export async function POST(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
if (!admin) {
|
||||
if (shouldVerifyEmail) {
|
||||
const token = randomUUID();
|
||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
await prisma.verificationToken.create({
|
||||
|
||||
@@ -4,9 +4,15 @@ import { prisma } from "../../../../lib/prisma";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key: "registration_enabled" }
|
||||
const settings = await prisma.setting.findMany({
|
||||
where: { key: { in: ["registration_enabled", "email_verification_required"] } }
|
||||
});
|
||||
const map = new Map(settings.map((record) => [record.key, record.value]));
|
||||
const registrationEnabled = map.get("registration_enabled") !== "false";
|
||||
const emailVerificationRequired =
|
||||
map.get("email_verification_required") !== "false";
|
||||
return NextResponse.json({
|
||||
registrationEnabled,
|
||||
emailVerificationRequired
|
||||
});
|
||||
const registrationEnabled = setting?.value !== "false";
|
||||
return NextResponse.json({ registrationEnabled });
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ export async function POST(request: Request) {
|
||||
apiKey,
|
||||
provider,
|
||||
registrationEnabled,
|
||||
publicAccessEnabled
|
||||
publicAccessEnabled,
|
||||
emailVerificationRequired
|
||||
} = body || {};
|
||||
|
||||
if (!provider || !["google", "osm"].includes(provider)) {
|
||||
@@ -59,6 +60,14 @@ export async function POST(request: Request) {
|
||||
create: { key: "registration_enabled", value: registrationValue }
|
||||
});
|
||||
|
||||
const verificationValue =
|
||||
emailVerificationRequired === false ? "false" : "true";
|
||||
await prisma.setting.upsert({
|
||||
where: { key: "email_verification_required" },
|
||||
update: { value: verificationValue },
|
||||
create: { key: "email_verification_required", value: verificationValue }
|
||||
});
|
||||
|
||||
const existing = await getSystemSettings();
|
||||
const nextPublicAccessEnabled =
|
||||
typeof publicAccessEnabled === "boolean"
|
||||
@@ -79,6 +88,7 @@ export async function POST(request: Request) {
|
||||
apiKey: apiKeySetting.value,
|
||||
provider: providerSetting.value,
|
||||
registrationEnabled: registrationValue !== "false",
|
||||
publicAccessEnabled: nextPublicAccessEnabled
|
||||
publicAccessEnabled: nextPublicAccessEnabled,
|
||||
emailVerificationRequired: verificationValue !== "false"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -266,6 +266,7 @@ export async function DELETE(request: Request) {
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const userId = searchParams.get("id");
|
||||
const hardDelete = searchParams.get("hard") === "true";
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "Ungültige Anfrage." }, { status: 400 });
|
||||
@@ -291,6 +292,71 @@ export async function DELETE(request: Request) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
if (hardDelete) {
|
||||
if (!isSuperAdminSession(session)) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { email: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Benutzer nicht gefunden." }, { status: 404 });
|
||||
}
|
||||
|
||||
const events = await prisma.event.findMany({
|
||||
where: { createdById: userId },
|
||||
select: { id: true }
|
||||
});
|
||||
const eventIds = events.map((event) => event.id);
|
||||
|
||||
const views = await prisma.userView.findMany({
|
||||
where: { userId },
|
||||
select: { id: true }
|
||||
});
|
||||
const viewIds = views.map((view) => view.id);
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
if (eventIds.length > 0) {
|
||||
await tx.userViewItem.deleteMany({
|
||||
where: { eventId: { in: eventIds } }
|
||||
});
|
||||
await tx.userViewExclusion.deleteMany({
|
||||
where: { eventId: { in: eventIds } }
|
||||
});
|
||||
await tx.event.deleteMany({
|
||||
where: { id: { in: eventIds } }
|
||||
});
|
||||
}
|
||||
|
||||
if (viewIds.length > 0) {
|
||||
await tx.userViewItem.deleteMany({
|
||||
where: { viewId: { in: viewIds } }
|
||||
});
|
||||
await tx.userViewCategory.deleteMany({
|
||||
where: { viewId: { in: viewIds } }
|
||||
});
|
||||
await tx.userViewExclusion.deleteMany({
|
||||
where: { viewId: { in: viewIds } }
|
||||
});
|
||||
await tx.userView.deleteMany({
|
||||
where: { id: { in: viewIds } }
|
||||
});
|
||||
}
|
||||
|
||||
await tx.session.deleteMany({ where: { userId } });
|
||||
await tx.account.deleteMany({ where: { userId } });
|
||||
await tx.passwordResetToken.deleteMany({ where: { userId } });
|
||||
await tx.loginAttempt.deleteMany({ where: { email: user.email } });
|
||||
await tx.verificationToken.deleteMany({ where: { identifier: user.email } });
|
||||
await tx.user.delete({ where: { id: userId } });
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, deleted: true });
|
||||
}
|
||||
|
||||
await prisma.session.deleteMany({ where: { userId } });
|
||||
await prisma.account.deleteMany({ where: { userId } });
|
||||
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
import { getSession, signIn } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showVerifyLink, setShowVerifyLink] = useState(false);
|
||||
const [registrationEnabled, setRegistrationEnabled] = useState<boolean | null>(
|
||||
null
|
||||
);
|
||||
const [registered, setRegistered] = useState(false);
|
||||
const [prefillEmail, setPrefillEmail] = useState("");
|
||||
const [emailVerificationRequired, setEmailVerificationRequired] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
setRegistered(params.get("registered") === "1");
|
||||
setPrefillEmail(params.get("email") || "");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadRegistration = async () => {
|
||||
@@ -20,8 +28,10 @@ export default function LoginPage() {
|
||||
if (!response.ok) return;
|
||||
const payload = await response.json();
|
||||
setRegistrationEnabled(payload.registrationEnabled !== false);
|
||||
setEmailVerificationRequired(payload.emailVerificationRequired !== false);
|
||||
} catch {
|
||||
setRegistrationEnabled(true);
|
||||
setEmailVerificationRequired(true);
|
||||
}
|
||||
};
|
||||
loadRegistration();
|
||||
@@ -38,9 +48,15 @@ export default function LoginPage() {
|
||||
const result = await signIn("credentials", {
|
||||
email,
|
||||
password,
|
||||
redirect: false
|
||||
redirect: false,
|
||||
callbackUrl: "/"
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
setError("Login fehlgeschlagen.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (result?.error) {
|
||||
if (result.error === "PENDING") {
|
||||
setError("Dein Konto wartet auf Freischaltung durch einen Admin.");
|
||||
@@ -65,7 +81,12 @@ export default function LoginPage() {
|
||||
|
||||
if (result?.ok) {
|
||||
setShowVerifyLink(false);
|
||||
router.push("/");
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
setError("Login fehlgeschlagen.");
|
||||
return;
|
||||
}
|
||||
window.location.href = result.url || "/";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,12 +96,20 @@ export default function LoginPage() {
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Bitte anmelden.
|
||||
</p>
|
||||
{registered && (
|
||||
<div className="mt-4 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700">
|
||||
{emailVerificationRequired
|
||||
? "Account erstellt. Bitte E-Mail bestätigen und auf die Freischaltung durch einen Admin warten."
|
||||
: "Account erstellt. Bitte auf die Freischaltung durch einen Admin warten."}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={onSubmit} className="mt-4 space-y-3">
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="E-Mail"
|
||||
required
|
||||
defaultValue={prefillEmail}
|
||||
className="w-full rounded-xl border border-slate-300 px-3 py-2"
|
||||
/>
|
||||
<input
|
||||
|
||||
@@ -2,17 +2,19 @@ import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import CalendarBoard from "../components/CalendarBoard";
|
||||
import { authOptions } from "../lib/auth";
|
||||
import { getAccessSettings } from "../lib/system-settings";
|
||||
import { getAccessSettings, getEmailVerificationRequired } from "../lib/system-settings";
|
||||
|
||||
export default async function HomePage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
const { publicAccessEnabled } = await getAccessSettings();
|
||||
const emailVerificationRequired = await getEmailVerificationRequired();
|
||||
if (!session?.user && !publicAccessEnabled) {
|
||||
redirect("/login");
|
||||
}
|
||||
const isBlocked =
|
||||
session?.user &&
|
||||
(session.user.status !== "ACTIVE" || session.user.emailVerified === false);
|
||||
(session.user.status !== "ACTIVE" ||
|
||||
(emailVerificationRequired && session.user.emailVerified === false));
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{isBlocked ? (
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [done, setDone] = useState(false);
|
||||
const [showVerifyLink, setShowVerifyLink] = useState(false);
|
||||
const [registrationEnabled, setRegistrationEnabled] = useState<boolean | null>(
|
||||
null
|
||||
);
|
||||
@@ -28,6 +27,7 @@ export default function RegisterPage() {
|
||||
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
setShowVerifyLink(false);
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const payload = {
|
||||
@@ -48,12 +48,11 @@ export default function RegisterPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
setDone(true);
|
||||
await signIn("credentials", {
|
||||
email: payload.email,
|
||||
password: payload.password,
|
||||
callbackUrl: "/"
|
||||
});
|
||||
const emailValue = String(payload.email || "").trim().toLowerCase();
|
||||
const nextUrl = emailValue
|
||||
? `/login?registered=1&email=${encodeURIComponent(emailValue)}`
|
||||
: "/login?registered=1";
|
||||
window.location.href = nextUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -94,15 +93,20 @@ export default function RegisterPage() {
|
||||
Konto anlegen
|
||||
</button>
|
||||
</form>
|
||||
{done && (
|
||||
<p className="mt-3 text-sm text-emerald-600">Account erstellt.</p>
|
||||
)}
|
||||
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
|
||||
</>
|
||||
)}
|
||||
<p className="mt-4 text-sm text-slate-600">
|
||||
Schon registriert? <Link href="/login" className="text-brand-700">Login</Link>
|
||||
</p>
|
||||
{showVerifyLink && (
|
||||
<p className="mt-2 text-sm text-slate-600">
|
||||
E-Mail nicht bestätigt?{" "}
|
||||
<Link href="/verify" className="text-brand-700">
|
||||
Link erneut senden
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user