Merge pull request #2975 from specklesystems/fabians/pwdreset-ioc-3

chore(server): pwdreset IoC 3 - finalizePasswordResetFactory
This commit is contained in:
Alessandro Magionami
2024-09-12 15:21:17 +02:00
committed by GitHub
4 changed files with 64 additions and 34 deletions
@@ -8,3 +8,5 @@ export type GetPendingToken = (
) => Promise<Optional<PasswordResetTokenRecord>>
export type CreateToken = (email: string) => Promise<PasswordResetTokenRecord>
export type DeleteTokens = (identity: EmailOrTokenId) => Promise<void>
@@ -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()
}
+12 -1
View File
@@ -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)
@@ -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<ReturnType<ReturnType<typeof initializeStateFactory>>>
type FinalizeNewPasswordDeps = {
deleteTokens: DeleteTokens
updateUserPassword: typeof updateUserPassword
deleteExistingAuthTokens: typeof deleteExistingAuthTokens
}
type FinalizationState = Awaited<ReturnType<typeof initializeState>>
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)
}