This improves the user experience by automatically filling the email field

and preventing users from changing the email they were invited with.

- Update invite link generation to include email parameter in URL
- Modify signup form to pre-fill and lock email field when provided via invite
- Update invite page and status card to preserve email through redirect chain
- Ensure existing invite URLs continue to work without breaking changes
This commit is contained in:
Adrian Astles
2025-07-25 22:46:40 +08:00
parent df31c13912
commit 350485612e
5 changed files with 35 additions and 11 deletions

View File

@@ -189,7 +189,7 @@ export async function inviteUser(
) )
); );
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`; const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
if (doEmail) { if (doEmail) {
await sendEmail( await sendEmail(
@@ -241,7 +241,7 @@ export async function inviteUser(
}); });
}); });
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`; const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
if (doEmail) { if (doEmail) {
await sendEmail( await sendEmail(

View File

@@ -75,6 +75,7 @@ type SignupFormProps = {
redirect?: string; redirect?: string;
inviteId?: string; inviteId?: string;
inviteToken?: string; inviteToken?: string;
emailParam?: string;
}; };
const formSchema = z const formSchema = z
@@ -103,7 +104,8 @@ const formSchema = z
export default function SignupForm({ export default function SignupForm({
redirect, redirect,
inviteId, inviteId,
inviteToken inviteToken,
emailParam
}: SignupFormProps) { }: SignupFormProps) {
const router = useRouter(); const router = useRouter();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
@@ -118,7 +120,7 @@ export default function SignupForm({
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
email: "", email: emailParam || "",
password: "", password: "",
confirmPassword: "", confirmPassword: "",
agreeToTerms: false agreeToTerms: false
@@ -209,7 +211,10 @@ export default function SignupForm({
<FormItem> <FormItem>
<FormLabel>{t("email")}</FormLabel> <FormLabel>{t("email")}</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input
{...field}
disabled={!!emailParam}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -11,7 +11,10 @@ import { getTranslations } from "next-intl/server";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<{ redirect: string | undefined }>; searchParams: Promise<{
redirect: string | undefined;
email: string | undefined;
}>;
}) { }) {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const getUser = cache(verifySession); const getUser = cache(verifySession);
@@ -69,6 +72,7 @@ export default async function Page(props: {
redirect={redirectUrl} redirect={redirectUrl}
inviteToken={inviteToken} inviteToken={inviteToken}
inviteId={inviteId} inviteId={inviteId}
emailParam={searchParams.email}
/> />
<p className="text-center text-muted-foreground mt-4"> <p className="text-center text-muted-foreground mt-4">

View File

@@ -17,11 +17,13 @@ import { useTranslations } from "next-intl";
type InviteStatusCardProps = { type InviteStatusCardProps = {
type: "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in"; type: "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in";
token: string; token: string;
email?: string;
}; };
export default function InviteStatusCard({ export default function InviteStatusCard({
type, type,
token, token,
email,
}: InviteStatusCardProps) { }: InviteStatusCardProps) {
const router = useRouter(); const router = useRouter();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
@@ -29,12 +31,18 @@ export default function InviteStatusCard({
async function goToLogin() { async function goToLogin() {
await api.post("/auth/logout", {}); await api.post("/auth/logout", {});
router.push(`/auth/login?redirect=/invite?token=${token}`); const redirectUrl = email
? `/auth/login?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}`
: `/auth/login?redirect=/invite?token=${token}`;
router.push(redirectUrl);
} }
async function goToSignup() { async function goToSignup() {
await api.post("/auth/logout", {}); await api.post("/auth/logout", {});
router.push(`/auth/signup?redirect=/invite?token=${token}`); const redirectUrl = email
? `/auth/signup?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}`
: `/auth/signup?redirect=/invite?token=${token}`;
router.push(redirectUrl);
} }
function renderBody() { function renderBody() {

View File

@@ -14,6 +14,7 @@ export default async function InvitePage(props: {
const params = await props.searchParams; const params = await props.searchParams;
const tokenParam = params.token as string; const tokenParam = params.token as string;
const emailParam = params.email as string;
if (!tokenParam) { if (!tokenParam) {
redirect("/"); redirect("/");
@@ -70,16 +71,22 @@ export default async function InvitePage(props: {
const type = cardType(); const type = cardType();
if (!user && type === "user_does_not_exist") { if (!user && type === "user_does_not_exist") {
redirect(`/auth/signup?redirect=/invite?token=${params.token}`); const redirectUrl = emailParam
? `/auth/signup?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}`
: `/auth/signup?redirect=/invite?token=${params.token}`;
redirect(redirectUrl);
} }
if (!user && type === "not_logged_in") { if (!user && type === "not_logged_in") {
redirect(`/auth/login?redirect=/invite?token=${params.token}`); const redirectUrl = emailParam
? `/auth/login?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}`
: `/auth/login?redirect=/invite?token=${params.token}`;
redirect(redirectUrl);
} }
return ( return (
<> <>
<InviteStatusCard type={type} token={tokenParam} /> <InviteStatusCard type={type} token={tokenParam} email={emailParam} />
</> </>
); );
} }