Upload beautify

This commit is contained in:
2026-02-14 15:14:12 +01:00
parent e129618910
commit 79192345e3
249 changed files with 24436 additions and 1021 deletions

View File

@@ -0,0 +1,46 @@
<?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|string|max:120',
'tags' => 'nullable|string|max:200',
'license' => 'nullable|boolean',
];
}
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(),
]);
}
}

View File

@@ -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',
];
}
}

View File

@@ -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',
];
}
}

View File

@@ -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(),
]);
}
}

View File

@@ -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(),
]);
}
}

View File

@@ -2,13 +2,36 @@
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
{
// Authorization is enforced in the controller via ArtworkPolicy.
$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;
}
@@ -21,4 +44,28 @@ class UpdateArtworkRequest extends FormRequest
'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(),
]);
}
}

View File

@@ -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(),
]);
}
}

View File

@@ -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(),
]);
}
}

View File

@@ -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 [
'name' => '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(),
]);
}
}

View File

@@ -16,7 +16,7 @@ class ProfileUpdateRequest extends FormRequest
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'username' => ['sometimes', 'string', 'max:255'],
'email' => [
'required',
'string',
@@ -25,6 +25,21 @@ class ProfileUpdateRequest extends FormRequest
'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', 'max:10'],
'mailing' => ['nullable', 'boolean'],
'notify' => ['nullable', 'boolean'],
'about' => ['nullable', 'string'],
'signature' => ['nullable', 'string'],
'description' => ['nullable', 'string'],
'avatar' => ['nullable', 'image', 'max:2048', 'mimes:jpg,jpeg,png,webp'],
'emoticon' => ['nullable', 'image', 'max:2048', 'mimes:jpg,jpeg,png,webp'],
'photo' => ['nullable', 'image', 'max:2048', 'mimes:jpg,jpeg,png,webp'],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Tags;
use Illuminate\Foundation\Http\FormRequest;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
final class PopularTagsRequest extends FormRequest
{
public function authorize(): bool
{
if (! $this->user()) {
throw new NotFoundHttpException();
}
return true;
}
public function rules(): array
{
return [
'limit' => 'nullable|integer|min:1|max:50',
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Tags;
use Illuminate\Foundation\Http\FormRequest;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
final class TagSearchRequest extends FormRequest
{
public function authorize(): bool
{
if (! $this->user()) {
throw new NotFoundHttpException();
}
return true;
}
public function rules(): array
{
return [
'q' => 'nullable|string|max:64',
];
}
}

View File

@@ -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(),
]);
}
}

View File

@@ -0,0 +1,91 @@
<?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 UploadChunkRequest 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
{
$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 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(),
]);
}
}

View File

@@ -0,0 +1,108 @@
<?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();
}
$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',
];
}
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(),
]);
}
}

View File

@@ -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',
];
}
}

View File

@@ -0,0 +1,87 @@
<?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 UploadStatusRequest extends FormRequest
{
public function authorize(): bool
{
$user = $this->user();
if (! $user) {
$this->logUnauthorized('missing_user');
$this->denyAsNotFound();
}
$sessionId = (string) $this->route('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();
}
}
return true;
}
private function denyAsNotFound(): void
{
throw new NotFoundHttpException();
}
private function logUnauthorized(string $reason): void
{
logger()->warning('Upload status unauthorized access', [
'reason' => $reason,
'session_id' => (string) $this->route('id'),
'user_id' => $this->user()?->id,
'ip' => $this->ip(),
]);
}
public function rules(): array
{
return [
'id' => 'required|uuid',
'upload_token' => 'nullable|string|min:40|max:200',
];
}
protected function prepareForValidation(): void
{
$this->merge([
'id' => $this->route('id'),
]);
}
}