Enrich invitation verify API and auto-verify invited signups
- /api/invitations/verify now returns inviterName and a structured
failure reason ("not_found" | "expired" | "accepted") plus orgName,
so the invite page can show contextual error messages and know who
to blame for an expired link.
- /api/auth/register marks emailVerified immediately and skips the
verification email when an inviteToken is present, since clicking
the tokenized invite link already proves email ownership.
Made-with: Cursor
This commit is contained in:
parent
4431778c0a
commit
47da94fc95
2 changed files with 74 additions and 11 deletions
|
|
@ -66,12 +66,18 @@ export async function POST(req: NextRequest) {
|
|||
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
// Invite-based signups have already proven email ownership by clicking the
|
||||
// tokenized link we sent to that exact address, so we mark the email as
|
||||
// verified up-front and skip the post-registration verification email.
|
||||
const isInviteSignup = !!inviteOrgId;
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
displayName,
|
||||
hashedPassword,
|
||||
role: "viewer",
|
||||
emailVerified: isInviteSignup ? new Date() : null,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -119,14 +125,20 @@ export async function POST(req: NextRequest) {
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = new URL(req.url).origin;
|
||||
await sendVerificationEmail(email, baseUrl);
|
||||
} catch (emailErr) {
|
||||
console.warn("[register] Verification email failed (non-blocking):", emailErr);
|
||||
if (!isInviteSignup) {
|
||||
try {
|
||||
const baseUrl = new URL(req.url).origin;
|
||||
await sendVerificationEmail(email, baseUrl);
|
||||
} catch (emailErr) {
|
||||
console.warn("[register] Verification email failed (non-blocking):", emailErr);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, userId: user.id });
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
userId: user.id,
|
||||
emailVerified: isInviteSignup,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[register] Error:", error);
|
||||
return NextResponse.json(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,36 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
type VerifyFailure = {
|
||||
valid: false;
|
||||
reason: "not_found" | "expired" | "accepted";
|
||||
orgName?: string;
|
||||
inviterName?: string;
|
||||
};
|
||||
|
||||
type VerifySuccess = {
|
||||
valid: true;
|
||||
email: string;
|
||||
role: string;
|
||||
orgName: string;
|
||||
inviterName: string | null;
|
||||
hasExistingAccount: boolean;
|
||||
};
|
||||
|
||||
async function resolveInviterName(invitedById: string): Promise<string | null> {
|
||||
const inviter = await prisma.user.findUnique({
|
||||
where: { id: invitedById },
|
||||
select: { displayName: true, username: true, email: true },
|
||||
});
|
||||
if (!inviter) return null;
|
||||
return inviter.displayName || inviter.username || inviter.email || null;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const token = req.nextUrl.searchParams.get("token");
|
||||
if (!token) {
|
||||
return NextResponse.json({ valid: false });
|
||||
const body: VerifyFailure = { valid: false, reason: "not_found" };
|
||||
return NextResponse.json(body);
|
||||
}
|
||||
|
||||
const invitation = await prisma.invitation.findUnique({
|
||||
|
|
@ -12,8 +38,31 @@ export async function GET(req: NextRequest) {
|
|||
include: { organization: { select: { name: true } } },
|
||||
});
|
||||
|
||||
if (!invitation || invitation.acceptedAt || invitation.expiresAt < new Date()) {
|
||||
return NextResponse.json({ valid: false });
|
||||
if (!invitation) {
|
||||
const body: VerifyFailure = { valid: false, reason: "not_found" };
|
||||
return NextResponse.json(body);
|
||||
}
|
||||
|
||||
const inviterName = await resolveInviterName(invitation.invitedById);
|
||||
|
||||
if (invitation.acceptedAt) {
|
||||
const body: VerifyFailure = {
|
||||
valid: false,
|
||||
reason: "accepted",
|
||||
orgName: invitation.organization.name,
|
||||
inviterName: inviterName ?? undefined,
|
||||
};
|
||||
return NextResponse.json(body);
|
||||
}
|
||||
|
||||
if (invitation.expiresAt < new Date()) {
|
||||
const body: VerifyFailure = {
|
||||
valid: false,
|
||||
reason: "expired",
|
||||
orgName: invitation.organization.name,
|
||||
inviterName: inviterName ?? undefined,
|
||||
};
|
||||
return NextResponse.json(body);
|
||||
}
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
|
|
@ -21,11 +70,13 @@ export async function GET(req: NextRequest) {
|
|||
select: { id: true },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
const body: VerifySuccess = {
|
||||
valid: true,
|
||||
email: invitation.email,
|
||||
role: invitation.role,
|
||||
orgName: invitation.organization.name,
|
||||
inviterName,
|
||||
hasExistingAccount: !!existingUser,
|
||||
});
|
||||
};
|
||||
return NextResponse.json(body);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue