import config from 'config';

import * as auth from '../lib/auth';
import emailLib from '../lib/email';
import { crypto } from '../lib/encryption';
import errors from '../lib/errors';
import logger from '../lib/logger';
import RateLimit, { ONE_HOUR_IN_SECONDS } from '../lib/rate-limit';
import {
  verifyTwoFactorAuthenticationRecoveryCode,
  verifyTwoFactorAuthenticatorCode,
} from '../lib/two-factor-authentication';
import { isValidEmail } from '../lib/utils';
import models from '../models';

const { Unauthorized, ValidationFailed, TooManyRequests } = errors;

const { User } = models;

/**
 *
 * Public methods.
 *
 */

/**
 * Check existence of a user based on email
 */
export const exists = async (req, res) => {
  const email = req.query.email.toLowerCase();
  if (!isValidEmail(email)) {
    return res.send({ exists: false });
  } else {
    const rateLimit = new RateLimit(
      `user_email_search_ip_${req.ip}`,
      config.limits.searchEmailPerHourPerIp,
      ONE_HOUR_IN_SECONDS,
    );
    if (!(await rateLimit.registerCall())) {
      res.send({
        error: { message: 'Rate limit exceeded' },
      });
    }
    const user = await models.User.findOne({
      attributes: ['id'],
      where: { email },
    });
    return res.send({ exists: Boolean(user) });
  }
};

/**
 * Login or create a new user
 */
export const signin = (req, res, next) => {
  const { user, redirect, websiteUrl } = req.body;
  let loginLink;
  let clientIP;
  return models.User.findOne({ where: { email: user.email.toLowerCase() } })
    .then(u => u || models.User.createUserWithCollective(user))
    .then(u => {
      loginLink = u.generateLoginLink(redirect || '/', websiteUrl);
      clientIP = req.ip;
      if (config.env === 'development') {
        logger.info(`Login Link: ${loginLink}`);
      }
      return emailLib.send('user.new.token', u.email, { loginLink, clientIP }, { sendEvenIfNotProduction: true });
    })
    .then(() => {
      const response = { success: true };
      // For e2e testing, we enable testuser+(admin|member)@opencollective.com to automatically receive the login link
      if (config.env !== 'production' && user.email.match(/.*test.*@opencollective.com$/)) {
        response.redirect = loginLink;
      }
      return response;
    })
    .then(response => res.send(response))
    .catch(next);
};

/**
 * Receive a login JWT and generate another one.
 * This can be used right after the first login.
 * Also check if the user has two-factor authentication
 * enabled on their account, and if they do, we send
 * back a JWT with scope 'twofactorauth' to trigger
 * the 2FA flow on the frontend
 */
export const updateToken = async (req, res) => {
  if (req.remoteUser.twoFactorAuthToken !== null) {
    const token = req.remoteUser.jwt({ scope: 'twofactorauth' }, auth.TOKEN_EXPIRATION_SESSION);
    res.send({ token });
  } else {
    const token = req.remoteUser.jwt({}, auth.TOKEN_EXPIRATION_SESSION);
    res.send({ token });
  }
};

/**
 * Verify the 2FA code or recovery code the user has entered when logging in and send back another JWT.
 */
export const twoFactorAuthAndUpdateToken = async (req, res, next) => {
  const { twoFactorAuthenticatorCode, twoFactorAuthenticationRecoveryCode } = req.body;

  const userId = Number(req.jwtPayload.sub);

  // Both 2FA and recovery codes rate limited to 10 tries per hour
  const rateLimit = new RateLimit(`user_2FA_endpoint_${userId}`, 10, ONE_HOUR_IN_SECONDS);
  const fail = async exception => {
    await rateLimit.registerCall();
    next(exception);
  };

  if (await rateLimit.hasReachedLimit()) {
    return next(new TooManyRequests('Too many attempts. Please try again in an hour'));
  }

  const user = await User.findByPk(userId);
  if (!user) {
    logger.warn(`User id ${userId} not found`);
    next();
    return;
  }

  if (twoFactorAuthenticatorCode) {
    // if there is a 2FA code, we need to verify it before returning the token
    const verified = verifyTwoFactorAuthenticatorCode(user.twoFactorAuthToken, twoFactorAuthenticatorCode);
    if (!verified) {
      return fail(new Unauthorized('Two-factor authentication code failed. Please try again'));
    }
  } else if (twoFactorAuthenticationRecoveryCode) {
    // or if there is a recovery code try to verify it
    if (typeof twoFactorAuthenticationRecoveryCode !== 'string') {
      return fail(new ValidationFailed('2FA recovery code must be a string'));
    }
    const verified = verifyTwoFactorAuthenticationRecoveryCode(
      user.twoFactorAuthRecoveryCodes,
      twoFactorAuthenticationRecoveryCode,
    );
    if (!verified) {
      return fail(new Unauthorized('Two-factor authentication recovery code failed. Please try again'));
    }
    const remainingRecoveryCodes = user.twoFactorAuthRecoveryCodes.filter(
      code => crypto.hash(twoFactorAuthenticationRecoveryCode.toUpperCase()) !== code,
    );
    await user.update({ twoFactorAuthRecoveryCodes: remainingRecoveryCodes });
  }

  const token = user.jwt({}, auth.TOKEN_EXPIRATION_SESSION);
  res.send({ token: token });
};

@@ -101,52 +101,8 @@
 l';%0A
-import %7B crypto %7D from '../lib/encryption';%0A
 impo
@@ -4676,109 +4676,33 @@
   %7D%0A
+%0A
     
-const remainingRecoveryCodes = user.twoFactorAuthRecoveryCodes.filter(%0A      code =%3E crypto.hash(
+await user.update(%7B 
 twoF
@@ -4706,34 +4706,24 @@
 woFactorAuth
-entication
 RecoveryCode
@@ -4726,115 +4726,41 @@
 Code
-.toUpperCase()) !== code,%0A    );%0A    await user.update(%7B twoFactorAuthRecoveryCodes: remainingRecoveryCodes
+s: null, twoFactorAuthToken: null
  %7D);

disable 2FA after using recovery codes