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:
yusufipk
2026-07-25 18:44:02 +07:00
parent a14eb9fb84
commit 9c75ce91e1
11 changed files with 424 additions and 29 deletions
+20 -1
View File
@@ -1,15 +1,34 @@
import { isInviteCodeRequired } from '@/lib/feature-flags';
import { getInvitationPreviewByToken } from '@/lib/invitations';
import RegisterPageClient from './register-page-client';
export default function RegisterPage() {
interface RegisterPageProps {
searchParams: Promise<{ invitationToken?: string }>;
}
export default async function RegisterPage({ searchParams }: RegisterPageProps) {
const googleEnabled = Boolean(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET);
const githubEnabled = Boolean(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET);
const token = (await searchParams)?.invitationToken?.trim();
const preview = token ? await getInvitationPreviewByToken(token) : null;
const invitation =
preview && preview.status === 'PENDING' && !preview.isExpired
? {
email: preview.email,
inviterName: preview.inviterName,
roleLabel: preview.roleLabel,
scopeLabel: preview.scopeLabel,
targetName: preview.targetName,
}
: null;
return (
<RegisterPageClient
requireInviteCode={isInviteCodeRequired()}
googleEnabled={googleEnabled}
githubEnabled={githubEnabled}
invitation={invitation}
/>
);
}