From a653a61b279631aa66d461ebd2012c4136358bd0 Mon Sep 17 00:00:00 2001 From: Jordan Husney Date: Thu, 25 Jul 2024 13:50:15 -0700 Subject: [PATCH] chore: migrate EmailVerification to pg (#9492) Signed-off-by: Matt Krick Co-authored-by: Matt Krick --- packages/server/__tests__/autoJoin.test.ts | 22 ++++---- .../database/types/EmailVerification.ts | 32 ----------- .../createEmailVerficationForExistingUser.ts | 25 +++++---- .../server/email/createEmailVerification.ts | 26 ++++----- .../public/mutations/signUpWithPassword.ts | 19 +++---- .../graphql/public/mutations/verifyEmail.ts | 16 +++--- .../1721868364099_addEmailVerification.ts | 53 +++++++++++++++++++ 7 files changed, 108 insertions(+), 85 deletions(-) delete mode 100644 packages/server/database/types/EmailVerification.ts create mode 100644 packages/server/postgres/migrations/1721868364099_addEmailVerification.ts diff --git a/packages/server/__tests__/autoJoin.test.ts b/packages/server/__tests__/autoJoin.test.ts index 3fe4564e6eb..e48415ee854 100644 --- a/packages/server/__tests__/autoJoin.test.ts +++ b/packages/server/__tests__/autoJoin.test.ts @@ -1,6 +1,6 @@ import faker from 'faker' -import getRethink from '../database/rethinkDriver' import createEmailVerification from '../email/createEmailVerification' +import getKysely from '../postgres/getKysely' import {getUserTeams, sendIntranet, sendPublic} from './common' const signUpVerified = async (email: string) => { @@ -23,12 +23,14 @@ const signUpVerified = async (email: string) => { // manually generate verification token so also the founder can be verified await createEmailVerification({email, password}) - const r = await getRethink() - const verificationToken = await r - .table('EmailVerification') - .getAll(email, {index: 'email'}) - .nth(0)('token') - .run() + const pg = getKysely() + const verificationToken = ( + await pg + .selectFrom('EmailVerification') + .select('token') + .where('email', '=', email) + .executeTakeFirstOrThrow(() => new Error(`No verification token found for ${email}`)) + ).token const verifyEmail = await sendPublic({ query: ` @@ -55,9 +57,9 @@ const signUpVerified = async (email: string) => { expect(verifyEmail).toMatchObject({ data: { verifyEmail: { - authToken: expect.toBeString(), + authToken: expect.any(String), user: { - id: expect.toBeString() + id: expect.any(String) } } } @@ -153,7 +155,7 @@ test.skip('autoJoin on multiple teams does not create duplicate `OrganizationUse const newEmail = `${faker.internet.userName()}@${domain}`.toLowerCase() const {user: newUser} = await signUpVerified(newEmail) - expect(newUser.tms).toIncludeSameMembers(teamIds) + expect(newUser.tms).toEqual(expect.arrayContaining(teamIds)) expect(newUser.organizations).toMatchObject([ { id: orgId diff --git a/packages/server/database/types/EmailVerification.ts b/packages/server/database/types/EmailVerification.ts deleted file mode 100644 index 0288eae2421..00000000000 --- a/packages/server/database/types/EmailVerification.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {Threshold} from 'parabol-client/types/constEnums' -import generateUID from '../../generateUID' - -interface Input { - id?: string - token: string - email: string - expiration?: Date - hashedPassword?: string - pseudoId?: string | null - invitationToken?: string | null -} - -export default class EmailVerification { - id: string - invitationToken?: string - token: string - email: string - expiration: Date - hashedPassword?: string - pseudoId?: string - constructor(input: Input) { - const {id, invitationToken, token, email, expiration, hashedPassword, pseudoId} = input - this.id = id || generateUID() - this.invitationToken = invitationToken || undefined - this.token = token - this.email = email - this.expiration = expiration || new Date(Date.now() + Threshold.EMAIL_VERIFICATION_LIFESPAN) - this.hashedPassword = hashedPassword - this.pseudoId = pseudoId || undefined - } -} diff --git a/packages/server/email/createEmailVerficationForExistingUser.ts b/packages/server/email/createEmailVerficationForExistingUser.ts index c72337f2708..a54b4c33968 100644 --- a/packages/server/email/createEmailVerficationForExistingUser.ts +++ b/packages/server/email/createEmailVerficationForExistingUser.ts @@ -1,9 +1,9 @@ import base64url from 'base64url' import crypto from 'crypto' -import getRethink from '../database/rethinkDriver' +import {Threshold} from 'parabol-client/types/constEnums' import AuthIdentityLocal from '../database/types/AuthIdentityLocal' -import EmailVerification from '../database/types/EmailVerification' import {DataLoaderWorker} from '../graphql/graphql' +import getKysely from '../postgres/getKysely' import emailVerificationEmailCreator from './emailVerificationEmailCreator' import getMailManager from './getMailManager' @@ -34,15 +34,18 @@ const createEmailVerficationForExistingUser = async ( if (!success) { return new Error('Unable to send verification email') } - const r = await getRethink() - const emailVerification = new EmailVerification({ - email, - token: verifiedEmailToken, - hashedPassword, - pseudoId, - invitationToken - }) - await r.table('EmailVerification').insert(emailVerification).run() + const pg = getKysely() + await pg + .insertInto('EmailVerification') + .values({ + email, + token: verifiedEmailToken, + hashedPassword, + pseudoId, + invitationToken, + expiration: new Date(Date.now() + Threshold.EMAIL_VERIFICATION_LIFESPAN) + }) + .execute() return undefined } diff --git a/packages/server/email/createEmailVerification.ts b/packages/server/email/createEmailVerification.ts index 2271c82163a..5a73d4716bc 100644 --- a/packages/server/email/createEmailVerification.ts +++ b/packages/server/email/createEmailVerification.ts @@ -1,9 +1,8 @@ import base64url from 'base64url' import bcrypt from 'bcryptjs' import crypto from 'crypto' -import {Security} from 'parabol-client/types/constEnums' -import getRethink from '../database/rethinkDriver' -import EmailVerification from '../database/types/EmailVerification' +import {Security, Threshold} from 'parabol-client/types/constEnums' +import getKysely from '../postgres/getKysely' import emailVerificationEmailCreator from './emailVerificationEmailCreator' import getMailManager from './getMailManager' @@ -35,16 +34,19 @@ const createEmailVerification = async (props: SignUpWithPasswordMutationVariable if (!success) { return {error: {message: 'Unable to send verification email'}} } - const r = await getRethink() const hashedPassword = await bcrypt.hash(password, Security.SALT_ROUNDS) - const emailVerification = new EmailVerification({ - email, - token: verifiedEmailToken, - hashedPassword, - pseudoId, - invitationToken - }) - await r.table('EmailVerification').insert(emailVerification).run() + const pg = getKysely() + await pg + .insertInto('EmailVerification') + .values({ + email, + token: verifiedEmailToken, + hashedPassword, + pseudoId, + invitationToken, + expiration: new Date(Date.now() + Threshold.EMAIL_VERIFICATION_LIFESPAN) + }) + .execute() return {error: {message: 'Verification required. Check your inbox.'}} } diff --git a/packages/server/graphql/public/mutations/signUpWithPassword.ts b/packages/server/graphql/public/mutations/signUpWithPassword.ts index 2982d3fbdc9..29828d789c9 100644 --- a/packages/server/graphql/public/mutations/signUpWithPassword.ts +++ b/packages/server/graphql/public/mutations/signUpWithPassword.ts @@ -1,10 +1,8 @@ import bcrypt from 'bcryptjs' import {AuthenticationError, Security} from 'parabol-client/types/constEnums' -import {URLSearchParams} from 'url' -import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' import createEmailVerification from '../../../email/createEmailVerification' import {USER_PREFERRED_NAME_LIMIT} from '../../../postgres/constants' +import getKysely from '../../../postgres/getKysely' import createNewLocalUser from '../../../utils/createNewLocalUser' import encodeAuthToken from '../../../utils/encodeAuthToken' import isEmailVerificationRequired from '../../../utils/isEmailVerificationRequired' @@ -21,7 +19,7 @@ const signUpWithPassword: MutationResolvers['signUpWithPassword'] = async ( if (email.length > USER_PREFERRED_NAME_LIMIT) { return {error: {message: 'Email is too long'}} } - const r = await getRethink() + const pg = getKysely() const isOrganic = !invitationToken const {ip, dataLoader} = context const loginAttempt = await attemptLogin(email, password, ip) @@ -49,13 +47,12 @@ const signUpWithPassword: MutationResolvers['signUpWithPassword'] = async ( } const verificationRequired = await isEmailVerificationRequired(email, dataLoader) if (verificationRequired) { - const existingVerification = await r - .table('EmailVerification') - .getAll(email, {index: 'email'}) - .filter((row: RValue) => row('expiration').gt(new Date())) - .nth(0) - .default(null) - .run() + const existingVerification = await pg + .selectFrom('EmailVerification') + .selectAll() + .where('email', '=', email) + .where('expiration', '>', new Date()) + .executeTakeFirst() if (existingVerification) { return {error: {message: 'Verification email already sent'}} } diff --git a/packages/server/graphql/public/mutations/verifyEmail.ts b/packages/server/graphql/public/mutations/verifyEmail.ts index 8f210c96e7e..f9b8be46b9d 100644 --- a/packages/server/graphql/public/mutations/verifyEmail.ts +++ b/packages/server/graphql/public/mutations/verifyEmail.ts @@ -1,8 +1,7 @@ import {AuthIdentityTypeEnum} from '../../../../client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import AuthIdentityLocal from '../../../database/types/AuthIdentityLocal' import AuthToken from '../../../database/types/AuthToken' -import EmailVerification from '../../../database/types/EmailVerification' +import getKysely from '../../../postgres/getKysely' import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails' import updateUser from '../../../postgres/queries/updateUser' import createNewLocalUser from '../../../utils/createNewLocalUser' @@ -16,14 +15,13 @@ const verifyEmail: MutationResolvers['verifyEmail'] = async ( context ) => { const {dataLoader} = context - const r = await getRethink() + const pg = getKysely() const now = new Date() - const emailVerification = (await r - .table('EmailVerification') - .getAll(verificationToken, {index: 'token'}) - .nth(0) - .default(null) - .run()) as EmailVerification + const emailVerification = await pg + .selectFrom('EmailVerification') + .selectAll() + .where('token', '=', verificationToken) + .executeTakeFirst() if (!emailVerification) { return {error: {message: 'Invalid verification token'}} diff --git a/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts b/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts new file mode 100644 index 00000000000..b725ec8f65f --- /dev/null +++ b/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts @@ -0,0 +1,53 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql` + CREATE TABLE "EmailVerification" ( + "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "email" "citext" NOT NULL, + "expiration" TIMESTAMP WITH TIME ZONE NOT NULL, + "token" VARCHAR(100) NOT NULL, + "hashedPassword" VARCHAR(100), + "invitationToken" VARCHAR(100), + "pseudoId" VARCHAR(100) + ); + + CREATE INDEX IF NOT EXISTS "idx_EmailVerification_email" ON "EmailVerification"("email"); + CREATE INDEX IF NOT EXISTS "idx_EmailVerification_token" ON "EmailVerification"("token"); + `.execute(pg) + + const rData = await r.table('EmailVerification').coerceTo('array').run() + const insertData = rData.map((row) => { + const {email, expiration, hashedPassword, token, invitationToken, pseudoId} = row + return { + email, + expiration, + hashedPassword, + token, + invitationToken, + pseudoId + } + }) + if (insertData.length === 0) return + await pg.insertInto('EmailVerification').values(insertData).execute() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "EmailVerification"; + `) + await client.end() +}