- Published on
Using Nuxt 3 to build a captcha flow
- Authors
- Name
- Kevin Olson
- @fumeapp
Let's build a captcha flow with Nuxt 3! We will use the new server routes to create a captcha flow that will compare the user input on the back end, hiding it from the browser to help guarantee security.
This example is online at captcha.fume.app, and the codebase is on github
Packages included
For this tutorial we're going to use:
- windicss to style things and go dark
- unstorage to store our captchas on the back end
- Our nuxt module tailvue for toasts and buttons
- svg-captcha to generate our captcha
- uuid to deliver unique ids when storing captchas
TypeScript Interfaces
First we're going to declare some interfaces that we will share between the submission form and our server routes.
export interface Captcha {
uuid: string
svg: string
}
export interface Submission {
name: string
captcha: string
uuid: string
}
Our Captcha
interface will be used on the captcha generation route, our Submission
interface will be used as a combination of our form, the captcha text, and the uuid.
Storage Access
To make things easy for accessing our storage, we will create a file server/utils/storage.ts
that
- Mounts a storage instance called
captcha
mounting to/tmp
on the server - Exports it as
captchaStorage
which can now be accessable from anywhere in our server routes
import fsDriver from 'unstorage/drivers/fs'
useStorage().mount('captcha', fsDriver({ base: '/tmp' }))
export const captchaStorage = useStorage('captcha')
Captcha Generation
Next up lets create our server route that serves up our captcha, we can use the helpful command
npx nuxi new api generate
This will create a file server/api/generate.ts
, adding the following code will
- Generate a new captcha using
svg-captcha
- Generate a new unique id using
uuid
- Store the captcha text using the unique id
- Return the unique id and the SVG of our captcha
import svgCaptcha from 'svg-captcha'
import { v4 as uuidv4 } from 'uuid'
export default defineEventHandler(async () => {
const captcha = svgCaptcha.create({ height: 40 })
const uuid = uuidv4()
await captchaStorage.setItem(uuid, captcha.text)
return { uuid, svg: captcha.data }
})
You can view the output of this endpoint at captcha.fume.app/api/generate
Submission form
Now that we have our captcha route, lets whip up a simple submission form page.
- We set refs for the response of the captcha generation, and our submission object
- We create a function to store the generated captcha, notice we add the uuid to our submission ref
- Next is a function to submit our form, useFetch to make a post request to our server route (coming next)
- We display our captcha svg using v-html, and not show that unless its loaded
<script lang="ts" setup>
import type { Captcha, Submission } from '@/types/vue-shim'
import { PushButton } from 'tailvue'
const { $toast } = useNuxtApp()
const captcha = ref<Captcha|undefined>(undefined)
const submission = ref<Submission>({
name: '',
captcha: '',
uuid: '',
})
const getCaptcha = async () => {
captcha.value = (await useFetch('/api/generate')).data.value as Captcha
submission.value.uuid = captcha.value.uuid
}
interface Response {
success: boolean
message: string
errors: string[]
}
const submit = async () => {
const { data } = await useFetch('/api/submit', {
method: 'POST',
body: submission.value,
}) as { data: { value: Response } }
if (data.value?.success) {
$toast.success(data.value?.message)
} else {
data.value.errors.map(e => $toast.show({ message: e, timeout: 0}))
getCaptcha()
}
}
onMounted(getCaptcha)
</script>
<template>
<div class="max-w-md m-16 mx-auto flex flex-col space-y-4 border border-gray-600 bg-gray-800 p-4 rounded-lg">
<div class="flex items-center justify-start">
<label class="w-30" for="name">Name</label>
<input id="name" type="text" class="w-full" v-model="submission.name" />
</div>
<div class="flex items-center justify-start">
<label class="w-30" for="captcha">Captcha</label>
<div class="w-40 mx-4 flex items-center bg-gray-300 border border-gray-600 rounded">
<span v-if="captcha" v-html="captcha.svg" />
</div>
<input id="captcha" type="text" class="w-40" @keydown.enter="submit" v-model="submission.captcha" />
</div>
<div class="flex items-center justify-end space-x-4">
<push-button value="Refresh" @click="getCaptcha">Refresh</push-button>
<push-button value="Submit" @click="submit">Submit</push-button>
</div>
</div>
</template>
API Route to process a submission
Now that we have our form, lets create our server route to verify and process it. We'll use the same command as before to create our server route
npx nuxi new api submit
This will create a file server/api/submit.ts
, let's add the following code that will:
- Read the body of the request using readBody in combination with the
Submission
interface - Check if there is an entry in our store for the unique id, if we have one we'll check if the text matches the captcha, if it does we'll return a success message, if not we'll return an error message
import type { Submission } from '@/types/vue-shim'
export default defineEventHandler(async (event) => {
const body = await readBody<Submission>(event)
const entry = await captchaStorage.getItem(body.uuid)
if (!entry || entry !== body.captcha)
return {
error: true,
errors: ['Invalid captcha.']
}
return {
success: true,
message: 'Captcha is valid.'
}
})
That's it! From here you can do whatever you want with your data, save it to a database? Send an email?
Fume allows you to use the power of Server Side rendering which these server routes use while staying serverless with AWS Lambda, allowing you to scale your application without having to worry about scale and cost. Start your trial today!