captcha.js

const _ = require('underscore');
const { tap } = require('./utils');

module.exports = AV => {
  /**
   * @class
   * @example
   * AV.Captcha.request().then(captcha => {
   *   captcha.bind({
   *     textInput: 'code', // the id for textInput
   *     image: 'captcha',
   *     verifyButton: 'verify',
   *   }, {
   *     success: (validateCode) => {}, // next step
   *     error: (error) => {}, // present error.message to user
   *   });
   * });
   */
  AV.Captcha = function Captcha(options, authOptions) {
    this._options = options;
    this._authOptions = authOptions;
    /**
     * The image url of the captcha
     * @type string
     */
    this.url = undefined;
    /**
     * The captchaToken of the captcha.
     * @type string
     */
    this.captchaToken = undefined;
    /**
     * The validateToken of the captcha.
     * @type string
     */
    this.validateToken = undefined;
  };

  /**
   * Refresh the captcha
   * @return {Promise.<string>} a new capcha url
   */
  AV.Captcha.prototype.refresh = function refresh() {
    return AV.Cloud._requestCaptcha(this._options, this._authOptions).then(
      ({ captchaToken, url }) => {
        _.extend(this, { captchaToken, url });
        return url;
      }
    );
  };

  /**
   * Verify the captcha
   * @param {String} code The code from user input
   * @return {Promise.<string>} validateToken if the code is valid
   */
  AV.Captcha.prototype.verify = function verify(code) {
    return AV.Cloud.verifyCaptcha(code, this.captchaToken).then(
      tap(validateToken => (this.validateToken = validateToken))
    );
  };

  if (process.env.PLATFORM === 'Browser') {
    /**
     * Bind the captcha to HTMLElements. <b>ONLY AVAILABLE in browsers</b>.
     * @param [elements]
     * @param {String|HTMLInputElement} [elements.textInput] An input element typed text, or the id for the element.
     * @param {String|HTMLImageElement} [elements.image] An image element, or the id for the element.
     * @param {String|HTMLElement} [elements.verifyButton] A button element, or the id for the element.
     * @param [callbacks]
     * @param {Function} [callbacks.success] Success callback will be called if the code is verified. The param `validateCode` can be used for further SMS request.
     * @param {Function} [callbacks.error] Error callback will be called if something goes wrong, detailed in param `error.message`.
     */
    AV.Captcha.prototype.bind = function bind(
      { textInput, image, verifyButton },
      { success, error }
    ) {
      if (typeof textInput === 'string') {
        textInput = document.getElementById(textInput);
        if (!textInput)
          throw new Error(`textInput with id ${textInput} not found`);
      }
      if (typeof image === 'string') {
        image = document.getElementById(image);
        if (!image) throw new Error(`image with id ${image} not found`);
      }
      if (typeof verifyButton === 'string') {
        verifyButton = document.getElementById(verifyButton);
        if (!verifyButton)
          throw new Error(`verifyButton with id ${verifyButton} not found`);
      }

      this.__refresh = () =>
        this.refresh()
          .then(url => {
            image.src = url;
            if (textInput) {
              textInput.value = '';
              textInput.focus();
            }
          })
          .catch(err => console.warn(`refresh captcha fail: ${err.message}`));
      if (image) {
        this.__image = image;
        image.src = this.url;
        image.addEventListener('click', this.__refresh);
      }

      this.__verify = () => {
        const code = textInput.value;
        this.verify(code)
          .catch(err => {
            this.__refresh();
            throw err;
          })
          .then(success, error)
          .catch(err => console.warn(`verify captcha fail: ${err.message}`));
      };
      if (textInput && verifyButton) {
        this.__verifyButton = verifyButton;
        verifyButton.addEventListener('click', this.__verify);
      }
    };

    /**
     * unbind the captcha from HTMLElements. <b>ONLY AVAILABLE in browsers</b>.
     */
    AV.Captcha.prototype.unbind = function unbind() {
      if (this.__image)
        this.__image.removeEventListener('click', this.__refresh);
      if (this.__verifyButton)
        this.__verifyButton.removeEventListener('click', this.__verify);
    };
  }

  /**
   * Request a captcha
   * @param [options]
   * @param {Number} [options.width] width(px) of the captcha, ranged 60-200
   * @param {Number} [options.height] height(px) of the captcha, ranged 30-100
   * @param {Number} [options.size=4] length of the captcha, ranged 3-6. MasterKey required.
   * @param {Number} [options.ttl=60] time to live(s), ranged 10-180. MasterKey required.
   * @return {Promise.<AV.Captcha>}
   */
  AV.Captcha.request = (options, authOptions) => {
    const captcha = new AV.Captcha(options, authOptions);
    return captcha.refresh().then(() => captcha);
  };
};