Prepare Kalendartool for MPM and disable invites
This commit is contained in:
@@ -1,17 +1,15 @@
|
||||
/**
|
||||
* POST /api/auth/register
|
||||
*
|
||||
* Registrierung (plan.md Abschnitt 11). Zwei Wege:
|
||||
* 1. Mit Invite-Token -> User wird dem Kalender des Invites zugeordnet.
|
||||
* 2. Ohne Token -> nur wenn ALLOW_OPEN_REGISTRATION=true; der erste
|
||||
* Lokale Registrierung für den eigenständigen Betrieb. Sie ist nur
|
||||
* verfügbar, wenn ALLOW_OPEN_REGISTRATION=true; der erste
|
||||
* Benutzer wird ADMIN, weitere offene Registrierungen werden
|
||||
* dann automatisch zu USER ohne Kalender (oder abgelehnt, je
|
||||
* nach Konfiguration).
|
||||
*
|
||||
* Sicherheit:
|
||||
* - Rate-Limit pro IP (Brute-Force-/Enumeration-Schutz).
|
||||
* - SERIALIZABLE-Transaktion: Ein Invite kann trotz paralleler
|
||||
* Registrierungen nur EINMAL verwendet werden; der "erste Benutzer
|
||||
* - SERIALIZABLE-Transaktion: Der "erste Benutzer
|
||||
* wird ADMIN"-Wettlauf kann nicht mehrere Admins erzeugen.
|
||||
* - Bei bereits registrierter E-Mail wird bewusst mit 400 und einer
|
||||
* generischen Meldung geantwortet (kein 409), damit die Antwort
|
||||
@@ -22,7 +20,6 @@ import { prisma } from '@/lib/db/client';
|
||||
import { hashPassword } from '@/lib/auth/password';
|
||||
import { createSessionToken, setSessionCookie } from '@/lib/auth/session';
|
||||
import { registerSchema } from '@/lib/validation';
|
||||
import { hashInviteToken, isInviteUsable } from '@/lib/invites/tokens';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { errorResponse } from '@/lib/api/responses';
|
||||
import {
|
||||
@@ -65,39 +62,9 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
const passwordHash = await hashPassword(body.password);
|
||||
|
||||
const createdUser = await withSerializableTx(async (tx) => {
|
||||
let calendarId: string | null = null;
|
||||
|
||||
if (body.inviteToken) {
|
||||
const tokenHash = hashInviteToken(body.inviteToken);
|
||||
const invite = await tx.invite.findUnique({
|
||||
where: { tokenHash },
|
||||
select: { id: true, calendarId: true, expiresAt: true, usedAt: true },
|
||||
});
|
||||
if (!invite || !isInviteUsable(invite)) {
|
||||
throw new RegistrationError(
|
||||
'Dieser Einladungslink ist ungueltig oder abgelaufen.',
|
||||
400,
|
||||
);
|
||||
}
|
||||
calendarId = invite.calendarId;
|
||||
|
||||
// Bedingungsloses Update: schlaegt fehl, wenn eine parallele
|
||||
// Transaktion den Invite inzwischen verbraucht hat (SERIALIZABLE
|
||||
// bricht eine der beiden ab; der Retry sieht dann usedAt und
|
||||
// bricht hier sauber ab).
|
||||
const updated = await tx.invite.updateMany({
|
||||
where: { id: invite.id, usedAt: null },
|
||||
data: { usedAt: new Date() },
|
||||
});
|
||||
if (updated.count === 0) {
|
||||
throw new RegistrationError(
|
||||
'Dieser Einladungslink ist ungueltig oder abgelaufen.',
|
||||
400,
|
||||
);
|
||||
}
|
||||
} else if (!config.allowOpenRegistration) {
|
||||
if (!config.allowOpenRegistration) {
|
||||
throw new RegistrationError(
|
||||
'Registrierung ist nur ueber Einladungslinks moeglich.',
|
||||
'Registrierung ist derzeit deaktiviert.',
|
||||
403,
|
||||
);
|
||||
}
|
||||
@@ -114,9 +81,6 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
passwordHash,
|
||||
role,
|
||||
...(body.username ? { username: body.username } : {}),
|
||||
...(calendarId
|
||||
? { memberships: { create: { calendarId } } }
|
||||
: {}),
|
||||
},
|
||||
select: { id: true, email: true, role: true, username: true },
|
||||
});
|
||||
@@ -156,4 +120,4 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
}
|
||||
return errorResponse(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user