invitation flow basis
This commit is contained in:
1 parent
b1f01208fa
commit
0817e8e72b
8 files changed
+507
-26
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth, clerkClient } from "@clerk/nextjs/server";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
/**
|
||||
* POST /api/invitations/[id]/resend
|
||||
*
|
||||
* Resend an invitation with same parameters
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id: invitationId } = await params;
|
||||
|
||||
// Fetch pending invitations to find the one being resent
|
||||
const client = await clerkClient();
|
||||
const invitationList = await client.invitations.getInvitationList({
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
// Find the invitation
|
||||
const invitation = invitationList.data.find(
|
||||
(inv) => inv.id === invitationId,
|
||||
);
|
||||
|
||||
if (!invitation) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invitation not found or already processed" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const metadata = invitation.publicMetadata as any;
|
||||
|
||||
// Check if current user created this invitation
|
||||
if (metadata?.createdBy !== userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Forbidden - You can only resend invitations you created" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
// Create new invitation with same parameters
|
||||
const role = metadata?.role;
|
||||
|
||||
// Determine redirect URL based on role
|
||||
const isStaffRole = ["admin", "trainer", "superAdmin"].includes(role);
|
||||
const redirectUrl = isStaffRole
|
||||
? `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/sign-up`
|
||||
: undefined;
|
||||
|
||||
const newInvitation = await client.invitations.createInvitation({
|
||||
emailAddress: invitation.emailAddress,
|
||||
publicMetadata: invitation.publicMetadata || undefined,
|
||||
redirectUrl,
|
||||
ignoreExisting: true,
|
||||
});
|
||||
|
||||
log.info("Invitation resent", {
|
||||
originalId: invitationId,
|
||||
newId: newInvitation.id,
|
||||
email: invitation.emailAddress,
|
||||
resentBy: userId,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
data: {
|
||||
invitation: {
|
||||
id: newInvitation.id,
|
||||
email: newInvitation.emailAddress,
|
||||
status: newInvitation.status,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("POST /api/invitations/[id]/resend error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to resend invitation" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth, clerkClient } from "@clerk/nextjs/server";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
/**
|
||||
* DELETE /api/invitations/[id]
|
||||
*
|
||||
* Revoke a pending invitation (creator-only permission)
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id: invitationId } = await params;
|
||||
|
||||
// Fetch pending invitations to find and verify the one being revoked
|
||||
const client = await clerkClient();
|
||||
const invitationList = await client.invitations.getInvitationList({
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
// Find the invitation
|
||||
const invitation = invitationList.data.find(
|
||||
(inv) => inv.id === invitationId,
|
||||
);
|
||||
|
||||
if (!invitation) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invitation not found or already processed" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const metadata = invitation.publicMetadata as any;
|
||||
|
||||
// Check if current user created this invitation
|
||||
if (metadata?.createdBy !== userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Forbidden - You can only cancel invitations you created" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
// Revoke the invitation
|
||||
await client.invitations.revokeInvitation(invitationId);
|
||||
|
||||
log.info("Invitation revoked", {
|
||||
invitationId,
|
||||
revokedBy: userId,
|
||||
email: invitation.emailAddress,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
data: { success: true },
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("DELETE /api/invitations/[id] error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to revoke invitation" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,90 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth, clerkClient } from "@clerk/nextjs/server";
|
||||
import { getAuthContext } from "@/lib/auth/context";
|
||||
import { validateGymAccess } from "@/lib/auth/permissions";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
/**
|
||||
* GET /api/invitations
|
||||
*
|
||||
* Fetch pending invitations with gym and creator filtering.
|
||||
* Users can only see invitations they created, scoped to their gym access.
|
||||
*
|
||||
* Query params:
|
||||
* - gymId: Optional gym filter (superAdmin only)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get auth context
|
||||
const authContext = await getAuthContext();
|
||||
const { role, gymId: userGymId } = authContext;
|
||||
|
||||
// Get query params
|
||||
const { searchParams } = new URL(request.url);
|
||||
const gymIdParam = searchParams.get("gymId");
|
||||
|
||||
// Validate gym access
|
||||
const targetGymId = gymIdParam || undefined;
|
||||
const accessError = validateGymAccess(role, userGymId, targetGymId);
|
||||
if (accessError) return accessError;
|
||||
|
||||
// Fetch all pending invitations from Clerk
|
||||
const client = await clerkClient();
|
||||
const invitationList = await client.invitations.getInvitationList({
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
// Filter invitations based on:
|
||||
// 1. Creator (only show invitations created by current user)
|
||||
// 2. Gym (apply gym-scoping rules)
|
||||
const filteredInvitations = invitationList.data
|
||||
.filter((inv) => {
|
||||
const metadata = inv.publicMetadata as any;
|
||||
|
||||
// Creator filter: only show if user created it
|
||||
if (metadata?.createdBy !== userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gym filter: SuperAdmin can see all, others only their gym
|
||||
if (role === "superAdmin") {
|
||||
// If gymId param provided, filter by it
|
||||
if (targetGymId && metadata?.gymId !== targetGymId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// Non-superAdmins: must match their gym
|
||||
return metadata?.gymId === userGymId;
|
||||
}
|
||||
})
|
||||
.map((inv) => ({
|
||||
id: inv.id,
|
||||
emailAddress: inv.emailAddress,
|
||||
publicMetadata: inv.publicMetadata,
|
||||
status: inv.status,
|
||||
url: inv.url,
|
||||
createdAt: inv.createdAt,
|
||||
updatedAt: inv.updatedAt,
|
||||
revoked: inv.revoked,
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
data: { invitations: filteredInvitations },
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("GET /api/invitations error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch invitations" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/invitations
|
||||
|
||||
@@ -88,6 +88,7 @@ export async function POST(request: NextRequest) {
|
||||
// Build publicMetadata - only include gymId if it exists
|
||||
const publicMetadata: Record<string, any> = {
|
||||
role: data.role,
|
||||
createdBy: userId,
|
||||
};
|
||||
if (assignedGymId) {
|
||||
publicMetadata.gymId = assignedGymId;
|
||||
|
||||
Reference in new issue
Block a user