Skip to content

Verify user emails#1151

Merged
allisonking merged 23 commits into
mainfrom
aking/1121/verify-user-emails
Apr 14, 2025
Merged

Verify user emails#1151
allisonking merged 23 commits into
mainfrom
aking/1121/verify-user-emails

Conversation

@allisonking
Copy link
Copy Markdown
Contributor

@allisonking allisonking commented Apr 8, 2025

Issue(s) Resolved

Closes #1121

High-level Explanation of PR

Adds a verify email flow.

Test Plan

I don't think this will work in the preview environment since we need to use emails.

Happy path:

  1. In a browser that is not signed in, visit a public form i.e. http://localhost:3000/c/croccroc/public/forms/review/fill
  2. You should see the public sign up page. Fill it out
  3. You should get to a page that says you need to verify your email
  4. Open up inbucket at localhost:5432/monitor
  5. You should have gotten an email with the subject 'Verify your email'. Click this link
  6. You should be redirected to the public form, and a toast saying you've verified your email should appear at the bottom.

Other things to test:

  • Request another verification email. This should invalidate the first one, and you should get a second email with a new token
    • You can try using the invalidated link and you will see a token invalid message, request another one
  • After step 3 above, instead of verifying your email, try to navigate to another part of the app. You should always be sent to the /verify page, but the "Forgot password" flow is still accessible

Screenshots (if applicable)

Screen.Recording.2025-04-10.at.2.49.30.PM.mov

Notes

@allisonking allisonking force-pushed the aking/1121/verify-user-emails branch from e91bbd0 to 6983493 Compare April 10, 2025 01:05
@allisonking allisonking force-pushed the aking/1121/verify-user-emails branch from 6983493 to 63e97a0 Compare April 10, 2025 16:01
new URL(`/not-found?from=${encodeURIComponent(redirectTo)}`, env.PUBPUB_URL),
opts
);
const url = createRedirectUrl(redirectTo, opts?.searchParams);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this to a helper file since I found I needed it elsewhere (mostly to easily tack on search params). but I think @tefkah 's lil routes lib would be a lot nicer


const { user: tokenUser, authTokenType } = tokenSettled.value;

if (!tokenUser.isVerified) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by putting this here in the magic-link route, we automatically mark users who go through this route as verified since they would've come from their email. this lets us get the requirement "Forgot password also functions as verification, in case where user signs up, never completes verification, and deletes verification email" for free

};

return (
<SubmitButton
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very handy button!!

Comment thread core/app/layout.tsx
Comment on lines -26 to +29
<Toaster />
<Suspense>
<RootToaster />
</Suspense>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hopefully this is okay—I wasn't sure how else to get the toast after redirect to work. it needs Suspense since RootToaster needs to useSearchParams in order to figure out whether to render the 'verified' toast

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is correct! Not sure if their are any repercussions from this, but for now it seems fine

Comment thread core/app/page.tsx
}

redirect(`/c/${communitySlug}/stages`);
redirect(createRedirectUrl(`/c/${communitySlug}/stages`, params).toString());
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the regular pages weren't passing on search params. I needed it to so that ?verified=true could get passed along to arbitrary pages

@allisonking allisonking marked this pull request as ready for review April 10, 2025 18:50
@allisonking allisonking requested a review from tefkah April 10, 2025 18:50
@tefkah tefkah added preview Auto-deploys a preview application and removed preview Auto-deploys a preview application labels Apr 14, 2025
Copy link
Copy Markdown
Member

@tefkah tefkah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it looks great! very elegant solution of making magic-links set the verified to true.

I made some optional suggestions which don't really need to be implemented if you don't feel like it, but there's one situation we do still need to cover.

if a user is invited through the email action, they are prompted to fill in their information+create a password. they can pick a different email during signup. i think in that case, we should force them to verify their email. this happens here:
https://github.com/pubpub/platform/blob/a54eb9b77fd202346153a294e3775c9dc9eb9bfb/core/lib/authentication/actions.ts#L493-L529

i think we'll get rid of this route as a special case soon, but we should still cover it for now!

greate work!

Comment thread core/app/layout.tsx
Comment on lines -26 to +29
<Toaster />
<Suspense>
<RootToaster />
</Suspense>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is correct! Not sure if their are any repercussions from this, but for now it seems fine

Comment thread core/app/RootToaster.tsx Outdated
Comment on lines +13 to +34
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();

useEffect(() => {
if (searchParams.has(VERIFIED_PARAM)) {
toast({
title: "Verified",
description: (
<span className="flex items-center gap-1">
<CircleCheck size="16" /> Your email is now verified
</span>
),
variant: "success",
});

// Remove the param so we don't see the popover again
const params = new URLSearchParams(searchParams);
params.delete(VERIFIED_PARAM);
router.replace(`${pathname}?${params.toString()}`);
}
}, [searchParams]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it may be nicer to use nuqs here. replacing the VERIFIED_PARAM causes another refresh of all the data on the server, which i don't think is what we want as the effect of the ?verified param is (i think) only relevant client side.

if you use nuqs, it by default uses shallow updating of the searchParams, which don't cause a server refresh. see here for how to do something like that

https://github.com/pubpub/platform/blob/415de673452e7952872c1d341ebfc01f3da8054c/core/app/components/FormBuilder/useIsChanged.tsx#L1-L13

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be nice to just wrap up all of this logic in a hook, eg usePersistedToasts or something, and define all the possible params in some configuration object, eg

const PERSISTED_TOAST = {
    'verified': {
        title: "Verified",
        description: <span...>,
        variant: "success"
    }
} as const satisfies ...

slug?: string;
existing?: false;
/**
* @default true
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good default

Comment thread core/lib/server/email.tsx
return buildSend(async () => {
const magicLink = await createMagicLink(
{
type: AuthTokenType.generic,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you maybe add a small comment here why this needs to be AuthTokenType.generic vs AuthTokenType.verifyEmail? I think it's bc /magic-link will set their email to be verified, but i think that's easy to forget!

@allisonking
Copy link
Copy Markdown
Contributor Author

Thanks @tefkah , did not realize the legacy signup needed this too! added, and also implemented using nuqs, great idea!

Screen.Recording.2025-04-14.at.3.35.18.PM.mov

@allisonking allisonking merged commit 0dff52f into main Apr 14, 2025
16 checks passed
@allisonking allisonking deleted the aking/1121/verify-user-emails branch April 14, 2025 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Verify user emails after signup

2 participants