added a help system, replaced the words "tenant" and "Pwa" with better alternatives. corrected and implemented cron jobs. prepared going live on a coolify-powered system.
This commit is contained in:
@@ -1251,6 +1251,8 @@ class EventPublicController extends BaseController
|
||||
'photos.emotion_id',
|
||||
'photos.task_id',
|
||||
'photos.guest_name',
|
||||
'photos.created_at',
|
||||
'photos.ingest_source',
|
||||
'tasks.title as task_title',
|
||||
])
|
||||
->where('photos.event_id', $eventId)
|
||||
@@ -1258,7 +1260,9 @@ class EventPublicController extends BaseController
|
||||
->limit(60);
|
||||
|
||||
// MyPhotos filter
|
||||
if ($filter === 'myphotos' && $deviceId !== 'anon') {
|
||||
if ($filter === 'photobooth') {
|
||||
$query->where('photos.ingest_source', Photo::SOURCE_PHOTOBOOTH);
|
||||
} elseif ($filter === 'myphotos' && $deviceId !== 'anon') {
|
||||
$query->where('guest_name', $deviceId);
|
||||
}
|
||||
|
||||
@@ -1276,6 +1280,8 @@ class EventPublicController extends BaseController
|
||||
$r->task_title = $this->getLocalized($r->task_title, $locale, 'Unbenannte Aufgabe');
|
||||
}
|
||||
|
||||
$r->ingest_source = $r->ingest_source ?? Photo::SOURCE_UNKNOWN;
|
||||
|
||||
return $r;
|
||||
});
|
||||
$latestPhotoAt = DB::table('photos')->where('event_id', $eventId)->max('created_at');
|
||||
@@ -1495,6 +1501,7 @@ class EventPublicController extends BaseController
|
||||
'file_path' => $url,
|
||||
'thumbnail_path' => $thumbUrl,
|
||||
'likes_count' => 0,
|
||||
'ingest_source' => Photo::SOURCE_GUEST_PWA,
|
||||
|
||||
// Handle emotion_id: prefer explicit ID, fallback to slug lookup, then default
|
||||
'emotion_id' => $this->resolveEmotionId($validated, $eventId),
|
||||
|
||||
129
app/Http/Controllers/Api/HelpController.php
Normal file
129
app/Http/Controllers/Api/HelpController.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Support\Help\HelpRepository;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
use RuntimeException;
|
||||
|
||||
class HelpController extends Controller
|
||||
{
|
||||
public function __construct(private readonly HelpRepository $repository) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
[$audience, $locale] = $this->resolveContext($request);
|
||||
|
||||
$articles = $this->getArticles($audience, $locale)
|
||||
->map(fn ($article) => Arr::only($article, config('help.list_fields')))
|
||||
->values();
|
||||
|
||||
return response()->json([
|
||||
'data' => $articles,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Request $request, string $slug): JsonResponse
|
||||
{
|
||||
[$audience, $locale] = $this->resolveContext($request);
|
||||
|
||||
$article = $this->getArticle($audience, $locale, $slug);
|
||||
|
||||
abort_if(! $article, 404, 'Help article not found.');
|
||||
|
||||
return response()->json([
|
||||
'data' => $article,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, string}
|
||||
*/
|
||||
private function resolveContext(Request $request): array
|
||||
{
|
||||
$this->attemptTokenAuthentication($request);
|
||||
|
||||
$audience = Str::of($request->string('audience', 'guest'))->lower()->value();
|
||||
$locale = Str::of($request->string('locale', config('help.default_locale')))->lower()->value();
|
||||
|
||||
if ($audience === 'admin' && ! $request->user()) {
|
||||
abort(401, 'Authentication required for admin help content.');
|
||||
}
|
||||
|
||||
if (! in_array($audience, config('help.audiences', []), true)) {
|
||||
abort(400, 'Invalid audience supplied.');
|
||||
}
|
||||
|
||||
return [$audience, $locale];
|
||||
}
|
||||
|
||||
private function attemptTokenAuthentication(Request $request): void
|
||||
{
|
||||
if ($request->user()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bearer = $request->bearerToken();
|
||||
|
||||
if (! $bearer) {
|
||||
return;
|
||||
}
|
||||
|
||||
$token = PersonalAccessToken::findToken($bearer);
|
||||
|
||||
if (! $token) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $token->tokenable;
|
||||
|
||||
if (! $user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (method_exists($user, 'withAccessToken')) {
|
||||
$user->withAccessToken($token);
|
||||
}
|
||||
|
||||
Auth::setUser($user);
|
||||
$request->setUserResolver(fn () => $user);
|
||||
}
|
||||
|
||||
private function getArticles(string $audience, string $locale)
|
||||
{
|
||||
try {
|
||||
return $this->repository->list($audience, $locale);
|
||||
} catch (RuntimeException $e) {
|
||||
$fallback = config('help.fallback_locale');
|
||||
|
||||
if ($locale === $fallback) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $this->repository->list($audience, $fallback);
|
||||
}
|
||||
}
|
||||
|
||||
private function getArticle(string $audience, string $locale, string $slug): ?array
|
||||
{
|
||||
try {
|
||||
$article = $this->repository->find($audience, $locale, $slug);
|
||||
} catch (RuntimeException $e) {
|
||||
$fallback = config('help.fallback_locale');
|
||||
|
||||
if ($locale !== $fallback) {
|
||||
return $this->repository->find($audience, $fallback, $slug);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
}
|
||||
@@ -153,14 +153,12 @@ class PhotoController extends Controller
|
||||
$thumbnailPath = $thumbnailRelative;
|
||||
}
|
||||
|
||||
// Create photo record
|
||||
$photo = Photo::create([
|
||||
$photoAttributes = [
|
||||
'event_id' => $event->id,
|
||||
'filename' => $filename,
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'size' => $file->getSize(),
|
||||
'path' => $path,
|
||||
'file_path' => $path,
|
||||
'thumbnail_path' => $thumbnailPath,
|
||||
'width' => null, // Filled below
|
||||
'height' => null,
|
||||
@@ -168,7 +166,17 @@ class PhotoController extends Controller
|
||||
'uploader_id' => null,
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
]);
|
||||
'ingest_source' => Photo::SOURCE_TENANT_ADMIN,
|
||||
];
|
||||
|
||||
if (Photo::supportsFilenameColumn()) {
|
||||
$photoAttributes['filename'] = $filename;
|
||||
}
|
||||
if (Photo::hasColumn('path')) {
|
||||
$photoAttributes['path'] = $path;
|
||||
}
|
||||
|
||||
$photo = Photo::create($photoAttributes);
|
||||
|
||||
// Record primary asset metadata
|
||||
$checksum = hash_file('sha256', $file->getRealPath());
|
||||
@@ -663,19 +671,27 @@ class PhotoController extends Controller
|
||||
$thumbnailPath = $thumbnailRelative;
|
||||
}
|
||||
|
||||
// Create photo record
|
||||
$photo = Photo::create([
|
||||
$photoAttributes = [
|
||||
'event_id' => $event->id,
|
||||
'filename' => $filename,
|
||||
'original_name' => $request->original_name,
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'size' => $file->getSize(),
|
||||
'path' => $path,
|
||||
'file_path' => $path,
|
||||
'thumbnail_path' => $thumbnailPath,
|
||||
'status' => 'pending',
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
]);
|
||||
'ingest_source' => Photo::SOURCE_TENANT_ADMIN,
|
||||
];
|
||||
|
||||
if (Photo::supportsFilenameColumn()) {
|
||||
$photoAttributes['filename'] = $filename;
|
||||
}
|
||||
if (Photo::hasColumn('path')) {
|
||||
$photoAttributes['path'] = $path;
|
||||
}
|
||||
|
||||
$photo = Photo::create($photoAttributes);
|
||||
|
||||
[$width, $height] = getimagesize($file->getRealPath());
|
||||
$photo->update(['width' => $width, 'height' => $height]);
|
||||
|
||||
79
app/Http/Controllers/Api/Tenant/PhotoboothController.php
Normal file
79
app/Http/Controllers/Api/Tenant/PhotoboothController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Tenant;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Tenant\PhotoboothStatusResource;
|
||||
use App\Models\Event;
|
||||
use App\Models\PhotoboothSetting;
|
||||
use App\Services\Photobooth\PhotoboothProvisioner;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PhotoboothController extends Controller
|
||||
{
|
||||
public function __construct(private readonly PhotoboothProvisioner $provisioner) {}
|
||||
|
||||
public function show(Request $request, Event $event): PhotoboothStatusResource
|
||||
{
|
||||
$this->assertEventBelongsToTenant($request, $event);
|
||||
|
||||
return $this->resource($event);
|
||||
}
|
||||
|
||||
public function enable(Request $request, Event $event): JsonResponse
|
||||
{
|
||||
$this->assertEventBelongsToTenant($request, $event);
|
||||
|
||||
$event->loadMissing('tenant');
|
||||
$updated = $this->provisioner->enable($event);
|
||||
|
||||
return response()->json([
|
||||
'message' => __('Photobooth-Zugang aktiviert.'),
|
||||
'data' => $this->resource($updated),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rotate(Request $request, Event $event): JsonResponse
|
||||
{
|
||||
$this->assertEventBelongsToTenant($request, $event);
|
||||
|
||||
$event->loadMissing('tenant');
|
||||
$updated = $this->provisioner->rotate($event);
|
||||
|
||||
return response()->json([
|
||||
'message' => __('Zugangsdaten neu generiert.'),
|
||||
'data' => $this->resource($updated),
|
||||
]);
|
||||
}
|
||||
|
||||
public function disable(Request $request, Event $event): JsonResponse
|
||||
{
|
||||
$this->assertEventBelongsToTenant($request, $event);
|
||||
|
||||
$event->loadMissing('tenant');
|
||||
$updated = $this->provisioner->disable($event);
|
||||
|
||||
return response()->json([
|
||||
'message' => __('Photobooth-Zugang deaktiviert.'),
|
||||
'data' => $this->resource($updated),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function resource(Event $event): PhotoboothStatusResource
|
||||
{
|
||||
return PhotoboothStatusResource::make([
|
||||
'event' => $event->fresh(),
|
||||
'settings' => PhotoboothSetting::current(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertEventBelongsToTenant(Request $request, Event $event): void
|
||||
{
|
||||
$tenantId = (int) $request->attributes->get('tenant_id');
|
||||
|
||||
if ($tenantId !== (int) $event->tenant_id) {
|
||||
abort(403, 'Event gehört nicht zu diesem Tenant.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ class PhotoResource extends JsonResource
|
||||
'is_liked' => false,
|
||||
'uploaded_at' => $this->created_at->toISOString(),
|
||||
'uploader_name' => $this->guest_name ?? null,
|
||||
'ingest_source' => $this->ingest_source,
|
||||
'event' => [
|
||||
'id' => $this->event->id,
|
||||
'name' => $this->event->name,
|
||||
@@ -57,4 +58,4 @@ class PhotoResource extends JsonResource
|
||||
{
|
||||
return url("storage/events/{$this->event->slug}/thumbnails/{$this->filename}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
76
app/Http/Resources/Tenant/PhotoboothStatusResource.php
Normal file
76
app/Http/Resources/Tenant/PhotoboothStatusResource.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Tenant;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\PhotoboothSetting;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PhotoboothStatusResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$payload = $this->resolvePayload();
|
||||
/** @var Event $event */
|
||||
$event = $payload['event'];
|
||||
/** @var PhotoboothSetting $settings */
|
||||
$settings = $payload['settings'];
|
||||
|
||||
$password = $event->getAttribute('plain_photobooth_password') ?? $event->photobooth_password;
|
||||
|
||||
return [
|
||||
'enabled' => (bool) $event->photobooth_enabled,
|
||||
'status' => $event->photobooth_status,
|
||||
'username' => $event->photobooth_username,
|
||||
'password' => $password,
|
||||
'path' => $event->photobooth_path,
|
||||
'ftp_url' => $this->buildFtpUrl($event, $settings, $password),
|
||||
'expires_at' => optional($event->photobooth_expires_at)->toIso8601String(),
|
||||
'rate_limit_per_minute' => (int) $settings->rate_limit_per_minute,
|
||||
'ftp' => [
|
||||
'host' => config('photobooth.ftp.host'),
|
||||
'port' => $settings->ftp_port,
|
||||
'require_ftps' => (bool) $settings->require_ftps,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{event: Event, settings: PhotoboothSetting}
|
||||
*/
|
||||
protected function resolvePayload(): array
|
||||
{
|
||||
$resource = $this->resource;
|
||||
|
||||
if ($resource instanceof Event) {
|
||||
return [
|
||||
'event' => $resource,
|
||||
'settings' => PhotoboothSetting::current(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'event' => $resource['event'] ?? $resource,
|
||||
'settings' => $resource['settings'] ?? PhotoboothSetting::current(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildFtpUrl(Event $event, PhotoboothSetting $settings, ?string $password): ?string
|
||||
{
|
||||
$host = config('photobooth.ftp.host');
|
||||
$username = $event->photobooth_username;
|
||||
|
||||
if (! $host || ! $username || ! $password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$scheme = $settings->require_ftps ? 'ftps' : 'ftp';
|
||||
$port = $settings->ftp_port ?: config('photobooth.ftp.port', 21);
|
||||
|
||||
return sprintf('%s://%s:%s@%s:%d', $scheme, $username, $password, $host, $port);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user