From 1039542652f717c6a8eb589db91de5f5acfad1a4 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:20:02 +0330 Subject: [PATCH] Import source: app/backend/src/routes/captcha Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/backend/src/routes/captcha.ts | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 app/backend/src/routes/captcha.ts diff --git a/app/backend/src/routes/captcha.ts b/app/backend/src/routes/captcha.ts new file mode 100644 index 0000000..3225a8a --- /dev/null +++ b/app/backend/src/routes/captcha.ts @@ -0,0 +1,89 @@ +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { z } from 'zod'; +import { createCaptcha, verifyCaptcha } from '../lib/captcha.js'; +import { redis } from '../redis.js'; + +const verifySchema = z.object({ + captchaId: z.string().trim().min(1).max(64), + userSolution: z.string().trim().min(1).max(20), +}); + +function badInput(reply: FastifyReply) { + return reply.code(400).send({ success: false, message: 'Invalid input' }); +} + +export default async function captchaRoutes(app: FastifyInstance) { + // Generate captcha + app.get('/api/v1/teknav-cap/generate', { + schema: { + response: { + 200: { + type: 'object', + properties: { + id: { type: 'string' }, + data: { type: 'string' }, + }, + }, + }, + }, + }, async (req: FastifyRequest, reply: FastifyReply) => { + // Rate limit: 20 captcha generations per IP per minute + const key = `captcha:gen:${req.ip}`; + const hits = await redis.incr(key); + if (hits === 1) await redis.expire(key, 60); + if (hits > 20) { + return reply.code(429).send({ error: 'too_many_requests' }); + } + return await createCaptcha(); + }); + + // Verify captcha + app.post('/api/v1/teknav-cap/verify', { + schema: { + body: { + type: 'object', + required: ['captchaId', 'userSolution'], + properties: { + captchaId: { type: 'string' }, + userSolution: { type: 'string' }, + }, + }, + response: { + 200: { + type: 'object', + properties: { + success: { type: 'boolean' }, + message: { type: 'string' }, + }, + }, + 400: { + type: 'object', + properties: { + success: { type: 'boolean' }, + message: { type: 'string' }, + }, + }, + }, + }, + }, async (req: FastifyRequest, reply: FastifyReply) => { + // Rate limit: 10 verify attempts per IP per minute + const key = `captcha:verify:${req.ip}`; + const hits = await redis.incr(key); + if (hits === 1) await redis.expire(key, 60); + if (hits > 10) { + return reply.code(429).send({ success: false, message: 'Too many attempts' }); + } + + const parsed = verifySchema.safeParse(req.body); + if (!parsed.success) return badInput(reply); + + const { captchaId, userSolution } = parsed.data; + const isValid = await verifyCaptcha(captchaId, userSolution); + + if (isValid) { + return { success: true, message: 'TeknavCAP Verified' }; + } else { + return reply.code(400).send({ success: false, message: 'Invalid Captcha' }); + } + }); +}