Add validateCaptcha function to @rondo.dev/captcha
This commit is contained in:
parent
b2620e8050
commit
a9f37103bd
28
packages/captcha/src/Captcha.ts
Normal file
28
packages/captcha/src/Captcha.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export type CaptchaType = 'image' | 'audio'
|
||||
|
||||
export interface Captcha {
|
||||
type: CaptchaType
|
||||
value: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line
|
||||
namespace Express {
|
||||
interface Session {
|
||||
captcha?: Captcha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createCaptcha(
|
||||
value: string,
|
||||
type: CaptchaType,
|
||||
): Captcha {
|
||||
return {
|
||||
value,
|
||||
type,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
}
|
||||
@ -67,7 +67,11 @@ describe('audio', () => {
|
||||
.set('cookie', cookie)
|
||||
.expect(200)
|
||||
|
||||
expect(res2.body.captcha).toEqual(jasmine.any(String))
|
||||
expect(res2.body.captcha).toEqual({
|
||||
value: jasmine.any(String),
|
||||
type: 'audio',
|
||||
timestamp: jasmine.any(Number),
|
||||
})
|
||||
})
|
||||
|
||||
it('fails with error 500 when unable to generate', async () => {
|
||||
|
||||
@ -2,6 +2,7 @@ import { Request, Response } from 'express'
|
||||
import SVGCaptcha from 'svg-captcha'
|
||||
import { Command, ReadableProcess, ReadableWritable, run } from './run'
|
||||
import { TextStream } from './TextStream'
|
||||
import { createCaptcha } from './Captcha'
|
||||
|
||||
export interface AudioConfig {
|
||||
commands: Command[]
|
||||
@ -14,7 +15,7 @@ export const audio = (config: AudioConfig) => async (
|
||||
) => {
|
||||
const { commands, size } = config
|
||||
const captcha = SVGCaptcha.randomText(size)
|
||||
req.session!.captcha = captcha
|
||||
req.session!.captcha = createCaptcha(captcha, 'audio')
|
||||
let speech: ReadableProcess
|
||||
try {
|
||||
speech = await speak('test', commands)
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import bodyParser from 'body-parser'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import express from 'express'
|
||||
import session from 'express-session'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import request from 'supertest'
|
||||
import { Captcha } from './Captcha'
|
||||
import { image } from './image'
|
||||
import { validateCaptcha } from './validateCaptcha'
|
||||
|
||||
describe('image', () => {
|
||||
|
||||
const app = express()
|
||||
app.use(cookieParser())
|
||||
app.use(bodyParser.json())
|
||||
app.use(session({
|
||||
saveUninitialized: false,
|
||||
resave: false,
|
||||
@ -17,22 +21,92 @@ describe('image', () => {
|
||||
app.get('/captcha/session', (req, res) => {
|
||||
res.json(req.session)
|
||||
})
|
||||
app.post('/captcha/validate', validateCaptcha(), (req, res) => {
|
||||
res.send('OK')
|
||||
})
|
||||
app.post('/captcha/validate/expired', validateCaptcha({
|
||||
property: 'c',
|
||||
ttl: -1,
|
||||
}), (req, res) => {
|
||||
res.send('OK')
|
||||
})
|
||||
|
||||
async function getImage() {
|
||||
const res = await request(app)
|
||||
.get('/captcha/image')
|
||||
.expect('Content-type', /svg/)
|
||||
.expect(200)
|
||||
|
||||
return res.header['set-cookie'][0]
|
||||
}
|
||||
|
||||
async function getCaptcha() {
|
||||
const cookie = await getImage()
|
||||
const res = await request(app)
|
||||
.get('/captcha/session')
|
||||
.set('cookie', cookie)
|
||||
.expect(200)
|
||||
|
||||
return {captcha: res.body.captcha as Captcha, cookie}
|
||||
}
|
||||
|
||||
describe('/captcha/image', () => {
|
||||
it('generates a new captcha', async () => {
|
||||
const res1 = await request(app)
|
||||
.get('/captcha/image')
|
||||
.expect('Content-type', /svg/)
|
||||
.expect(200)
|
||||
const { captcha } = await getCaptcha()
|
||||
expect(captcha).toEqual({
|
||||
value: jasmine.any(String),
|
||||
type: 'image',
|
||||
timestamp: jasmine.any(Number),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const cookie = res1.header['set-cookie'][0]
|
||||
|
||||
const res2 = await request(app)
|
||||
.get('/captcha/session')
|
||||
describe('/captcha/validate', () => {
|
||||
it('fails when no captcha in session', async () => {
|
||||
await request(app)
|
||||
.post('/captcha/validate')
|
||||
.send({ captcha: '123' })
|
||||
.expect(403)
|
||||
})
|
||||
it('fails when captcha is expired', async () => {
|
||||
const { captcha, cookie } = await getCaptcha()
|
||||
await request(app)
|
||||
.post('/captcha/validate/expired')
|
||||
.send({ captcha: captcha.value })
|
||||
.set('cookie', cookie)
|
||||
.expect(403)
|
||||
})
|
||||
it('fails when captcha does not match', async () => {
|
||||
const { cookie } = await getCaptcha()
|
||||
await request(app)
|
||||
.post('/captcha/validate')
|
||||
.set('cookie', cookie)
|
||||
.send({ captcha: 'invalid-captcha' })
|
||||
.expect(403)
|
||||
})
|
||||
it('validates captcha', async () => {
|
||||
const { captcha, cookie } = await getCaptcha()
|
||||
await request(app)
|
||||
.post('/captcha/validate')
|
||||
.set('cookie', cookie)
|
||||
.send({ captcha: captcha.value })
|
||||
.expect(200)
|
||||
.expect('OK')
|
||||
})
|
||||
it('cannot validate same captcha twice', async () => {
|
||||
const { captcha, cookie } = await getCaptcha()
|
||||
await request(app)
|
||||
.post('/captcha/validate')
|
||||
.set('cookie', cookie)
|
||||
.send({ captcha: captcha.value })
|
||||
.expect(200)
|
||||
.expect('OK')
|
||||
|
||||
expect(res2.body.captcha).toEqual(jasmine.any(String))
|
||||
await request(app)
|
||||
.post('/captcha/validate')
|
||||
.set('cookie', cookie)
|
||||
.send({ captcha: captcha.value })
|
||||
.expect(403)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import SVGCaptcha from 'svg-captcha'
|
||||
import { Request, Response } from 'express'
|
||||
import { createCaptcha } from './Captcha'
|
||||
|
||||
export interface ImageConfig {
|
||||
size: number
|
||||
@ -9,7 +10,7 @@ export const image = (config: ImageConfig) => (req: Request, res: Response) => {
|
||||
const { text, data } = SVGCaptcha.create({
|
||||
size: config.size,
|
||||
})
|
||||
req.session!.captcha = text
|
||||
req.session!.captcha = createCaptcha(text, 'image')
|
||||
res.type('svg')
|
||||
res.status(200)
|
||||
res.send(data)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './audio'
|
||||
export * from './image'
|
||||
export * from './commands'
|
||||
export * from './validateCaptcha'
|
||||
|
||||
33
packages/captcha/src/validateCaptcha.ts
Normal file
33
packages/captcha/src/validateCaptcha.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { RequestHandler } from 'express'
|
||||
import createError from 'http-errors'
|
||||
|
||||
export interface ValidationConfig {
|
||||
readonly property: string
|
||||
readonly ttl: number
|
||||
}
|
||||
|
||||
const defaultConfig: ValidationConfig = {
|
||||
property: 'captcha',
|
||||
ttl: 10 * 60 * 1000,
|
||||
}
|
||||
|
||||
export const validateCaptcha = (
|
||||
config?: Partial<ValidationConfig>,
|
||||
): RequestHandler => {
|
||||
const cfg: ValidationConfig = Object.assign({}, defaultConfig, config)
|
||||
|
||||
return (req, res, next) => {
|
||||
const captcha = req.session && req.session.captcha
|
||||
if (!captcha) {
|
||||
return next(createError(403, 'Invalid captcha'))
|
||||
}
|
||||
if (Date.now() >= captcha.timestamp + cfg.ttl) {
|
||||
return next(createError(403, 'Invalid captcha'))
|
||||
}
|
||||
if (captcha.value !== req.body[cfg.property]) {
|
||||
return next(createError(403, 'Invalid captcha'))
|
||||
}
|
||||
delete req.session!.captcha
|
||||
next()
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user