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);
|
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({
|
const user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email,
|
email,
|
||||||
displayName,
|
displayName,
|
||||||
hashedPassword,
|
hashedPassword,
|
||||||
role: "viewer",
|
role: "viewer",
|
||||||
|
emailVerified: isInviteSignup ? new Date() : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -119,14 +125,20 @@ export async function POST(req: NextRequest) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isInviteSignup) {
|
||||||
try {
|
try {
|
||||||
const baseUrl = new URL(req.url).origin;
|
const baseUrl = new URL(req.url).origin;
|
||||||
await sendVerificationEmail(email, baseUrl);
|
await sendVerificationEmail(email, baseUrl);
|
||||||
} catch (emailErr) {
|
} catch (emailErr) {
|
||||||
console.warn("[register] Verification email failed (non-blocking):", 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) {
|
} catch (error) {
|
||||||
console.error("[register] Error:", error);
|
console.error("[register] Error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,36 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/db";
|
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) {
|
export async function GET(req: NextRequest) {
|
||||||
const token = req.nextUrl.searchParams.get("token");
|
const token = req.nextUrl.searchParams.get("token");
|
||||||
if (!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({
|
const invitation = await prisma.invitation.findUnique({
|
||||||
|
|
@ -12,8 +38,31 @@ export async function GET(req: NextRequest) {
|
||||||
include: { organization: { select: { name: true } } },
|
include: { organization: { select: { name: true } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!invitation || invitation.acceptedAt || invitation.expiresAt < new Date()) {
|
if (!invitation) {
|
||||||
return NextResponse.json({ valid: false });
|
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({
|
const existingUser = await prisma.user.findUnique({
|
||||||
|
|
@ -21,11 +70,13 @@ export async function GET(req: NextRequest) {
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
const body: VerifySuccess = {
|
||||||
valid: true,
|
valid: true,
|
||||||
email: invitation.email,
|
email: invitation.email,
|
||||||
role: invitation.role,
|
role: invitation.role,
|
||||||
orgName: invitation.organization.name,
|
orgName: invitation.organization.name,
|
||||||
|
inviterName,
|
||||||
hasExistingAccount: !!existingUser,
|
hasExistingAccount: !!existingUser,
|
||||||
});
|
};
|
||||||
|
return NextResponse.json(body);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue