mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +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:
@@ -9,17 +9,21 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { getSafeCallbackUrl, isInvitationCallbackUrl } from '@/lib/safe-redirect';
|
||||
|
||||
function getSafeCallbackUrl(value: string | null): string {
|
||||
if (!value) return '/dashboard';
|
||||
try {
|
||||
const baseOrigin = typeof window === 'undefined' ? 'http://localhost' : window.location.origin;
|
||||
const parsed = new URL(value, baseOrigin);
|
||||
if (parsed.origin !== baseOrigin) return '/dashboard';
|
||||
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
||||
} catch {
|
||||
return '/dashboard';
|
||||
/**
|
||||
* Sign-up link that carries the pending destination — and, when that destination is an
|
||||
* invitation, the invitation token itself so the new account is bound to the invite.
|
||||
*/
|
||||
function buildRegisterHref(callbackUrl: string): string {
|
||||
if (callbackUrl === '/dashboard') return '/register';
|
||||
|
||||
const params = new URLSearchParams({ callbackUrl });
|
||||
if (isInvitationCallbackUrl(callbackUrl)) {
|
||||
const token = new URLSearchParams(callbackUrl.split('?')[1] ?? '').get('token');
|
||||
if (token) params.set('invitationToken', token);
|
||||
}
|
||||
return `/register?${params.toString()}`;
|
||||
}
|
||||
|
||||
const ERROR_MESSAGES: Record<string, string> = {
|
||||
@@ -50,6 +54,8 @@ function LoginFormInner({ googleEnabled, githubEnabled }: LoginFormInnerProps) {
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [showVerifiedSuccess, setShowVerifiedSuccess] = useState(false);
|
||||
const callbackUrl = getSafeCallbackUrl(searchParams.get('callbackUrl'));
|
||||
const isInvitationFlow = isInvitationCallbackUrl(callbackUrl);
|
||||
const registerHref = buildRegisterHref(callbackUrl);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('registered') === 'true') {
|
||||
@@ -105,7 +111,11 @@ function LoginFormInner({ googleEnabled, githubEnabled }: LoginFormInnerProps) {
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Welcome back</CardTitle>
|
||||
<CardDescription>Sign in to your account to continue</CardDescription>
|
||||
<CardDescription>
|
||||
{isInvitationFlow
|
||||
? 'Sign in to accept your invitation'
|
||||
: 'Sign in to your account to continue'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{showSuccess && (
|
||||
@@ -242,7 +252,7 @@ function LoginFormInner({ googleEnabled, githubEnabled }: LoginFormInnerProps) {
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground mt-6">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/register" className="text-primary hover:underline">
|
||||
<Link href={registerHref} className="text-primary hover:underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user