Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/app/api/invite/consume/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';

import { authOptions } from '@/lib/auth';
import { db } from '@/lib/db';

export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ redirect: null }, { status: 200 });
}

const inviteToken = request.cookies.get('invite_token')?.value;
const inviteLibrary = request.cookies.get('invite_library')?.value;
if (!inviteToken || !inviteLibrary) {
return NextResponse.json({ redirect: null }, { status: 200 });
}

const userId = (session.user as { id?: string } | undefined)?.id;
if (!userId) {
return NextResponse.json({ redirect: null }, { status: 200 });
}

// Validate invite
const invitation = await db.invitation.findFirst({
where: {
token: inviteToken,
libraryId: inviteLibrary,
type: 'library',
status: { in: ['PENDING', 'SENT'] },
},
select: { id: true, expiresAt: true },
});

// Build base response now so we can always clear cookies
const res = NextResponse.json({ redirect: `/collection/${inviteLibrary}` });
res.cookies.set('invite_token', '', { path: '/', maxAge: 0 });
res.cookies.set('invite_library', '', { path: '/', maxAge: 0 });

if (!invitation || new Date() > invitation.expiresAt) {
return res;
}

// Ensure membership
const existing = await db.collectionMember.findUnique({
where: { userId_collectionId: { userId, collectionId: inviteLibrary } },
select: { id: true, isActive: true },
});
if (!existing) {
await db.collectionMember.create({
data: {
userId,
collectionId: inviteLibrary,
role: 'member',
isActive: true,
},
});
} else if (!existing.isActive) {
await db.collectionMember.update({
where: { id: existing.id },
data: { isActive: true },
});
}

// Mark invite accepted
await db.invitation.updateMany({
where: { token: inviteToken, libraryId: inviteLibrary },
data: { status: 'ACCEPTED', acceptedAt: new Date(), receiverId: userId },
});

return res;
} catch {
return NextResponse.json({ redirect: null }, { status: 200 });
}
}
33 changes: 23 additions & 10 deletions src/app/auth/callback/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ function AuthCallbackContent() {

const invitationToken = searchParams.get('invitation');
const libraryId = searchParams.get('library');
// Helper to read invite cookies (set by /invite/[token])
const getCookie = (name: string) =>
`; ${document.cookie}`.split(`; ${name}=`).pop()?.split(';')?.[0];
// Client cookie reader no longer used; invite handling is server-side

useEffect(() => {
if (status === 'loading' || isRedirecting) return;
Expand Down Expand Up @@ -62,13 +60,28 @@ function AuthCallbackContent() {
}
}

// Handle invite cookie flow (guest pass): redirect to collection
const inviteLibrary = getCookie('invite_library');
const inviteToken = getCookie('invite_token');
if (inviteLibrary && inviteToken) {
// Always prefer dropping into the invited collection over profile flows
router.replace(`/collection/${inviteLibrary}`);
return;
// Handle invite cookie flow server-side: create membership and decide destination
try {
const consumeRes = await fetch('/api/invite/consume', {
method: 'POST',
});
if (consumeRes.ok) {
const body = await consumeRes
.json()
.catch(() => ({}) as { redirect?: string });
if (body?.redirect) {
const dest = body.redirect as string;
if (user?.profileCompleted) {
router.replace(dest);
} else {
const returnTo = encodeURIComponent(dest);
router.replace(`/profile/create?returnTo=${returnTo}`);
}
return;
}
}
} catch {
// ignore and continue
}

// Normal flow based on profile completion
Expand Down
10 changes: 5 additions & 5 deletions src/app/collection/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ export default async function CollectionPage({
const cookieStore = await cookies();
const inviteToken = cookieStore.get('invite_token')?.value;
const inviteLibrary = cookieStore.get('invite_library')?.value;
console.log('[collection page] redirecting to signin (no session)', {
hasInviteToken: !!inviteToken,
inviteLibrary,
});
redirect('/auth/signin');
const { id: collectionIdForCheck } = await params;
const allowGuest = inviteToken && inviteLibrary === collectionIdForCheck;
if (!allowGuest) {
redirect('/auth/signin');
}
}

const { id: collectionId } = await params;
Expand Down