mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
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.
32 lines
1.1 KiB
TypeScript
32 lines
1.1 KiB
TypeScript
/**
|
|
* Reduce an untrusted `callbackUrl`/`next` value to a same-origin relative path.
|
|
* Anything absolute, cross-origin or unparsable falls back to `fallback`.
|
|
*
|
|
* Works on both sides: in the browser the origin defaults to `window.location.origin`
|
|
* (so next-auth's absolute `result.url` still passes), on the server pass the public origin.
|
|
*/
|
|
export function getSafeCallbackUrl(
|
|
value: string | null | undefined,
|
|
options?: { origin?: string; fallback?: string }
|
|
): string {
|
|
const fallback = options?.fallback ?? '/dashboard';
|
|
if (!value) return fallback;
|
|
|
|
const baseOrigin =
|
|
options?.origin ??
|
|
(typeof window === 'undefined' ? 'http://localhost' : window.location.origin);
|
|
|
|
try {
|
|
const parsed = new URL(value, baseOrigin);
|
|
if (parsed.origin !== baseOrigin) return fallback;
|
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
/** True when a sanitized path points at the invitation acceptance route. */
|
|
export function isInvitationCallbackUrl(path: string): boolean {
|
|
return path.startsWith('/invitations/accept');
|
|
}
|