From d171d43a9af7559f3884a870fc54c4b742052125 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 25 Jul 2024 13:37:35 -0700 Subject: [PATCH] fix: migrate all the rows Signed-off-by: Matt Krick --- .../database/types/EmailVerification.ts | 28 ------- .../createEmailVerficationForExistingUser.ts | 23 +++--- .../server/email/createEmailVerification.ts | 22 +++--- .../public/mutations/signUpWithPassword.ts | 13 ++-- .../graphql/public/mutations/verifyEmail.ts | 12 ++- .../1709351538000_addEmailVerification.ts | 31 -------- .../1709351575000_moveEmailVerification.ts | 76 ------------------- .../1721868364099_addEmailVerification.ts | 52 +++++++++++++ 8 files changed, 88 insertions(+), 169 deletions(-) delete mode 100644 packages/server/database/types/EmailVerification.ts delete mode 100644 packages/server/postgres/migrations/1709351538000_addEmailVerification.ts delete mode 100644 packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts create mode 100644 packages/server/postgres/migrations/1721868364099_addEmailVerification.ts diff --git a/packages/server/database/types/EmailVerification.ts b/packages/server/database/types/EmailVerification.ts deleted file mode 100644 index a7f88f45b17..00000000000 --- a/packages/server/database/types/EmailVerification.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {Threshold} from 'parabol-client/types/constEnums' - -interface Input { - token: string - email: string - expiration?: Date - hashedPassword?: string - pseudoId?: string | null - invitationToken?: string | null -} - -export default class EmailVerification { - invitationToken?: string - token: string - email: string - expiration: Date - hashedPassword?: string - pseudoId?: string - constructor(input: Input) { - const {invitationToken, token, email, expiration, hashedPassword, pseudoId} = input - 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 5f99422857e..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 getKysely from '../postgres/getKysely' +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 emailVerification = new EmailVerification({ - email, - token: verifiedEmailToken, - hashedPassword, - pseudoId, - invitationToken - }) const pg = getKysely() - await pg.insertInto('EmailVerification').values(emailVerification).execute() + 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 7733c3f34e4..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 {Security, Threshold} from 'parabol-client/types/constEnums' import getKysely from '../postgres/getKysely' -import EmailVerification from '../database/types/EmailVerification' import emailVerificationEmailCreator from './emailVerificationEmailCreator' import getMailManager from './getMailManager' @@ -36,15 +35,18 @@ const createEmailVerification = async (props: SignUpWithPasswordMutationVariable return {error: {message: 'Unable to send verification email'}} } const hashedPassword = await bcrypt.hash(password, Security.SALT_ROUNDS) - const emailVerification = new EmailVerification({ - email, - token: verifiedEmailToken, - hashedPassword, - pseudoId, - invitationToken - }) const pg = getKysely() - await pg.insertInto('EmailVerification').values(emailVerification).execute() + 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 bf19c0a487a..29828d789c9 100644 --- a/packages/server/graphql/public/mutations/signUpWithPassword.ts +++ b/packages/server/graphql/public/mutations/signUpWithPassword.ts @@ -47,13 +47,12 @@ const signUpWithPassword: MutationResolvers['signUpWithPassword'] = async ( } const verificationRequired = await isEmailVerificationRequired(email, dataLoader) if (verificationRequired) { - const existingVerification = - (await pg - .selectFrom('EmailVerification') - .selectAll() - .where('email', '=', email) - .where('expiration', '>', new Date()) - .executeTakeFirst()) || null + 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 e76f0aede1e..f9b8be46b9d 100644 --- a/packages/server/graphql/public/mutations/verifyEmail.ts +++ b/packages/server/graphql/public/mutations/verifyEmail.ts @@ -1,7 +1,6 @@ import {AuthIdentityTypeEnum} from '../../../../client/types/constEnums' 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' @@ -18,12 +17,11 @@ const verifyEmail: MutationResolvers['verifyEmail'] = async ( const {dataLoader} = context const pg = getKysely() const now = new Date() - const emailVerification = - ((await pg - .selectFrom('EmailVerification') - .selectAll() - .where('token', '=', verificationToken) - .executeTakeFirst()) as EmailVerification) || null + 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/1709351538000_addEmailVerification.ts b/packages/server/postgres/migrations/1709351538000_addEmailVerification.ts deleted file mode 100644 index a249ba55099..00000000000 --- a/packages/server/postgres/migrations/1709351538000_addEmailVerification.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {Client} from 'pg' -import getPgConfig from '../getPgConfig' - -export async function up() { - const client = new Client(getPgConfig()) - await client.connect() - await client.query(` - CREATE TABLE "EmailVerification" ( - "id" SERIAL 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"); - `) - await client.end() -} - -export async function down() { - const client = new Client(getPgConfig()) - await client.connect() - await client.query(` - DROP TABLE IF EXISTS "EmailVerification"; - `) - await client.end() -} diff --git a/packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts b/packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts deleted file mode 100644 index 3591ed43b2a..00000000000 --- a/packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts +++ /dev/null @@ -1,76 +0,0 @@ -import {FirstParam} from 'parabol-client/types/generics' -import {Client} from 'pg' -import {r} from 'rethinkdb-ts' -import getPgConfig from '../getPgConfig' -import connectRethinkDB from '../../database/connectRethinkDB' -import getPgp from '../getPgp' - -export async function up() { - await connectRethinkDB() - const {pgp, pg} = getPgp() - const batchSize = 1000 - - try { - await r.table('EmailVerification').indexCreate('expiration').run() - await r.table('EmailVerification').indexWait().run() - } catch {} - - const columnSet = new pgp.helpers.ColumnSet( - [ - 'email', - 'expiration', - 'hashedPassword', - 'token', - {name: 'invitationToken', def: null}, - {name: 'pseudoId', def: null} - ], - {table: 'EmailVerification'} - ) - - const getNextData = async (leftBoundCursor: Date | undefined) => { - const expiration = leftBoundCursor || r.minval - const nextBatch = await r - .table('EmailVerification') - .between(expiration, r.maxval, {index: 'expiration', leftBound: 'open'}) - .orderBy({index: 'expiration'}) - .limit(batchSize) - .run() - if (nextBatch.length === 0) return null - if (nextBatch.length < batchSize) return nextBatch - const lastItem = nextBatch.pop() - const lastMatchingExpiration = nextBatch.findLastIndex( - (item) => item.expiration !== lastItem!.expiration - ) - if (lastMatchingExpiration === -1) { - throw new Error( - 'batchSize is smaller than the number of items that share the same cursor. Increase batchSize' - ) - } - return nextBatch.slice(0, lastMatchingExpiration) - } - - await pg.tx('EmailVerification', (task) => { - const fetchAndProcess: FirstParam = async ( - _index, - leftBoundCursor: undefined | Date - ) => { - const nextData = await getNextData(leftBoundCursor) - if (!nextData) return undefined - const insert = pgp.helpers.insert(nextData, columnSet) - await task.none(insert) - return nextData.at(-1)!.runAt - } - return task.sequence(fetchAndProcess) - }) - await r.getPoolMaster()?.drain() -} - -export async function down() { - const client = new Client(getPgConfig()) - await client.connect() - await client.query(`DELETE FROM "EmailVerification"`) - await client.end() - try { - await r.table('EmailVerification').indexDrop('expiration').run() - } catch {} -} diff --git a/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts b/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts new file mode 100644 index 00000000000..52033bf4c01 --- /dev/null +++ b/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts @@ -0,0 +1,52 @@ +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 + } + }) + 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() +}