Forum: - TipTap WYSIWYG editor with full toolbar - @emoji-mart/react emoji picker (consistent with tweets) - @mention autocomplete with user search API - Fix PHP 8.4 parse errors in Blade templates - Fix thread data display (paginator items) - Align forum page widths to max-w-5xl Discover: - Extract shared _nav.blade.php partial - Add missing nav links to for-you page - Add Following link for authenticated users Feed/Posts: - Post model, controllers, policies, migrations - Feed page components (PostComposer, FeedCard, etc) - Post reactions, comments, saves, reports, sharing - Scheduled publishing support - Link preview controller Profile: - Profile page components (ProfileHero, ProfileTabs) - Profile API controller Uploads: - Upload wizard enhancements - Scheduled publish picker - Studio status bar and readiness checklist
573 lines
35 KiB
PHP
573 lines
35 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
// ── Per-artwork signal tracking (public) ────────────────────────────────────
|
|
// GET /api/art/{id}/similar → up to 12 similar artworks (Meilisearch)
|
|
// POST /api/art/{id}/view → record a view (session-deduped, 5 per 10 min)
|
|
// POST /api/art/{id}/download → record a download, returns file URL (10/min)
|
|
Route::middleware(['web', 'throttle:300,1'])
|
|
->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
|
|
*/
|
|
|
|
// ── Studio Pro API (authenticated) ─────────────────────────────────────────────
|
|
Route::middleware(['web', 'auth'])->prefix('studio')->name('api.studio.')->group(function () {
|
|
Route::get('artworks', [\App\Http\Controllers\Studio\StudioArtworksApiController::class, 'index'])->name('artworks.index');
|
|
Route::post('artworks/bulk', [\App\Http\Controllers\Studio\StudioArtworksApiController::class, 'bulk'])->name('artworks.bulk');
|
|
Route::put('artworks/{id}', [\App\Http\Controllers\Studio\StudioArtworksApiController::class, 'update'])->whereNumber('id')->name('artworks.update');
|
|
Route::post('artworks/{id}/toggle', [\App\Http\Controllers\Studio\StudioArtworksApiController::class, 'toggle'])->whereNumber('id')->name('artworks.toggle');
|
|
Route::get('artworks/{id}/analytics', [\App\Http\Controllers\Studio\StudioArtworksApiController::class, 'analytics'])->whereNumber('id')->name('artworks.analytics');
|
|
Route::post('artworks/{id}/replace-file', [\App\Http\Controllers\Studio\StudioArtworksApiController::class, 'replaceFile'])->whereNumber('id')->name('artworks.replaceFile');
|
|
// Versioning
|
|
Route::get('artworks/{id}/versions', [\App\Http\Controllers\Studio\StudioArtworksApiController::class, 'versions'])->whereNumber('id')->name('artworks.versions');
|
|
Route::post('artworks/{id}/restore/{version_id}', [\App\Http\Controllers\Studio\StudioArtworksApiController::class, 'restoreVersion'])->whereNumber('id')->whereNumber('version_id')->name('artworks.restoreVersion');
|
|
Route::get('tags/search', [\App\Http\Controllers\Studio\StudioArtworksApiController::class, 'searchTags'])->name('tags.search');
|
|
});
|
|
|
|
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');
|
|
|
|
// Synchronous Vision gateway call — returns suggested tags immediately for Step 2 pre-fill
|
|
Route::post('{id}/vision-suggest', \App\Http\Controllers\Api\UploadVisionSuggestController::class)
|
|
->middleware('throttle:60,1')
|
|
->name('vision-suggest');
|
|
});
|
|
|
|
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');
|
|
|
|
// ── Profile API (public, throttled) ─────────────────────────────────────────
|
|
// GET /api/profile/{username}/artworks?sort=latest|trending|rising|views|favs&cursor=...
|
|
// GET /api/profile/{username}/favourites?cursor=...
|
|
// GET /api/profile/{username}/stats
|
|
Route::middleware(['web', 'throttle:60,1'])
|
|
->prefix('profile/{username}')
|
|
->name('api.profile.')
|
|
->where(['username' => '[A-Za-z0-9_-]{3,20}'])
|
|
->group(function () {
|
|
Route::get('artworks', [\App\Http\Controllers\Api\ProfileApiController::class, 'artworks'])->name('artworks');
|
|
Route::get('favourites', [\App\Http\Controllers\Api\ProfileApiController::class, 'favourites'])->name('favourites');
|
|
Route::get('stats', [\App\Http\Controllers\Api\ProfileApiController::class, 'stats'])->name('stats');
|
|
});
|
|
|
|
// ── Link Preview (auth, throttled) ─────────────────────────────────────────────
|
|
// GET /api/link-preview?url=... → fetch OG tags for a URL
|
|
|
|
Route::middleware(['web', 'auth', 'throttle:30,1'])
|
|
->get('link-preview', \App\Http\Controllers\Api\LinkPreviewController::class)
|
|
->name('api.link-preview');
|
|
|
|
// ── Posts / Feed System ───────────────────────────────────────────────────────
|
|
// Public: profile feed (respects visibility)
|
|
// Auth : create/edit/delete posts, following feed, reactions, comments, reports, shares
|
|
|
|
Route::middleware(['web', 'throttle:60,1'])
|
|
->prefix('posts')
|
|
->name('api.posts.')
|
|
->group(function () {
|
|
// Profile feed (public, visibility-filtered)
|
|
Route::get('profile/{username}', [\App\Http\Controllers\Api\Posts\PostFeedController::class, 'profile'])
|
|
->where('username', '[A-Za-z0-9_-]{3,20}')
|
|
->name('profile');
|
|
|
|
// Per-post comments (public)
|
|
Route::get('{id}/comments', [\App\Http\Controllers\Api\Posts\PostCommentController::class, 'index'])
|
|
->whereNumber('id')
|
|
->name('comments.index');
|
|
});
|
|
|
|
Route::middleware(['web', 'auth', 'normalize.username'])
|
|
->prefix('posts')
|
|
->name('api.posts.')
|
|
->group(function () {
|
|
// Following feed (auth)
|
|
Route::get('following', [\App\Http\Controllers\Api\Posts\PostFeedController::class, 'following'])
|
|
->middleware('throttle:60,1')
|
|
->name('following');
|
|
|
|
// CRUD
|
|
Route::post('/', [\App\Http\Controllers\Api\Posts\PostController::class, 'store'])->name('store');
|
|
Route::patch('{id}', [\App\Http\Controllers\Api\Posts\PostController::class, 'update'])->whereNumber('id')->name('update');
|
|
Route::delete('{id}', [\App\Http\Controllers\Api\Posts\PostController::class, 'destroy'])->whereNumber('id')->name('destroy');
|
|
|
|
// Share artwork
|
|
Route::post('share/artwork/{artwork_id}', [\App\Http\Controllers\Api\Posts\PostShareController::class, 'shareArtwork'])
|
|
->whereNumber('artwork_id')
|
|
->middleware('throttle:30,1')
|
|
->name('share.artwork');
|
|
|
|
// Reactions
|
|
Route::post('{id}/reactions', [\App\Http\Controllers\Api\Posts\PostReactionController::class, 'store'])
|
|
->whereNumber('id')
|
|
->middleware('throttle:60,1')
|
|
->name('reactions.store');
|
|
Route::delete('{id}/reactions/{reaction}', [\App\Http\Controllers\Api\Posts\PostReactionController::class, 'destroy'])
|
|
->whereNumber('id')
|
|
->middleware('throttle:60,1')
|
|
->name('reactions.destroy');
|
|
|
|
// Comments
|
|
Route::post('{id}/comments', [\App\Http\Controllers\Api\Posts\PostCommentController::class, 'store'])->whereNumber('id')->name('comments.store');
|
|
Route::delete('{id}/comments/{comment_id}', [\App\Http\Controllers\Api\Posts\PostCommentController::class, 'destroy'])->whereNumber(['id', 'comment_id'])->name('comments.destroy');
|
|
|
|
// Reports
|
|
Route::post('{id}/report', [\App\Http\Controllers\Api\Posts\PostReportController::class, 'store'])
|
|
->whereNumber('id')
|
|
->middleware('throttle:10,1')
|
|
->name('report');
|
|
|
|
// ── Feed 2.0 ───────────────────────────────────────────────────────
|
|
|
|
// Pinned posts
|
|
Route::post('{id}/pin', [\App\Http\Controllers\Api\Posts\PostPinController::class, 'pin'])->whereNumber('id')->name('pin');
|
|
Route::delete('{id}/pin', [\App\Http\Controllers\Api\Posts\PostPinController::class, 'unpin'])->whereNumber('id')->name('unpin');
|
|
|
|
// Saves / bookmarks
|
|
Route::post('{id}/save', [\App\Http\Controllers\Api\Posts\PostSaveController::class, 'save'])->whereNumber('id')->middleware('throttle:60,1')->name('save');
|
|
Route::delete('{id}/save', [\App\Http\Controllers\Api\Posts\PostSaveController::class, 'unsave'])->whereNumber('id')->middleware('throttle:60,1')->name('unsave');
|
|
Route::get('saved', [\App\Http\Controllers\Api\Posts\PostSaveController::class, 'index'])->middleware('throttle:60,1')->name('saved');
|
|
|
|
// Analytics
|
|
Route::post('{id}/impression', [\App\Http\Controllers\Api\Posts\PostAnalyticsController::class, 'impression'])->whereNumber('id')->middleware('throttle:120,1')->name('impression');
|
|
Route::get('{id}/analytics', [\App\Http\Controllers\Api\Posts\PostAnalyticsController::class, 'show'])->whereNumber('id')->name('analytics');
|
|
|
|
// Comment highlight
|
|
Route::post('{post_id}/comments/{comment_id}/highlight', [\App\Http\Controllers\Api\Posts\PostCommentHighlightController::class, 'highlight'])->whereNumber(['post_id','comment_id'])->name('comments.highlight');
|
|
Route::delete('{post_id}/comments/{comment_id}/highlight', [\App\Http\Controllers\Api\Posts\PostCommentHighlightController::class, 'unhighlight'])->whereNumber(['post_id','comment_id'])->name('comments.unhighlight');
|
|
});
|
|
|
|
// ── Feed 2.0: Trending + Hashtag + Search (public) ───────────────────────────
|
|
Route::middleware(['web', 'throttle:60,1'])
|
|
->prefix('feed')
|
|
->name('api.feed.')
|
|
->group(function () {
|
|
Route::get('trending', [\App\Http\Controllers\Api\Posts\PostTrendingFeedController::class, 'trending'])->name('trending');
|
|
Route::get('hashtag/{tag}', [\App\Http\Controllers\Api\Posts\PostTrendingFeedController::class, 'hashtag'])->name('hashtag');
|
|
Route::get('hashtags/trending', [\App\Http\Controllers\Api\Posts\PostTrendingFeedController::class, 'trendingHashtags'])->name('hashtags.trending');
|
|
Route::get('search', [\App\Http\Controllers\Api\Posts\PostSearchController::class, 'search'])->name('search');
|
|
});
|
|
|
|
// ── Notifications (digest) ────────────────────────────────────────────────────
|
|
Route::middleware(['web', 'auth'])
|
|
->prefix('notifications')
|
|
->name('api.notifications.')
|
|
->group(function () {
|
|
Route::get('/', [\App\Http\Controllers\Api\NotificationController::class, 'index'])->middleware('throttle:30,1')->name('index');
|
|
Route::post('read-all', [\App\Http\Controllers\Api\NotificationController::class, 'readAll'])->name('read-all');
|
|
Route::post('{id}/read', [\App\Http\Controllers\Api\NotificationController::class, 'markRead'])->name('mark-read');
|
|
});
|
|
|
|
// ── Artwork search for share modal (public, throttled) ────────────────────────
|
|
// GET /api/search/artworks?q=...&shareable=1 → reuses existing ArtworkSearchController
|