From 180ce4a16947b9f821c70538e5c663204b12995e Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Thu, 12 Sep 2024 15:10:30 +0300 Subject: [PATCH] chore(server): pwdreset IoC 3 - finalizePasswordResetFactory --- .../modules/pwdreset/domain/operations.ts | 2 + .../modules/pwdreset/repositories/index.ts | 4 +- .../server/modules/pwdreset/rest/index.ts | 13 ++- .../modules/pwdreset/services/finalize.ts | 79 +++++++++++-------- 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/packages/server/modules/pwdreset/domain/operations.ts b/packages/server/modules/pwdreset/domain/operations.ts index c6f9f50ab..7e9833256 100644 --- a/packages/server/modules/pwdreset/domain/operations.ts +++ b/packages/server/modules/pwdreset/domain/operations.ts @@ -8,3 +8,5 @@ export type GetPendingToken = ( ) => Promise> export type CreateToken = (email: string) => Promise + +export type DeleteTokens = (identity: EmailOrTokenId) => Promise diff --git a/packages/server/modules/pwdreset/repositories/index.ts b/packages/server/modules/pwdreset/repositories/index.ts index 6f04a6e3f..000697b3f 100644 --- a/packages/server/modules/pwdreset/repositories/index.ts +++ b/packages/server/modules/pwdreset/repositories/index.ts @@ -6,6 +6,7 @@ import { InvalidArgumentError } from '@/modules/shared/errors' import { Knex } from 'knex' import { CreateToken, + DeleteTokens, EmailOrTokenId, GetPendingToken } from '@/modules/pwdreset/domain/operations' @@ -56,7 +57,8 @@ export const getPendingTokenFactory = * Delete all tokens that fit the specified identity */ export const deleteTokensFactory = - (deps: { db: Knex }) => async (identity: EmailOrTokenId) => { + (deps: { db: Knex }): DeleteTokens => + async (identity: EmailOrTokenId) => { const q = baseQueryFactory(deps) await q(identity).del() } diff --git a/packages/server/modules/pwdreset/rest/index.ts b/packages/server/modules/pwdreset/rest/index.ts index b01082a9c..4c0a39a38 100644 --- a/packages/server/modules/pwdreset/rest/index.ts +++ b/packages/server/modules/pwdreset/rest/index.ts @@ -1,13 +1,16 @@ import { db } from '@/db/knex' +import { deleteExistingAuthTokens } from '@/modules/auth/repositories' import { getUserByEmail } from '@/modules/core/repositories/users' import { getServerInfo } from '@/modules/core/services/generic' +import { updateUserPassword } from '@/modules/core/services/users' import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { createTokenFactory, + deleteTokensFactory, getPendingTokenFactory } from '@/modules/pwdreset/repositories' -import { finalizePasswordReset } from '@/modules/pwdreset/services/finalize' +import { finalizePasswordResetFactory } from '@/modules/pwdreset/services/finalize' import { requestPasswordRecoveryFactory } from '@/modules/pwdreset/services/request' import { ensureError } from '@/modules/shared/helpers/errorHelper' import { Express } from 'express' @@ -38,6 +41,14 @@ export default function (app: Express) { // Finalizes password recovery. app.post('/auth/pwdreset/finalize', async (req, res) => { try { + const finalizePasswordReset = finalizePasswordResetFactory({ + getUserByEmail, + getPendingToken: getPendingTokenFactory({ db }), + deleteTokens: deleteTokensFactory({ db }), + updateUserPassword, + deleteExistingAuthTokens + }) + if (!req.body.tokenId || !req.body.password) throw new Error('Invalid request.') await finalizePasswordReset(req.body.tokenId, req.body.password) diff --git a/packages/server/modules/pwdreset/services/finalize.ts b/packages/server/modules/pwdreset/services/finalize.ts index f23ac71d0..54de98d01 100644 --- a/packages/server/modules/pwdreset/services/finalize.ts +++ b/packages/server/modules/pwdreset/services/finalize.ts @@ -1,49 +1,64 @@ -import { db } from '@/db/knex' import { deleteExistingAuthTokens } from '@/modules/auth/repositories' import { getUserByEmail } from '@/modules/core/repositories/users' import { updateUserPassword } from '@/modules/core/services/users' +import { DeleteTokens, GetPendingToken } from '@/modules/pwdreset/domain/operations' import { PasswordRecoveryFinalizationError } from '@/modules/pwdreset/errors' -import { - deleteTokensFactory, - getPendingTokenFactory -} from '@/modules/pwdreset/repositories' -async function initializeState(tokenId: string, password: string) { - if (!tokenId && !password) - throw new PasswordRecoveryFinalizationError('Both the token & password must be set') +type InitializeStateDeps = { + getUserByEmail: typeof getUserByEmail + getPendingToken: GetPendingToken +} - const token = await getPendingTokenFactory({ db })({ tokenId }) - if (!token) - throw new PasswordRecoveryFinalizationError( - 'Invalid reset token, it may be expired' - ) +const initializeStateFactory = + (deps: InitializeStateDeps) => async (tokenId: string, password: string) => { + if (!tokenId && !password) + throw new PasswordRecoveryFinalizationError( + 'Both the token & password must be set' + ) - const user = await getUserByEmail(token.email) - if (!user) { - throw new PasswordRecoveryFinalizationError('Invalid finalization request') + const token = await deps.getPendingToken({ tokenId }) + if (!token) + throw new PasswordRecoveryFinalizationError( + 'Invalid reset token, it may be expired' + ) + + const user = await deps.getUserByEmail(token.email) + if (!user) { + throw new PasswordRecoveryFinalizationError('Invalid finalization request') + } + + return { tokenId, password, token, user } } - return { tokenId, password, token, user } +type FinalizationState = Awaited>> + +type FinalizeNewPasswordDeps = { + deleteTokens: DeleteTokens + updateUserPassword: typeof updateUserPassword + deleteExistingAuthTokens: typeof deleteExistingAuthTokens } -type FinalizationState = Awaited> +const finalizeNewPasswordFactory = + (deps: FinalizeNewPasswordDeps) => async (state: FinalizationState) => { + const { user, password, tokenId } = state + await deps.updateUserPassword({ id: user.id, newPassword: password }) -async function finalizeNewPassword(state: FinalizationState) { - const { user, password, tokenId } = state - await updateUserPassword({ id: user.id, newPassword: password }) - const deleteTokens = deleteTokensFactory({ db }) + // Delete password reset tokens + await Promise.all([ + deps.deleteTokens({ tokenId }), + deps.deleteTokens({ email: user.email }) + ]) - // Delete password reset tokens - await Promise.all([deleteTokens({ tokenId }), deleteTokens({ email: user.email })]) - - // Delete existing auth tokens - await deleteExistingAuthTokens(user.id) -} + // Delete existing auth tokens + await deps.deleteExistingAuthTokens(user.id) + } /** * Attempt to finalize an initiated password recovery flow */ -export async function finalizePasswordReset(tokenId: string, password: string) { - const state = await initializeState(tokenId, password) - await finalizeNewPassword(state) -} +export const finalizePasswordResetFactory = + (deps: InitializeStateDeps & FinalizeNewPasswordDeps) => + async (tokenId: string, password: string) => { + const state = await initializeStateFactory(deps)(tokenId, password) + await finalizeNewPasswordFactory(deps)(state) + }