feathersjs-ecosystem/feathers-authentication-management

View on GitHub
src/methods/reset-password.ts

Summary

Maintainability
A
1 hr
Test Coverage
A
90%
import { BadRequest } from '@feathersjs/errors';
import makeDebug from 'debug';
import {
  comparePasswords,
  deconstructId,
  ensureObjPropsValid,
  ensureValuesAreStrings,
  getUserData,
  hashPassword,
  notify
} from '../helpers';
import type { Id, Params } from '@feathersjs/feathers';

import type {
  IdentifyUser,
  ResetPasswordOptions,
  ResetPwdWithShortTokenOptions,
  SanitizedUser,
  Tokens,
  GetUserDataCheckProps,
  NotifierOptions,
  User
} from '../types';

const debug = makeDebug('authLocalMgnt:resetPassword');

export async function resetPwdWithLongToken (
  options: ResetPasswordOptions,
  resetToken: string,
  password: string,
  notifierOptions: NotifierOptions = {},
  params?: Params
): Promise<SanitizedUser> {
  ensureValuesAreStrings(resetToken, password);

  return await resetPassword(
    options,
    { resetToken },
    { resetToken },
    password,
    notifierOptions,
    params
  );
}

export async function resetPwdWithShortToken (
  options: ResetPwdWithShortTokenOptions,
  resetShortToken: string,
  identifyUser: IdentifyUser,
  password: string,
  notifierOptions: NotifierOptions = {},
  params?: Params
): Promise<SanitizedUser> {
  ensureValuesAreStrings(resetShortToken, password);
  ensureObjPropsValid(identifyUser, options.identifyUserProps);

  return await resetPassword(
    options,
    identifyUser,
    { resetShortToken },
    password,
    notifierOptions,
    params
  );
}

async function resetPassword (
  options: ResetPasswordOptions,
  identifyUser: IdentifyUser,
  tokens: Tokens,
  password: string,
  notifierOptions: NotifierOptions = {},
  params?: Params
): Promise<SanitizedUser> {
  debug('resetPassword', identifyUser, tokens, password);

  if (params && "query" in params) {
    params = Object.assign({}, params);
    delete params.query;
  }

  const {
    app,
    service,
    skipIsVerifiedCheck,
    reuseResetToken,
    passwordField,
    skipPasswordHash,
    sanitizeUserForClient,
    notifier
  } = options;

  const usersService = app.service(service);
  const usersServiceId = usersService.id;
  let users;

  if (tokens.resetToken) {
    const id = deconstructId(tokens.resetToken);
    const user = await usersService.get(id, Object.assign({}, params));
    users = [user];
  } else if (tokens.resetShortToken) {
    users = await usersService.find({
        ...params,
        query: { ...identifyUser, $limit: 2 },
        paginate: false,
    }) as User[];
  } else {
    throw new BadRequest(
      'resetToken and resetShortToken are missing. (authLocalMgnt)',
      { errors: { $className: 'missingToken' } }
    );
  }

  const checkProps: GetUserDataCheckProps = skipIsVerifiedCheck ? ['resetNotExpired'] : ['resetNotExpired', 'isVerified'];
  const user = getUserData(users, checkProps);

  // compare all tokens (hashed)
  const tokenChecks = Object.keys(tokens).map(async key => {
    if (reuseResetToken) {
      // Comparing token directly as reused resetToken is not hashed
      if (tokens[key] !== user[key]) {
        throw new BadRequest('Reset Token is incorrect. (authLocalMgnt)', {
          errors: { $className: 'incorrectToken' }
        });
      }
    } else {
      return await comparePasswords(
        tokens[key],
        user[key] as string,
        () =>
          new BadRequest(
            'Reset Token is incorrect. (authLocalMgnt)',
            { errors: { $className: 'incorrectToken' } }
          )
      );
    }
  });

  try {
    await Promise.all(tokenChecks);
  } catch (err) {
    // if token check fail, either decrease remaining attempts or cancel reset
    if (user.resetAttempts > 0) {
      await usersService.patch(user[usersServiceId], {
        resetAttempts: user.resetAttempts - 1
      }, Object.assign({}, params));

      throw err;
    } else {
      await usersService.patch(user[usersServiceId], {
        resetToken: null,
        resetAttempts: null,
        resetShortToken: null,
        resetExpires: null
      }, Object.assign({}, params));

      throw new BadRequest(
        'Invalid token. Get for a new one. (authLocalMgnt)',
        { errors: { $className: 'invalidToken' } });
    }
  }

  const patchedUser = await usersService.patch(user[usersServiceId] as Id, {
    [passwordField]: skipPasswordHash ? password : await hashPassword(app, password, passwordField),
    resetExpires: null,
    resetAttempts: null,
    resetToken: null,
    resetShortToken: null
  }, Object.assign({}, params));

  const userResult = await notify(notifier, 'resetPwd', patchedUser, notifierOptions);
  return sanitizeUserForClient(userResult);
}