Files
speckle-server/packages/server/modules/emails/repositories/index.ts
T

112 lines
3.1 KiB
TypeScript

import { EmailVerifications } from '@/modules/core/dbSchema'
import {
DeleteOldAndInsertNewVerification,
DeleteVerifications,
GetPendingToken,
GetPendingVerificationByEmail
} from '@/modules/emails/domain/operations'
import { InvalidArgumentError } from '@/modules/shared/errors'
import cryptoRandomString from 'crypto-random-string'
import dayjs from 'dayjs'
import { Knex } from 'knex'
import { hash } from 'bcrypt'
import { EmailVerification } from '@/modules/emails/domain/types'
const tables = {
emailVerifications: (db: Knex) => db<EmailVerification>(EmailVerifications.name)
}
const hashEmailVerificationCode = async (code: string) => hash(code, 10)
export type EmailVerificationRecord = {
id: string
email: string
createdAt: Date
}
/**
* Attempt to find a valid & pending token entry for email verification
*/
export const getPendingTokenFactory =
(deps: { db: Knex }): GetPendingToken =>
async ({ token, email }) => {
if (!token && !email) throw new InvalidArgumentError('Token & email is empty')
const aWeekAgo = dayjs().subtract(1, 'week')
const q = tables
.emailVerifications(deps.db)
.where(EmailVerifications.col.createdAt, '>', aWeekAgo.toISOString())
.first()
if (token) {
q.andWhere(EmailVerifications.col.id, token)
} else {
q.andWhere(EmailVerifications.col.email, email)
}
return await q
}
export const deleteVerificationsFactory =
(deps: { db: Knex }): DeleteVerifications =>
async (email) => {
if (!email) throw new InvalidArgumentError('E-mail address is empty')
const q = tables
.emailVerifications(deps.db)
.where(EmailVerifications.col.email, email)
.del()
await q
}
function generateEmailVerificationCode() {
return cryptoRandomString({ length: 6, type: 'numeric' })
}
/**
* Delete all previous verification entries and create a new one
*/
export const deleteOldAndInsertNewVerificationFactory =
(deps: { db: Knex }): DeleteOldAndInsertNewVerification =>
async (email) => {
if (!email) throw new InvalidArgumentError('E-mail address is empty')
// delete all existing verifications first
await deleteVerificationsFactory({ db: deps.db })(email)
// insert new one
const EmailVerificationCols = EmailVerifications.with({
withoutTablePrefix: true
}).col
const code = generateEmailVerificationCode()
await tables.emailVerifications(deps.db).insert({
[EmailVerificationCols.id]: cryptoRandomString({ length: 20 }),
[EmailVerificationCols.email]: email,
[EmailVerificationCols.code]: await hashEmailVerificationCode(code)
})
return code
}
export const getPendingVerificationByEmailFactory =
({
db,
verificationTimeoutMinutes
}: {
db: Knex
verificationTimeoutMinutes: number
}): GetPendingVerificationByEmail =>
async ({ email }) => {
return await tables
.emailVerifications(db)
.where(EmailVerifications.col.email, email)
.where(
EmailVerifications.col.createdAt,
'>',
dayjs().subtract(verificationTimeoutMinutes, 'minutes').toISOString()
)
.orderBy(EmailVerifications.col.createdAt, 'desc')
.first()
}