feat: add captcha-backed forum security hardening
This commit is contained in:
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user