12-05-2026 Frontend dev
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Kevin Adametz 2026-05-12 18:32:33 +02:00
parent 405df0a122
commit 5b8bdf4182
779 changed files with 480564 additions and 6241 deletions

View file

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\CategoryResource;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class CategoryController extends Controller
{
public function index(Request $request): AnonymousResourceCollection
{
abort_unless($request->user()->tokenCan('press-releases:read'), 403);
$categories = Category::query()
->with('translations')
->where('is_active', true)
->orderBy('id')
->get();
return CategoryResource::collection($categories);
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\CompanyResource;
use App\Models\Company;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class CompanyController extends Controller
{
public function index(Request $request): AnonymousResourceCollection
{
abort_unless($request->user()->tokenCan('companies:read'), 403);
$companies = Company::withoutGlobalScopes()
->where(function ($query) use ($request): void {
$query->where('owner_user_id', $request->user()->id)
->orWhereHas('users', fn ($users) => $users->whereKey($request->user()->id));
})
->orderBy('name')
->paginate(min((int) $request->query('per_page', 25), 100));
return CompanyResource::collection($companies);
}
public function store(Request $request)
{
abort(404);
}
public function show(Request $request, int $company): CompanyResource
{
abort_unless($request->user()->tokenCan('companies:read'), 403);
$company = Company::withoutGlobalScopes()
->whereKey($company)
->where(function ($query) use ($request): void {
$query->where('owner_user_id', $request->user()->id)
->orWhereHas('users', fn ($users) => $users->whereKey($request->user()->id));
})
->first();
abort_unless($company !== null, 403);
return CompanyResource::make($company);
}
public function update(Request $request, string $id)
{
abort(404);
}
public function destroy(string $id)
{
abort(404);
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\SubscribeNewsletterRequest;
use App\Models\NewsletterSubscription;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
class NewsletterSubscriptionController extends Controller
{
public function store(SubscribeNewsletterRequest $request): JsonResponse
{
$validated = $request->validated();
$subscription = NewsletterSubscription::withoutGlobalScopes()->updateOrCreate(
[
'portal' => $validated['portal'],
'email' => mb_strtolower($validated['email']),
],
[
'user_id' => $request->user()->id,
'salutation_key' => $validated['salutation_key'] ?? null,
'first_name' => $validated['first_name'] ?? null,
'last_name' => $validated['last_name'] ?? null,
'ip_address' => $request->ip(),
'is_confirmed' => false,
'confirmation_token' => Str::random(32),
'subscribed_at' => now(),
'unsubscribed_at' => null,
],
);
return response()->json([
'message' => 'Newsletter subscription created.',
'data' => [
'id' => $subscription->id,
'portal' => $subscription->portal->value,
'email' => $subscription->email,
'is_confirmed' => $subscription->is_confirmed,
],
], 201);
}
}

View file

@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\StorePressReleaseRequest;
use App\Http\Requests\Api\V1\UpdatePressReleaseRequest;
use App\Http\Resources\PressReleaseResource;
use App\Models\Company;
use App\Models\PressRelease;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
class PressReleaseController extends Controller
{
public function index(Request $request): AnonymousResourceCollection
{
abort_unless($request->user()->tokenCan('press-releases:read'), 403);
$pressReleases = PressRelease::withoutGlobalScopes()
->where('user_id', $request->user()->id)
->with(['company:id,name,portal,legacy_portal,legacy_id', 'category.translations', 'images'])
->when($request->query('status'), fn ($query, string $status) => $query->where('status', $status))
->latest()
->paginate(min((int) $request->query('per_page', 25), 100));
return PressReleaseResource::collection($pressReleases);
}
public function store(StorePressReleaseRequest $request): JsonResponse
{
$validated = $request->validated();
$company = $this->findOwnedCompany((int) $validated['company_id'], $request);
abort_unless($company !== null, 403);
$pressRelease = PressRelease::withoutGlobalScopes()->create([
...$validated,
'uuid' => (string) Str::uuid(),
'user_id' => $request->user()->id,
'portal' => $company->portal->value,
'slug' => $this->uniqueSlug(
Str::slug($validated['title']),
$company->portal->value,
$validated['language'],
),
'status' => $validated['status'] ?? 'draft',
]);
return PressReleaseResource::make(
$pressRelease->load(['company:id,name,portal,legacy_portal,legacy_id', 'category.translations', 'images'])
)->response()->setStatusCode(201);
}
public function show(Request $request, int $pressRelease): PressReleaseResource
{
abort_unless($request->user()->tokenCan('press-releases:read'), 403);
$pressRelease = $this->findOwnedPressRelease($pressRelease, $request);
abort_unless($pressRelease !== null, 403);
return PressReleaseResource::make(
$pressRelease->load(['company:id,name,portal,legacy_portal,legacy_id', 'category.translations', 'images'])
);
}
public function update(UpdatePressReleaseRequest $request, int $pressRelease): PressReleaseResource|JsonResponse
{
$pressRelease = $this->findOwnedPressRelease($pressRelease, $request);
abort_unless($pressRelease !== null, 403);
if (! in_array($pressRelease->status->value, ['draft', 'rejected'], true)) {
return response()->json([
'message' => 'Only draft or rejected press releases may be edited.',
], 409);
}
$validated = $request->validated();
$company = isset($validated['company_id'])
? $this->findOwnedCompany((int) $validated['company_id'], $request)
: $this->findOwnedCompany((int) $pressRelease->company_id, $request);
abort_unless($company !== null, 403);
$language = $validated['language'] ?? $pressRelease->language;
$title = $validated['title'] ?? $pressRelease->title;
$portal = $company->portal->value;
$pressRelease->fill([
...$validated,
'portal' => $portal,
]);
if (
array_key_exists('title', $validated)
|| array_key_exists('language', $validated)
|| array_key_exists('company_id', $validated)
) {
$pressRelease->slug = $this->uniqueSlug(Str::slug($title), $portal, $language, $pressRelease->id);
}
$pressRelease->save();
return PressReleaseResource::make(
$pressRelease->load(['company:id,name,portal,legacy_portal,legacy_id', 'category.translations', 'images'])
);
}
public function destroy(Request $request, int $pressRelease): JsonResponse|Response
{
abort_unless($request->user()->tokenCan('press-releases:write'), 403);
$pressRelease = $this->findOwnedPressRelease($pressRelease, $request);
abort_unless($pressRelease !== null, 403);
if ($pressRelease->status->value === 'published') {
return response()->json([
'message' => 'Published press releases cannot be deleted via API.',
], 409);
}
$pressRelease->delete();
return response()->noContent();
}
private function findOwnedCompany(int $companyId, Request $request): ?Company
{
return Company::withoutGlobalScopes()
->whereKey($companyId)
->where(function ($query) use ($request): void {
$query->where('owner_user_id', $request->user()->id)
->orWhereHas('users', fn ($users) => $users->whereKey($request->user()->id));
})
->first();
}
private function findOwnedPressRelease(int $pressReleaseId, Request $request): ?PressRelease
{
return PressRelease::withoutGlobalScopes()
->whereKey($pressReleaseId)
->where('user_id', $request->user()->id)
->first();
}
private function uniqueSlug(string $baseSlug, string $portal, string $language, ?int $excludeId = null): string
{
$slug = $baseSlug !== '' ? $baseSlug : Str::random(8);
$candidate = $slug;
$suffix = 2;
while (
PressRelease::withoutGlobalScopes()
->where('portal', $portal)
->where('language', $language)
->where('slug', $candidate)
->when($excludeId !== null, fn ($query) => $query->whereKeyNot($excludeId))
->exists()
) {
$candidate = "{$slug}-{$suffix}";
$suffix++;
}
return $candidate;
}
}

View file

@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Enums\PressReleaseStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\StorePressReleaseImageRequest;
use App\Http\Resources\PressReleaseImageResource;
use App\Models\PressRelease;
use App\Models\PressReleaseImage;
use App\Services\Image\ImageService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;
use Illuminate\Http\UploadedFile;
use RuntimeException;
class PressReleaseImageController extends Controller
{
public function __construct(private readonly ImageService $imageService) {}
public function index(Request $request, int $pressRelease): AnonymousResourceCollection
{
abort_unless($request->user()->tokenCan('press-releases:read'), 403);
$pressRelease = $this->findOwnedPressRelease($pressRelease, $request);
abort_unless($pressRelease !== null, 403);
return PressReleaseImageResource::collection(
$pressRelease->images()->orderBy('sort_order')->orderBy('id')->get()
);
}
public function store(StorePressReleaseImageRequest $request, int $pressRelease): JsonResponse
{
$pressRelease = $this->findOwnedPressRelease($pressRelease, $request);
abort_unless($pressRelease !== null, 403);
if (! $this->canChangeImages($pressRelease)) {
return response()->json([
'message' => 'Only draft or rejected press releases may be edited.',
], 409);
}
$validated = $request->validated();
/** @var UploadedFile $file */
$file = $request->file('image');
try {
$stored = $this->imageService->storePressReleaseImage($file, $pressRelease->id);
} catch (RuntimeException $exception) {
return response()->json([
'message' => $exception->getMessage(),
], 422);
}
if ((bool) ($validated['is_preview'] ?? false)) {
$pressRelease->images()->update(['is_preview' => false]);
}
$image = $pressRelease->images()->create([
'disk' => 'public',
'path' => $stored['path'],
'variants' => $stored['variants'],
'title' => $validated['title'] ?? null,
'description' => $validated['description'] ?? null,
'copyright' => $validated['copyright'] ?? null,
'is_preview' => (bool) ($validated['is_preview'] ?? false),
'sort_order' => ((int) $pressRelease->images()->max('sort_order')) + 1,
'width' => $stored['width'],
'height' => $stored['height'],
'mime' => $stored['mime'],
]);
return PressReleaseImageResource::make($image)
->response()
->setStatusCode(201);
}
public function destroy(Request $request, int $pressReleaseImage): Response|JsonResponse
{
abort_unless($request->user()->tokenCan('press-release-images:write'), 403);
$image = PressReleaseImage::query()
->whereKey($pressReleaseImage)
->whereHas('pressRelease', fn ($query) => $query->withoutGlobalScopes()->where('user_id', $request->user()->id))
->with(['pressRelease' => fn ($query) => $query->withoutGlobalScopes()])
->first();
abort_unless($image !== null, 403);
if (! $this->canChangeImages($image->pressRelease)) {
return response()->json([
'message' => 'Only draft or rejected press releases may be edited.',
], 409);
}
$this->imageService->deletePressReleaseImage($image->disk, $image->path, $image->variants);
$image->delete();
return response()->noContent();
}
private function findOwnedPressRelease(int $pressReleaseId, Request $request): ?PressRelease
{
return PressRelease::withoutGlobalScopes()
->whereKey($pressReleaseId)
->where('user_id', $request->user()->id)
->first();
}
private function canChangeImages(PressRelease $pressRelease): bool
{
$status = $pressRelease->status instanceof PressReleaseStatus
? $pressRelease->status->value
: (string) $pressRelease->status;
return in_array($status, [PressReleaseStatus::Draft->value, PressReleaseStatus::Rejected->value], true);
}
}