Save workspace changes
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ArtworkIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'sort' => 'nullable|in:latest,oldest',
|
||||
'q' => 'nullable|string|max:255',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Artworks;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class ArtworkCreateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (! $this->user()) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'required|string|max:150',
|
||||
'description' => 'nullable|string',
|
||||
'category' => 'nullable|integer|exists:categories,id',
|
||||
'tags' => 'nullable|string|max:200',
|
||||
'license' => 'nullable|boolean',
|
||||
'is_mature' => 'nullable|boolean',
|
||||
'group' => 'nullable|string|max:90',
|
||||
];
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Artwork create unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Artworks;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class ArtworkTagsStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (! $this->user()) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tags' => 'required|array|max:15',
|
||||
'tags.*' => 'required|string|max:64',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Artworks;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class ArtworkTagsUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (! $this->user()) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tags' => 'required|array|max:15',
|
||||
'tags.*' => 'required|string|max:64',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AvatarUploadRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'avatar' => [
|
||||
'required',
|
||||
'file',
|
||||
'image',
|
||||
'max:2048',
|
||||
'mimes:jpg,jpeg,png,webp',
|
||||
'mimetypes:image/jpeg,image/png,image/webp',
|
||||
],
|
||||
'avatar_position' => ['nullable', 'in:top-left,top,top-right,left,center,right,bottom-left,bottom,bottom-right'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AttachCollectionArtworksRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'artwork_ids' => ['required', 'array', 'min:1'],
|
||||
'artwork_ids.*' => ['integer', 'distinct'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Models\Collection;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CollectionBulkActionsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'action' => ['required', 'string', 'in:archive,assign_campaign,update_lifecycle,request_ai_review,mark_editorial_review'],
|
||||
'collection_ids' => ['required', 'array', 'min:1'],
|
||||
'collection_ids.*' => ['integer', 'distinct'],
|
||||
'campaign_key' => ['nullable', 'string', 'max:80'],
|
||||
'campaign_label' => ['nullable', 'string', 'max:120'],
|
||||
'lifecycle_state' => ['nullable', 'string', 'in:' . implode(',', [
|
||||
Collection::LIFECYCLE_DRAFT,
|
||||
Collection::LIFECYCLE_PUBLISHED,
|
||||
Collection::LIFECYCLE_ARCHIVED,
|
||||
])],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator): void {
|
||||
$action = (string) $this->input('action', '');
|
||||
|
||||
if ($action === 'assign_campaign' && blank($this->input('campaign_key'))) {
|
||||
$validator->errors()->add('campaign_key', 'Campaign key is required for campaign assignment.');
|
||||
}
|
||||
|
||||
if ($action === 'update_lifecycle' && blank($this->input('lifecycle_state'))) {
|
||||
$validator->errors()->add('lifecycle_state', 'Lifecycle state is required for lifecycle updates.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Models\Collection;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CollectionOwnerSearchRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'q' => ['nullable', 'string', 'max:120'],
|
||||
'type' => ['nullable', 'string', 'in:' . implode(',', [
|
||||
Collection::TYPE_PERSONAL,
|
||||
Collection::TYPE_COMMUNITY,
|
||||
Collection::TYPE_EDITORIAL,
|
||||
])],
|
||||
'visibility' => ['nullable', 'string', 'in:' . implode(',', [
|
||||
Collection::VISIBILITY_PUBLIC,
|
||||
Collection::VISIBILITY_UNLISTED,
|
||||
Collection::VISIBILITY_PRIVATE,
|
||||
])],
|
||||
'lifecycle_state' => ['nullable', 'string', 'in:' . implode(',', [
|
||||
Collection::LIFECYCLE_DRAFT,
|
||||
Collection::LIFECYCLE_SCHEDULED,
|
||||
Collection::LIFECYCLE_PUBLISHED,
|
||||
Collection::LIFECYCLE_FEATURED,
|
||||
Collection::LIFECYCLE_ARCHIVED,
|
||||
Collection::LIFECYCLE_HIDDEN,
|
||||
Collection::LIFECYCLE_RESTRICTED,
|
||||
Collection::LIFECYCLE_UNDER_REVIEW,
|
||||
Collection::LIFECYCLE_EXPIRED,
|
||||
])],
|
||||
'mode' => ['nullable', 'string', 'in:' . implode(',', [
|
||||
Collection::MODE_MANUAL,
|
||||
Collection::MODE_SMART,
|
||||
])],
|
||||
'campaign_key' => ['nullable', 'string', 'max:80'],
|
||||
'program_key' => ['nullable', 'string', 'max:80'],
|
||||
'workflow_state' => ['nullable', 'string', 'in:' . implode(',', [
|
||||
Collection::WORKFLOW_DRAFT,
|
||||
Collection::WORKFLOW_IN_REVIEW,
|
||||
Collection::WORKFLOW_APPROVED,
|
||||
Collection::WORKFLOW_PROGRAMMED,
|
||||
Collection::WORKFLOW_ARCHIVED,
|
||||
])],
|
||||
'health_state' => ['nullable', 'string', 'in:' . implode(',', [
|
||||
Collection::HEALTH_HEALTHY,
|
||||
Collection::HEALTH_NEEDS_METADATA,
|
||||
Collection::HEALTH_STALE,
|
||||
Collection::HEALTH_LOW_CONTENT,
|
||||
Collection::HEALTH_BROKEN_ITEMS,
|
||||
Collection::HEALTH_WEAK_COVER,
|
||||
Collection::HEALTH_LOW_ENGAGEMENT,
|
||||
Collection::HEALTH_ATTRIBUTION_INCOMPLETE,
|
||||
Collection::HEALTH_NEEDS_REVIEW,
|
||||
Collection::HEALTH_DUPLICATE_RISK,
|
||||
Collection::HEALTH_MERGE_CANDIDATE,
|
||||
])],
|
||||
'partner_key' => ['nullable', 'string', 'max:80'],
|
||||
'experiment_key' => ['nullable', 'string', 'max:80'],
|
||||
'placement_eligibility' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CollectionProgramAssignmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'collection_id' => ['required', 'integer', 'exists:collections,id'],
|
||||
'program_key' => ['required', 'string', 'max:80'],
|
||||
'campaign_key' => ['nullable', 'string', 'max:80'],
|
||||
'placement_scope' => ['nullable', 'string', 'max:80'],
|
||||
'starts_at' => ['nullable', 'date'],
|
||||
'ends_at' => ['nullable', 'date', 'after:starts_at'],
|
||||
'priority' => ['nullable', 'integer', 'min:-100', 'max:100'],
|
||||
'notes' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CollectionProgrammingCollectionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'collection_id' => ['nullable', 'integer', 'exists:collections,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CollectionProgrammingMergePairRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'source_collection_id' => ['required', 'integer', 'exists:collections,id'],
|
||||
'target_collection_id' => ['required', 'integer', 'exists:collections,id', 'different:source_collection_id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CollectionProgrammingMetadataRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'collection_id' => ['required', 'integer', 'exists:collections,id'],
|
||||
'experiment_key' => ['nullable', 'string', 'max:80'],
|
||||
'experiment_treatment' => ['nullable', 'string', 'max:80'],
|
||||
'placement_variant' => ['nullable', 'string', 'max:80'],
|
||||
'ranking_mode_variant' => ['nullable', 'string', 'max:80'],
|
||||
'collection_pool_version' => ['nullable', 'string', 'max:80'],
|
||||
'test_label' => ['nullable', 'string', 'max:120'],
|
||||
'placement_eligibility' => ['nullable', 'boolean'],
|
||||
'promotion_tier' => ['nullable', 'string', 'max:40'],
|
||||
'partner_key' => ['nullable', 'string', 'max:80'],
|
||||
'trust_tier' => ['nullable', 'string', 'max:40'],
|
||||
'sponsorship_state' => ['nullable', 'string', 'max:40'],
|
||||
'ownership_domain' => ['nullable', 'string', 'max:80'],
|
||||
'commercial_review_state' => ['nullable', 'string', 'max:40'],
|
||||
'legal_review_state' => ['nullable', 'string', 'max:40'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CollectionProgrammingPreviewRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'program_key' => ['required', 'string', 'max:80'],
|
||||
'limit' => ['nullable', 'integer', 'min:1', 'max:24'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CollectionSavedLibraryRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'q' => ['nullable', 'string', 'max:120'],
|
||||
'filter' => ['nullable', 'string', 'in:all,editorial,community,personal,seasonal,noted,revisited'],
|
||||
'sort' => ['nullable', 'string', 'in:saved_desc,saved_asc,updated_desc,revisited_desc,ranking_desc,title_asc'],
|
||||
'list' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Models\Collection;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CollectionTargetActionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'target_collection_id' => ['required', 'integer', 'exists:collections,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator): void {
|
||||
$targetCollectionId = (int) $this->input('target_collection_id');
|
||||
$collection = $this->route('collection');
|
||||
|
||||
if ($collection instanceof Collection && $targetCollectionId === (int) $collection->id) {
|
||||
$validator->errors()->add('target_collection_id', 'Choose a different target collection.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReorderCollectionArtworksRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ordered_artwork_ids' => ['required', 'array'],
|
||||
'ordered_artwork_ids.*' => ['integer', 'distinct'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReorderProfileCollectionsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'collection_ids' => ['required', 'array', 'min:1'],
|
||||
'collection_ids.*' => ['required', 'integer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReorderSavedCollectionListItemsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'collection_ids' => ['required', 'array', 'min:1'],
|
||||
'collection_ids.*' => ['required', 'integer', 'distinct'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SmartCollectionRulesRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'smart_rules_json' => ['required', 'array'],
|
||||
'smart_rules_json.match' => ['nullable', 'string'],
|
||||
'smart_rules_json.sort' => ['nullable', 'string'],
|
||||
'smart_rules_json.rules' => ['required', 'array'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Models\Collection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StoreCollectionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$title = (string) $this->input('title', '');
|
||||
$slug = (string) $this->input('slug', '');
|
||||
$mode = (string) ($this->input('mode') ?: Collection::MODE_MANUAL);
|
||||
$sortMode = (string) ($this->input('sort_mode') ?: ($mode === Collection::MODE_SMART ? Collection::SORT_NEWEST : Collection::SORT_MANUAL));
|
||||
|
||||
if ($slug === '' && $title !== '') {
|
||||
$slug = Str::slug(Str::limit($title, 140, ''));
|
||||
}
|
||||
|
||||
$this->merge([
|
||||
'slug' => $slug,
|
||||
'mode' => $mode,
|
||||
'sort_mode' => $sortMode,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:2', 'max:120'],
|
||||
'slug' => ['required', 'string', 'min:2', 'max:140', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
|
||||
'group' => ['nullable', 'string', 'max:90'],
|
||||
'type' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::TYPE_PERSONAL,
|
||||
Collection::TYPE_COMMUNITY,
|
||||
Collection::TYPE_EDITORIAL,
|
||||
])],
|
||||
'editorial_owner_mode' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::EDITORIAL_OWNER_CREATOR,
|
||||
Collection::EDITORIAL_OWNER_STAFF_ACCOUNT,
|
||||
Collection::EDITORIAL_OWNER_SYSTEM,
|
||||
])],
|
||||
'editorial_owner_username' => ['nullable', 'string', 'max:60'],
|
||||
'editorial_owner_label' => ['nullable', 'string', 'max:120'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
'subtitle' => ['nullable', 'string', 'max:160'],
|
||||
'summary' => ['nullable', 'string', 'max:320'],
|
||||
'lifecycle_state' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::LIFECYCLE_DRAFT,
|
||||
Collection::LIFECYCLE_SCHEDULED,
|
||||
Collection::LIFECYCLE_PUBLISHED,
|
||||
Collection::LIFECYCLE_FEATURED,
|
||||
Collection::LIFECYCLE_ARCHIVED,
|
||||
Collection::LIFECYCLE_EXPIRED,
|
||||
])],
|
||||
'collaboration_mode' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::COLLABORATION_CLOSED,
|
||||
Collection::COLLABORATION_INVITE_ONLY,
|
||||
Collection::COLLABORATION_OPEN,
|
||||
])],
|
||||
'allow_submissions' => ['nullable', 'boolean'],
|
||||
'allow_comments' => ['nullable', 'boolean'],
|
||||
'allow_saves' => ['nullable', 'boolean'],
|
||||
'event_key' => ['nullable', 'string', 'max:80'],
|
||||
'event_label' => ['nullable', 'string', 'max:120'],
|
||||
'season_key' => ['nullable', 'string', 'max:80'],
|
||||
'banner_text' => ['nullable', 'string', 'max:200'],
|
||||
'badge_label' => ['nullable', 'string', 'max:80'],
|
||||
'spotlight_style' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::SPOTLIGHT_STYLE_DEFAULT,
|
||||
Collection::SPOTLIGHT_STYLE_EDITORIAL,
|
||||
Collection::SPOTLIGHT_STYLE_SEASONAL,
|
||||
Collection::SPOTLIGHT_STYLE_CHALLENGE,
|
||||
Collection::SPOTLIGHT_STYLE_COMMUNITY,
|
||||
])],
|
||||
'analytics_enabled' => ['nullable', 'boolean'],
|
||||
'presentation_style' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::PRESENTATION_STANDARD,
|
||||
Collection::PRESENTATION_EDITORIAL_GRID,
|
||||
Collection::PRESENTATION_HERO_GRID,
|
||||
Collection::PRESENTATION_MASONRY,
|
||||
])],
|
||||
'emphasis_mode' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::EMPHASIS_COVER_HEAVY,
|
||||
Collection::EMPHASIS_BALANCED,
|
||||
Collection::EMPHASIS_ARTWORK_FIRST,
|
||||
])],
|
||||
'theme_token' => ['nullable', 'in:default,subtle-blue,violet,amber'],
|
||||
'series_key' => ['nullable', 'string', 'max:80'],
|
||||
'series_title' => ['nullable', 'string', 'max:160'],
|
||||
'series_description' => ['nullable', 'string', 'max:400'],
|
||||
'series_order' => ['nullable', 'integer', 'min:1', 'max:9999'],
|
||||
'campaign_key' => ['nullable', 'string', 'max:80'],
|
||||
'campaign_label' => ['nullable', 'string', 'max:120'],
|
||||
'commercial_eligibility' => ['nullable', 'boolean'],
|
||||
'promotion_tier' => ['nullable', 'string', 'max:40'],
|
||||
'sponsorship_label' => ['nullable', 'string', 'max:120'],
|
||||
'partner_label' => ['nullable', 'string', 'max:120'],
|
||||
'monetization_ready_status' => ['nullable', 'string', 'max:40'],
|
||||
'brand_safe_status' => ['nullable', 'string', 'max:40'],
|
||||
'published_at' => ['nullable', 'date'],
|
||||
'unpublished_at' => ['nullable', 'date', 'after:published_at'],
|
||||
'archived_at' => ['nullable', 'date'],
|
||||
'expired_at' => ['nullable', 'date'],
|
||||
'visibility' => ['required', 'in:' . implode(',', [
|
||||
Collection::VISIBILITY_PUBLIC,
|
||||
Collection::VISIBILITY_UNLISTED,
|
||||
Collection::VISIBILITY_PRIVATE,
|
||||
])],
|
||||
'mode' => ['required', 'in:' . implode(',', [
|
||||
Collection::MODE_MANUAL,
|
||||
Collection::MODE_SMART,
|
||||
])],
|
||||
'sort_mode' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::SORT_MANUAL,
|
||||
Collection::SORT_NEWEST,
|
||||
Collection::SORT_OLDEST,
|
||||
Collection::SORT_POPULAR,
|
||||
])],
|
||||
'smart_rules_json' => ['nullable', 'array'],
|
||||
'layout_modules_json' => ['nullable', 'array'],
|
||||
'layout_modules_json.*.key' => ['required_with:layout_modules_json', 'string', 'max:60'],
|
||||
'layout_modules_json.*.enabled' => ['nullable', 'boolean'],
|
||||
'layout_modules_json.*.slot' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator): void {
|
||||
$type = (string) ($this->input('type') ?: Collection::TYPE_PERSONAL);
|
||||
|
||||
if ($type === Collection::TYPE_EDITORIAL && ! $this->user()?->hasRole('admin')) {
|
||||
$validator->errors()->add('type', 'Only staff can create editorial collections.');
|
||||
}
|
||||
|
||||
if ($type === Collection::TYPE_EDITORIAL && (string) $this->input('editorial_owner_mode') === Collection::EDITORIAL_OWNER_STAFF_ACCOUNT) {
|
||||
$username = trim((string) $this->input('editorial_owner_username', ''));
|
||||
|
||||
if ($username === '') {
|
||||
$validator->errors()->add('editorial_owner_username', 'Choose the staff account that should own this editorial collection.');
|
||||
} else {
|
||||
$target = User::query()->whereRaw('LOWER(username) = ?', [Str::lower($username)])->first();
|
||||
|
||||
if (! $target || ! ($target->isAdmin() || $target->isModerator())) {
|
||||
$validator->errors()->add('editorial_owner_username', 'The editorial owner must be an admin or moderator account.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->filled('unpublished_at') && ! $this->filled('published_at')) {
|
||||
$validator->errors()->add('published_at', 'Set a publish time before adding an unpublish time.');
|
||||
}
|
||||
|
||||
if ((string) $this->input('mode') !== Collection::MODE_SMART) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_array($this->input('smart_rules_json'))) {
|
||||
$validator->errors()->add('smart_rules_json', 'Smart collections require at least one valid rule.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Models\Collection;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCollectionCampaignRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
foreach ([
|
||||
'event_key',
|
||||
'event_label',
|
||||
'season_key',
|
||||
'banner_text',
|
||||
'badge_label',
|
||||
'campaign_key',
|
||||
'campaign_label',
|
||||
'promotion_tier',
|
||||
'sponsorship_label',
|
||||
'partner_label',
|
||||
'monetization_ready_status',
|
||||
'brand_safe_status',
|
||||
'editorial_notes',
|
||||
'staff_commercial_notes',
|
||||
] as $field) {
|
||||
if ($this->has($field) && trim((string) $this->input($field)) === '') {
|
||||
$this->merge([$field => null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'event_key' => ['nullable', 'string', 'max:80'],
|
||||
'event_label' => ['nullable', 'string', 'max:120'],
|
||||
'season_key' => ['nullable', 'string', 'max:80'],
|
||||
'banner_text' => ['nullable', 'string', 'max:200'],
|
||||
'badge_label' => ['nullable', 'string', 'max:80'],
|
||||
'spotlight_style' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::SPOTLIGHT_STYLE_DEFAULT,
|
||||
Collection::SPOTLIGHT_STYLE_EDITORIAL,
|
||||
Collection::SPOTLIGHT_STYLE_SEASONAL,
|
||||
Collection::SPOTLIGHT_STYLE_CHALLENGE,
|
||||
Collection::SPOTLIGHT_STYLE_COMMUNITY,
|
||||
])],
|
||||
'campaign_key' => ['nullable', 'string', 'max:80'],
|
||||
'campaign_label' => ['nullable', 'string', 'max:120'],
|
||||
'commercial_eligibility' => ['nullable', 'boolean'],
|
||||
'promotion_tier' => ['nullable', 'string', 'max:40'],
|
||||
'sponsorship_label' => ['nullable', 'string', 'max:120'],
|
||||
'partner_label' => ['nullable', 'string', 'max:120'],
|
||||
'monetization_ready_status' => ['nullable', 'string', 'max:40'],
|
||||
'brand_safe_status' => ['nullable', 'string', 'max:40'],
|
||||
'editorial_notes' => ['nullable', 'string', 'max:2000'],
|
||||
'staff_commercial_notes' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator): void {
|
||||
if ($this->filled('staff_commercial_notes') && ! $this->user()?->isAdmin()) {
|
||||
$validator->errors()->add('staff_commercial_notes', 'Only admins can update staff commercial notes.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Services\CollectionLinkService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateCollectionEntityLinksRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'entity_links' => ['nullable', 'array', 'max:18'],
|
||||
'entity_links.*.linked_type' => ['required', 'string', Rule::in(CollectionLinkService::supportedTypes())],
|
||||
'entity_links.*.linked_id' => ['required', 'integer', 'min:1'],
|
||||
'entity_links.*.relationship_type' => ['nullable', 'string', 'max:80'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Models\Collection;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCollectionLifecycleRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
foreach (['published_at', 'unpublished_at', 'archived_at', 'expired_at'] as $field) {
|
||||
if ($this->has($field) && trim((string) $this->input($field)) === '') {
|
||||
$this->merge([$field => null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'lifecycle_state' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::LIFECYCLE_DRAFT,
|
||||
Collection::LIFECYCLE_SCHEDULED,
|
||||
Collection::LIFECYCLE_PUBLISHED,
|
||||
Collection::LIFECYCLE_FEATURED,
|
||||
Collection::LIFECYCLE_ARCHIVED,
|
||||
Collection::LIFECYCLE_EXPIRED,
|
||||
])],
|
||||
'visibility' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::VISIBILITY_PUBLIC,
|
||||
Collection::VISIBILITY_UNLISTED,
|
||||
Collection::VISIBILITY_PRIVATE,
|
||||
])],
|
||||
'published_at' => ['nullable', 'date'],
|
||||
'unpublished_at' => ['nullable', 'date', 'after:published_at'],
|
||||
'archived_at' => ['nullable', 'date'],
|
||||
'expired_at' => ['nullable', 'date'],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator): void {
|
||||
if ($this->filled('unpublished_at')) {
|
||||
$collection = $this->route('collection');
|
||||
|
||||
if (! $this->filled('published_at') && ! optional($collection)->published_at) {
|
||||
$validator->errors()->add('published_at', 'Set a publish time before adding an unpublish time.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCollectionLinkedCollectionsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'related_collection_ids' => ['nullable', 'array', 'max:12'],
|
||||
'related_collection_ids.*' => ['required', 'integer', 'distinct'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Models\Collection;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCollectionPresentationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
foreach (['subtitle', 'summary', 'theme_token'] as $field) {
|
||||
if ($this->has($field) && trim((string) $this->input($field)) === '') {
|
||||
$this->merge([$field => null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'subtitle' => ['nullable', 'string', 'max:160'],
|
||||
'summary' => ['nullable', 'string', 'max:320'],
|
||||
'presentation_style' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::PRESENTATION_STANDARD,
|
||||
Collection::PRESENTATION_EDITORIAL_GRID,
|
||||
Collection::PRESENTATION_HERO_GRID,
|
||||
Collection::PRESENTATION_MASONRY,
|
||||
])],
|
||||
'emphasis_mode' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::EMPHASIS_COVER_HEAVY,
|
||||
Collection::EMPHASIS_BALANCED,
|
||||
Collection::EMPHASIS_ARTWORK_FIRST,
|
||||
])],
|
||||
'theme_token' => ['nullable', 'in:default,subtle-blue,violet,amber'],
|
||||
'layout_modules_json' => ['nullable', 'array'],
|
||||
'layout_modules_json.*.key' => ['required_with:layout_modules_json', 'string', 'max:60'],
|
||||
'layout_modules_json.*.enabled' => ['nullable', 'boolean'],
|
||||
'layout_modules_json.*.slot' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\Collection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class UpdateCollectionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$title = (string) $this->input('title', '');
|
||||
$slug = (string) $this->input('slug', '');
|
||||
/** @var \App\Models\Collection|null $collection */
|
||||
$collection = $this->route('collection');
|
||||
$mode = (string) ($this->input('mode') ?: ($collection?->mode ?? Collection::MODE_MANUAL));
|
||||
$defaultSortMode = $collection?->sort_mode ?? Collection::SORT_MANUAL;
|
||||
$sortMode = (string) ($this->input('sort_mode') ?: ($mode === Collection::MODE_SMART ? ($defaultSortMode ?: Collection::SORT_NEWEST) : $defaultSortMode));
|
||||
|
||||
if ($slug === '' && $title !== '') {
|
||||
$slug = Str::slug(Str::limit($title, 140, ''));
|
||||
}
|
||||
|
||||
$this->merge([
|
||||
'slug' => $slug,
|
||||
'mode' => $mode,
|
||||
'sort_mode' => $sortMode,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:2', 'max:120'],
|
||||
'slug' => ['required', 'string', 'min:2', 'max:140', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
|
||||
'group' => ['nullable', 'string', 'max:90'],
|
||||
'type' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::TYPE_PERSONAL,
|
||||
Collection::TYPE_COMMUNITY,
|
||||
Collection::TYPE_EDITORIAL,
|
||||
])],
|
||||
'editorial_owner_mode' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::EDITORIAL_OWNER_CREATOR,
|
||||
Collection::EDITORIAL_OWNER_STAFF_ACCOUNT,
|
||||
Collection::EDITORIAL_OWNER_SYSTEM,
|
||||
])],
|
||||
'editorial_owner_username' => ['nullable', 'string', 'max:60'],
|
||||
'editorial_owner_label' => ['nullable', 'string', 'max:120'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
'subtitle' => ['nullable', 'string', 'max:160'],
|
||||
'summary' => ['nullable', 'string', 'max:320'],
|
||||
'lifecycle_state' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::LIFECYCLE_DRAFT,
|
||||
Collection::LIFECYCLE_SCHEDULED,
|
||||
Collection::LIFECYCLE_PUBLISHED,
|
||||
Collection::LIFECYCLE_FEATURED,
|
||||
Collection::LIFECYCLE_ARCHIVED,
|
||||
Collection::LIFECYCLE_EXPIRED,
|
||||
])],
|
||||
'collaboration_mode' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::COLLABORATION_CLOSED,
|
||||
Collection::COLLABORATION_INVITE_ONLY,
|
||||
Collection::COLLABORATION_OPEN,
|
||||
])],
|
||||
'allow_submissions' => ['nullable', 'boolean'],
|
||||
'allow_comments' => ['nullable', 'boolean'],
|
||||
'allow_saves' => ['nullable', 'boolean'],
|
||||
'event_key' => ['nullable', 'string', 'max:80'],
|
||||
'event_label' => ['nullable', 'string', 'max:120'],
|
||||
'season_key' => ['nullable', 'string', 'max:80'],
|
||||
'banner_text' => ['nullable', 'string', 'max:200'],
|
||||
'badge_label' => ['nullable', 'string', 'max:80'],
|
||||
'spotlight_style' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::SPOTLIGHT_STYLE_DEFAULT,
|
||||
Collection::SPOTLIGHT_STYLE_EDITORIAL,
|
||||
Collection::SPOTLIGHT_STYLE_SEASONAL,
|
||||
Collection::SPOTLIGHT_STYLE_CHALLENGE,
|
||||
Collection::SPOTLIGHT_STYLE_COMMUNITY,
|
||||
])],
|
||||
'analytics_enabled' => ['nullable', 'boolean'],
|
||||
'presentation_style' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::PRESENTATION_STANDARD,
|
||||
Collection::PRESENTATION_EDITORIAL_GRID,
|
||||
Collection::PRESENTATION_HERO_GRID,
|
||||
Collection::PRESENTATION_MASONRY,
|
||||
])],
|
||||
'emphasis_mode' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::EMPHASIS_COVER_HEAVY,
|
||||
Collection::EMPHASIS_BALANCED,
|
||||
Collection::EMPHASIS_ARTWORK_FIRST,
|
||||
])],
|
||||
'theme_token' => ['nullable', 'in:default,subtle-blue,violet,amber'],
|
||||
'series_key' => ['nullable', 'string', 'max:80'],
|
||||
'series_title' => ['nullable', 'string', 'max:160'],
|
||||
'series_description' => ['nullable', 'string', 'max:400'],
|
||||
'series_order' => ['nullable', 'integer', 'min:1', 'max:9999'],
|
||||
'campaign_key' => ['nullable', 'string', 'max:80'],
|
||||
'campaign_label' => ['nullable', 'string', 'max:120'],
|
||||
'commercial_eligibility' => ['nullable', 'boolean'],
|
||||
'promotion_tier' => ['nullable', 'string', 'max:40'],
|
||||
'sponsorship_label' => ['nullable', 'string', 'max:120'],
|
||||
'partner_label' => ['nullable', 'string', 'max:120'],
|
||||
'monetization_ready_status' => ['nullable', 'string', 'max:40'],
|
||||
'brand_safe_status' => ['nullable', 'string', 'max:40'],
|
||||
'published_at' => ['nullable', 'date'],
|
||||
'unpublished_at' => ['nullable', 'date', 'after:published_at'],
|
||||
'archived_at' => ['nullable', 'date'],
|
||||
'expired_at' => ['nullable', 'date'],
|
||||
'visibility' => ['required', 'in:' . implode(',', [
|
||||
Collection::VISIBILITY_PUBLIC,
|
||||
Collection::VISIBILITY_UNLISTED,
|
||||
Collection::VISIBILITY_PRIVATE,
|
||||
])],
|
||||
'mode' => ['required', 'in:' . implode(',', [
|
||||
Collection::MODE_MANUAL,
|
||||
Collection::MODE_SMART,
|
||||
])],
|
||||
'sort_mode' => ['nullable', 'in:' . implode(',', [
|
||||
Collection::SORT_MANUAL,
|
||||
Collection::SORT_NEWEST,
|
||||
Collection::SORT_OLDEST,
|
||||
Collection::SORT_POPULAR,
|
||||
])],
|
||||
'cover_artwork_id' => ['nullable', 'integer'],
|
||||
'smart_rules_json' => ['nullable', 'array'],
|
||||
'layout_modules_json' => ['nullable', 'array'],
|
||||
'layout_modules_json.*.key' => ['required_with:layout_modules_json', 'string', 'max:60'],
|
||||
'layout_modules_json.*.enabled' => ['nullable', 'boolean'],
|
||||
'layout_modules_json.*.slot' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator): void {
|
||||
$type = (string) ($this->input('type') ?: Collection::TYPE_PERSONAL);
|
||||
|
||||
if ($type === Collection::TYPE_EDITORIAL && ! $this->user()?->hasRole('admin')) {
|
||||
$validator->errors()->add('type', 'Only staff can edit collections into editorial collections.');
|
||||
}
|
||||
|
||||
if ($type === Collection::TYPE_EDITORIAL && (string) $this->input('editorial_owner_mode') === Collection::EDITORIAL_OWNER_STAFF_ACCOUNT) {
|
||||
$username = trim((string) $this->input('editorial_owner_username', ''));
|
||||
|
||||
if ($username === '') {
|
||||
$validator->errors()->add('editorial_owner_username', 'Choose the staff account that should own this editorial collection.');
|
||||
} else {
|
||||
$target = User::query()->whereRaw('LOWER(username) = ?', [Str::lower($username)])->first();
|
||||
|
||||
if (! $target || ! ($target->isAdmin() || $target->isModerator())) {
|
||||
$validator->errors()->add('editorial_owner_username', 'The editorial owner must be an admin or moderator account.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->filled('unpublished_at') && ! $this->filled('published_at') && ! optional($this->route('collection'))->published_at) {
|
||||
$validator->errors()->add('published_at', 'Set a publish time before adding an unpublish time.');
|
||||
}
|
||||
|
||||
if ((string) $this->input('mode') !== Collection::MODE_SMART) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_array($this->input('smart_rules_json'))) {
|
||||
$validator->errors()->add('smart_rules_json', 'Smart collections require at least one valid rule.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function passedValidation(): void
|
||||
{
|
||||
if (($this->input('mode') ?? Collection::MODE_MANUAL) === Collection::MODE_SMART) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coverArtworkId = $this->integer('cover_artwork_id');
|
||||
if (! $coverArtworkId) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \App\Models\Collection|null $collection */
|
||||
$collection = $this->route('collection');
|
||||
$userId = $this->user()?->id;
|
||||
|
||||
if (! $collection || ! $userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$belongsToUser = Artwork::query()
|
||||
->where('id', $coverArtworkId)
|
||||
->where('user_id', $userId)
|
||||
->whereNull('deleted_at')
|
||||
->exists();
|
||||
|
||||
$isAttached = DB::table('collection_artwork')
|
||||
->where('collection_id', $collection->id)
|
||||
->where('artwork_id', $coverArtworkId)
|
||||
->exists();
|
||||
|
||||
if (! $belongsToUser || ! $isAttached) {
|
||||
throw ValidationException::withMessages([
|
||||
'cover_artwork_id' => 'Choose a cover artwork that is already attached to this collection.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCollectionSeriesRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
foreach (['series_key', 'series_title', 'series_description'] as $field) {
|
||||
if ($this->has($field) && trim((string) $this->input($field)) === '') {
|
||||
$this->merge([$field => null]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->has('series_order') && trim((string) $this->input('series_order')) === '') {
|
||||
$this->merge(['series_order' => null]);
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'series_key' => ['nullable', 'string', 'max:80'],
|
||||
'series_title' => ['nullable', 'string', 'max:160'],
|
||||
'series_description' => ['nullable', 'string', 'max:400'],
|
||||
'series_order' => ['nullable', 'integer', 'min:1', 'max:9999'],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator): void {
|
||||
$seriesKey = $this->input('series_key');
|
||||
$seriesTitle = $this->input('series_title');
|
||||
$seriesDescription = $this->input('series_description');
|
||||
$seriesOrder = $this->input('series_order');
|
||||
|
||||
$hasSeriesMetadata = filled($seriesTitle) || filled($seriesDescription) || filled($seriesOrder);
|
||||
|
||||
if ($hasSeriesMetadata && blank($seriesKey)) {
|
||||
$validator->errors()->add('series_key', 'Series key is required when series metadata is provided.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Collections;
|
||||
|
||||
use App\Models\Collection;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCollectionWorkflowRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'workflow_state' => ['nullable', 'string', 'in:' . implode(',', [
|
||||
Collection::WORKFLOW_DRAFT,
|
||||
Collection::WORKFLOW_IN_REVIEW,
|
||||
Collection::WORKFLOW_APPROVED,
|
||||
Collection::WORKFLOW_PROGRAMMED,
|
||||
Collection::WORKFLOW_ARCHIVED,
|
||||
])],
|
||||
'program_key' => ['nullable', 'string', 'max:80'],
|
||||
'partner_key' => ['nullable', 'string', 'max:80'],
|
||||
'experiment_key' => ['nullable', 'string', 'max:80'],
|
||||
'placement_eligibility' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Dashboard;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class ArtworkDestroyRequest extends FormRequest
|
||||
{
|
||||
private ?Artwork $artwork = null;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
if (! $user) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$id = (int) $this->route('id');
|
||||
if ($id <= 0) {
|
||||
$this->logUnauthorized('missing_artwork_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$artwork = Artwork::query()->whereKey($id)->first();
|
||||
if (! $artwork || (int) $artwork->user_id !== (int) $user->id) {
|
||||
$this->logUnauthorized('artwork_not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$this->artwork = $artwork;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function artwork(): Artwork
|
||||
{
|
||||
if (! $this->artwork) {
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return $this->artwork;
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Dashboard artwork delete unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'artwork_id' => $this->route('id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Dashboard;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class ArtworkEditRequest extends FormRequest
|
||||
{
|
||||
private ?Artwork $artwork = null;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
if (! $user) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$id = (int) $this->route('id');
|
||||
if ($id <= 0) {
|
||||
$this->logUnauthorized('missing_artwork_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$artwork = Artwork::query()->whereKey($id)->first();
|
||||
if (! $artwork || (int) $artwork->user_id !== (int) $user->id) {
|
||||
$this->logUnauthorized('artwork_not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$this->artwork = $artwork;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function artwork(): Artwork
|
||||
{
|
||||
if (! $this->artwork) {
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return $this->artwork;
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Dashboard artwork edit unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'artwork_id' => $this->route('id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Dashboard;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class UpdateArtworkRequest extends FormRequest
|
||||
{
|
||||
private ?Artwork $artwork = null;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
if (! $user) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$id = (int) $this->route('id');
|
||||
if ($id <= 0) {
|
||||
$this->logUnauthorized('missing_artwork_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$artwork = Artwork::query()->whereKey($id)->first();
|
||||
if (! $artwork || (int) $artwork->user_id !== (int) $user->id) {
|
||||
$this->logUnauthorized('artwork_not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$this->artwork = $artwork;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'required|string|max:150',
|
||||
'description' => 'nullable|string',
|
||||
// 100MB max (Laravel uses kilobytes)
|
||||
'file' => 'nullable|image|max:102400',
|
||||
];
|
||||
}
|
||||
|
||||
public function artwork(): Artwork
|
||||
{
|
||||
if (! $this->artwork) {
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return $this->artwork;
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Dashboard artwork update unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'artwork_id' => $this->route('id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AttachArtworkToGroupChallengeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'artwork_id' => ['required', 'integer', 'exists:artworks,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AttachArtworkToGroupProjectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'artwork_id' => ['required', 'integer', 'exists:artworks,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AttachArtworkToGroupReleaseRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'artwork_id' => ['required', 'integer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AttachAssetToGroupProjectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'asset_id' => ['required', 'integer', 'exists:group_assets,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AttachContributorToGroupReleaseRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => ['required', 'integer'],
|
||||
'role_label' => ['nullable', 'string', 'max:80'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PinGroupActivityItemRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'is_pinned' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReviewGroupArtworkRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'review_notes' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReviewGroupJoinRequestRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'role' => ['nullable', 'in:' . implode(',', [Group::ROLE_ADMIN, Group::ROLE_EDITOR, Group::ROLE_MEMBER, Group::ROLE_CONTRIBUTOR])],
|
||||
'review_notes' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGroupAssetRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:2', 'max:180'],
|
||||
'description' => ['nullable', 'string', 'max:40000'],
|
||||
'category' => ['nullable', 'in:' . implode(',', (array) config('groups.assets.categories', []))],
|
||||
'visibility' => ['nullable', 'in:' . implode(',', (array) config('groups.assets.visibility_options', []))],
|
||||
'status' => ['nullable', 'in:' . implode(',', (array) config('groups.assets.statuses', []))],
|
||||
'linked_project_id' => ['nullable', 'integer'],
|
||||
'is_featured' => ['nullable', 'boolean'],
|
||||
'file' => ['required', 'file', 'mimes:' . implode(',', (array) config('groups.assets.allowed_extensions', [])), 'max:' . (int) config('groups.assets.max_upload_kb', 20480)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGroupChallengeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:2', 'max:180'],
|
||||
'summary' => ['nullable', 'string', 'max:320'],
|
||||
'description' => ['nullable', 'string', 'max:40000'],
|
||||
'cover_path' => ['nullable', 'string', 'max:2048'],
|
||||
'cover_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||
'visibility' => ['nullable', 'in:' . implode(',', (array) config('groups.challenges.visibility_options', []))],
|
||||
'participation_scope' => ['nullable', 'in:' . implode(',', (array) config('groups.challenges.participation_scopes', []))],
|
||||
'status' => ['nullable', 'in:' . implode(',', (array) config('groups.challenges.statuses', []))],
|
||||
'start_at' => ['nullable', 'date'],
|
||||
'end_at' => ['nullable', 'date', 'after_or_equal:start_at'],
|
||||
'rules_text' => ['nullable', 'string', 'max:20000'],
|
||||
'submission_instructions' => ['nullable', 'string', 'max:20000'],
|
||||
'judging_mode' => ['nullable', 'in:' . implode(',', (array) config('groups.challenges.judging_modes', []))],
|
||||
'linked_collection_id' => ['nullable', 'integer'],
|
||||
'linked_project_id' => ['nullable', 'integer'],
|
||||
'featured_artwork_id' => ['nullable', 'integer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGroupEventRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:2', 'max:180'],
|
||||
'summary' => ['nullable', 'string', 'max:320'],
|
||||
'description' => ['nullable', 'string', 'max:40000'],
|
||||
'event_type' => ['nullable', 'in:' . implode(',', (array) config('groups.events.types', []))],
|
||||
'visibility' => ['nullable', 'in:' . implode(',', (array) config('groups.events.visibility_options', []))],
|
||||
'status' => ['nullable', 'in:' . implode(',', (array) config('groups.events.statuses', []))],
|
||||
'start_at' => ['nullable', 'date'],
|
||||
'end_at' => ['nullable', 'date', 'after_or_equal:start_at'],
|
||||
'timezone' => ['nullable', 'string', 'max:80'],
|
||||
'cover_path' => ['nullable', 'string', 'max:2048'],
|
||||
'cover_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||
'location' => ['nullable', 'string', 'max:180'],
|
||||
'external_url' => ['nullable', 'url', 'max:2048'],
|
||||
'linked_project_id' => ['nullable', 'integer'],
|
||||
'linked_collection_id' => ['nullable', 'integer'],
|
||||
'linked_challenge_id' => ['nullable', 'integer'],
|
||||
'is_featured' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGroupJoinRequestRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'message' => ['nullable', 'string', 'max:2000'],
|
||||
'portfolio_url' => ['nullable', 'url', 'max:2048'],
|
||||
'desired_role' => ['nullable', 'string', 'max:32'],
|
||||
'skills_json' => ['nullable', 'array', 'max:12'],
|
||||
'skills_json.*' => ['string', 'max:80'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGroupMemberRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => ['required', 'string', 'max:20'],
|
||||
'role' => ['required', 'in:' . implode(',', [Group::ROLE_ADMIN, Group::ROLE_EDITOR, Group::ROLE_MEMBER, Group::ROLE_CONTRIBUTOR])],
|
||||
'note' => ['nullable', 'string', 'max:500'],
|
||||
'expires_in_days' => ['nullable', 'integer', 'min:1', 'max:30'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGroupMilestoneRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:2', 'max:180'],
|
||||
'summary' => ['nullable', 'string', 'max:320'],
|
||||
'status' => ['nullable', 'in:' . implode(',', (array) config('groups.milestones.statuses', []))],
|
||||
'due_date' => ['nullable', 'date'],
|
||||
'owner_user_id' => ['nullable', 'integer'],
|
||||
'notes' => ['nullable', 'string', 'max:40000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use App\Models\GroupPost;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGroupPostRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['required', 'in:' . implode(',', [
|
||||
GroupPost::TYPE_ANNOUNCEMENT,
|
||||
GroupPost::TYPE_RELEASE,
|
||||
GroupPost::TYPE_RECRUITMENT,
|
||||
GroupPost::TYPE_UPDATE,
|
||||
])],
|
||||
'title' => ['required', 'string', 'min:2', 'max:180'],
|
||||
'excerpt' => ['nullable', 'string', 'max:320'],
|
||||
'content' => ['nullable', 'string', 'max:40000'],
|
||||
'cover_path' => ['nullable', 'string', 'max:2048'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGroupProjectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:2', 'max:180'],
|
||||
'summary' => ['nullable', 'string', 'max:320'],
|
||||
'description' => ['nullable', 'string', 'max:40000'],
|
||||
'cover_path' => ['nullable', 'string', 'max:2048'],
|
||||
'cover_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||
'status' => ['nullable', 'in:' . implode(',', (array) config('groups.projects.statuses', []))],
|
||||
'visibility' => ['nullable', 'in:' . implode(',', (array) config('groups.projects.visibility_options', []))],
|
||||
'start_date' => ['nullable', 'date'],
|
||||
'target_date' => ['nullable', 'date', 'after_or_equal:start_date'],
|
||||
'lead_user_id' => ['nullable', 'integer'],
|
||||
'linked_collection_id' => ['nullable', 'integer'],
|
||||
'linked_featured_artwork_id' => ['nullable', 'integer'],
|
||||
'pinned_post_id' => ['nullable', 'integer'],
|
||||
'member_user_ids' => ['nullable', 'array'],
|
||||
'member_user_ids.*' => ['integer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGroupReleaseRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:2', 'max:180'],
|
||||
'summary' => ['nullable', 'string', 'max:320'],
|
||||
'description' => ['nullable', 'string', 'max:40000'],
|
||||
'cover_path' => ['nullable', 'string', 'max:2048'],
|
||||
'cover_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
|
||||
'status' => ['nullable', 'in:' . implode(',', (array) config('groups.releases.statuses', []))],
|
||||
'current_stage' => ['nullable', 'in:' . implode(',', (array) config('groups.releases.stages', []))],
|
||||
'visibility' => ['nullable', 'in:' . implode(',', (array) config('groups.releases.visibility_options', []))],
|
||||
'planned_release_at' => ['nullable', 'date'],
|
||||
'lead_user_id' => ['nullable', 'integer'],
|
||||
'linked_project_id' => ['nullable', 'integer'],
|
||||
'linked_collection_id' => ['nullable', 'integer'],
|
||||
'featured_artwork_id' => ['nullable', 'integer'],
|
||||
'release_notes' => ['nullable', 'string', 'max:40000'],
|
||||
'is_featured' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StoreGroupRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$name = (string) $this->input('name', '');
|
||||
$slug = (string) $this->input('slug', '');
|
||||
|
||||
if ($slug === '' && $name !== '') {
|
||||
$slug = Str::slug(Str::limit($name, 90, ''));
|
||||
}
|
||||
|
||||
$this->merge([
|
||||
'slug' => $slug,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'min:2', 'max:80'],
|
||||
'slug' => ['required', 'string', 'min:2', 'max:90', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
|
||||
'headline' => ['nullable', 'string', 'max:160'],
|
||||
'bio' => ['nullable', 'string', 'max:3000'],
|
||||
'visibility' => ['required', 'in:' . implode(',', Group::acceptedVisibilityValues())],
|
||||
'membership_policy' => ['nullable', 'in:' . implode(',', Group::acceptedMembershipPolicies())],
|
||||
'type' => ['nullable', 'string', 'max:80'],
|
||||
'founded_at' => ['nullable', 'date'],
|
||||
'website_url' => ['nullable', 'url', 'max:2048'],
|
||||
'links_json' => ['nullable', 'array', 'max:8'],
|
||||
'links_json.*.label' => ['required_with:links_json', 'string', 'max:40'],
|
||||
'links_json.*.url' => ['required_with:links_json', 'url', 'max:2048'],
|
||||
'avatar_path' => ['nullable', 'string', 'max:2048'],
|
||||
'banner_path' => ['nullable', 'string', 'max:2048'],
|
||||
'avatar_file' => ['nullable', 'file', 'image', 'max:5120', 'mimes:jpg,jpeg,png,webp'],
|
||||
'banner_file' => ['nullable', 'file', 'image', 'max:5120', 'mimes:jpg,jpeg,png,webp'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateGroupAssetRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'min:2', 'max:180'],
|
||||
'description' => ['nullable', 'string', 'max:40000'],
|
||||
'category' => ['nullable', 'in:' . implode(',', (array) config('groups.assets.categories', []))],
|
||||
'visibility' => ['nullable', 'in:' . implode(',', (array) config('groups.assets.visibility_options', []))],
|
||||
'status' => ['nullable', 'in:' . implode(',', (array) config('groups.assets.statuses', []))],
|
||||
'linked_project_id' => ['nullable', 'integer'],
|
||||
'is_featured' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
class UpdateGroupChallengeRequest extends StoreGroupChallengeRequest
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
class UpdateGroupEventRequest extends StoreGroupEventRequest
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateGroupMemberPermissionsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'permission_overrides' => ['required', 'array'],
|
||||
'permission_overrides.*.key' => ['required', 'string', Rule::in(Group::permissionKeys())],
|
||||
'permission_overrides.*.is_allowed' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateGroupMemberRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'role' => ['required', 'in:' . implode(',', [Group::ROLE_ADMIN, Group::ROLE_EDITOR, Group::ROLE_MEMBER, Group::ROLE_CONTRIBUTOR])],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
class UpdateGroupMilestoneRequest extends StoreGroupMilestoneRequest
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use App\Models\GroupPost;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateGroupPostRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['sometimes', 'in:' . implode(',', [
|
||||
GroupPost::TYPE_ANNOUNCEMENT,
|
||||
GroupPost::TYPE_RELEASE,
|
||||
GroupPost::TYPE_RECRUITMENT,
|
||||
GroupPost::TYPE_UPDATE,
|
||||
])],
|
||||
'title' => ['sometimes', 'string', 'min:2', 'max:180'],
|
||||
'excerpt' => ['nullable', 'string', 'max:320'],
|
||||
'content' => ['nullable', 'string', 'max:40000'],
|
||||
'cover_path' => ['nullable', 'string', 'max:2048'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
class UpdateGroupProjectRequest extends StoreGroupProjectRequest
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateGroupProjectStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['required', 'in:' . implode(',', (array) config('groups.projects.statuses', []))],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateGroupRecruitmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$allowedRoles = config('groups.recruitment.roles', []);
|
||||
$allowedSkills = config('groups.recruitment.skills', []);
|
||||
$allowedContactModes = config('groups.recruitment.contact_modes', ['join_request', 'direct_message', 'external_link']);
|
||||
$allowedVisibility = config('groups.recruitment.visibility_options', ['public', 'members_only', 'private']);
|
||||
|
||||
return [
|
||||
'is_recruiting' => ['required', 'boolean'],
|
||||
'headline' => ['nullable', 'string', 'max:180'],
|
||||
'description' => ['nullable', 'string', 'max:4000'],
|
||||
'roles_json' => ['nullable', 'array', 'max:12'],
|
||||
'roles_json.*' => ['string', 'max:80', 'in:' . implode(',', $allowedRoles)],
|
||||
'skills_json' => ['nullable', 'array', 'max:20'],
|
||||
'skills_json.*' => ['string', 'max:80', 'in:' . implode(',', $allowedSkills)],
|
||||
'contact_mode' => ['nullable', 'string', 'max:32', 'in:' . implode(',', $allowedContactModes)],
|
||||
'visibility' => ['nullable', 'in:' . implode(',', $allowedVisibility)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
class UpdateGroupReleaseRequest extends StoreGroupReleaseRequest
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateGroupReleaseStageRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'current_stage' => ['required', 'in:' . implode(',', (array) config('groups.releases.stages', []))],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Groups;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UpdateGroupRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$name = (string) $this->input('name', '');
|
||||
$slug = (string) $this->input('slug', '');
|
||||
|
||||
if ($slug === '' && $name !== '') {
|
||||
$slug = Str::slug(Str::limit($name, 90, ''));
|
||||
}
|
||||
|
||||
$this->merge([
|
||||
'slug' => $slug,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'min:2', 'max:80'],
|
||||
'slug' => ['required', 'string', 'min:2', 'max:90', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
|
||||
'headline' => ['nullable', 'string', 'max:160'],
|
||||
'bio' => ['nullable', 'string', 'max:3000'],
|
||||
'visibility' => ['required', 'in:' . implode(',', Group::acceptedVisibilityValues())],
|
||||
'membership_policy' => ['nullable', 'in:' . implode(',', Group::acceptedMembershipPolicies())],
|
||||
'type' => ['nullable', 'string', 'max:80'],
|
||||
'founded_at' => ['nullable', 'date'],
|
||||
'website_url' => ['nullable', 'url', 'max:2048'],
|
||||
'links_json' => ['nullable', 'array', 'max:8'],
|
||||
'links_json.*.label' => ['required_with:links_json', 'string', 'max:40'],
|
||||
'links_json.*.url' => ['required_with:links_json', 'url', 'max:2048'],
|
||||
'avatar_path' => ['nullable', 'string', 'max:2048'],
|
||||
'banner_path' => ['nullable', 'string', 'max:2048'],
|
||||
'avatar_file' => ['nullable', 'file', 'image', 'max:5120', 'mimes:jpg,jpeg,png,webp'],
|
||||
'banner_file' => ['nullable', 'file', 'image', 'max:5120', 'mimes:jpg,jpeg,png,webp'],
|
||||
'featured_artwork_id' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Manage;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class ManageArtworkDestroyRequest extends FormRequest
|
||||
{
|
||||
private ?object $artwork = null;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
if (! $user) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$id = (int) $this->route('id');
|
||||
if ($id <= 0) {
|
||||
$this->logUnauthorized('missing_artwork_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$artwork = DB::table('artworks')->where('id', $id)->first();
|
||||
if (! $artwork || (int) $artwork->user_id !== (int) $user->id) {
|
||||
$this->logUnauthorized('artwork_not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$this->artwork = $artwork;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function artwork(): object
|
||||
{
|
||||
if (! $this->artwork) {
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return $this->artwork;
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Manage artwork delete unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'artwork_id' => $this->route('id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Manage;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class ManageArtworkEditRequest extends FormRequest
|
||||
{
|
||||
private ?object $artwork = null;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
if (! $user) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$id = (int) $this->route('id');
|
||||
if ($id <= 0) {
|
||||
$this->logUnauthorized('missing_artwork_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$artwork = DB::table('artworks')->where('id', $id)->first();
|
||||
if (! $artwork || (int) $artwork->user_id !== (int) $user->id) {
|
||||
$this->logUnauthorized('artwork_not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$this->artwork = $artwork;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function artwork(): object
|
||||
{
|
||||
if (! $this->artwork) {
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return $this->artwork;
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Manage artwork edit unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'artwork_id' => $this->route('id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Manage;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class ManageArtworkUpdateRequest extends FormRequest
|
||||
{
|
||||
private ?object $artwork = null;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
if (! $user) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$id = (int) $this->route('id');
|
||||
if ($id <= 0) {
|
||||
$this->logUnauthorized('missing_artwork_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$artwork = DB::table('artworks')->where('id', $id)->first();
|
||||
if (! $artwork || (int) $artwork->user_id !== (int) $user->id) {
|
||||
$this->logUnauthorized('artwork_not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$this->artwork = $artwork;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'required|string|max:255',
|
||||
'section' => 'nullable|integer',
|
||||
'description' => 'nullable|string',
|
||||
'artwork' => 'nullable|file|image',
|
||||
'attachment' => 'nullable|file',
|
||||
];
|
||||
}
|
||||
|
||||
public function artwork(): object
|
||||
{
|
||||
if (! $this->artwork) {
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return $this->artwork;
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Manage artwork update unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'artwork_id' => $this->route('id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Messaging;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ManageConversationParticipantRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'required|integer|exists:users,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Messaging;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RenameConversationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'required|string|max:120',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Messaging;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreConversationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'required|in:direct,group',
|
||||
'recipient_id' => 'required_if:type,direct|integer|exists:users,id',
|
||||
'participant_ids' => 'required_if:type,group|array|min:2',
|
||||
'participant_ids.*' => 'integer|exists:users,id',
|
||||
'title' => 'required_if:type,group|nullable|string|max:120',
|
||||
'body' => 'required|string|max:5000',
|
||||
'client_temp_id' => 'nullable|string|max:120',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Messaging;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreMessageRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'body' => 'nullable|string|max:5000',
|
||||
'attachments' => 'sometimes|array|max:5',
|
||||
'attachments.*' => 'file|max:25600',
|
||||
'client_temp_id' => 'nullable|string|max:120',
|
||||
'reply_to_message_id' => 'nullable|integer|exists:messages,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Messaging;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ToggleMessageReactionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'reaction' => 'required|string|max:32',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Messaging;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateMessageRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'body' => 'required|string|max:5000',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\NovaCards;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AdminStoreNovaCardAssetPackRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'slug' => ['required', 'string', 'max:140', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'type' => ['required', Rule::in(['asset', 'template'])],
|
||||
'preview_image' => ['nullable', 'string', 'max:255'],
|
||||
'manifest_json' => ['nullable', 'array'],
|
||||
'official' => ['sometimes', 'boolean'],
|
||||
'active' => ['sometimes', 'boolean'],
|
||||
'order_num' => ['sometimes', 'integer', 'min:0', 'max:9999'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\NovaCards;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AdminStoreNovaCardCategoryRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'slug' => ['required', 'string', 'max:120', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'description' => ['nullable', 'string', 'max:400'],
|
||||
'active' => ['sometimes', 'boolean'],
|
||||
'order_num' => ['sometimes', 'integer', 'min:0', 'max:9999'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\NovaCards;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AdminStoreNovaCardChallengeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'slug' => ['required', 'string', 'max:140', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
|
||||
'title' => ['required', 'string', 'max:140'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
'prompt' => ['nullable', 'string', 'max:1000'],
|
||||
'rules_json' => ['nullable', 'array'],
|
||||
'status' => ['required', Rule::in(['draft', 'active', 'completed', 'archived'])],
|
||||
'official' => ['sometimes', 'boolean'],
|
||||
'featured' => ['sometimes', 'boolean'],
|
||||
'winner_card_id' => ['nullable', 'integer', 'exists:nova_cards,id'],
|
||||
'starts_at' => ['nullable', 'date'],
|
||||
'ends_at' => ['nullable', 'date', 'after_or_equal:starts_at'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\NovaCards;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AdminStoreNovaCardTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'slug' => ['required', 'string', 'max:120', 'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/'],
|
||||
'name' => ['required', 'string', 'max:160'],
|
||||
'description' => ['nullable', 'string', 'max:400'],
|
||||
'preview_image' => ['nullable', 'string', 'max:255'],
|
||||
'config_json' => ['required', 'array'],
|
||||
'supported_formats' => ['required', 'array', 'min:1'],
|
||||
'supported_formats.*' => ['string', Rule::in(array_keys((array) config('nova_cards.formats', [])))],
|
||||
'active' => ['sometimes', 'boolean'],
|
||||
'official' => ['sometimes', 'boolean'],
|
||||
'order_num' => ['sometimes', 'integer', 'min:0', 'max:9999'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\NovaCards;
|
||||
|
||||
use App\Models\NovaCard;
|
||||
use App\Services\NovaCards\NovaCardPublishModerationService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AdminUpdateNovaCardRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['sometimes', Rule::in([
|
||||
NovaCard::STATUS_DRAFT,
|
||||
NovaCard::STATUS_PROCESSING,
|
||||
NovaCard::STATUS_PUBLISHED,
|
||||
NovaCard::STATUS_HIDDEN,
|
||||
NovaCard::STATUS_REJECTED,
|
||||
])],
|
||||
'moderation_status' => ['sometimes', Rule::in([
|
||||
NovaCard::MOD_PENDING,
|
||||
NovaCard::MOD_APPROVED,
|
||||
NovaCard::MOD_FLAGGED,
|
||||
NovaCard::MOD_REJECTED,
|
||||
])],
|
||||
'disposition' => ['sometimes', 'nullable', Rule::in(array_keys(NovaCardPublishModerationService::DISPOSITION_LABELS))],
|
||||
'featured' => ['sometimes', 'boolean'],
|
||||
'allow_remix' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\NovaCards;
|
||||
|
||||
use App\Models\NovaCard;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class SaveNovaCardDraftRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$validation = (array) config('nova_cards.validation', []);
|
||||
|
||||
return [
|
||||
'title' => ['sometimes', 'string', 'min:2', 'max:' . (int) ($validation['title_max'] ?? 120)],
|
||||
'quote_text' => ['sometimes', 'string', 'min:' . (int) ($validation['quote_min'] ?? 3), 'max:' . (int) ($validation['quote_max'] ?? 420)],
|
||||
'quote_author' => ['sometimes', 'nullable', 'string', 'max:160'],
|
||||
'quote_source' => ['sometimes', 'nullable', 'string', 'max:180'],
|
||||
'description' => ['sometimes', 'nullable', 'string', 'max:' . (int) ($validation['description_max'] ?? 500)],
|
||||
'format' => ['sometimes', Rule::in(array_keys((array) config('nova_cards.formats', [])))],
|
||||
'template_id' => ['sometimes', 'nullable', 'integer', 'exists:nova_card_templates,id'],
|
||||
'category_id' => ['sometimes', 'nullable', 'integer', 'exists:nova_card_categories,id'],
|
||||
'background_type' => ['sometimes', Rule::in(['gradient', 'upload', 'template', 'solid'])],
|
||||
'background_image_id' => ['sometimes', 'nullable', 'integer', 'exists:nova_card_backgrounds,id'],
|
||||
'visibility' => ['sometimes', Rule::in([NovaCard::VISIBILITY_PUBLIC, NovaCard::VISIBILITY_UNLISTED, NovaCard::VISIBILITY_PRIVATE])],
|
||||
'allow_download' => ['sometimes', 'boolean'],
|
||||
'allow_remix' => ['sometimes', 'boolean'],
|
||||
'editor_mode_last_used' => ['sometimes', Rule::in(['quick', 'full'])],
|
||||
'publish_mode' => ['sometimes', Rule::in(['now', 'schedule'])],
|
||||
'scheduled_for' => ['sometimes', 'nullable', 'date'],
|
||||
'scheduling_timezone' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||
'tags' => ['sometimes', 'array', 'max:' . (int) ($validation['max_tags'] ?? 8)],
|
||||
'tags.*' => ['string', 'min:2', 'max:32'],
|
||||
'project_json' => ['sometimes', 'array'],
|
||||
'project_json.schema_version' => ['sometimes', 'integer', 'min:1', 'max:9'],
|
||||
'project_json.text_blocks' => ['sometimes', 'array', 'max:' . (int) ($validation['max_text_blocks'] ?? 8)],
|
||||
'project_json.text_blocks.*.key' => ['sometimes', 'string', 'max:40'],
|
||||
'project_json.text_blocks.*.type' => ['sometimes', Rule::in(['title', 'quote', 'author', 'source', 'body', 'caption'])],
|
||||
'project_json.text_blocks.*.text' => ['sometimes', 'nullable', 'string', 'max:' . (int) ($validation['quote_max'] ?? 420)],
|
||||
'project_json.text_blocks.*.enabled' => ['sometimes', 'boolean'],
|
||||
'project_json.text_blocks.*.pos_x' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:100'],
|
||||
'project_json.text_blocks.*.pos_y' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:100'],
|
||||
'project_json.text_blocks.*.pos_width' => ['sometimes', 'nullable', 'numeric', 'min:1', 'max:100'],
|
||||
'project_json.assets.pack_ids' => ['sometimes', 'array'],
|
||||
'project_json.assets.pack_ids.*' => ['integer'],
|
||||
'project_json.assets.template_pack_ids' => ['sometimes', 'array'],
|
||||
'project_json.assets.template_pack_ids.*' => ['integer'],
|
||||
'project_json.assets.items' => ['sometimes', 'array', 'max:' . (int) ($validation['max_asset_items'] ?? 12)],
|
||||
'project_json.assets.items.*.asset_key' => ['sometimes', 'string', 'max:80'],
|
||||
'project_json.assets.items.*.label' => ['sometimes', 'string', 'max:120'],
|
||||
'project_json.assets.items.*.glyph' => ['sometimes', 'string', 'max:4'],
|
||||
'project_json.layout.alignment' => ['sometimes', Rule::in((array) ($validation['allowed_alignments'] ?? ['left', 'center', 'right']))],
|
||||
'project_json.layout.layout' => ['sometimes', Rule::in((array) ($validation['allowed_layouts'] ?? []))],
|
||||
'project_json.layout.position' => ['sometimes', Rule::in((array) ($validation['allowed_positions'] ?? []))],
|
||||
'project_json.layout.padding' => ['sometimes', Rule::in((array) ($validation['allowed_padding_presets'] ?? []))],
|
||||
'project_json.layout.max_width' => ['sometimes', Rule::in((array) ($validation['allowed_max_widths'] ?? []))],
|
||||
'project_json.typography.font_preset' => ['sometimes', Rule::in(array_keys((array) config('nova_cards.font_presets', [])))],
|
||||
'project_json.typography.quote_size' => ['sometimes', 'integer', 'min:10', 'max:160'],
|
||||
'project_json.typography.quote_width' => ['sometimes', 'nullable', 'integer', 'min:30', 'max:100'],
|
||||
'project_json.typography.text_opacity' => ['sometimes', 'nullable', 'integer', 'min:10', 'max:100'],
|
||||
'project_json.decorations' => ['sometimes', 'array'],
|
||||
'project_json.decorations.*.pos_x' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:100'],
|
||||
'project_json.decorations.*.pos_y' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:100'],
|
||||
'project_json.decorations.*.opacity' => ['sometimes', 'nullable', 'integer', 'min:10', 'max:100'],
|
||||
'project_json.typography.author_size' => ['sometimes', 'integer', 'min:12', 'max:72'],
|
||||
'project_json.typography.letter_spacing' => ['sometimes', 'integer', 'min:-2', 'max:12'],
|
||||
'project_json.typography.line_height' => ['sometimes', 'numeric', 'min:0.9', 'max:1.8'],
|
||||
'project_json.typography.shadow_preset' => ['sometimes', Rule::in(array_column((array) config('nova_cards.shadow_presets', []), 'key'))],
|
||||
'project_json.typography.text_color' => ['sometimes', 'regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
|
||||
'project_json.typography.accent_color' => ['sometimes', 'regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
|
||||
'project_json.background.type' => ['sometimes', Rule::in(['gradient', 'upload', 'template', 'solid'])],
|
||||
'project_json.background.gradient_preset' => ['sometimes', Rule::in(array_keys((array) config('nova_cards.gradient_presets', [])))],
|
||||
'project_json.background.gradient_colors' => ['sometimes', 'array', 'min:2', 'max:3'],
|
||||
'project_json.background.gradient_colors.*' => ['regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
|
||||
'project_json.background.solid_color' => ['sometimes', 'regex:/^#(?:[0-9a-fA-F]{3}){1,2}$/'],
|
||||
'project_json.background.overlay_style' => ['sometimes', Rule::in((array) ($validation['allowed_overlay_styles'] ?? []))],
|
||||
'project_json.background.focal_position' => ['sometimes', Rule::in(array_column((array) config('nova_cards.focal_positions', []), 'key'))],
|
||||
'project_json.background.blur_level' => ['sometimes', Rule::in((array) ($validation['allowed_blur_levels'] ?? []))],
|
||||
'project_json.background.opacity' => ['sometimes', Rule::in((array) ($validation['allowed_opacity_levels'] ?? []))],
|
||||
'project_json.source_context.editor_mode' => ['sometimes', Rule::in(['quick', 'full'])],
|
||||
'project_json.decorations' => ['sometimes', 'array', 'max:' . (int) ($validation['max_decorations'] ?? 6)],
|
||||
'project_json.decorations.*.key' => ['sometimes', 'string', 'max:40'],
|
||||
'project_json.decorations.*.glyph' => ['sometimes', 'string', 'max:4'],
|
||||
'project_json.decorations.*.placement' => ['sometimes', 'string', 'max:24'],
|
||||
'project_json.decorations.*.x' => ['sometimes', 'numeric', 'min:0', 'max:1920'],
|
||||
'project_json.decorations.*.y' => ['sometimes', 'numeric', 'min:0', 'max:1920'],
|
||||
'project_json.decorations.*.size' => ['sometimes', 'integer', 'min:2', 'max:120'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\NovaCards;
|
||||
|
||||
use App\Models\NovaCard;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreNovaCardDraftRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['nullable', 'string', 'min:2', 'max:' . (int) config('nova_cards.validation.title_max', 120)],
|
||||
'quote_text' => ['nullable', 'string', 'min:' . (int) config('nova_cards.validation.quote_min', 3), 'max:' . (int) config('nova_cards.validation.quote_max', 420)],
|
||||
'quote_author' => ['nullable', 'string', 'max:160'],
|
||||
'quote_source' => ['nullable', 'string', 'max:180'],
|
||||
'description' => ['nullable', 'string', 'max:' . (int) config('nova_cards.validation.description_max', 500)],
|
||||
'format' => ['nullable', Rule::in(array_keys((array) config('nova_cards.formats', [])))],
|
||||
'template_id' => ['nullable', 'integer', 'exists:nova_card_templates,id'],
|
||||
'category_id' => ['nullable', 'integer', 'exists:nova_card_categories,id'],
|
||||
'background_type' => ['nullable', Rule::in(['gradient', 'upload', 'template', 'solid'])],
|
||||
'tags' => ['nullable', 'array', 'max:' . (int) config('nova_cards.validation.max_tags', 8)],
|
||||
'tags.*' => ['string', 'min:2', 'max:32'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\NovaCards;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class UploadNovaCardBackgroundRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$bytes = (int) config('nova_cards.validation.max_background_upload_bytes', 8 * 1024 * 1024);
|
||||
$maxKilobytes = (int) ceil($bytes / 1024);
|
||||
|
||||
return [
|
||||
'background' => [
|
||||
'bail',
|
||||
'required',
|
||||
'file',
|
||||
function (string $attribute, mixed $value, Closure $fail): void {
|
||||
$this->validateUpload($attribute, $value, $fail);
|
||||
},
|
||||
'image',
|
||||
'mimes:jpeg,jpg,png,webp',
|
||||
'max:' . $maxKilobytes,
|
||||
function (string $attribute, mixed $value, Closure $fail): void {
|
||||
$this->validateMinimumDimensions($attribute, $value, $fail, 480, 480);
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function validateUpload(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (! $value instanceof UploadedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $value->getRealPath() ?: $value->getPathname();
|
||||
|
||||
if (! $value->isValid() || ! is_string($path) || trim($path) === '' || ! is_readable($path)) {
|
||||
$fail('The ' . $attribute . ' upload is invalid.');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateMinimumDimensions(string $attribute, mixed $value, Closure $fail, int $minWidth, int $minHeight): void
|
||||
{
|
||||
if (! $value instanceof UploadedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $value->getRealPath() ?: $value->getPathname();
|
||||
|
||||
if (! is_string($path) || trim($path) === '' || ! is_readable($path)) {
|
||||
$fail('The ' . $attribute . ' upload is invalid.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$binary = @file_get_contents($path);
|
||||
if ($binary === false || $binary === '') {
|
||||
$fail('The ' . $attribute . ' upload is invalid.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$dimensions = @getimagesizefromstring($binary);
|
||||
if (! is_array($dimensions) || ($dimensions[0] ?? 0) < $minWidth || ($dimensions[1] ?? 0) < $minHeight) {
|
||||
$fail(sprintf('The %s must be at least %dx%d pixels.', $attribute, $minWidth, $minHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Posts;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CreateCommentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'body' => ['required', 'string', 'min:1', 'max:1000'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'body.max' => 'Comment cannot exceed 1,000 characters.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Posts;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CreatePostRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['required', 'string', 'in:text,artwork_share,upload,achievement'],
|
||||
'visibility' => ['required', 'string', 'in:public,followers,private'],
|
||||
'body' => ['nullable', 'string', 'max:2000'],
|
||||
'targets' => ['nullable', 'array', 'max:1'],
|
||||
'targets.*.type' => ['required_with:targets', 'string', 'in:artwork'],
|
||||
'targets.*.id' => ['required_with:targets', 'integer', 'min:1'],
|
||||
'link_preview' => ['nullable', 'array'],
|
||||
'link_preview.url' => ['nullable', 'string', 'url', 'max:2048'],
|
||||
'link_preview.title' => ['nullable', 'string', 'max:300'],
|
||||
'link_preview.description' => ['nullable', 'string', 'max:500'],
|
||||
'link_preview.image' => ['nullable', 'string', 'url', 'max:2048'],
|
||||
'link_preview.site_name' => ['nullable', 'string', 'max:100'],
|
||||
'tagged_users' => ['nullable', 'array', 'max:10'],
|
||||
'tagged_users.*.id' => ['required_with:tagged_users', 'integer', 'min:1'],
|
||||
'tagged_users.*.username' => ['required_with:tagged_users', 'string', 'max:50'],
|
||||
'tagged_users.*.name' => ['nullable', 'string', 'max:100'],
|
||||
'publish_at' => ['nullable', 'date', 'after:now'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'body.max' => 'Post body cannot exceed 2,000 characters.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Posts;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ShareArtworkRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'body' => ['nullable', 'string', 'max:2000'],
|
||||
'visibility' => ['required', 'string', 'in:public,followers,private'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Posts;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdatePostRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'body' => ['nullable', 'string', 'max:2000'],
|
||||
'visibility' => ['nullable', 'string', 'in:public,followers,private'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Support\UsernamePolicy;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => ['sometimes', ...UsernameRequest::rulesFor((int) $this->user()->id)],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
'name' => ['nullable', 'string', 'max:255'],
|
||||
'web' => ['nullable', 'url', 'max:255'],
|
||||
'day' => ['nullable', 'numeric', 'between:1,31'],
|
||||
'month' => ['nullable', 'numeric', 'between:1,12'],
|
||||
'year' => ['nullable', 'numeric', 'digits:4'],
|
||||
'gender' => ['nullable', 'in:m,f,n,M,F,N,X,x'],
|
||||
'country' => ['nullable', 'string', 'size:2'],
|
||||
'country_id' => ['nullable', 'integer', Rule::exists('countries', 'id')],
|
||||
'mailing' => ['nullable', 'boolean'],
|
||||
'notify' => ['nullable', 'boolean'],
|
||||
'auto_post_upload' => ['nullable', 'boolean'],
|
||||
'about' => ['nullable', 'string'],
|
||||
'signature' => ['nullable', 'string'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'avatar' => ['nullable', 'file', 'image', 'max:2048', 'mimes:jpg,jpeg,png,webp', 'mimetypes:image/jpeg,image/png,image/webp'],
|
||||
'emoticon' => ['nullable', 'image', 'max:2048', 'mimes:jpg,jpeg,png,webp'],
|
||||
'photo' => ['nullable', 'image', 'max:2048', 'mimes:jpg,jpeg,png,webp'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('username')) {
|
||||
$this->merge([
|
||||
'username' => UsernamePolicy::normalize((string) $this->input('username')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RequestEmailChangeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('new_email')) {
|
||||
$this->merge([
|
||||
'new_email' => strtolower(trim((string) $this->input('new_email'))),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'new_email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class, 'email')->ignore((int) $this->user()->id),
|
||||
function (string $attribute, mixed $value, \Closure $fail): void {
|
||||
if (strtolower((string) $value) === strtolower((string) $this->user()->email)) {
|
||||
$fail('Please enter a different email address.');
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Http\Requests\UsernameRequest;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateAccountSectionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('username')) {
|
||||
$this->merge([
|
||||
'username' => \App\Support\UsernamePolicy::normalize((string) $this->input('username')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => ['required', ...UsernameRequest::rulesFor((int) $this->user()->id)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateContentPreferencesRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'mature_content_visibility' => ['required', 'in:hide,blur,show'],
|
||||
'mature_content_warning_enabled' => ['required', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateNotificationsSectionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email_notifications' => ['required', 'boolean'],
|
||||
'upload_notifications' => ['required', 'boolean'],
|
||||
'follower_notifications' => ['required', 'boolean'],
|
||||
'comment_notifications' => ['required', 'boolean'],
|
||||
'newsletter' => ['required', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdatePersonalSectionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'birthday' => ['nullable', 'date', 'before:today'],
|
||||
'gender' => ['nullable', 'in:m,f,x,M,F,X'],
|
||||
'country_id' => ['nullable', 'integer', Rule::exists('countries', 'id')],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateProfileSectionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'display_name' => ['required', 'string', 'max:60'],
|
||||
'website' => ['nullable', 'url', 'max:255'],
|
||||
'bio' => ['nullable', 'string', 'max:200'],
|
||||
'signature' => ['nullable', 'string', 'max:1000'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
'avatar' => ['nullable', 'file', 'image', 'max:2048', 'mimes:jpg,jpeg,png,webp', 'mimetypes:image/jpeg,image/png,image/webp'],
|
||||
'remove_avatar' => ['nullable', 'boolean'],
|
||||
'avatar_position' => ['nullable', 'in:top-left,top,top-right,left,center,right,bottom-left,bottom,bottom-right'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class UpdateSecurityPasswordRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'new_password' => ['required', 'confirmed', Password::min(8)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class VerifyEmailChangeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('code')) {
|
||||
$this->merge([
|
||||
'code' => preg_replace('/\D+/', '', (string) $this->input('code')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'code' => ['required', 'digits:6'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Studio;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
final class ApplyArtworkAiAssistRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'title_mode' => ['sometimes', Rule::in(['replace', 'insert'])],
|
||||
'description' => ['sometimes', 'nullable', 'string', 'max:5000'],
|
||||
'description_mode' => ['sometimes', Rule::in(['replace', 'append'])],
|
||||
'tags' => ['sometimes', 'array', 'max:15'],
|
||||
'tags.*' => ['string', 'max:64'],
|
||||
'tag_mode' => ['sometimes', Rule::in(['add', 'replace', 'remove'])],
|
||||
'category_id' => ['sometimes', 'nullable', 'integer', 'exists:categories,id'],
|
||||
'content_type_id' => ['sometimes', 'nullable', 'integer', 'exists:content_types,id'],
|
||||
'similar_actions' => ['sometimes', 'array', 'max:10'],
|
||||
'similar_actions.*.artwork_id' => ['required_with:similar_actions', 'integer'],
|
||||
'similar_actions.*.state' => ['required_with:similar_actions', Rule::in(['ignored', 'reviewed'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Tags;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
final class PopularTagsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // public endpoint
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'limit' => 'nullable|integer|min:1|max:50',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Tags;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
final class TagSearchRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // public endpoint
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'q' => 'nullable|string|max:64',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Uploads;
|
||||
|
||||
use App\Repositories\Uploads\UploadSessionRepository;
|
||||
use App\Services\Uploads\UploadTokenService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class UploadCancelRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
if (! $user) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$sessionId = (string) $this->input('session_id');
|
||||
if ($sessionId === '') {
|
||||
$this->logUnauthorized('missing_session_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$token = $this->header('X-Upload-Token') ?: $this->input('upload_token');
|
||||
if (! $token) {
|
||||
$this->logUnauthorized('missing_token');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$sessions = $this->container->make(UploadSessionRepository::class);
|
||||
$session = $sessions->get($sessionId);
|
||||
if (! $session || $session->userId !== $user->id) {
|
||||
$this->logUnauthorized('not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$tokens = $this->container->make(UploadTokenService::class);
|
||||
$payload = $tokens->get((string) $token);
|
||||
if (! $payload) {
|
||||
$this->logUnauthorized('invalid_token');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
if (($payload['session_id'] ?? null) !== $sessionId) {
|
||||
$this->logUnauthorized('token_session_mismatch');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
if ((int) ($payload['user_id'] ?? 0) !== (int) $user->id) {
|
||||
$this->logUnauthorized('token_user_mismatch');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'session_id' => 'required|uuid',
|
||||
'upload_token' => 'nullable|string|min:40|max:200',
|
||||
];
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Upload cancel unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'session_id' => (string) $this->input('session_id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Uploads;
|
||||
|
||||
use App\Repositories\Uploads\UploadSessionRepository;
|
||||
use App\Services\Uploads\UploadTokenService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class UploadChunkRequest extends FormRequest
|
||||
{
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$uploadError = $this->detectChunkUploadError();
|
||||
|
||||
if ($uploadError !== null && $uploadError !== UPLOAD_ERR_OK) {
|
||||
$this->logChunkUploadFailure($uploadError);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'chunk' => [$this->messageForUploadError($uploadError)],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
if (! $user) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$sessionId = (string) $this->input('session_id');
|
||||
if ($sessionId === '') {
|
||||
$this->logUnauthorized('missing_session_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$token = $this->header('X-Upload-Token') ?: $this->input('upload_token');
|
||||
if (! $token) {
|
||||
$this->logUnauthorized('missing_token');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$sessions = $this->container->make(UploadSessionRepository::class);
|
||||
$session = $sessions->get($sessionId);
|
||||
if (! $session || $session->userId !== $user->id) {
|
||||
$this->logUnauthorized('not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$tokens = $this->container->make(UploadTokenService::class);
|
||||
$payload = $tokens->get((string) $token);
|
||||
if (! $payload) {
|
||||
$this->logUnauthorized('invalid_token');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
if (($payload['session_id'] ?? null) !== $sessionId) {
|
||||
$this->logUnauthorized('token_session_mismatch');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
if ((int) ($payload['user_id'] ?? 0) !== (int) $user->id) {
|
||||
$this->logUnauthorized('token_user_mismatch');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$maxBytes = (int) config('uploads.chunk.max_bytes', 0);
|
||||
$maxKb = $maxBytes > 0 ? (int) ceil($maxBytes / 1024) : 5120;
|
||||
$chunkSizeRule = $maxBytes > 0 ? 'required|integer|min:1|max:' . $maxBytes : 'required|integer|min:1';
|
||||
|
||||
return [
|
||||
'session_id' => 'required|uuid',
|
||||
'offset' => 'required|integer|min:0',
|
||||
'total_size' => 'required|integer|min:1',
|
||||
'chunk_size' => $chunkSizeRule,
|
||||
'chunk' => 'required|file|max:' . $maxKb,
|
||||
'upload_token' => 'nullable|string|min:40|max:200',
|
||||
];
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function detectChunkUploadError(): ?int
|
||||
{
|
||||
$uploadedFile = $this->file('chunk');
|
||||
if ($uploadedFile !== null) {
|
||||
return (int) $uploadedFile->getError();
|
||||
}
|
||||
|
||||
$rawError = data_get($_FILES, 'chunk.error');
|
||||
if ($rawError === null || $rawError === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $rawError;
|
||||
}
|
||||
|
||||
private function messageForUploadError(int $error): string
|
||||
{
|
||||
return match ($error) {
|
||||
UPLOAD_ERR_INI_SIZE => 'The upload chunk exceeded PHP upload_max_filesize. Lower UPLOAD_CHUNK_MAX_BYTES or raise upload_max_filesize/post_max_size.',
|
||||
UPLOAD_ERR_FORM_SIZE => 'The upload chunk exceeded the allowed form upload size.',
|
||||
UPLOAD_ERR_PARTIAL => 'The upload chunk was only partially received. Check Nginx/PHP-FPM request handling and network stability.',
|
||||
UPLOAD_ERR_NO_FILE => 'No upload chunk file was received by PHP.',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'PHP upload_tmp_dir is missing or unavailable. Check the configured temporary upload directory on the server.',
|
||||
UPLOAD_ERR_CANT_WRITE => 'PHP could not write the upload chunk to the temporary directory. Check upload_tmp_dir permissions and free disk space.',
|
||||
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload chunk before Laravel could process it.',
|
||||
default => 'The upload chunk failed before Laravel could read it. Check PHP temporary upload storage and request size limits.',
|
||||
};
|
||||
}
|
||||
|
||||
private function logChunkUploadFailure(int $error): void
|
||||
{
|
||||
$uploadTmpDir = (string) (ini_get('upload_tmp_dir') ?: sys_get_temp_dir() ?: '');
|
||||
$tmpExists = $uploadTmpDir !== '' ? is_dir($uploadTmpDir) : false;
|
||||
$tmpWritable = $tmpExists ? is_writable($uploadTmpDir) : false;
|
||||
|
||||
logger()->warning('Upload chunk failed before validation completed', [
|
||||
'session_id' => (string) $this->input('session_id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
'upload_error' => $error,
|
||||
'upload_error_message' => $this->messageForUploadError($error),
|
||||
'content_length' => $this->server('CONTENT_LENGTH'),
|
||||
'post_max_size' => ini_get('post_max_size'),
|
||||
'upload_max_filesize' => ini_get('upload_max_filesize'),
|
||||
'upload_tmp_dir' => $uploadTmpDir,
|
||||
'tmp_exists' => $tmpExists,
|
||||
'tmp_writable' => $tmpWritable,
|
||||
'raw_files' => isset($_FILES['chunk']) ? [
|
||||
'name' => $_FILES['chunk']['name'] ?? null,
|
||||
'type' => $_FILES['chunk']['type'] ?? null,
|
||||
'size' => $_FILES['chunk']['size'] ?? null,
|
||||
'tmp_name' => $_FILES['chunk']['tmp_name'] ?? null,
|
||||
'error' => $_FILES['chunk']['error'] ?? null,
|
||||
] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Upload chunk unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'session_id' => (string) $this->input('session_id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Uploads;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Repositories\Uploads\UploadSessionRepository;
|
||||
use App\Services\Uploads\UploadTokenService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class UploadFinishRequest extends FormRequest
|
||||
{
|
||||
private ?Artwork $artwork = null;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->user();
|
||||
if (! $user) {
|
||||
$this->logUnauthorized('missing_user');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$sessionId = (string) $this->input('session_id');
|
||||
if ($sessionId === '') {
|
||||
$this->logUnauthorized('missing_session_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$sessions = $this->container->make(UploadSessionRepository::class);
|
||||
$session = $sessions->get($sessionId);
|
||||
if (! $session || $session->userId !== $user->id) {
|
||||
$this->logUnauthorized('not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$token = $this->header('X-Upload-Token') ?: $this->input('upload_token');
|
||||
if ($token) {
|
||||
$tokens = $this->container->make(UploadTokenService::class);
|
||||
$payload = $tokens->get((string) $token);
|
||||
if (! $payload) {
|
||||
$this->logUnauthorized('invalid_token');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
if (($payload['session_id'] ?? null) !== $sessionId) {
|
||||
$this->logUnauthorized('token_session_mismatch');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
if ((int) ($payload['user_id'] ?? 0) !== (int) $user->id) {
|
||||
$this->logUnauthorized('token_user_mismatch');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
}
|
||||
|
||||
$artworkId = (int) $this->input('artwork_id');
|
||||
if ($artworkId <= 0) {
|
||||
$this->logUnauthorized('missing_artwork_id');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$archiveSessionId = (string) $this->input('archive_session_id');
|
||||
if ($archiveSessionId !== '') {
|
||||
$archiveSession = $sessions->get($archiveSessionId);
|
||||
if (! $archiveSession || $archiveSession->userId !== $user->id) {
|
||||
$this->logUnauthorized('archive_session_not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
}
|
||||
|
||||
$additionalScreenshotSessions = $this->input('additional_screenshot_sessions', []);
|
||||
if (is_array($additionalScreenshotSessions)) {
|
||||
foreach ($additionalScreenshotSessions as $index => $payload) {
|
||||
$screenshotSessionId = (string) data_get($payload, 'session_id', '');
|
||||
if ($screenshotSessionId === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$screenshotSession = $sessions->get($screenshotSessionId);
|
||||
if (! $screenshotSession || $screenshotSession->userId !== $user->id) {
|
||||
$this->logUnauthorized('additional_screenshot_session_not_owned_or_missing');
|
||||
logger()->warning('Upload finish additional screenshot session rejected', [
|
||||
'index' => $index,
|
||||
'session_id' => $screenshotSessionId,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$artwork = Artwork::query()->find($artworkId);
|
||||
if (! $artwork || (int) $artwork->user_id !== (int) $user->id) {
|
||||
$this->logUnauthorized('artwork_not_owned_or_missing');
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
$this->artwork = $artwork;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'session_id' => 'required|uuid',
|
||||
'artwork_id' => 'required|integer',
|
||||
'upload_token' => 'nullable|string|min:40|max:200',
|
||||
'file_name' => 'nullable|string|max:255',
|
||||
'archive_session_id' => 'nullable|uuid|different:session_id',
|
||||
'archive_file_name' => 'nullable|string|max:255',
|
||||
'additional_screenshot_sessions' => 'nullable|array|max:4',
|
||||
'additional_screenshot_sessions.*.session_id' => 'required|uuid|distinct|different:session_id|different:archive_session_id',
|
||||
'additional_screenshot_sessions.*.file_name' => 'nullable|string|max:255',
|
||||
];
|
||||
}
|
||||
|
||||
public function artwork(): Artwork
|
||||
{
|
||||
if (! $this->artwork) {
|
||||
$this->denyAsNotFound();
|
||||
}
|
||||
|
||||
return $this->artwork;
|
||||
}
|
||||
|
||||
private function denyAsNotFound(): void
|
||||
{
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
private function logUnauthorized(string $reason): void
|
||||
{
|
||||
logger()->warning('Upload finish unauthorized access', [
|
||||
'reason' => $reason,
|
||||
'session_id' => (string) $this->input('session_id'),
|
||||
'artwork_id' => $this->input('artwork_id'),
|
||||
'user_id' => $this->user()?->id,
|
||||
'ip' => $this->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Uploads;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
final class UploadInitRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'client' => 'nullable|string|max:64',
|
||||
];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user