Package Exports
- @baanihali/captcha
- @baanihali/captcha/server
Readme
🎥 Demo Screenshots
User Registration with Captcha Verification
Custom Captcha
A customizable sliding puzzle captcha component for React applications with server-side validation. Perfect for preventing bot registrations and enhancing form security.
🧩 Features
- 🎯 Sliding puzzle captcha with random puzzle piece positioning
- 🔒 Server-side validation for enhanced security
- ⚡ TypeScript support with comprehensive type definitions
- 🎨 Customizable styling with CSS variables
- 📱 Responsive design that works on all devices
- 🚀 Easy integration with any React project
- 🛡️ Replay attack prevention with unique captcha IDs
- 🖼️ Flexible image sources (API, local files, custom images)
📦 Installation
npm install @baanihali/captcha🔄 How It Works
This captcha system follows a secure workflow:
- User Registration Flow: User navigates to signup page and fills credentials
- Captcha Challenge: User clicks "Verify you're human" to open the captcha
- Server Request: Client requests new captcha data from server
- Puzzle Generation: Server generates random puzzle piece position and stores solution
- User Interaction: User slides puzzle piece to correct position
- Verification: Server validates the position and marks captcha as verified
- Registration: Server checks captcha verification status before allowing signup
🚀 Quick Start Examples
Next.js App Router Example
Client Component (components/SignupForm.tsx)
'use client';
import { useState } from 'react';
import CaptchaComponent from '@baanihali/captcha/client';
import {
createCaptcha,
verifyCaptcha,
signup
} from '@/actions/auth';
export default function SignupForm() {
const [loading, setLoading] = useState(false);
const [captchaId, setCaptchaId] = useState<string>('');
const [formData, setFormData] = useState({
email: '',
password: '',
name: ''
});
const handleRefresh = async () => {
setLoading(true);
try {
const captcha = await createCaptcha();
setCaptchaId(captcha.id);
return captcha;
} finally {
setLoading(false);
}
};
const handleVerify = async (id: string, value: string) => {
setLoading(true);
try {
return await verifyCaptcha(id, value);
} finally {
setLoading(false);
}
};
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
const result = await signup({
...formData,
captchaId
});
if (result.success) {
alert('Signup successful!');
} else {
alert(result.message);
}
};
return (
<form onSubmit={handleSignup} className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">Sign Up</h2>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Name</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className="w-full p-2 border rounded"
required
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Email</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
className="w-full p-2 border rounded"
required
/>
</div>
<div className="mb-6">
<label className="block text-sm font-medium mb-2">Password</label>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData({...formData, password: e.target.value})}
className="w-full p-2 border rounded"
required
/>
</div>
<div className="mb-6">
<CaptchaComponent
loading={loading}
refreshCaptcha={handleRefresh}
verifyCaptcha={handleVerify}
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
>
Sign Up
</button>
</form>
);
}Next JS Server Actions
Server Actions (app/actions/auth.ts)
'use server';
import {
createCaptcha as createCaptchaLib,
verifyCaptcha as verifyCaptchaLib,
hasCaptchaBeenVerified
} from '@baanihali/captcha/server';
import Redis from 'ioredis';
import bcrypt from 'bcryptjs';
const redis = new Redis(process.env.REDIS_URL!);
export async function createCaptcha() {
try {
const captcha = await createCaptchaLib({
fallbackImgPath: './public/fallback.jpg',
storeCaptchaId: async (id, value) => {
await redis.setex(`captcha:${id}`, 600, value); // 10 minute TTL
}
});
return captcha;
} catch (error) {
throw new Error('Failed to create captcha');
}
}
export async function verifyCaptcha(id: string, value: string) {
try {
const result = await verifyCaptchaLib({
captchaId: id,
value,
getCaptchaValue: async (id) => {
return redis.get(`captcha:${id}`);
},
changeCaptchaIdOnSuccess: async (id, value) => {
redis.setex(`captcha:${id}`, 600, value);
}
});
return result;
} catch (error) {
return { success: false, error: 'Verification failed' };
}
}
export async function signup(data: {
email: string;
password: string;
name: string;
captchaId: string;
}) {
try {
const { email, password, name, captchaId } = data;
// Verify captcha first
const isVerified = await hasCaptchaBeenVerified({
captchaId: captchaId,
getCaptchaValue: async (id) => await redis.get(`captcha:${id}`)
});
if (!isVerified) {
return {
success: false,
message: 'Please complete the captcha verification'
};
}
//... Do your stuff ....
// Clean up captcha
await redis.del(`captcha:${captchaId}`);
return {
success: true,
message: 'User created successfully',
userId: user.id
};
} catch (error) {
return {
success: false,
message: 'Internal server error'
};
}
}React + Express.js + Redis Example
React Component (src/components/SignupForm.jsx)
import React, { useState } from 'react';
import CaptchaComponent from '@baanihali/captcha/client';
function SignupForm() {
const [loading, setLoading] = useState(false);
const [captchaId, setCaptchaId] = useState('');
const [formData, setFormData] = useState({
email: '',
password: '',
name: ''
});
const handleRefresh = async () => {
setLoading(true);
try {
const response = await fetch('http://localhost:5000/captcha/create', {
method: 'POST'
});
const data = await response.json();
setCaptchaId(data.id);
return data;
} finally {
setLoading(false);
}
};
const handleVerify = async (id, value) => {
setLoading(true);
try {
const response = await fetch('http://localhost:5000/captcha/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, value })
});
return await response.json();
} finally {
setLoading(false);
}
};
const handleSignup = async (e) => {
e.preventDefault();
const response = await fetch('http://localhost:5000/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...formData,
captchaId
})
});
if (response.ok) {
alert('Signup successful!');
} else {
const error = await response.json();
alert(error.message);
}
};
return (
<form onSubmit={handleSignup}>
<h2>Sign Up</h2>
<div>
<label>Name:</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
required
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
required
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData({...formData, password: e.target.value})}
required
/>
</div>
<div>
<CaptchaComponent
loading={loading}
refreshCaptcha={handleRefresh}
verifyCaptcha={handleVerify}
/>
</div>
<button type="submit">Sign Up</button>
</form>
);
}
export default SignupForm;Express.js Server (server.js)
const express = require('express');
const cors = require('cors');
const Redis = require('ioredis');
const bcrypt = require('bcryptjs');
const { createCaptcha, verifyCaptcha, hasCaptchaBeenVerified } = require('@baanihali/captcha/server');
const app = express();
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
app.use(cors());
app.use(express.json());
// Create captcha endpoint
app.post('/captcha/create', async (req, res) => {
try {
const captcha = await createCaptcha({
fallbackImgPath: './assets/fallback.jpg',
storeCaptchaId: async (id, value) => {
await redis.setex(`captcha:${id}`, 600, value);
}
});
res.json(captcha);
} catch (error) {
res.status(500).json({ error: 'Failed to create captcha' });
}
});
// Verify captcha endpoint
app.post('/captcha/verify', async (req, res) => {
try {
const { id, value } = req.body;
const result = await verifyCaptcha({
captchaId: id,
value,
getCaptchaValue: async (id) => {
return await redis.get(`captcha:${id}`);
},
changeCaptchaIdOnSuccess: async (id, value) => {
await redis.setex(`captcha:${id}`, 600, value);
}
});
res.json(result);
} catch (error) {
res.status(500).json({ error: 'Verification failed' });
}
});
// Signup endpoint
app.post('/auth/signup', async (req, res) => {
try {
const { email, password, name, captchaId } = req.body;
// Verify captcha
const isVerified = await hasCaptchaBeenVerified({
captchaId: captchaId,
getCaptchaValue: async (id) => await redis.get(`captcha:${id}`)
});
if (!isVerified) {
return res.status(400).json({
message: 'Please complete the captcha verification'
});
};
// Check if user exists (implement your user checking logic)
const existingUser = await checkUserExists(email);
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password and create user
const hashedPassword = await bcrypt.hash(password, 12);
const user = await createUser({ email, password: hashedPassword, name });
// Clean up captcha
await redis.del(`captcha:${captchaId}`);
res.json({ message: 'User created successfully', userId: user.id });
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
});
app.listen(5000, '0.0.0.0', () => {
console.log('Server running on http://0.0.0.0:5000');
});NestJS Example
Controller (src/captcha/captcha.controller.ts)
import { Controller, Post, Body } from '@nestjs/common';
import { CaptchaService } from './captcha.service';
@Controller('captcha')
export class CaptchaController {
constructor(private readonly captchaService: CaptchaService) {}
@Post('create')
async createCaptcha() {
return this.captchaService.createCaptcha();
}
@Post('verify')
async verifyCaptcha(@Body() body: { id: string; value: string }) {
return this.captchaService.verifyCaptcha(body.id, body.value);
}
}Service (src/captcha/captcha.service.ts)
import { Injectable } from '@nestjs/common';
import { createCaptcha, verifyCaptcha } from '@baanihali/captcha/server';
import Redis from 'ioredis';
@Injectable()
export class CaptchaService {
private redis = new Redis(process.env.REDIS_URL);
async createCaptcha() {
return createCaptcha({
fallbackImgPath: './assets/fallback.jpg',
storeCaptchaId: async (id, value) => {
await this.redis.setex(`captcha:${id}`, 600, value);
}
});
}
async verifyCaptcha(id: string, value: string) {
return verifyCaptcha({
captchaId: id,
value,
getCaptchaValue: async (id) => {
return await this.redis.get(`captcha:${id}`);
},
changeCaptchaIdOnSuccess: async (id, value) => {
await this.redis.setex(`captcha:${id}`, 600, value);
}
});
}
}🛠️ API Reference
Client Component Props
interface CaptchaData {
puzzle: string; // Base64 encoded puzzle piece
background: string; // Base64 encoded background
id: string; // Unique captcha ID
}
interface CustomCaptchaProps {
loading: boolean;
refreshCaptcha: () => Promise<CaptchaData> | { error: string; } | undefined | null>;
verifyCaptcha: (id: string, value: string) => Promise<{
success?: boolean; // Verification success status
error?: string; // Error message if verification fails
}>;
}Server Functions
createCaptcha(options)
Creates a new sliding puzzle captcha with random positioning.
interface CreateCaptchaProps {
image?: Buffer; // Custom image buffer
captchaId?: string; // Custom captcha ID
fallbackImgPath?: string; // Local fallback image
storeCaptchaId: (id: string, value: string) => Promise<void>; // Storage function
}
// Returns
interface CaptchaData {
puzzle: string; // Base64 encoded puzzle piece
background: string; // Base64 encoded background
id: string; // Unique captcha ID
}verifyCaptcha(options)
Verifies the user's captcha solution.
interface VerifyCaptchaProps {
captchaId: string; // Captcha ID to verify
value: string; // User's slider position
getCaptchaValue: (id: string) => Promise<string | null>; // Retrieve stored value
changeCaptchaIdOnSuccess: (id: string, value: string) => Promise<void>; // Mark as verified
tolerance?: number; // Position tolerance (default: 10px)
}
// Returns
interface VerificationResult {
success: boolean; // Verification success
reason?: string; // Failure reason
}hasCaptchaBeenVerified(props)
Utility to check if a captcha has been successfully verified.
interface HasCaptchaBeenVerifiedProps {
captchaId: string,
getCaptchaValue: (captchaId: string) => Promise<string | null | undefined>
};
// Returns boolean🎨 Styling Customization
The component uses CSS variables for easy theming:
:root {
--primary-blue: #3b82f6;
--primary-blue-dark: #2563eb;
--primary-blue-darker: #1d4ed8;
--gray-50: #f8fafc;
--gray-100: #f1f5f9;
--gray-200: #e2e8f0;
--gray-300: #cbd5e1;
--gray-400: #94a3b8;
--gray-500: #64748b;
--gray-600: #475569;
--red-500: #ef4444;
--white: #ffffff;
}
/* Override for dark theme */
.dark {
--primary-blue: #60a5fa;
--gray-50: #0f172a;
--gray-100: #1e293b;
/* ... etc */
}🔒 Security Features
- Server-side validation prevents client-side bypassing
- Unique captcha IDs prevent replay attacks
- Time-based expiration limits captcha lifespan
- Position tolerance accounts for user precision
- Rate limiting ready (implement in your routes)
📄 License
MIT © Murtaza Baanihali
🤝 Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📞 Support
- Issues: GitHub Issues
- Documentation: GitHub Wiki
Made with ❤️ By Murtaza Baanihali for the React community