-
Notifications
You must be signed in to change notification settings - Fork 124
Replace email verification with custom implementation #7504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Summary of ChangesHello @XiNiHa, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request overhauls the email verification mechanism by transitioning from SuperTokens' built-in solution to a custom-developed system. This change addresses limitations in the previous implementation, particularly concerning advanced authentication flows like SSO account linking. The modifications span across the database schema, API layer with new GraphQL mutations, and the frontend user interface, ensuring a cohesive and flexible email verification process. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
🚀 Snapshot Release (
|
| Package | Version | Info |
|---|---|---|
@graphql-hive/cli |
0.57.2-alpha-20260115190127-3f467ee3b8a0ee56828fda44325e6277c8f9f545 |
npm ↗︎ unpkg ↗︎ |
hive |
9.0.0-alpha-20260115190127-3f467ee3b8a0ee56828fda44325e6277c8f9f545 |
npm ↗︎ unpkg ↗︎ |
📚 Storybook DeploymentThe latest changes are available as preview in: https://pr-7504.hive-storybook.pages.dev |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request replaces the SuperTokens email verification with a custom implementation, providing more flexibility. The changes include a new database migration for email verifications, backend logic to handle the verification flow, and frontend updates to use the new system. My review focuses on a style guide violation regarding legacy module usage, a potential bug in token generation logic, and a React best practice issue in the frontend.
| async checkUserEmailVerified({ superTokensUserId }) { | ||
| return tracedTransaction('checkUserEmailVerified', pool, async t => { | ||
| const user = await shared.getUserBySuperTokenId({ superTokensUserId }, t); | ||
| if (!user) { | ||
| throw new Error('User not found.'); | ||
| } | ||
|
|
||
| const emailVerification = await t.maybeOne<{ | ||
| verifiedAt: number | null; | ||
| }>(sql`/* checkUserEmailVerified */ | ||
| SELECT "verified_at" "verifiedAt" | ||
| FROM "email_verifications" | ||
| WHERE | ||
| "user_id" = ${user.id} | ||
| AND "provider" = ${user.provider} | ||
| AND "email" = ${user.email} | ||
| `); | ||
| return { | ||
| verified: emailVerification?.verifiedAt != null, | ||
| }; | ||
| }); | ||
| }, | ||
| async getOrCreateEmailVerification({ superTokensUserId }) { | ||
| return tracedTransaction('getOrCreateEmailVerification', pool, async t => { | ||
| const user = await shared.getUserBySuperTokenId({ superTokensUserId }, t); | ||
|
|
||
| if (!user) { | ||
| return { | ||
| ok: false, | ||
| message: 'User not found.', | ||
| }; | ||
| } | ||
|
|
||
| let emailVerification = await t.maybeOne<{ | ||
| token: string; | ||
| expiresAt: number | null; | ||
| verifiedAt?: number | null; | ||
| }>(sql`/* getOrCreateEmailVerification */ | ||
| SELECT | ||
| "token" | ||
| , "expires_at" "expiresAt" | ||
| , "verified_at" "verifiedAt" | ||
| FROM "email_verifications" | ||
| WHERE | ||
| "user_id" = ${user.id} | ||
| AND "provider" = ${user.provider} | ||
| AND "email" = ${user.email} | ||
| `); | ||
|
|
||
| if (emailVerification?.verifiedAt) { | ||
| return { | ||
| ok: false, | ||
| message: 'Your email address has already been verified.', | ||
| }; | ||
| } | ||
|
|
||
| if (!emailVerification) { | ||
| emailVerification = await t.one<{ | ||
| token: string; | ||
| expiresAt: number; | ||
| }>(sql`/* getOrCreateEmailVerification */ | ||
| INSERT INTO "email_verifications" ( | ||
| "user_id" | ||
| , "provider" | ||
| , "email" | ||
| , "token" | ||
| , "expires_at" | ||
| ) | ||
| VALUES ( | ||
| ${user.id} | ||
| , ${user.provider} | ||
| , ${user.email} | ||
| , ${randomBytes(16).toString('hex')} | ||
| , now() + INTERVAL '30 minutes' | ||
| ) | ||
| RETURNING | ||
| "token" | ||
| , "expires_at" "expiresAt" | ||
| `); | ||
| } | ||
|
|
||
| return { | ||
| ok: true, | ||
| userId: user.id, | ||
| token: emailVerification.token, | ||
| expiresAt: new Date(emailVerification.expiresAt!), | ||
| }; | ||
| }); | ||
| }, | ||
| async verifyEmail({ superTokensUserId, token }) { | ||
| return tracedTransaction('verifyEmail', pool, async t => { | ||
| const user = await shared.getUserBySuperTokenId({ superTokensUserId }, t); | ||
|
|
||
| if (!user) { | ||
| return { | ||
| ok: false, | ||
| message: 'User not found.', | ||
| }; | ||
| } | ||
|
|
||
| const emailVerification = await t.maybeOne<{ | ||
| id: string; | ||
| expiresAt: number; | ||
| }>(sql`/* verifyEmail */ | ||
| SELECT "id", "expires_at" "expiresAt" | ||
| FROM "email_verifications" | ||
| WHERE | ||
| "user_id" = ${user.id} | ||
| AND "provider" = ${user.provider} | ||
| AND "email" = ${user.email} | ||
| AND "token" = ${token} | ||
| AND "expires_at" IS NOT NULL | ||
| AND "verified_at" IS NULL | ||
| `); | ||
|
|
||
| if (!emailVerification) { | ||
| return { | ||
| ok: false, | ||
| message: 'The email verification link is invalid.', | ||
| }; | ||
| } | ||
|
|
||
| if (emailVerification.expiresAt <= Date.now()) { | ||
| await t.query(sql`/* verifyEmail */ | ||
| DELETE FROM "email_verifications" | ||
| WHERE "id" = ${emailVerification.id} | ||
| `); | ||
|
|
||
| return { | ||
| ok: false, | ||
| message: 'The email verification link has expired.', | ||
| }; | ||
| } | ||
|
|
||
| await t.query(sql`/* verifyEmail */ | ||
| UPDATE "email_verifications" | ||
| SET | ||
| "verified_at" = now() | ||
| , "expires_at" = NULL | ||
| WHERE "id" = ${emailVerification.id} | ||
| `); | ||
|
|
||
| return { | ||
| ok: true, | ||
| verified: true, | ||
| }; | ||
| }); | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the repository's style guide (lines 60-66), new logic for database interactions should not be added to the legacy /packages/services/storage module. Instead, it should be encapsulated in smaller classes within the corresponding GraphQL modules.
The new methods checkUserEmailVerified, getOrCreateEmailVerification, and verifyEmail should be moved out of this file and into a new data-access class within packages/services/api/src/modules/auth/. This will improve modularity and adhere to the established architectural pattern.
References
- Adding new major logic to
/packages/services/storageis discouraged. This module is considered legacy. Instead, we now have smaller classes within the corresponding GraphQL modules that hold the logic for interacting with Postgres. (link)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, had no idea about this and actually was feeling weird dealing with the Storage module 🫠
| async getOrCreateEmailVerification({ superTokensUserId }) { | ||
| return tracedTransaction('getOrCreateEmailVerification', pool, async t => { | ||
| const user = await shared.getUserBySuperTokenId({ superTokensUserId }, t); | ||
|
|
||
| if (!user) { | ||
| return { | ||
| ok: false, | ||
| message: 'User not found.', | ||
| }; | ||
| } | ||
|
|
||
| let emailVerification = await t.maybeOne<{ | ||
| token: string; | ||
| expiresAt: number | null; | ||
| verifiedAt?: number | null; | ||
| }>(sql`/* getOrCreateEmailVerification */ | ||
| SELECT | ||
| "token" | ||
| , "expires_at" "expiresAt" | ||
| , "verified_at" "verifiedAt" | ||
| FROM "email_verifications" | ||
| WHERE | ||
| "user_id" = ${user.id} | ||
| AND "provider" = ${user.provider} | ||
| AND "email" = ${user.email} | ||
| `); | ||
|
|
||
| if (emailVerification?.verifiedAt) { | ||
| return { | ||
| ok: false, | ||
| message: 'Your email address has already been verified.', | ||
| }; | ||
| } | ||
|
|
||
| if (!emailVerification) { | ||
| emailVerification = await t.one<{ | ||
| token: string; | ||
| expiresAt: number; | ||
| }>(sql`/* getOrCreateEmailVerification */ | ||
| INSERT INTO "email_verifications" ( | ||
| "user_id" | ||
| , "provider" | ||
| , "email" | ||
| , "token" | ||
| , "expires_at" | ||
| ) | ||
| VALUES ( | ||
| ${user.id} | ||
| , ${user.provider} | ||
| , ${user.email} | ||
| , ${randomBytes(16).toString('hex')} | ||
| , now() + INTERVAL '30 minutes' | ||
| ) | ||
| RETURNING | ||
| "token" | ||
| , "expires_at" "expiresAt" | ||
| `); | ||
| } | ||
|
|
||
| return { | ||
| ok: true, | ||
| userId: user.id, | ||
| token: emailVerification.token, | ||
| expiresAt: new Date(emailVerification.expiresAt!), | ||
| }; | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation of getOrCreateEmailVerification may re-send an already expired verification link. If an expired, unverified token exists, it will be returned and emailed to the user, who will then find that the link is expired. This leads to a poor user experience.
The logic should be updated to create a new token if the existing one is expired. Using ON CONFLICT ... DO UPDATE would make this more robust by either inserting a new record or updating an existing one (including expired ones), ensuring a fresh token and expiry date are always generated when needed.
return tracedTransaction('getOrCreateEmailVerification', pool, async t => {
const user = await shared.getUserBySuperTokenId({ superTokensUserId }, t);
if (!user) {
return {
ok: false,
message: 'User not found.',
};
}
const emailVerification = await t.maybeOne<{
verifiedAt: number | null;
}>(sql`/* getOrCreateEmailVerification: check if verified */
SELECT "verified_at" "verifiedAt"
FROM "email_verifications"
WHERE
"user_id" = ${user.id}
AND "provider" = ${user.provider}
AND "email" = ${user.email}
`);
if (emailVerification?.verifiedAt) {
return {
ok: false,
message: 'Your email address has already been verified.',
};
}
const newEmailVerification = await t.one<{
token: string;
expiresAt: number;
}>(sql`/* getOrCreateEmailVerification: upsert token */
INSERT INTO "email_verifications" (
"user_id"
, "provider"
, "email"
, "token"
, "expires_at"
)
VALUES (
${user.id}
, ${user.provider}
, ${user.email}
, ${randomBytes(16).toString('hex')}
, now() + INTERVAL '30 minutes'
)
ON CONFLICT ("user_id", "provider", "email") DO UPDATE SET
"token" = EXCLUDED.token,
"expires_at" = EXCLUDED.expires_at,
"verified_at" = NULL -- Ensure it's not marked as verified
RETURNING
"token"
, "expires_at" "expiresAt"
`);
return {
ok: true,
userId: user.id,
token: newEmailVerification.token,
expiresAt: new Date(newEmailVerification.expiresAt),
};
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@n1ru4l what do you think about this?
|
🐋 This PR was built and pushed to the following Docker images: Targets: Platforms: Image Tag: |
💻 Website PreviewThe latest changes are available as preview in: https://pr-7504.hive-landing-page.pages.dev |
Background
The existing email verification relied on SuperTokens' implementation, which was not flexible enough to be used in wider use cases like email verification for SSO accounts during account linking.
Description
email_verificationstable and fills it based on the existingsupertokens_emailverification_verified_emailstableChecklist