irabbi360 / laravel-goal-captcha
A football goal slider CAPTCHA system for Laravel — anti-bot, mobile-friendly, themeable.
dev-main
2026-05-28 19:45 UTC
Requires
- php: ^8.2
- illuminate/cache: ^11.0||^12.0
- illuminate/contracts: ^11.0||^12.0
- illuminate/http: ^11.0||^12.0
- illuminate/routing: ^11.0||^12.0
- illuminate/support: ^11.0||^12.0
Requires (Dev)
- larastan/larastan: ^2.9
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1
- orchestra/testbench: ^9.0
- pestphp/pest: ^2.0
- pestphp/pest-plugin-arch: ^2.0
- pestphp/pest-plugin-laravel: ^2.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1
- phpstan/phpstan-phpunit: ^1.3
This package is auto-updated.
Last update: 2026-05-28 19:45:13 UTC
README
A production-ready, anti-bot football-goal slider CAPTCHA for Laravel — built like Sanctum / Telescope / Pulse.
Users drag a football into the goal net. The backend verifies alignment, drag speed, and human motion patterns. Bots are rejected.
Features
- ⚽ Football goal canvas scene — randomised stadium, goalkeeper, weather, decoys
- 🖱 Drag slider interaction — mouse + touch, fully accessible (keyboard)
- 🤖 Anti-bot motion analysis — speed variance, jerk, micro-corrections, interval consistency
- 🔒 Replay attack protection — token deleted after first use
- ⏱ Auto-expiring challenges — configurable TTL (default 2 min)
- 🎨 Theme system — football theme included, extendable
- 📱 Mobile responsive — works on touch devices
- 🧩 Blade component ``
- 🖼 Vue 3 component ``
- 🔌 Inertia / SPA / Nuxt compatible
- 🗃 Pluggable storage — Redis, Cache (array/file/database)
- 🎉 Event system — `CaptchaGenerated`, `CaptchaVerified`, `CaptchaFailed`
Installation
composer require irabbi360/laravel-goal-captcha
Publish assets and config:
php artisan goal-captcha:install
Quick Start — Blade
Add the component anywhere in your form:
<form method="POST" action="/login"> @csrf <x-goal-captcha /> <button type="submit">Login</button> </form>
Protect the route with the middleware:
Route::middleware('goal-captcha')->post('/login', LoginController::class);
Quick Start — Vue 3 / Inertia
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import vue from '@vitejs/plugin-vue'
import goalCaptcha from './vendor/irabbi360/laravel-goal-captcha/vite-plugin.js'
export default defineConfig({
plugins: [
laravel({ input: ['resources/js/app.js'] }),
vue(),
goalCaptcha(), // ← adds the alias automatically
],
})
<script setup> import { GoalCaptcha } from '@irabbi360/goal-captcha' import '@irabbi360/goal-captcha/style' const token = ref(null) </script> <template> <GoalCaptcha generate-url="/_goal_captcha/generate" verify-url="/_goal_captcha/verify" field-name="captcha_token" @verified="token = $event" /> </template>
Vue Plugin
import GoalCaptchaPlugin from '@irabbi360/goal-captcha' createApp(App) .use(GoalCaptchaPlugin, { generateUrl: '/_goal_captcha/generate', verifyUrl: '/_goal_captcha/verify', theme: 'football', difficulty: 'medium', }) .mount('#app')
API Endpoints
| Method | URL | Description |
|---|---|---|
| POST | /_goal_captcha/generate |
Returns a CAPTCHA challenge |
| POST | /_goal_captcha/verify |
Verifies submission, returns one-time token |
Configuration
return [ 'driver' => 'cache', // 'redis' | 'cache' 'expire' => 120, 'tolerance' => 12, 'min_drag_time' => 400, 'max_attempts' => 5, 'theme' => 'football', 'difficulty' => 'medium', // 'easy' | 'medium' | 'hard' 'enable_behavior_analysis' => true, ];
Events
Event::listen(CaptchaVerified::class, fn($e) => logger('solved', ['id' => $e->captcha->captchaId]));
| Event | When |
|---|---|
CaptchaGenerated |
Challenge created |
CaptchaVerified |
Human confirmed |
CaptchaFailed |
Verification rejected |
Architecture
src/
├── GoalCaptchaServiceProvider.php
├── LaravelGoalCaptcha.php
├── Contracts/ CaptchaStoreInterface, MotionAnalyzerInterface
├── DTO/ CaptchaData, VerificationData
├── Events/ Generated, Verified, Failed
├── Exceptions/ Expired, VerificationFailed, TooManyAttempts
├── Facades/ GoalCaptcha
├── Http/ Controllers, Middleware, Requests
├── Services/ Generator, Verifier, MotionAnalyzer, SceneBuilder, TokenManager
└── Support/Stores/ CacheStore, RedisStore
resources/js/
├── components/ GoalCaptcha.vue, GoalCanvas.vue, GoalSlider.vue, SuccessAnimation.vue
├── composables/ useGoalCaptcha.js
├── canvas/ renderer.js, animation.js, physics.js
├── utils/ motionTracker.js
└── index.js Vue plugin + Blade auto-mount
<link rel="stylesheet" href="{{ asset('vendor/goal-captcha/goal-captcha.css') }}">
<script src="{{ asset('vendor/goal-captcha/goal-captcha.umd.js') }}" defer></script>
<form id="contact-form" method="POST" action="/contact">
@csrf
<input type="text" name="name" required>
<input type="email" name="email" required>
{{-- CAPTCHA mounts here; on solve it injects a hidden captcha_token input --}}
<div id="goal-captcha"></div>
<button type="submit" id="submit-btn" disabled>Submit</button>
</form>
<script>
document.addEventListener('DOMContentLoaded', () => {
const { initMount } = window.GoalCaptcha
initMount('#goal-captcha', {
fieldName: 'captcha_token', // hidden input name injected into the form
})
// Enable submit only after CAPTCHA is solved
document.getElementById('goal-captcha').addEventListener('gc:verified', () => {
document.getElementById('submit-btn').disabled = false
})
})
</script>
<?php // routes/web.php use Illuminate\Support\Facades\Route; Route::post('/contact', [ContactController::class, 'store']) ->middleware('goal-captcha');
Use in your form component:
<template> <form @submit.prevent="submitForm"> <input v-model="form.name" type="text" required /> <input v-model="form.email" type="email" required /> <GoalCaptcha field-name="captcha_token" @verified="onCaptchaSolved" @failed="captchaToken = null" /> <button type="submit" :disabled="!captchaToken">Submit</button> </form> </template> <script setup> import { ref } from 'vue' const form = ref({ name: '', email: '' }) const captchaToken = ref(null) function onCaptchaSolved(token) { captchaToken.value = token } async function submitForm() { if (!captchaToken.value) return await fetch('/contact', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, }, body: JSON.stringify({ ...form.value, captcha_token: captchaToken.value, // ← send token with form data }), }) } </script>
Testing
composer test # Pest (PHP) npm run test # Vitest (JS)
License
MIT — Fazle Rabbi