mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat(invitations): guide invited users without an account through sign-up
Clicking an invitation link while signed out dropped the visitor on a bare login form, even though most invitees have no account yet and nothing on screen told them to create one. Signed-out visitors now get the invitation itself: who invited them, which workspace/project, which role, and which address it was sent to. The primary call to action follows whether an account already exists for that address — "Create your account" when it does not, "Sign in to accept" when it does. The sign-up path carries the invitation forward, so a new account lands back on the invitation and from there on the shared workspace/project instead of the onboarding wizard: - the register link passes invitationToken, the invited email and a callbackUrl - the register form locks the email to the invited address and shows what is being joined - the verification email round-trips the destination through a sanitized `next` parameter - login and verify-email keep the pending destination in their sign-in links Signing in with a different address than the one invited now explains the mismatch instead of silently redirecting to the dashboard. Callback sanitization moves to lib/safe-redirect.ts so login, register, verify-email and the verification route share one open-redirect guard.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
import { acceptInvitationTokenForUser } from '@/lib/invitations';
|
||||
import { acceptInvitationTokenForUser, getInvitationPreviewByToken } from '@/lib/invitations';
|
||||
import { InvitationAccountMismatch, InvitationLanding } from './invitation-landing';
|
||||
|
||||
interface InvitationAcceptPageProps {
|
||||
searchParams: Promise<{
|
||||
@@ -19,14 +20,17 @@ export default async function InvitationAcceptPage({ searchParams }: InvitationA
|
||||
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
const callbackUrl = `/invitations/accept?token=${encodeURIComponent(token)}`;
|
||||
redirect(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`);
|
||||
// Signed-out visitors get the invitation itself instead of a bare login form:
|
||||
// most of them have no account yet and need to be told to create one.
|
||||
const preview = await getInvitationPreviewByToken(token);
|
||||
return <InvitationLanding token={token} preview={preview} />;
|
||||
}
|
||||
|
||||
const invitation = await db.invitation.findUnique({
|
||||
where: { token },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
status: true,
|
||||
scope: true,
|
||||
workspaceId: true,
|
||||
@@ -63,7 +67,14 @@ export default async function InvitationAcceptPage({ searchParams }: InvitationA
|
||||
redirect('/dashboard?invite=expired');
|
||||
}
|
||||
if (result === 'forbidden') {
|
||||
redirect('/dashboard?invite=wrong_account');
|
||||
// Signed in with a different address than the one invited — say so instead of
|
||||
// dropping the user on the dashboard with no explanation.
|
||||
return (
|
||||
<InvitationAccountMismatch
|
||||
invitedEmail={invitation?.email ?? 'another address'}
|
||||
signedInEmail={userEmail}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (result === 'not_found' && invitation?.status === 'ACCEPTED') {
|
||||
|
||||
Reference in New Issue
Block a user