get('art/{id}/similar', \App\Http\Controllers\Api\SimilarArtworksController::class) ->whereNumber('id') ->name('api.art.similar'); Route::middleware(['web', 'throttle:5,10']) ->post('art/{id}/view', \App\Http\Controllers\Api\ArtworkViewController::class) ->whereNumber('id') ->name('api.art.view'); Route::middleware(['web', 'throttle:10,1']) ->post('art/{id}/download', \App\Http\Controllers\Api\ArtworkDownloadController::class) ->whereNumber('id') ->name('api.art.download'); // ── 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 // GET /api/rank/type/{contentType}?type=trending|new_hot|best Route::prefix('rank')->name('api.rank.')->middleware(['throttle:60,1'])->group(function () { Route::get('global', [\App\Http\Controllers\Api\RankController::class, 'global']) ->name('global'); Route::get('category/{id}', [\App\Http\Controllers\Api\RankController::class, 'byCategory']) ->whereNumber('id') ->name('category'); Route::get('type/{contentType}', [\App\Http\Controllers\Api\RankController::class, 'byContentType']) ->where('contentType', '[a-z0-9\-]+') ->name('content_type'); }); /** * API v1 routes for Artworks module * * GET /api/v1/artworks/{slug} * GET /api/v1/categories/{slug}/artworks */ Route::prefix('v1')->name('api.v1.')->group(function () { // Public browse feed (authoritative tables only) Route::get('browse', [\App\Http\Controllers\Api\BrowseController::class, 'index']) ->name('browse'); // Browse by content type + category path (slug-based) Route::get('browse/{contentTypeSlug}/{categoryPath}', [\App\Http\Controllers\Api\BrowseController::class, 'byCategoryPath']) ->where('contentTypeSlug', '[a-z0-9\-]+') ->where('categoryPath', '.+') ->name('browse.category'); // Browse by content type only (slug-based) Route::get('browse/{contentTypeSlug}', [\App\Http\Controllers\Api\BrowseController::class, 'byContentType']) ->where('contentTypeSlug', '[a-z0-9\-]+') ->name('browse.content_type'); // Public artwork by slug Route::get('artworks/{slug}', [\App\Http\Controllers\Api\ArtworkController::class, 'show']) ->where('slug', '[A-Za-z0-9\-]+') ->name('artworks.show'); // Category artworks (Category route-model binding uses slug) Route::get('categories/{category}/artworks', [\App\Http\Controllers\Api\ArtworkController::class, 'categoryArtworks']) ->name('categories.artworks'); // Personalized feed (auth required) Route::middleware(['web', 'auth'])->get('feed', [\App\Http\Controllers\Api\FeedController::class, 'index']) ->name('feed'); }); Route::middleware(['web', 'normalize.username', 'throttle:30,1']) ->get('username/availability', \App\Http\Controllers\Api\UsernameAvailabilityController::class) ->name('api.username.availability'); // Artwork navigation — prev/next neighbors for the fullscreen viewer Route::middleware(['throttle:60,1']) ->get('artworks/navigation/{id}', [\App\Http\Controllers\Api\ArtworkNavigationController::class, 'neighbors']) ->where('id', '[0-9]+') ->name('api.artworks.navigation'); // Artwork page data by ID — for client-side (no-reload) navigation Route::middleware(['throttle:60,1']) ->get('artworks/{id}/page', [\App\Http\Controllers\Api\ArtworkNavigationController::class, 'pageData']) ->where('id', '[0-9]+') ->name('api.artworks.page-data'); Route::middleware(['web', 'auth', 'normalize.username'])->prefix('artworks')->name('api.artworks.')->group(function () { Route::post('/', [\App\Http\Controllers\Api\ArtworkController::class, 'store']) ->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') ->name('init'); Route::post('preload', [\App\Http\Controllers\Api\UploadController::class, 'preload']) ->middleware('throttle:uploads-init') ->name('preload'); Route::post('{id}/autosave', [\App\Http\Controllers\Api\UploadController::class, 'autosave']) ->middleware('throttle:uploads-finish') ->name('autosave'); Route::post('{id}/publish', [\App\Http\Controllers\Api\UploadController::class, 'publish']) ->middleware('throttle:uploads-finish') ->name('publish'); Route::get('{id}/status', [\App\Http\Controllers\Api\UploadController::class, 'processingStatus']) ->middleware('throttle:uploads-status') ->name('processing-status'); Route::post('chunk', [\App\Http\Controllers\Api\UploadController::class, 'chunk']) ->middleware('throttle:uploads-init') ->name('chunk'); Route::post('finish', [\App\Http\Controllers\Api\UploadController::class, 'finish']) ->middleware('throttle:uploads-finish') ->name('finish'); Route::post('cancel', [\App\Http\Controllers\Api\UploadController::class, 'cancel']) ->middleware('throttle:uploads-finish') ->name('cancel'); Route::get('status/{id}', [\App\Http\Controllers\Api\UploadController::class, 'status']) ->middleware('throttle:uploads-status') ->name('status'); }); Route::middleware(['web', 'auth', 'admin.moderation'])->prefix('admin/uploads')->name('api.admin.uploads.')->group(function () { Route::get('pending', [\App\Http\Controllers\Api\Admin\UploadModerationController::class, 'pending']) ->name('pending'); Route::post('{id}/approve', [\App\Http\Controllers\Api\Admin\UploadModerationController::class, 'approve']) ->whereUuid('id') ->name('approve'); Route::post('{id}/reject', [\App\Http\Controllers\Api\Admin\UploadModerationController::class, 'reject']) ->whereUuid('id') ->name('reject'); }); Route::middleware(['web', 'auth', 'admin.moderation'])->prefix('admin/reports')->name('api.admin.reports.')->group(function () { Route::get('queue', [\App\Http\Controllers\Api\Admin\ModerationReportQueueController::class, 'index']) ->name('queue'); Route::get('similar-artworks', [\App\Http\Controllers\Api\Admin\SimilarArtworkReportController::class, 'index']) ->name('similar-artworks'); Route::get('feed-performance', [\App\Http\Controllers\Api\Admin\FeedPerformanceReportController::class, 'index']) ->name('feed-performance'); }); Route::middleware(['web', 'auth', 'admin.moderation'])->prefix('admin/usernames')->name('api.admin.usernames.')->group(function () { Route::get('pending', [\App\Http\Controllers\Api\Admin\UsernameApprovalController::class, 'pending']) ->name('pending'); Route::post('{id}/approve', [\App\Http\Controllers\Api\Admin\UsernameApprovalController::class, 'approve']) ->whereNumber('id') ->name('approve'); Route::post('{id}/reject', [\App\Http\Controllers\Api\Admin\UsernameApprovalController::class, 'reject']) ->whereNumber('id') ->name('reject'); }); Route::post('analytics/similar-artworks', [\App\Http\Controllers\Api\SimilarArtworkAnalyticsController::class, 'store']) ->middleware('throttle:uploads-status') ->name('api.analytics.similar-artworks.store'); Route::middleware(['web', 'auth'])->post('analytics/feed', [\App\Http\Controllers\Api\FeedAnalyticsController::class, 'store']) ->middleware('throttle:uploads-status') ->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') ->name('events.store'); }); // ─── Artwork Search (Meilisearch-powered, public) ──────────────────────────── Route::prefix('search')->name('api.search.')->middleware(['web', 'throttle:60,1'])->group(function () { Route::get('artworks', [\App\Http\Controllers\Api\Search\ArtworkSearchController::class, 'index']) ->name('artworks'); Route::get('artworks/tag/{slug}', [\App\Http\Controllers\Api\Search\ArtworkSearchController::class, 'byTag']) ->where('slug', '[a-z0-9\-]+') ->name('artworks.tag'); Route::get('artworks/category/{cat}', [\App\Http\Controllers\Api\Search\ArtworkSearchController::class, 'byCategory']) ->where('cat', '[a-z0-9\-]+') ->name('artworks.category'); Route::get('artworks/related/{id}', [\App\Http\Controllers\Api\Search\ArtworkSearchController::class, 'related']) ->whereNumber('id') ->name('artworks.related'); }); // Tag search/popular: public endpoints (used by SearchBar for all visitors) Route::middleware(['web', 'throttle:60,1'])->prefix('tags')->name('api.tags.')->group(function () { Route::get('search', [\App\Http\Controllers\Api\TagController::class, 'search'])->name('search'); Route::get('popular', [\App\Http\Controllers\Api\TagController::class, 'popular'])->name('popular'); }); // User/creator search (public, supports @mention prefix) Route::middleware(['web', 'throttle:60,1']) ->get('search/users', \App\Http\Controllers\Api\Search\UserSearchController::class) ->name('api.search.users'); Route::middleware(['web', 'auth', 'normalize.username'])->prefix('artworks')->name('api.artworks.tags.')->group(function () { Route::get('{id}/tags', [\App\Http\Controllers\Api\ArtworkTagController::class, 'index'])->whereNumber('id')->name('index'); Route::post('{id}/tags', [\App\Http\Controllers\Api\ArtworkTagController::class, 'store'])->whereNumber('id')->name('store'); Route::put('{id}/tags', [\App\Http\Controllers\Api\ArtworkTagController::class, 'update'])->whereNumber('id')->name('update'); Route::delete('{id}/tags/{tag}', [\App\Http\Controllers\Api\ArtworkTagController::class, 'destroy'])->whereNumber('id')->name('destroy'); }); // Artwork Awards Route::middleware(['web', 'auth', 'normalize.username', 'throttle:20,1']) ->prefix('artworks') ->name('api.artworks.awards.') ->group(function () { Route::post('{id}/award', [\App\Http\Controllers\Api\ArtworkAwardController::class, 'store']) ->whereNumber('id')->name('store'); Route::put('{id}/award', [\App\Http\Controllers\Api\ArtworkAwardController::class, 'update']) ->whereNumber('id')->name('update'); Route::delete('{id}/award', [\App\Http\Controllers\Api\ArtworkAwardController::class, 'destroy']) ->whereNumber('id')->name('destroy'); }); // ── Latest Comments feed ────────────────────────────────────────────────────── // GET /api/comments/latest?type=all|following|mine&page=N Route::middleware(['web', 'throttle:60,1']) ->get('comments/latest', [\App\Http\Controllers\Api\LatestCommentsApiController::class, 'index']) ->name('api.comments.latest'); Route::middleware(['web']) ->prefix('artworks') ->name('api.artworks.awards.show.') ->group(function () { Route::get('{id}/awards', [\App\Http\Controllers\Api\ArtworkAwardController::class, 'show'])->whereNumber('id')->name('show'); }); Route::middleware(['web', 'auth', 'normalize.username'])->group(function () { Route::post('artworks/{id}/favorite', [\App\Http\Controllers\Api\ArtworkInteractionController::class, 'favorite']) ->whereNumber('id') ->name('api.artworks.favorite'); Route::post('artworks/{id}/like', [\App\Http\Controllers\Api\ArtworkInteractionController::class, 'like']) ->whereNumber('id') ->name('api.artworks.like'); Route::post('artworks/{id}/report', [\App\Http\Controllers\Api\ArtworkInteractionController::class, 'report']) ->whereNumber('id') ->name('api.artworks.report'); Route::post('users/{id}/follow', [\App\Http\Controllers\Api\ArtworkInteractionController::class, 'follow']) ->whereNumber('id') ->name('api.users.follow'); }); // ── Share tracking (public, throttled) ──────────────────────────────────────── // POST /api/artworks/{id}/share → record a share event Route::middleware(['web', 'throttle:30,1']) ->post('artworks/{id}/share', [\App\Http\Controllers\Api\ArtworkInteractionController::class, 'share']) ->whereNumber('id') ->name('api.artworks.share'); // ── Comment CRUD ────────────────────────────────────────────────────────────── // GET /api/artworks/{id}/comments list comments (public) // POST /api/artworks/{id}/comments post a comment (auth) // PUT /api/artworks/{id}/comments/{commentId} edit own comment (auth) // DELETE /api/artworks/{id}/comments/{commentId} delete own/admin (auth) Route::middleware(['web', 'throttle:60,1']) ->get('artworks/{id}/comments', [\App\Http\Controllers\Api\ArtworkCommentController::class, 'index']) ->whereNumber('id') ->name('api.artworks.comments.index'); Route::middleware(['web', 'auth', 'normalize.username', 'throttle:30,1'])->group(function () { Route::post('artworks/{id}/comments', [\App\Http\Controllers\Api\ArtworkCommentController::class, 'store']) ->whereNumber('id') ->name('api.artworks.comments.store'); Route::put('artworks/{id}/comments/{commentId}', [\App\Http\Controllers\Api\ArtworkCommentController::class, 'update']) ->whereNumber(['id', 'commentId']) ->name('api.artworks.comments.update'); Route::delete('artworks/{id}/comments/{commentId}', [\App\Http\Controllers\Api\ArtworkCommentController::class, 'destroy']) ->whereNumber(['id', 'commentId']) ->name('api.artworks.comments.destroy'); }); // ── Reactions ───────────────────────────────────────────────────────────────── // GET /api/artworks/{id}/reactions list artwork reaction totals (public) // POST /api/artworks/{id}/reactions toggle artwork reaction (auth) // GET /api/comments/{id}/reactions list comment reaction totals (public) // POST /api/comments/{id}/reactions toggle comment reaction (auth) Route::middleware(['web', 'throttle:60,1'])->group(function () { Route::get('artworks/{id}/reactions', [\App\Http\Controllers\Api\ReactionController::class, 'artworkReactions']) ->whereNumber('id') ->name('api.artworks.reactions.index'); Route::get('comments/{id}/reactions', [\App\Http\Controllers\Api\ReactionController::class, 'commentReactions']) ->whereNumber('id') ->name('api.comments.reactions.index'); }); Route::middleware(['web', 'auth', 'normalize.username', 'throttle:60,1'])->group(function () { Route::post('artworks/{id}/reactions', [\App\Http\Controllers\Api\ReactionController::class, 'toggleArtworkReaction']) ->whereNumber('id') ->name('api.artworks.reactions.toggle'); Route::post('comments/{id}/reactions', [\App\Http\Controllers\Api\ReactionController::class, 'toggleCommentReaction']) ->whereNumber('id') ->name('api.comments.reactions.toggle'); }); // ── Personalised suggestions (auth required) ──────────────────────────────── // GET /api/user/suggestions/creators → up to 12 suggested creators to follow // GET /api/user/suggestions/tags → up to 20 suggested tags (foundation) Route::middleware(['web', 'auth', 'normalize.username', 'throttle:30,1']) ->prefix('user/suggestions') ->name('api.user.suggestions.') ->group(function () { Route::get('creators', \App\Http\Controllers\Api\SuggestedCreatorsController::class) ->name('creators'); Route::get('tags', \App\Http\Controllers\Api\SuggestedTagsController::class) ->name('tags'); }); // ── Follow system ───────────────────────────────────────────────────────────── // POST /api/user/{username}/follow → follow a user // DELETE /api/user/{username}/follow → unfollow a user // GET /api/user/{username}/followers → paginated followers (public) // GET /api/user/{username}/following → paginated following (public) Route::middleware(['web', 'throttle:60,1']) ->prefix('user') ->name('api.user.follow.') ->group(function () { // Public: list followers / following Route::get('{username}/followers', [\App\Http\Controllers\Api\FollowController::class, 'followers']) ->where('username', '[A-Za-z0-9_-]{3,20}') ->name('followers'); Route::get('{username}/following', [\App\Http\Controllers\Api\FollowController::class, 'following']) ->where('username', '[A-Za-z0-9_-]{3,20}') ->name('following'); // Auth-required: follow / unfollow Route::middleware(['auth', 'normalize.username'])->group(function () { Route::post('{username}/follow', [\App\Http\Controllers\Api\FollowController::class, 'follow']) ->where('username', '[A-Za-z0-9_-]{3,20}') ->name('follow'); Route::delete('{username}/follow', [\App\Http\Controllers\Api\FollowController::class, 'unfollow']) ->where('username', '[A-Za-z0-9_-]{3,20}') ->name('unfollow'); }); }); // ── Messaging ──────────────────────────────────────────────────────────────── // GET /api/messages/conversations → list conversations // POST /api/messages/conversation → create conversation // GET /api/messages/conversation/{id} → show conversation // GET /api/messages/{conversation_id} → paginated messages // POST /api/messages/{conversation_id} → send message // POST /api/messages/{conversation_id}/read → mark as read // POST /api/messages/{conversation_id}/archive → toggle archive // POST /api/messages/{conversation_id}/mute → toggle mute // DELETE /api/messages/{conversation_id}/leave → leave conversation // POST /api/messages/{conversation_id}/add-user → add user (admin) // DELETE /api/messages/{conversation_id}/remove-user → remove user (admin) // POST /api/messages/{conversation_id}/rename → rename group (admin) // POST /api/messages/{conversation_id}/{message_id}/react → add reaction // DELETE /api/messages/{conversation_id}/{message_id}/react → remove reaction // DELETE /api/messages/message/{message_id} → soft-delete message Route::middleware(['web', 'auth', 'normalize.username', 'throttle:60,1']) ->prefix('messages') ->name('api.messages.') ->group(function () { Route::get('settings', [\App\Http\Controllers\Api\Messaging\MessagingSettingsController::class, 'show'])->name('settings.show'); Route::patch('settings', [\App\Http\Controllers\Api\Messaging\MessagingSettingsController::class, 'update'])->name('settings.update'); Route::get('conversations', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'index'])->name('conversations.index'); Route::post('conversation', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'store'])->middleware('throttle:messages-send')->name('conversations.store'); Route::get('conversation/{id}', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'show'])->whereNumber('id')->name('conversations.show'); Route::post('{conversation_id}/read', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'markRead'])->whereNumber('conversation_id')->name('read'); Route::post('{conversation_id}/archive', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'archive'])->whereNumber('conversation_id')->name('archive'); Route::post('{conversation_id}/mute', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'mute'])->whereNumber('conversation_id')->name('mute'); Route::post('{conversation_id}/pin', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'pin'])->whereNumber('conversation_id')->name('pin'); Route::post('{conversation_id}/unpin', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'unpin'])->whereNumber('conversation_id')->name('unpin'); Route::delete('{conversation_id}/leave', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'leave'])->whereNumber('conversation_id')->name('leave'); Route::post('{conversation_id}/add-user', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'addUser'])->whereNumber('conversation_id')->name('add-user'); Route::delete('{conversation_id}/remove-user', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'removeUser'])->whereNumber('conversation_id')->name('remove-user'); Route::post('{conversation_id}/rename', [\App\Http\Controllers\Api\Messaging\ConversationController::class, 'rename'])->whereNumber('conversation_id')->name('rename'); Route::get('search', [\App\Http\Controllers\Api\Messaging\MessageSearchController::class, 'index'])->name('search.index'); Route::post('search/rebuild', [\App\Http\Controllers\Api\Messaging\MessageSearchController::class, 'rebuild'])->name('search.rebuild'); Route::get('{conversation_id}', [\App\Http\Controllers\Api\Messaging\MessageController::class, 'index'])->whereNumber('conversation_id')->name('messages.index'); Route::post('{conversation_id}', [\App\Http\Controllers\Api\Messaging\MessageController::class, 'store'])->middleware('throttle:messages-send')->whereNumber('conversation_id')->name('messages.store'); Route::post('{conversation_id}/typing', [\App\Http\Controllers\Api\Messaging\TypingController::class, 'start'])->whereNumber('conversation_id')->name('typing.start'); Route::post('{conversation_id}/typing/stop', [\App\Http\Controllers\Api\Messaging\TypingController::class, 'stop'])->whereNumber('conversation_id')->name('typing.stop'); Route::get('{conversation_id}/typing', [\App\Http\Controllers\Api\Messaging\TypingController::class, 'index'])->whereNumber('conversation_id')->name('typing.index'); Route::post('{conversation_id}/{message_id}/react', [\App\Http\Controllers\Api\Messaging\MessageController::class, 'react'])->whereNumber(['conversation_id', 'message_id'])->name('react'); Route::delete('{conversation_id}/{message_id}/react', [\App\Http\Controllers\Api\Messaging\MessageController::class, 'unreact'])->whereNumber(['conversation_id', 'message_id'])->name('unreact'); Route::post('{message_id}/reactions', [\App\Http\Controllers\Api\Messaging\MessageController::class, 'reactByMessage']) ->middleware('throttle:messages-react') ->whereNumber('message_id') ->name('messages.reactions.toggle'); Route::delete('{message_id}/reactions', [\App\Http\Controllers\Api\Messaging\MessageController::class, 'unreactByMessage']) ->middleware('throttle:messages-react') ->whereNumber('message_id') ->name('messages.reactions.delete'); Route::patch('message/{message_id}', [\App\Http\Controllers\Api\Messaging\MessageController::class, 'update'])->whereNumber('message_id')->name('messages.update'); Route::delete('message/{message_id}', [\App\Http\Controllers\Api\Messaging\MessageController::class, 'destroy'])->whereNumber('message_id')->name('messages.destroy'); }); Route::middleware(['web', 'auth', 'normalize.username', 'throttle:60,1']) ->post('reports', [\App\Http\Controllers\Api\ReportController::class, 'store']) ->name('api.reports.store');