Replace captcha system

This commit is contained in:
2026-01-18 18:37:08 +01:00
parent e5ef197ed6
commit ccfd5b996b
15 changed files with 242 additions and 200 deletions

View File

@@ -8,6 +8,8 @@ use App\Models\Episode;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use GrantHolle\Altcha\Rules\ValidAltcha;
class DownloadApiController extends Controller
{
/**
@@ -16,11 +18,12 @@ class DownloadApiController extends Controller
public function getDownload(Request $request)
{
$validated = $request->validate([
'episode_id' => 'required',
'captcha' => 'required|captcha'
'episode_id' => ['required'],
'captcha' => ['required', new ValidAltcha],
]);
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
$episode = Episode::where('id', $request->input('episode_id'))
->firstOrFail();
// Increase download count, as we assume the user
// downloads after submitting the captcha

View File

@@ -11,6 +11,8 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use GrantHolle\Altcha\Rules\ValidAltcha;
class RegisteredUserController extends Controller
{
/**
@@ -24,6 +26,7 @@ class RegisteredUserController extends Controller
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'altcha' => ['required', new ValidAltcha],
]);
$user = User::create([

View File

@@ -5,6 +5,8 @@ namespace App\Http\Controllers;
use App\Models\Contact;
use Illuminate\Http\Request;
use GrantHolle\Altcha\Rules\ValidAltcha;
class ContactController extends Controller
{
/**
@@ -25,7 +27,7 @@ class ContactController extends Controller
'email' => 'required|max:50',
'message' => 'required|max:1000',
'subject' => 'required|max:50',
'captcha' => 'required|captcha',
'altcha' => ['required', new ValidAltcha],
]);
$contact = new Contact();
@@ -37,9 +39,4 @@ class ContactController extends Controller
return back()->with('status', 'contact-submitted');
}
public function reloadCaptcha(): \Illuminate\Http\JsonResponse
{
return response()->json(['captcha'=> captcha_img()]);
}
}

View File

@@ -9,6 +9,8 @@ use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use GrantHolle\Altcha\Rules\ValidAltcha;
class LoginRequest extends FormRequest
{
/**
@@ -29,6 +31,7 @@ class LoginRequest extends FormRequest
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
'altcha' => ['required', new ValidAltcha],
];
}

View File

@@ -9,6 +9,7 @@
"license": "MIT",
"require": {
"php": "^8.2",
"grantholle/laravel-altcha": "^2.1",
"guzzlehttp/guzzle": "^7.8.1",
"hisorange/browser-detect": "^5.0",
"http-interop/http-factory-guzzle": "^1.2",
@@ -22,7 +23,6 @@
"livewire/livewire": "^3.7.0",
"maize-tech/laravel-markable": "^2.3.0",
"meilisearch/meilisearch-php": "^1.16",
"mews/captcha": "^3.4.4",
"predis/predis": "^2.2",
"realrashid/sweet-alert": "^7.2",
"rtconner/laravel-tagging": "^5.0",

200
composer.lock generated
View File

@@ -4,8 +4,55 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9287e7ef1f943600ac3e2b78bc9cd7c8",
"content-hash": "1664694fd60e8e74305716bd3253c59e",
"packages": [
{
"name": "altcha-org/altcha",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/altcha-org/altcha-lib-php.git",
"reference": "9e9e70c864a9db960d071c77c778be0c9ff1a4d0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/altcha-org/altcha-lib-php/zipball/9e9e70c864a9db960d071c77c778be0c9ff1a4d0",
"reference": "9e9e70c864a9db960d071c77c778be0c9ff1a4d0",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=8.2"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.72",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^11.5"
},
"type": "library",
"autoload": {
"psr-4": {
"AltchaOrg\\Altcha\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Regeci",
"email": "536331+ovx@users.noreply.github.com"
}
],
"support": {
"issues": "https://github.com/altcha-org/altcha-lib-php/issues",
"source": "https://github.com/altcha-org/altcha-lib-php/tree/v1.3.1"
},
"time": "2025-12-13T10:03:53+00:00"
},
{
"name": "brick/math",
"version": "0.14.1",
@@ -776,6 +823,82 @@
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "grantholle/laravel-altcha",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/grantholle/laravel-altcha.git",
"reference": "c0dcc6d0805e8640d46709e5f8d05c7c65b2687c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/grantholle/laravel-altcha/zipball/c0dcc6d0805e8640d46709e5f8d05c7c65b2687c",
"reference": "c0dcc6d0805e8640d46709e5f8d05c7c65b2687c",
"shasum": ""
},
"require": {
"altcha-org/altcha": "^1.3.1",
"illuminate/contracts": "^10.0|^11.0|^12.0",
"php": "^8.2",
"spatie/laravel-package-tools": "^1.14.0"
},
"require-dev": {
"laravel/pint": "^1.0",
"nunomaduro/collision": "^8.1.1||^7.10.0",
"orchestra/testbench": "^10.0.0||^9.0.0||^8.22.0",
"pestphp/pest": "^3.0||^2.0",
"pestphp/pest-plugin-arch": "^3.0||^2.0",
"pestphp/pest-plugin-laravel": "^3.0||^2.0",
"spatie/laravel-ray": "^1.26"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Altcha": "GrantHolle\\Altcha\\Facades\\Altcha"
},
"providers": [
"GrantHolle\\Altcha\\AltchaServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"GrantHolle\\Altcha\\": "src/",
"GrantHolle\\Altcha\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grant Holle",
"email": "hollegrant@gmail.com",
"role": "Developer"
}
],
"description": "A Laravel server implementation for Altcha.",
"homepage": "https://github.com/grantholle/laravel-altcha",
"keywords": [
"Grant Holle",
"laravel",
"laravel-altcha"
],
"support": {
"issues": "https://github.com/grantholle/laravel-altcha/issues",
"source": "https://github.com/grantholle/laravel-altcha/tree/2.1.1"
},
"funding": [
{
"url": "https://github.com/Grant Holle",
"type": "github"
}
],
"time": "2025-12-16T03:39:06+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.10.0",
@@ -3216,79 +3339,6 @@
},
"time": "2025-09-18T10:15:45+00:00"
},
{
"name": "mews/captcha",
"version": "3.4.7",
"source": {
"type": "git",
"url": "https://github.com/mewebstudio/captcha.git",
"reference": "2622c4f90dd621f19fe57e03e45f6f099509e839"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mewebstudio/captcha/zipball/2622c4f90dd621f19fe57e03e45f6f099509e839",
"reference": "2622c4f90dd621f19fe57e03e45f6f099509e839",
"shasum": ""
},
"require": {
"ext-gd": "*",
"illuminate/config": "~5|^6|^7|^8|^9|^10|^11|^12",
"illuminate/filesystem": "~5|^6|^7|^8|^9|^10|^11|^12",
"illuminate/hashing": "~5|^6|^7|^8|^9|^10|^11|^12",
"illuminate/session": "~5|^6|^7|^8|^9|^10|^11|^12",
"illuminate/support": "~5|^6|^7|^8|^9|^10|^11|^12",
"intervention/image": "^3.7",
"php": "^7.2|^8.1|^8.2|^8.3"
},
"require-dev": {
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^8.5|^9.5.10|^10.5|^11"
},
"type": "package",
"extra": {
"laravel": {
"aliases": {
"Captcha": "Mews\\Captcha\\Facades\\Captcha"
},
"providers": [
"Mews\\Captcha\\CaptchaServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Mews\\Captcha\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Muharrem ERİN",
"email": "me@mewebstudio.com",
"homepage": "https://github.com/mewebstudio",
"role": "Developer"
}
],
"description": "Laravel 5/6/7/8/9/10/11/12 Captcha Package",
"homepage": "https://github.com/mewebstudio/captcha",
"keywords": [
"captcha",
"laravel12 Captcha",
"laravel12 Security",
"laravel5 Security"
],
"support": {
"issues": "https://github.com/mewebstudio/captcha/issues",
"source": "https://github.com/mewebstudio/captcha/tree/3.4.7"
},
"time": "2025-10-11T14:42:33+00:00"
},
{
"name": "mobiledetect/mobiledetectlib",
"version": "4.8.10",
@@ -11673,5 +11723,5 @@
"php": "^8.2"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View File

@@ -1,50 +0,0 @@
<?php
return [
'characters' => ['2', '3', '4', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'X', 'Y', 'Z'],
'default' => [
'length' => 5,
'width' => 120,
'height' => 36,
'quality' => 90,
'math' => false,
'expire' => 60,
'encrypt' => false,
],
'math' => [
'length' => 9,
'width' => 120,
'height' => 36,
'quality' => 90,
'math' => true,
],
'flat' => [
'length' => 6,
'width' => 160,
'height' => 46,
'quality' => 90,
'lines' => 6,
'bgImage' => false,
'bgColor' => '#ecf2f4',
'fontColors' => ['#2c3e50', '#c0392b', '#16a085', '#c0392b', '#8e44ad', '#303f9f', '#f57c00', '#795548'],
'contrast' => -5,
],
'mini' => [
'length' => 3,
'width' => 60,
'height' => 32,
],
'inverse' => [
'length' => 5,
'width' => 120,
'height' => 36,
'quality' => 90,
'sensitive' => true,
'angle' => 12,
'sharpen' => 10,
'blur' => 2,
'invert' => true,
'contrast' => -5,
]
];

50
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1",
"@yaireo/tagify": "^4.21.2",
"altcha": "^2.3.0",
"chart.js": "^4.5.0",
"dashjs": "^5.0.0",
"hammerjs": "^2.0.8",
@@ -40,6 +41,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@altcha/crypto": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@altcha/crypto/-/crypto-0.0.1.tgz",
"integrity": "sha512-qZMdnoD3lAyvfSUMNtC2adRi666Pxdcw9zqfMU5qBOaJWqpN9K+eqQGWqeiKDMqL0SF+EytNG4kR/Pr/99GJ6g==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
@@ -1161,6 +1168,31 @@
"@vue/reactivity": "~3.1.1"
}
},
"node_modules/altcha": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/altcha/-/altcha-2.3.0.tgz",
"integrity": "sha512-vl8I0dQvSQB7/Mx09XuWZ1+LdSP7vEda6OLbg9kUQ2ZO2LT7MzgUyLK7Iips+GAV6c0ntVcS1XWOqhEPpwbDhQ==",
"license": "MIT",
"dependencies": {
"@altcha/crypto": "^0.0.1"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.18.0"
}
},
"node_modules/altcha/node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz",
"integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -3064,6 +3096,15 @@
"postcss": "^8.0.9"
}
},
"node_modules/tw-elements/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
@@ -3302,15 +3343,6 @@
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT"
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
}
}
}

View File

@@ -20,6 +20,7 @@
"@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1",
"@yaireo/tagify": "^4.21.2",
"altcha": "^2.3.0",
"chart.js": "^4.5.0",
"dashjs": "^5.0.0",
"hammerjs": "^2.0.8",

View File

@@ -122,4 +122,33 @@ input:checked~.dot {
font-display: swap;
src: url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff) format('woff');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Captcha */
:root {
--altcha-border-width: 1px;
--altcha-border-radius: 0.375rem;
--altcha-color-base: #333;
--altcha-color-border: #a0a0a0;
--altcha-color-text: #fff;
--altcha-color-border-focus: currentColor;
--altcha-color-error-text: #f23939;
--altcha-color-footer-bg: #141414;
--altcha-max-width: 260px;
}
.altcha-footer {
border-bottom-left-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
}
input[type="checkbox"] {
background-color: #ffffff;
border-color: #a0a0a0;
color: rgb(225,29,72);
}
input[type="checkbox"]:checked {
background-color: rgb(225,29,72);
box-shadow: 0 0 0 0px #fff, 0 0 0 calc(2px + 0px) rgba(246, 59, 118, 0.5), 0 0 #0000;
}

View File

@@ -12,6 +12,9 @@ import {
initTE,
} from "tw-elements";
// Captcha
import 'altcha';
// import Alpine from 'alpinejs';
// window.Alpine = Alpine;

View File

@@ -69,6 +69,11 @@
</label>
</div>
<div class="block">
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
@if (Route::has('password.request'))
<a class="underline text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 dark:focus:ring-offset-neutral-800" href="{{ route('password.request') }}">
@@ -127,6 +132,11 @@
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="block">
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button class="ms-4">
{{ __('Register') }}

View File

@@ -33,19 +33,7 @@
<x-input-error class="mt-2" :messages="$errors->get('message')" />
</div>
<div>
<x-input-label for="message" :value="__('Captcha')" />
<div class="flex pt-2">
<div id="captchaImg">
{!! captcha_img() !!}
</div>
<button type="button" class="inline-flex items-center ml-2 px-2 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150" id="reloadcaptcha">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<br>
<x-text-input id="captcha" class="block " type="text" name="captcha" required />
</div>
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Submit') }}</x-primary-button>
@@ -65,18 +53,4 @@
@endif
</div>
</form>
<script>
function reloadCaptcha() {
window.axios.get('/reload-captcha').then(function(response) {
if (response.status == 200) {
document.querySelector("#captchaImg").innerHTML = response.data.captcha;
}
}).catch(function(error) {
console.log(error);
});
}
document.querySelector("#reloadcaptcha").addEventListener("click", reloadCaptcha);
</script>
</section>

View File

@@ -8,18 +8,7 @@
<p id="message" class="text-red-600">
</p>
<div class="flex pt-2">
<div id="captchaImg">
{!! captcha_img() !!}
</div>
<button type="button" class="inline-flex items-center ml-2 px-2 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150" id="reloadcaptcha" >
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<div class="flex pt-2 mt-1">
<x-text-input id="captcha_text" class="block " type="text" name="captcha_text"/>
<button type="button" class="inline-flex items-center ml-2 px-2 -pt-1 bg-rose-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-rose-700 active:bg-rose-900 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150" id="submitcaptcha" >
Submit
</button>
<altcha-widget id="altcha" challengeurl="/altcha-challenge"></altcha-widget>
</div>
<br>
<p class="text-gray-800 dark:text-gray-200 text-sm">
@@ -51,21 +40,12 @@
<script>
var downloadCounter = 0;
function reloadCaptcha() {
window.axios.get('/reload-captcha').then(function (response) {
if (response.status == 200) {
document.querySelector("#captchaImg").innerHTML = response.data.captcha;
}
}).catch(function (error) {
console.log(error);
});
}
function submitCaptcha() {
function submitCaptcha(captchaToken) {
document.querySelector("#message").innerHTML = '';
window.axios.post('/get-download', {
captcha: document.getElementById('captcha_text').value,
episode_id: document.getElementById('e_id').value
episode_id: document.getElementById('e_id').value,
captcha: captchaToken,
}).then(function (response) {
document.querySelector("#captcharequired").style.display = "none";
document.querySelector("#captchsolved").style.display = "block";
@@ -89,6 +69,16 @@
document.querySelector("#downloadEpisode").addEventListener("click", increaseDownloadCounter);
document.querySelector("#reloadcaptcha").addEventListener("click", reloadCaptcha);
document.querySelector("#submitcaptcha").addEventListener("click", submitCaptcha);
document.addEventListener("DOMContentLoaded", () => {
const altcha = document.querySelector("#altcha");
altcha.addEventListener("statechange", (ev) => {
if (ev.detail.state === "verified") {
submitCaptcha(ev.detail.payload);
// Remove captcha from DOM
altcha.remove();
}
});
});
</script>

View File

@@ -42,9 +42,6 @@ Route::post('/contact', [ContactController::class, 'store'])->name('contact.stor
Route::get('/playlists', [PlaylistController::class, 'index'])->name('playlist.index');
Route::get('/playlist/{playlist_id}', [PlaylistController::class, 'show'])->name('playlist.show');
// Captcha Reload
Route::get('/reload-captcha', [ContactController::class, 'reloadCaptcha']);
// Download
Route::post('/get-download', [DownloadApiController::class, 'getDownload']);