JSPM

@baanihali/captcha

2.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • 0
  • Score
    100M100P100Q27175F
  • License MIT

A customizable sliding puzzle captcha component for React applications with server-side validation

Package Exports

  • @baanihali/captcha
  • @baanihali/captcha/server

Readme

🎥 Demo Screenshots

User Registration with Captcha Verification

Registration Form         Captcha Challenge

🧩 Custom Captcha

⚠️ Important:
This package is designed for Node.js server-side environments or those who support sharp (e.g. Next.js API routes or server actions, Express, NestJS, etc.).
It uses sharp for image manipulation, which relies on native Node.js bindings.
If you want to run captchas inside Cloudflare Workers or any V8 isolate/serverless edge runtime, check out our alternative Worker-compatible package 👉 @baanihali/captcha-workers (WASM-based, no native deps)

Custom Captcha

A customizable sliding puzzle captcha component for React applications with server-side validation.
Perfect for preventing bot registrations and enhancing form security when running a Node.js backend.

Custom Captcha Demo TypeScript React

🧩 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:

  1. User Registration Flow: User navigates to signup page and fills credentials
  2. Captcha Challenge: User clicks "Verify you're human" to open the captcha
  3. Server Request: Client requests new captcha data from server
  4. Puzzle Generation: Server generates random puzzle piece position and stores solution
  5. User Interaction: User slides puzzle piece to correct position
  6. Verification: Server validates the position and marks captcha as verified
  7. 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

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📞 Support


Made with ❤️ By Murtaza Baanihali for the React community