Implementing Captcha and file upload in Express and Vue
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:
-
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 asmultipart/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. -
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 likefetch
,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);
});