feat: add captcha-backed forum security hardening

This commit is contained in:
2026-03-17 16:06:28 +01:00
parent 980a15f66e
commit b3fc889452
40 changed files with 2849 additions and 108 deletions

View File

@@ -11,20 +11,20 @@ Route::middleware(['web', 'auth'])->prefix('dashboard')->name('api.dashboard.')-
});
Route::middleware(['web', 'auth', 'creator.access'])->prefix('stories')->name('api.stories.')->group(function () {
Route::post('create', [\App\Http\Controllers\StoryController::class, 'apiCreate'])->name('create');
Route::put('update', [\App\Http\Controllers\StoryController::class, 'apiUpdate'])->name('update');
Route::post('autosave', [\App\Http\Controllers\StoryController::class, 'apiAutosave'])->name('autosave');
Route::post('create', [\App\Http\Controllers\StoryController::class, 'apiCreate'])->middleware('forum.bot.protection:api_write')->name('create');
Route::put('update', [\App\Http\Controllers\StoryController::class, 'apiUpdate'])->middleware('forum.bot.protection:api_write')->name('update');
Route::post('autosave', [\App\Http\Controllers\StoryController::class, 'apiAutosave'])->middleware('forum.bot.protection:api_write')->name('autosave');
});
Route::middleware(['web', 'auth', 'creator.access'])->prefix('story')->name('api.story.')->group(function () {
Route::post('upload-image', [\App\Http\Controllers\StoryController::class, 'apiUploadImage'])->name('upload-image');
Route::post('upload-image', [\App\Http\Controllers\StoryController::class, 'apiUploadImage'])->middleware('forum.bot.protection:api_write')->name('upload-image');
Route::get('artworks', [\App\Http\Controllers\StoryController::class, 'apiArtworks'])->name('artworks');
});
Route::middleware(['web', 'auth', 'normalize.username'])->prefix('profile/cover')->name('api.profile.cover.')->group(function () {
Route::post('upload', [\App\Http\Controllers\User\ProfileCoverController::class, 'upload'])->middleware('throttle:20,1')->name('upload');
Route::post('position', [\App\Http\Controllers\User\ProfileCoverController::class, 'updatePosition'])->middleware('throttle:30,1')->name('position');
Route::delete('/', [\App\Http\Controllers\User\ProfileCoverController::class, 'destroy'])->middleware('throttle:20,1')->name('destroy');
Route::post('upload', [\App\Http\Controllers\User\ProfileCoverController::class, 'upload'])->middleware(['throttle:20,1', 'forum.bot.protection:profile_update'])->name('upload');
Route::post('position', [\App\Http\Controllers\User\ProfileCoverController::class, 'updatePosition'])->middleware(['throttle:30,1', 'forum.bot.protection:profile_update'])->name('position');
Route::delete('/', [\App\Http\Controllers\User\ProfileCoverController::class, 'destroy'])->middleware(['throttle:20,1', 'forum.bot.protection:profile_update'])->name('destroy');
});
// ── Per-artwork signal tracking (public) ────────────────────────────────────
@@ -38,14 +38,20 @@ Route::middleware(['web', 'throttle:300,1'])
Route::middleware(['web', 'throttle:5,10'])
->post('art/{id}/view', \App\Http\Controllers\Api\ArtworkViewController::class)
->middleware('forum.bot.protection:api_write')
->whereNumber('id')
->name('api.art.view');
Route::middleware(['web', 'throttle:10,1'])
->post('art/{id}/download', \App\Http\Controllers\Api\ArtworkDownloadController::class)
->middleware('forum.bot.protection:api_write')
->whereNumber('id')
->name('api.art.download');
Route::middleware(['web', 'throttle:reactions-read'])
->get('community/activity', [\App\Http\Controllers\Api\CommunityActivityController::class, 'index'])
->name('api.community.activity');
// ── Ranking lists (public, throttled, Redis-cached) ─────────────────────────
// GET /api/rank/global?type=trending|new_hot|best
// GET /api/rank/category/{id}?type=trending|new_hot|best
@@ -136,24 +142,25 @@ Route::middleware(['throttle:60,1'])
Route::middleware(['web', 'auth', 'normalize.username'])->prefix('artworks')->name('api.artworks.')->group(function () {
Route::post('/', [\App\Http\Controllers\Api\ArtworkController::class, 'store'])
->middleware('forum.bot.protection:api_write')
->name('store');
});
Route::middleware(['web', 'auth', 'normalize.username'])->prefix('uploads')->name('api.uploads.')->group(function () {
Route::post('init', [\App\Http\Controllers\Api\UploadController::class, 'init'])
->middleware('throttle:uploads-init')
->middleware(['throttle:uploads-init', 'forum.bot.protection:api_write'])
->name('init');
Route::post('preload', [\App\Http\Controllers\Api\UploadController::class, 'preload'])
->middleware('throttle:uploads-init')
->middleware(['throttle:uploads-init', 'forum.bot.protection:api_write'])
->name('preload');
Route::post('{id}/autosave', [\App\Http\Controllers\Api\UploadController::class, 'autosave'])
->middleware('throttle:uploads-finish')
->middleware(['throttle:uploads-finish', 'forum.bot.protection:api_write'])
->name('autosave');
Route::post('{id}/publish', [\App\Http\Controllers\Api\UploadController::class, 'publish'])
->middleware('throttle:uploads-finish')
->middleware(['throttle:uploads-finish', 'forum.bot.protection:api_write'])
->name('publish');
Route::get('{id}/status', [\App\Http\Controllers\Api\UploadController::class, 'processingStatus'])
@@ -161,15 +168,15 @@ Route::middleware(['web', 'auth', 'normalize.username'])->prefix('uploads')->nam
->name('processing-status');
Route::post('chunk', [\App\Http\Controllers\Api\UploadController::class, 'chunk'])
->middleware('throttle:uploads-init')
->middleware(['throttle:uploads-init', 'forum.bot.protection:api_write'])
->name('chunk');
Route::post('finish', [\App\Http\Controllers\Api\UploadController::class, 'finish'])
->middleware('throttle:uploads-finish')
->middleware(['throttle:uploads-finish', 'forum.bot.protection:api_write'])
->name('finish');
Route::post('cancel', [\App\Http\Controllers\Api\UploadController::class, 'cancel'])
->middleware('throttle:uploads-finish')
->middleware(['throttle:uploads-finish', 'forum.bot.protection:api_write'])
->name('cancel');
Route::get('status/{id}', [\App\Http\Controllers\Api\UploadController::class, 'status'])
@@ -204,6 +211,9 @@ Route::middleware(['web', 'auth', 'admin.moderation'])->prefix('admin/reports')-
Route::get('feed-performance', [\App\Http\Controllers\Api\Admin\FeedPerformanceReportController::class, 'index'])
->name('feed-performance');
Route::get('tags', [\App\Http\Controllers\Api\Admin\TagInteractionReportController::class, 'index'])
->name('tags');
});
Route::middleware(['web', 'auth', 'admin.moderation'])->prefix('admin/usernames')->name('api.admin.usernames.')->group(function () {
@@ -223,13 +233,17 @@ Route::post('analytics/similar-artworks', [\App\Http\Controllers\Api\SimilarArtw
->middleware('throttle:uploads-status')
->name('api.analytics.similar-artworks.store');
Route::middleware(['web'])->post('analytics/tags', [\App\Http\Controllers\Api\TagInteractionAnalyticsController::class, 'store'])
->middleware(['throttle:uploads-status', 'forum.bot.protection:api_write'])
->name('api.analytics.tags.store');
Route::middleware(['web', 'auth'])->post('analytics/feed', [\App\Http\Controllers\Api\FeedAnalyticsController::class, 'store'])
->middleware('throttle:uploads-status')
->middleware(['throttle:uploads-status', 'forum.bot.protection:api_write'])
->name('api.analytics.feed.store');
Route::middleware(['web', 'auth', 'normalize.username'])->prefix('discovery')->name('api.discovery.')->group(function () {
Route::post('events', [\App\Http\Controllers\Api\DiscoveryEventController::class, 'store'])
->middleware('throttle:uploads-status')
->middleware(['throttle:uploads-status', 'forum.bot.protection:api_write'])
->name('events.store');
});

View File

@@ -35,7 +35,7 @@ Route::middleware(['guest', 'normalize.username'])->group(function () {
->name('register.notice');
Route::post('register', [RegisteredUserController::class, 'store'])
->middleware(['throttle:register-ip', 'throttle:register-ip-daily']);
->middleware(['throttle:register-ip', 'throttle:register-ip-daily', 'forum.security.firewall:register', 'forum.bot.protection:register']);
Route::post('register/resend-verification', [RegisteredUserController::class, 'resendVerification'])
->middleware('throttle:register')
@@ -47,7 +47,8 @@ Route::middleware(['guest', 'normalize.username'])->group(function () {
Route::get('login', [AuthenticatedSessionController::class, 'create'])
->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']);
Route::post('login', [AuthenticatedSessionController::class, 'store'])
->middleware(['forum.security.firewall:login', 'forum.bot.protection:login']);
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');

View File

@@ -110,7 +110,26 @@ Schedule::command('nova:recalculate-rankings --sync-rank-scores')
->withoutOverlapping()
->runInBackground();
Schedule::command('forum:scan-posts')
Schedule::command('forum:ai-scan')
->everyTenMinutes()
->name('forum-scan-posts')
->withoutOverlapping();
->name('forum-ai-scan')
->withoutOverlapping()
->runInBackground();
Schedule::command('forum:bot-scan')
->everyFiveMinutes()
->name('forum-bot-scan')
->withoutOverlapping()
->runInBackground();
Schedule::command('forum:scan-posts --limit=250')
->everyFifteenMinutes()
->name('forum-post-scan')
->withoutOverlapping()
->runInBackground();
Schedule::command('forum:firewall-scan')
->everyFiveMinutes()
->name('forum-firewall-scan')
->withoutOverlapping()
->runInBackground();

View File

@@ -273,18 +273,18 @@ Route::middleware(['auth', 'normalize.username', 'ensure.onboarding.complete'])-
Route::match(['post', 'put'], '/profile/password', [ProfileController::class, 'password'])->name('profile.password');
Route::post('/avatar/upload', [AvatarController::class, 'upload'])->middleware('throttle:20,1')->name('avatar.upload');
Route::post('/settings/profile/update', [ProfileController::class, 'updateProfileSection'])->name('settings.profile.update');
Route::post('/settings/account/username', [ProfileController::class, 'updateUsername'])->name('settings.account.username');
Route::post('/settings/account/update', [ProfileController::class, 'updateAccountSection'])->name('settings.account.update');
Route::post('/settings/profile/update', [ProfileController::class, 'updateProfileSection'])->middleware('forum.bot.protection:profile_update')->name('settings.profile.update');
Route::post('/settings/account/username', [ProfileController::class, 'updateUsername'])->middleware('forum.bot.protection:profile_update')->name('settings.account.username');
Route::post('/settings/account/update', [ProfileController::class, 'updateAccountSection'])->middleware('forum.bot.protection:profile_update')->name('settings.account.update');
Route::post('/settings/email/request', [ProfileController::class, 'requestEmailChange'])
->middleware('throttle:email-change-request')
->name('settings.email.request');
Route::post('/settings/email/verify', [ProfileController::class, 'verifyEmailChange'])
->middleware('throttle:10,1')
->name('settings.email.verify');
Route::post('/settings/personal/update', [ProfileController::class, 'updatePersonalSection'])->name('settings.personal.update');
Route::post('/settings/notifications/update', [ProfileController::class, 'updateNotificationsSection'])->name('settings.notifications.update');
Route::post('/settings/security/password', [ProfileController::class, 'updateSecurityPassword'])->name('settings.security.password');
Route::post('/settings/personal/update', [ProfileController::class, 'updatePersonalSection'])->middleware('forum.bot.protection:profile_update')->name('settings.personal.update');
Route::post('/settings/notifications/update', [ProfileController::class, 'updateNotificationsSection'])->middleware('forum.bot.protection:profile_update')->name('settings.notifications.update');
Route::post('/settings/security/password', [ProfileController::class, 'updateSecurityPassword'])->middleware('forum.bot.protection:profile_update')->name('settings.security.password');
});
// ── UPLOAD ────────────────────────────────────────────────────────────────────
@@ -377,6 +377,10 @@ Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function ()
->middleware('admin.moderation')
->name('reports.queue');
Route::get('reports/tags', [\App\Http\Controllers\Admin\TagInteractionReportController::class, 'index'])
->middleware('admin.moderation')
->name('reports.tags');
Route::middleware('admin.moderation')->prefix('early-growth')->name('early-growth.')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\EarlyGrowthAdminController::class, 'index'])->name('index');
Route::delete('/cache', [\App\Http\Controllers\Admin\EarlyGrowthAdminController::class, 'flushCache'])->name('cache.flush');
@@ -411,6 +415,9 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('messages')->n
});
// ── COMMUNITY ACTIVITY ────────────────────────────────────────────────────────
Route::match(['get', 'post'], '/community/chat', [\App\Http\Controllers\Community\ChatController::class, 'index'])
->name('community.chat');
Route::get('/community/activity', [\App\Http\Controllers\Web\CommunityActivityController::class, 'index'])
->name('community.activity');
@@ -430,9 +437,11 @@ Route::get('/feed/search', [\App\Http\Controllers\Web\Posts\SearchFeedController
->name('feed.search');
// ── CONTENT BROWSER (artwork / category universal router) ─────────────────────
// Bind the artwork route parameter to the Artwork model by slug.
// Bind the artwork route parameter by slug when possible, but don't hard-fail.
// Some URLs that match this shape are actually nested category paths such as
// /skins/audio/blazemedia-pro, which should fall through to category handling.
Route::bind('artwork', function ($value) {
return Artwork::where('slug', $value)->firstOrFail();
return Artwork::where('slug', $value)->first();
});
Route::get('/{contentTypeSlug}/{categoryPath}/{artwork}', [BrowseGalleryController::class, 'showArtwork'])