d1f040051d
* Update email verification. And workspace invite emails. * Footer logos * Workspace requests * Trial * Misc emails * New logo * Updates to font, spacing and footer * Remove space * Update invite emails * Correct classes
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
import { GetServerInfo } from '@/modules/core/domain/server/operations'
|
|
import { GetUserByEmail } from '@/modules/core/domain/users/operations'
|
|
import { getPasswordResetFinalizationRoute } from '@/modules/core/helpers/routeHelper'
|
|
import { EmailTemplateParams } from '@/modules/emails/domain/operations'
|
|
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
|
import { sendEmail } from '@/modules/emails/services/sending'
|
|
import { CreateToken, GetPendingToken } from '@/modules/pwdreset/domain/operations'
|
|
import { InvalidPasswordRecoveryRequestError } from '@/modules/pwdreset/errors'
|
|
import { PasswordResetTokenRecord } from '@/modules/pwdreset/repositories'
|
|
import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
|
|
|
|
const EMAIL_SUBJECT = 'Speckle Account Password Reset'
|
|
|
|
type InitializeNewTokenDeps = {
|
|
getUserByEmail: GetUserByEmail
|
|
getPendingToken: GetPendingToken
|
|
createToken: CreateToken
|
|
getServerInfo: GetServerInfo
|
|
}
|
|
|
|
/**
|
|
* Initialize and validate password reset request
|
|
*/
|
|
const initializeNewTokenFactory =
|
|
(deps: InitializeNewTokenDeps) => async (email: string) => {
|
|
if (!email) throw new InvalidPasswordRecoveryRequestError('E-mail address is empty')
|
|
|
|
const [user, tokenAlreadyExists] = await Promise.all([
|
|
deps.getUserByEmail(email),
|
|
deps.getPendingToken({ email }).then((t) => !!t)
|
|
])
|
|
|
|
if (!user) {
|
|
throw new InvalidPasswordRecoveryRequestError(
|
|
'No user with that e-mail address found'
|
|
)
|
|
}
|
|
|
|
if (tokenAlreadyExists) {
|
|
throw new InvalidPasswordRecoveryRequestError(
|
|
'Password reset already requested. Please try again in 1h, or check your email for the instructions we have sent.'
|
|
)
|
|
}
|
|
|
|
const [serverInfo, newToken] = await Promise.all([
|
|
deps.getServerInfo(),
|
|
deps.createToken(email)
|
|
])
|
|
|
|
return { user, serverInfo, email, newToken }
|
|
}
|
|
|
|
type PasswordRecoveryRequestState = Awaited<
|
|
ReturnType<ReturnType<typeof initializeNewTokenFactory>>
|
|
>
|
|
|
|
function buildResetLink(token: PasswordResetTokenRecord) {
|
|
return new URL(
|
|
getPasswordResetFinalizationRoute(token.id),
|
|
getFrontendOrigin()
|
|
).toString()
|
|
}
|
|
|
|
function buildMjmlBody() {
|
|
const bodyStart = `<mj-text align="center" line-height="2" >Hello,<br/><br/>You have just requested a password reset a few moments ago for your Speckle account. Please click on the button below to complete the process:</mj-text>`
|
|
|
|
const bodyEnd = `<mj-text align="center" line-height="2" >The link above is valid for <strong>1 hour</strong>. If you didn't request a password reset, feel free to ignore this email - nothing will happen, and your account is secure.</mj-text>`
|
|
|
|
return {
|
|
bodyStart,
|
|
bodyEnd
|
|
}
|
|
}
|
|
|
|
function buildTextBody() {
|
|
const bodyStart = `Hello,\n\nYou have just requested a password reset a few moments ago for your Speckle account. Please use the link below to complete the process:`
|
|
const bodyEnd = `The link above is valid for 1 hour. If you didn't request a password reset, feel free to ignore this email - nothing will happen, and your account is secure.`
|
|
|
|
return {
|
|
bodyStart,
|
|
bodyEnd
|
|
}
|
|
}
|
|
|
|
function buildEmailTemplateParams(
|
|
state: PasswordRecoveryRequestState
|
|
): EmailTemplateParams {
|
|
const { newToken } = state
|
|
|
|
return {
|
|
mjml: buildMjmlBody(),
|
|
text: buildTextBody(),
|
|
cta: {
|
|
title: 'Reset Your Password',
|
|
url: buildResetLink(newToken)
|
|
}
|
|
}
|
|
}
|
|
|
|
type SendResetEmailDeps = {
|
|
renderEmail: typeof renderEmail
|
|
sendEmail: typeof sendEmail
|
|
}
|
|
|
|
const sendResetEmailFactory =
|
|
(deps: SendResetEmailDeps) => async (state: PasswordRecoveryRequestState) => {
|
|
const emailTemplateParams = buildEmailTemplateParams(state)
|
|
const { html, text } = await deps.renderEmail(
|
|
emailTemplateParams,
|
|
state.serverInfo,
|
|
state.user
|
|
)
|
|
await deps.sendEmail({
|
|
to: state.email,
|
|
subject: EMAIL_SUBJECT,
|
|
text,
|
|
html
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Request a new password recovery and send out the e-mail if the request is valid
|
|
*/
|
|
export const requestPasswordRecoveryFactory =
|
|
(deps: InitializeNewTokenDeps & SendResetEmailDeps) => async (email: string) => {
|
|
const state = await initializeNewTokenFactory(deps)(email)
|
|
await sendResetEmailFactory(deps)(state)
|
|
}
|