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.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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` |
|
||||
|
||||
+14
-1
@@ -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');
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user