Implementing Captcha and file upload in Express and Vue


Setting up projects

Installing NVM

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
nvm install node
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
curl https://gist.githubusercontent.com/hkbu-kennycheng/fa9cb4a52651e24626d26a90603af9de/raw/5a902dd66ff334733f7685de5944d5e4b3895f0f/.bash_profile >> ~/.bash_profile

Create an Express project

npx express-generator --no-view --git lab5-express
cd lab5-express
npm install
npm start

Create a Vue project

npx create-vue --router lab5-vue
cd lab5-vue
npm install
npm run dev

Adding proxy config to Vue project

Please modify vite.config.js, adding a block server to the config file.

server:{
  proxy: {
    '/api': {
      target: 'http://localhost:3000',
      ws: true,
      changeOrigin: true
    }
  }
},

Implementing Captcha with reCAPTCHA

What is reCAPTCHA?

reCAPTCHA is a free service that protects your website from spam and abuse. It uses advanced risk analysis techniques to tell humans and bots apart. With a simple integration, you can help protect your site from spam, abuse, and malicious content.

reCAPTCHA V2 vs V3

Current latest version of reCAPTCHA is v3. The main different between V2 and V3 is that V2 provides three type of verification methods including I'm not a robot button, image challenge and invisible captcha. V3 is an invisible captcha providing a score that is given to the user based on their behavior on the website. The score is between 0 and 1, with 0 being a bot and 1 being a human. The score is given to the website and the website can decide what to do with the score. For example, if the score is below 0.5, the website can block the user from accessing the website.

How to use reCAPTCHA V2

Step 1: Register your site

To use reCAPTCHA, you need to register your site. You can do this by going to the reCAPTCHA admin console and registering your site. You will need to provide a label for your site and specify whether you are registering a domain or an IP address. You will also need to provide contact information in case we need to contact you about your site.

Step 2: Get your keys

After registering your site, you will be given a site key and secret key. The site key is used to display the widget on your site. The secret key is used to communicate between your application backend and the reCAPTCHA server to verify the user’s response.

Step 3: Add reCAPTCHA to front-end

To add reCAPTCHA to your site, please modify src/views/HomeView.vue as follows:

<script setup>
// import TheWelcome from '../components/TheWelcome.vue'
import { onMounted, ref } from 'vue'
import GoogleReCaptcha from 'google-recaptcha-v2';

const formData = ref({})

onMounted(() => {
    GoogleReCaptcha.init({
        siteKey: '6Ldjg9YkAAAAALRMcvffg0XFNsG7KE3cTbtOH9ZH',
        callback: (token) => {
          console.log(token)
          formData.value['g-recaptcha-response'] = token

          // AJAX form submit goes here
          let response = await fetch('/api/upload', {
            method:'post',
            headers:{
              'Content-Type':'application/json'
            },
            body: JSON.stringify(formData.value)
          })

          if (response.ok) {
            let data = await response.json()
            console.log(data)
          } else {
            alert(await response.text())
          }

          // AJAX form submit goes here
        }
    })
})

</script>

<template>
  <main>
    <!-- <TheWelcome /> -->
    <form @submit.prevent="GoogleReCaptcha.validate($event)">
      <div class="g-recaptcha" data-sitekey="your_site_key"></div>
      <input type="hidden" name="g-recaptcha-response" v-model="formData.captcha" />
      <input type="submit" value="Submit"/>
    </form>
  </main>
</template>

Please use 6Ldjg9YkAAAAALRMcvffg0XFNsG7KE3cTbtOH9ZH as site key for your project.

Step 4: Verify the user’s response in back-end

When the user submits a form that includes reCAPTCHA, the widget will return a response token with name g-recaptcha-response along with the other form values. You will need to verify this token on your server using the reCAPTCHA server-side integration library. Let’s take a look to the restful API they provided:

URL: https://www.google.com/recaptcha/api/siteverify METHOD: POST

Parameter Description
secret Required. The shared key between your site and reCAPTCHA.
response Required. The user response token provided by the reCAPTCHA client-side integration on your site.
remoteip Optional. The user’s IP address.

The response will be a JSON object with the following:

{
  "success": true|false,      // whether this request was a valid reCAPTCHA token for your site
  "challenge_ts": timestamp,  // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
  "hostname": string,         // the hostname of the site where the reCAPTCHA was solved
  "error-codes": [...]        // optional
}

The success field indicates whether or not the user passed the reCAPTCHA test. If the user failed the test, you can inspect the error-codes field to determine why. Possible error codes are listed below.

Error code Description
missing-input-secret The secret parameter is missing.
invalid-input-secret The secret parameter is invalid or malformed.
missing-input-response The response parameter is missing.
invalid-input-response The response parameter is invalid or malformed.

Install package

To simplify our works, we could use express-recaptcha to help use make the request in middleware. Let’s add it to our project.

npm install --save express-recaptcha

Configure the reCAPTCHA in Express server

Please adding the following block of code before router.get('/', ... in routes/index.js.

var Recaptcha = require('express-recaptcha').RecaptchaV2;
var recaptcha = new Recaptcha('6Ldjg9YkAAAAALRMcvffg0XFNsG7KE3cTbtOH9ZH', '6Ldjg9YkAAAAAGNz7M1zjOhlqOJRpYpKSKNmV3PP')

router.post('/api/upload', recaptcha.middleware.verify, function(req, res, next) {
  if (!req.recaptcha.error) {
    // success code
    return res.json(req.body);
  } else {
    // error code
    return res.status(403).send('captcha incorrect')
  }
});

After that, let’s try it out by restarting the server and submitting the form.

Uploading file to server

What is file upload?

File upload is the process of sending files from a client to a server. The server can then store the file on the local file system or in a database. The client can be a web browser, a mobile app, or a desktop app.

Methods of file upload

There are two main methods of file upload:

  1. Traditional approach: HTML form encoded with multipart/form-data - this approach available from HTML 1.0 and is still the most widely used method. The form is encoded as multipart/form-data and the file is sent as a binary stream. It is also the most complex method of file upload. The client sends the file as a binary stream in the body of the request separated in multiple parts. The server then parses the request body and extracts the file.

  2. Modern approach: Sending file with base64 encoding using AJAX - The client uploads the file using JavaScript. File content will be read in to browser memory and sent to the server using AJAX request like fetch, XMLHttpRequest or others. The server then processes the file and stores it on the local file system or in a database or cloud storage.

Modern approach with Vue

Let’s take a closer look to the modern approach.

Step 1: Create a component

<script setup>
import { defineEmits, ref } from 'vue'

const emit = defineEmits(['change'])
const files = ref([])

const fileChange = (e) => {
    if (e.target.files.length == 0) {
        files.value = [];
        emit('change', files.value);
        return
    }

    Array.from(e.target.files).forEach((f) => {
        const reader = new FileReader(f)
        reader.addEventListener("load", (event) => {
            files.value.push(event.target.result)
            emit('change', files.value);
        }, false );
        reader.readAsDataURL(f);
    })
}
</script>

<template>
    <input type="file" @change="fileChange" v-bind="$attrs" />
</template>

Step 2: Adding the component to form

import FileInput from '../components/FileInput.vue';

const fileChanges = (files) => {
  formData.value.files = files
}
<FileInput @change="fileChanges" class="test" accept=".jpg,.jpeg" multiple />

No changes required on server-side

All the file contents are encoded in base64 and sent to the server as a string. The server can then decode the string and store the file on the local file system or in a database or cloud storage.

File Storage on Cloud

Storing files on Cloud platform is very popular nowadays. It is very convenient to store files on Cloud platform because we don’t need to worry about the storage capacity of our server. We can also access the files from anywhere in the world. There are many Cloud storage providers such as Amazon S3, Google Cloud Storage, Azure Blob Storage, etc. In this tutorial, we will use Backblaze B2 which provides an Amazon S3 compatiable API.

Setup Backblaze B2

Create an account

Go to Backblaze B2 and click Sign Up button to create an account.

Create a bucket

After creating an account, you will be redirected to the dashboard. Click Create a bucket button to create a bucket.

Create an application key

Click Create an application key button to create an application key.

Get the application key ID and application key

After creating an application key, you will see the application key ID and application key. Please copy them to somewhere safe.

Upload file to Backblaze B2 with AWS S3 API

Install package

npm install --save aws-sdk node-uuid mime-types

Configure with AWS S3 API

Please create a new file with name config.json and the following content in the root of Express project. You will need to replace the key and secret key with the one I provide to your group.

{
  "accessKeyId": "your_b2_keyid", 
  "secretAccessKey": "your_appKey"
}

Let’s add following lines in app.js.

const AWS = require('aws-sdk');
const BUCKET_NAME = 'your_bucket_name'

AWS.config.loadFromPath('config.json');

const s3 = new AWS.S3({
  endpoint: 'your_s3_endpoint',
  region: 'your_s3_endpoint_region'
});

Modifying route handler

var uuid = require('node-uuid');
var mime = require('mime-types')

router.post('/api/upload', recaptcha.middleware.verify, async function(req, res, next) {
  if (!req.recaptcha.error) {
    // success code
    for (const file of req.body.files) {
      const filename_parts = file.split(',')
      const extension = mime.extension(filename_parts[0].replace('data:','').replace(';base64',''))
      const params = {
        Bucket: BUCKET_NAME,
        Key: `files/${uuid.v4()}.${extension}`,
        Body: Buffer.from(filename_parts[1], 'base64')
      };
      try {
        const stored = await s3.upload(params).promise()
        console.log(stored);
      } catch (err) {
        console.log(err)
        return res.status(403).send(err)
      }
    }
    return res.json(req.body);
  } else {
    // error code
    return res.status(403).send('captcha incorrect')
  }
});

Extending size limit in app.js

Please modify the following lines inapp.js

From

app.use(express.json());
app.use(express.urlencoded({extended: false }));

To

app.use(express.json({limit: '200mb'}));
app.use(express.urlencoded({limit: '200mb', extended: false }));

Listing files in files folder on S3

router.get('/api/files', async function(req, res, next) {
  let data = await s3.listObjects({
    Bucket: BUCKET_NAME,
    Prefix:'files/'
  }).promise()

  return res.json(data)
});

Reading a single file

router.get('/api/files/:filename', async function(req, res, next) {
  let data = await s3.getObject({
    Bucket: BUCKET_NAME,
    Key:`files/${req.params.filename}`
  }).promise();

  res.writeHead(200, {
    'Content-Type': mime.lookup(req.params.filename),
    'Content-Length': data.Body.length
  });
  return res.end(data.Body);
});