From 9c537e08d08197ebd4895ffed51c24ed97bd2f09 Mon Sep 17 00:00:00 2001 From: test Date: Sun, 20 Sep 2026 14:51:04 +0200 Subject: [PATCH] Wire new routes and environment keys for the recent modules. Register AdSense, artwork review, comment captcha, lazy collections, and upload review props, and document the matching env vars and CLI commands. --- .env.example | 37 +++++++++++++++++++++++++++++++++++++ docs/cli-reference.md | 2 ++ routes/api.php | 15 ++++++++++++++- routes/web.php | 22 ++++++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index bc8108fd..2768b02a 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,9 @@ APP_KEY= APP_DEBUG=true APP_URL=http://localhost +# Maximum secondary artwork categories in addition to the primary category. +CATEGORIES_MAX_SECONDARY=5 + # Artwork original downloads: PHP fallback unless nginx X-Accel is configured. # Leave false until the internal location exists and has been verified. DOWNLOAD_ACCEL_ENABLED=false @@ -71,6 +74,26 @@ SKINBASE_SESSION_DEBUG_HEADER=false BROADCAST_CONNECTION=reverb FILESYSTEM_DISK=local QUEUE_CONNECTION=redis + +# Comment spam protection: observe records scores without changing visibility. +COMMENT_SPAM_ENABLED=true +COMMENT_SPAM_MODE=enforce +COMMENT_SPAM_AI_ENABLED=true +COMMENT_SPAM_AI_PROVIDER=together +# Supported: Prism-ML/Ternary-Bonsai-27B or meta-llama/Llama-3.2-3B-Instruct-Turbo +COMMENT_SPAM_AI_MODEL=meta-llama/Llama-3.2-3B-Instruct-Turbo +COMMENT_SPAM_AI_TIMEOUT=5 +TOGETHER_API_KEY= +TOGETHER_API_BASE_URL=https://api.together.xyz/v1 +COMMENT_SPAM_LOCAL_SAFE_MAX=29 +COMMENT_SPAM_LOCAL_SPAM_MIN=70 +COMMENT_SPAM_AI_SPAM_MIN=70 +COMMENT_SPAM_SIGNATURE_CACHE_MINUTES=1440 +COMMENT_TURNSTILE_ENABLED=false +COMMENT_TURNSTILE_SITE_KEY= +COMMENT_TURNSTILE_SECRET_KEY= +COMMENT_TURNSTILE_RISK_THRESHOLD=40 +COMMENT_TURNSTILE_FAIL_OPEN=false # Must exceed Horizon supervisor-default timeout (960s). Production used 90s # and nightly RecComputeSimilar* jobs failed with MaxAttemptsExceeded. REDIS_QUEUE_RETRY_AFTER=1080 @@ -133,6 +156,13 @@ UPLOAD_CHUNK_REQUEST_TIMEOUT_MS=45000 UPLOAD_RATE_CHUNK_USER=180 UPLOAD_RATE_CHUNK_IP=360 +# New accounts without this many moderator/legacy published artworks go to review. +# Queued uploads and trusted_auto publishes do not count, so a new user's 6th upload stays hidden. +UPLOAD_MIN_APPROVED_UPLOADS=5 +UPLOAD_MIN_ACCOUNT_AGE_DAYS=7 +UPLOAD_MAX_UNTRUSTED_LEVEL=1 +UPLOAD_BOT_RISK_REVIEW_THRESHOLD=40 + # Draft abuse prevention controls SKINBASE_MAX_DRAFTS=10 SKINBASE_MAX_DRAFT_STORAGE_MB=1024 @@ -445,6 +475,13 @@ GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_REDIRECT_URI=/auth/google/callback +# Google AdSense Management API (admin analytics, read-only) +# Redirect URI must match the Google Cloud OAuth client exactly. +ADSENSE_CLIENT_ID= +ADSENSE_CLIENT_SECRET= +ADSENSE_REDIRECT_URI=https://skinbase.org/admin/adsense/oauth/callback +ADSENSE_SYNC_ENABLED=true + # Optional light theme feature # Set LIGHT_THEME_ENABLED=true to allow a light theme in the client-side toggle. # Set LIGHT_THEME_SHOW_SWITCH=true to display the theme switch in the toolbar. diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 314acac4..17a63ebf 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -48,6 +48,7 @@ Examples below are representative. For the full option list of any Artisan comma | `analytics:compare-feed-ab` | Compare baseline versus candidate feed algorithms over a date window | `php artisan analytics:compare-feed-ab baseline_v1 candidate_v2 --from=2026-04-01 --to=2026-04-16` | | `analytics:evaluate-feed-weights` | Run offline feed-weight evaluation against stored daily metrics | `php artisan analytics:evaluate-feed-weights --algo=v2 --from=2026-04-01 --to=2026-04-16` | | `analytics:seed-tag-interaction-demo` | Seed demo analytics data for local dashboards and ranking experiments | `php artisan analytics:seed-tag-interaction-demo --days=7 --per-day=100` | +| `adsense:sync` | Synchronize Google AdSense entities and daily reporting into the admin analytics tables | `php artisan adsense:sync --days=3` | ### AI Biography @@ -79,6 +80,7 @@ Examples below are representative. For the full option list of any Artisan comma | `artworks:check-user-ids` | Verify that every `artworks.user_id` exists in `users` | `php artisan artworks:check-user-ids --show-missing=50` | | `artworks:check-user-refs` | Full artwork-user reference audit with optional legacy-user recovery helpers | `php artisan artworks:check-user-refs --show-missing=50 --json` | | `artworks:embeddings-backfill` | Queue resumable CLIP embedding backfill jobs for artworks | `php artisan artworks:embeddings-backfill --after-id=0 --batch=200` | +| `artworks:backfill-primary-category` | Report and backfill `artworks.primary_category_id` from unique pivot memberships | `php artisan artworks:backfill-primary-category --report --apply` | | `artworks:generate-missing-sq-thumbs` | Generate missing smart square thumbnails | `php artisan artworks:generate-missing-sq-thumbs --limit=200 --dry-run` | | `artworks:import-hashes` | Import artwork hash and extension metadata from a CSV file | `php artisan artworks:import-hashes artworks_hash_skinbase.csv --start=0 --limit=1000` | | `artworks:normalize-slugs` | Normalize stored artwork slugs from titles without enforcing uniqueness | `php artisan artworks:normalize-slugs --only-mismatched --dry-run` | diff --git a/routes/api.php b/routes/api.php index 1b7c257a..2b4c241b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -486,6 +486,11 @@ Route::middleware(['web', 'auth', 'admin.moderation'])->prefix('admin/uploads')- Route::get('pending', [\App\Http\Controllers\Api\Admin\UploadModerationController::class, 'pending']) ->name('pending'); + Route::get('{id}/preview', [\App\Http\Controllers\Api\Admin\UploadModerationController::class, 'preview']) + ->middleware('signed') + ->whereUuid('id') + ->name('preview'); + Route::post('{id}/approve', [\App\Http\Controllers\Api\Admin\UploadModerationController::class, 'approve']) ->whereUuid('id') ->name('approve'); @@ -495,6 +500,12 @@ Route::middleware(['web', 'auth', 'admin.moderation'])->prefix('admin/uploads')- ->name('reject'); }); +Route::middleware(['web', 'auth', 'admin.moderation'])->prefix('admin/artwork-review')->name('api.admin.artwork-review.')->group(function () { + Route::get('pending', [\App\Http\Controllers\Api\Admin\ArtworkModerationController::class, 'pending'])->name('pending'); + Route::post('{id}/approve', [\App\Http\Controllers\Api\Admin\ArtworkModerationController::class, 'approve'])->whereNumber('id')->name('approve'); + Route::post('{id}/reject', [\App\Http\Controllers\Api\Admin\ArtworkModerationController::class, 'reject'])->whereNumber('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'); @@ -713,7 +724,7 @@ Route::middleware(['web', 'throttle:60,1']) ->whereNumber('id') ->name('api.artworks.comments.index'); -Route::middleware(['web', 'auth', 'verified', 'normalize.username', 'throttle:20,1'])->group(function () { +Route::middleware(['web', 'auth', 'verified', 'normalize.username', 'throttle:20,1', 'comment.captcha', 'forum.spam.detection:artwork_comment'])->group(function () { Route::post('artworks/{id}/comments', [\App\Http\Controllers\Api\ArtworkCommentController::class, 'store']) ->whereNumber('id') ->name('api.artworks.comments.store'); @@ -901,6 +912,7 @@ Route::middleware(['web', 'auth', 'normalize.username', 'throttle:60,1']) // ── 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}/collections // GET /api/profile/{username}/stats Route::middleware(['web', 'throttle:60,1']) ->prefix('profile/{username}') @@ -909,6 +921,7 @@ Route::middleware(['web', 'throttle:60,1']) ->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('collections', [\App\Http\Controllers\Api\ProfileApiController::class, 'collections'])->name('collections'); Route::get('stats', [\App\Http\Controllers\Api\ProfileApiController::class, 'stats'])->name('stats'); Route::get('featured-artworks', [\App\Http\Controllers\User\ProfileController::class, 'featuredArtworks'])->name('featured-artworks'); Route::get('world-history', [\App\Http\Controllers\User\ProfileController::class, 'worldHistory'])->name('world-history'); diff --git a/routes/web.php b/routes/web.php index 4d5899d4..b2eaed41 100644 --- a/routes/web.php +++ b/routes/web.php @@ -40,6 +40,7 @@ use App\Http\Controllers\RSS\TagFeedController; use App\Http\Controllers\RSS\CreatorFeedController; use App\Http\Controllers\RSS\BlogFeedController; use App\Http\Controllers\Admin\AdminController; +use App\Http\Controllers\Admin\AdsenseAnalyticsController; use App\Http\Controllers\Moderation\StaffApplicationsController; use App\Http\Controllers\Moderation\StoriesController as ModerationStoriesController; use App\Http\Controllers\Moderation\UsernameQueueController; @@ -78,6 +79,7 @@ use App\Http\Controllers\Settings\CollectionManageController; use App\Http\Controllers\Settings\CollectionProgrammingController; use App\Http\Controllers\Settings\CollectionSurfaceController; use App\Services\GroupMembershipService; +use App\Services\Moderation\ArtworkUploadPolicy; use Inertia\Inertia; Route::get('/build-info.json', BuildInfoController::class)->name('build-info'); @@ -976,6 +978,10 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->group(function () { 'filesCdnUrl' => config('cdn.files_url'), 'chunkSize' => (int) config('uploads.chunk.max_bytes', 5242880), 'chunkRequestTimeoutMs' => (int) config('uploads.chunk.request_timeout_ms', 45000), + 'max_secondary_categories' => (int) config('categories.max_secondary_categories', 5), + 'requires_upload_review' => $user + ? (bool) app(ArtworkUploadPolicy::class)->assess($user)['requires_review'] + : false, 'feature_flags' => [ 'uploads_v2' => (bool) config('features.uploads_v2', false), ], @@ -1036,6 +1042,10 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->group(function () { 'filesCdnUrl' => config('cdn.files_url'), 'chunkSize' => (int) config('uploads.chunk.max_bytes', 5242880), 'chunkRequestTimeoutMs' => (int) config('uploads.chunk.request_timeout_ms', 45000), + 'max_secondary_categories' => (int) config('categories.max_secondary_categories', 5), + 'requires_upload_review' => $user + ? (bool) app(ArtworkUploadPolicy::class)->assess($user)['requires_review'] + : false, 'feature_flags' => [ 'uploads_v2' => (bool) config('features.uploads_v2', false), ], @@ -1069,6 +1079,18 @@ Route::middleware(['auth', 'verified', 'ensure.onboarding.complete'])->prefix('m Route::get('/{id}', [\App\Http\Controllers\Messaging\MessagesPageController::class, 'show'])->whereNumber('id')->name('show'); }); +Route::middleware(['auth', 'admin.role']) + ->prefix('admin/adsense') + ->name('admin.adsense.') + ->group(function (): void { + Route::get('/', [AdsenseAnalyticsController::class, 'index'])->name('index'); + Route::get('/connect', [AdsenseAnalyticsController::class, 'connect'])->name('connect'); + Route::get('/oauth/callback', [AdsenseAnalyticsController::class, 'callback'])->name('callback'); + Route::post('/account', [AdsenseAnalyticsController::class, 'selectAccount'])->name('account'); + Route::post('/sync', [AdsenseAnalyticsController::class, 'sync'])->name('sync'); + Route::delete('/disconnect', [AdsenseAnalyticsController::class, 'disconnect'])->name('disconnect'); + }); + Route::middleware(['auth']) ->prefix('admin') ->group(function () {