sparkbooth anbindung gefixt

This commit is contained in:
2025-12-07 17:39:23 +01:00
parent 08ee2205f5
commit b0d835d142
10 changed files with 597 additions and 40 deletions

View File

@@ -50,6 +50,10 @@ class SparkboothConnections extends Page implements HasTable
->label('Upload-Pfad')
->copyable()
->toggleable(),
TextColumn::make('sparkbooth_username')
->label('Benutzername')
->copyable()
->toggleable(),
TextColumn::make('created_at')
->label('Angelegt')
->since()
@@ -62,7 +66,7 @@ class SparkboothConnections extends Page implements HasTable
->color('primary')
->modalHeading('Upload-Zugangsdaten')
->modalSubmitAction(false)
->modalCancelActionLabel('Schließen')
->modalCancelActionLabel('Schliessen')
->modalContent(function (Gallery $record) {
$plainToken = $record->regenerateUploadToken();
@@ -71,11 +75,14 @@ class SparkboothConnections extends Page implements HasTable
'upload_token' => $plainToken,
'upload_url' => route('api.sparkbooth.upload'),
'gallery_url' => route('gallery.show', $record),
'sparkbooth_username' => $record->sparkbooth_username,
'sparkbooth_password' => $record->sparkbooth_password,
'response_format' => $record->sparkbooth_response_format,
];
Notification::make()
->title('Upload-Token wurde erneuert.')
->body('Bitte verwende den neuen Token in Sparkbooth.')
->title('Zugangsdaten aktualisiert.')
->body('Der Upload-Token wurde erneuert. Username/Passwort bleiben unveraendert.')
->success()
->send();
@@ -83,6 +90,9 @@ class SparkboothConnections extends Page implements HasTable
}),
])
->emptyStateHeading('Keine Sparkbooth-Verbindungen')
->emptyStateDescription('Lege eine neue Verbindung an oder aktiviere Uploads für eine Galerie.');
->emptyStateDescription('Lege eine neue Verbindung an oder aktiviere Uploads fuer eine Galerie.');
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Pages;
use App\Models\Gallery;
use BackedEnum;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
@@ -62,6 +63,23 @@ class SparkboothSetup extends Page implements HasForms
Toggle::make('upload_enabled')
->label('Uploads aktivieren')
->default(true),
TextInput::make('sparkbooth_username')
->label('Sparkbooth Benutzername')
->helperText('Wird in Sparkbooth unter „Username“ eingetragen. Erlaubt sind Buchstaben, Zahlen sowie ._-')
->default(fn (): string => 'spark-'.Str::lower(Str::random(6)))
->required()
->maxLength(64)
->rule('regex:/^[A-Za-z0-9._-]+$/')
->unique(table: Gallery::class, column: 'sparkbooth_username'),
Select::make('sparkbooth_response_format')
->label('Standard-Antwortformat')
->helperText('Sparkbooth kann JSON oder XML erwarten. JSON ist empfohlen.')
->options([
'json' => 'JSON',
'xml' => 'XML',
])
->default('json')
->required(),
])
->statePath('data');
}
@@ -70,6 +88,9 @@ class SparkboothSetup extends Page implements HasForms
{
$data = $this->form->getState();
$sparkboothUsername = $this->normalizeUsername($data['sparkbooth_username'] ?? '');
$sparkboothPassword = Str::random(24);
$gallery = new Gallery([
'name' => $data['name'],
'title' => $data['title'],
@@ -78,6 +99,9 @@ class SparkboothSetup extends Page implements HasForms
'allow_ai_styles' => (bool) $data['allow_ai_styles'],
'allow_print' => (bool) $data['allow_print'],
'upload_enabled' => (bool) $data['upload_enabled'],
'sparkbooth_username' => $sparkboothUsername,
'sparkbooth_password' => $sparkboothPassword,
'sparkbooth_response_format' => $data['sparkbooth_response_format'] ?? 'json',
]);
$gallery->slug = Str::uuid()->toString();
@@ -91,6 +115,9 @@ class SparkboothSetup extends Page implements HasForms
'upload_token' => $plainToken,
'upload_url' => route('api.sparkbooth.upload'),
'gallery_url' => route('gallery.show', $gallery),
'sparkbooth_username' => $sparkboothUsername,
'sparkbooth_password' => $sparkboothPassword,
'response_format' => $gallery->sparkbooth_response_format,
];
Notification::make()
@@ -114,4 +141,11 @@ class SparkboothSetup extends Page implements HasForms
->url(route('filament.admin.pages.dashboard')),
];
}
protected function normalizeUsername(string $value): string
{
$clean = preg_replace('/[^A-Za-z0-9._-]/', '', $value) ?? '';
return Str::of($clean)->lower()->trim()->value();
}
}

View File

@@ -6,59 +6,110 @@ use App\Http\Controllers\Controller;
use App\Models\Gallery;
use App\Models\Image;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
class SparkboothUploadController extends Controller
{
private const ALLOWED_MIMES = [
'image/jpeg',
'image/png',
'image/gif',
'image/bmp',
'image/webp',
];
private const MAX_FILE_SIZE = 10240 * 1024; // 10 MB
public function store(Request $request)
{
$request->validate([
'token' => ['required', 'string'],
'file' => ['required', 'file', 'mimes:jpeg,png,gif,bmp,webp', 'max:10240'],
'filename' => ['nullable', 'string'],
]);
$this->validateRequest($request);
$gallery = $this->resolveGalleryByToken($request->string('token'));
[$gallery, $authMethod] = $this->resolveGallery($request);
if (! $gallery) {
return response()->json(['error' => 'Invalid token.'], Response::HTTP_FORBIDDEN);
return $this->respondError($request, 'Invalid credentials.', Response::HTTP_FORBIDDEN);
}
if (! $gallery->upload_enabled) {
return response()->json(['error' => 'Uploads are disabled for this gallery.'], Response::HTTP_FORBIDDEN);
return $this->respondError($request, 'Uploads are disabled for this gallery.', Response::HTTP_FORBIDDEN, $gallery);
}
if ($gallery->upload_token_expires_at && now()->greaterThanOrEqualTo($gallery->upload_token_expires_at)) {
return response()->json(['error' => 'Upload token expired.'], Response::HTTP_FORBIDDEN);
if ($authMethod === 'token'
&& $gallery->upload_token_expires_at
&& now()->greaterThanOrEqualTo($gallery->upload_token_expires_at)
) {
return $this->respondError($request, 'Upload token expired.', Response::HTTP_FORBIDDEN, $gallery);
}
$file = $request->file('file');
$safeName = $this->buildFilename($file->getClientOriginalExtension(), $request->input('filename'));
$relativePath = trim($gallery->images_path, '/').'/'.$safeName;
$destinationPath = public_path('storage/'.dirname($relativePath));
[$file, $base64Payload] = $this->extractMediaPayload($request);
if (! File::exists($destinationPath)) {
File::makeDirectory($destinationPath, 0755, true);
if (! $file && $base64Payload === null) {
return $this->respondError($request, 'No media payload provided.', Response::HTTP_BAD_REQUEST, $gallery);
}
$file->move($destinationPath, basename($relativePath));
try {
[$relativePath, $publicUrl] = $this->persistMedia(
$gallery,
$file,
$base64Payload,
$request->input('filename')
);
} catch (RuntimeException $exception) {
return $this->respondError($request, $exception->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY, $gallery);
}
$image = Image::create([
Image::create([
'gallery_id' => $gallery->id,
'path' => $relativePath,
'is_public' => true,
]);
return response()->json([
'message' => 'Upload ok',
'image_id' => $image->id,
'url' => asset('storage/'.$relativePath),
return $this->respondSuccess($request, $publicUrl, $gallery);
}
private function validateRequest(Request $request): void
{
$request->validate([
'token' => ['nullable', 'string'],
'username' => ['nullable', 'string'],
'password' => ['nullable', 'string'],
'media' => ['nullable'],
'file' => ['nullable', 'file', 'mimes:jpeg,png,gif,bmp,webp', 'max:10240'],
'filename' => ['nullable', 'string'],
'name' => ['nullable', 'string'],
'email' => ['nullable', 'string'],
'message' => ['nullable', 'string'],
'response_format' => ['nullable', 'in:json,xml'],
]);
}
/**
* @return array{0: ?Gallery, 1: ?string}
*/
private function resolveGallery(Request $request): array
{
if ($request->filled('token')) {
return [$this->resolveGalleryByToken($request->string('token')), 'token'];
}
if ($request->filled('username')) {
return [
$this->resolveGalleryByCredentials(
$request->string('username'),
$request->string('password')
),
'credentials',
];
}
return [null, null];
}
private function resolveGalleryByToken(string $token): ?Gallery
{
$galleries = Gallery::query()
@@ -75,13 +126,226 @@ class SparkboothUploadController extends Controller
return null;
}
private function buildFilename(string $extension, ?string $preferred = null): string
private function resolveGalleryByCredentials(?string $username, ?string $password): ?Gallery
{
if (blank($username) || blank($password)) {
return null;
}
$normalized = Str::of($username)->lower()->trim()->value();
$gallery = Gallery::query()
->whereNotNull('sparkbooth_username')
->where('sparkbooth_username', $normalized)
->first();
if (! $gallery) {
return null;
}
if (! hash_equals((string) $gallery->sparkbooth_password, (string) $password)) {
return null;
}
return $gallery;
}
/**
* @return array{0: ?UploadedFile, 1: ?string}
*/
private function extractMediaPayload(Request $request): array
{
$file = $request->file('media') ?? $request->file('file');
if ($file) {
return [$file, null];
}
$payload = $request->input('media');
if ($payload === null || $payload === '') {
return [null, null];
}
return [null, $payload];
}
/**
* @return array{0: string, 1: string}
*/
private function persistMedia(
Gallery $gallery,
?UploadedFile $file,
?string $base64Payload,
?string $preferredFilename
): array {
$directory = trim($gallery->images_path, '/');
if ($directory === '') {
$directory = 'uploads';
}
if ($file) {
$extension = $file->getClientOriginalExtension();
$filename = $this->buildFilename($extension, $preferredFilename);
$relativePath = $directory.'/'.$filename;
$file->storeAs($directory, $filename, 'public');
return [$relativePath, asset('storage/'.$relativePath)];
}
if ($base64Payload === null) {
throw new RuntimeException('No media payload provided.');
}
[$binary, $extension] = $this->decodeBase64Media($base64Payload);
$filename = $this->buildFilename($extension, $preferredFilename);
$relativePath = $directory.'/'.$filename;
if (! Storage::disk('public')->put($relativePath, $binary)) {
throw new RuntimeException('Failed to store uploaded image.');
}
return [$relativePath, asset('storage/'.$relativePath)];
}
/**
* @return array{0: string, 1: string}
*/
private function decodeBase64Media(string $payload): array
{
$mime = null;
if (preg_match('/^data:(image\\/[^;]+);base64,(.+)$/', $payload, $matches)) {
$mime = $matches[1];
$payload = $matches[2];
}
$binary = base64_decode($payload, true);
if ($binary === false) {
throw new RuntimeException('Invalid media payload.');
}
if (strlen($binary) > self::MAX_FILE_SIZE) {
throw new RuntimeException('File too large.');
}
if (! $mime) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = $finfo ? finfo_buffer($finfo, $binary) : null;
if ($finfo) {
finfo_close($finfo);
}
}
if (! $mime || ! $this->isAllowedMime($mime)) {
throw new RuntimeException('Unsupported image type.');
}
$extension = $this->extensionFromMime($mime) ?? 'jpg';
return [$binary, $extension];
}
private function isAllowedMime(string $mime): bool
{
return in_array(strtolower($mime), self::ALLOWED_MIMES, true);
}
private function extensionFromMime(string $mime): ?string
{
return match (strtolower($mime)) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/bmp' => 'bmp',
'image/webp' => 'webp',
default => null,
};
}
private function buildFilename(?string $extension, ?string $preferred = null): string
{
$extension = strtolower($extension ?: 'jpg');
$base = $preferred
? Str::slug(pathinfo($preferred, PATHINFO_FILENAME))
: 'sparkbooth_'.now()->format('Ymd_His').'_'.Str::random(6);
if ($base === '') {
$base = 'sparkbooth_'.now()->format('Ymd_His').'_'.Str::random(6);
}
return $base.'.'.$extension;
}
private function respondSuccess(Request $request, string $url, ?Gallery $gallery = null)
{
$format = $this->determineResponseFormat($request, $gallery);
if ($format === 'xml') {
$body = $this->buildXmlResponse('ok', $url);
return response($body, Response::HTTP_OK)->header('Content-Type', 'application/xml');
}
return response()->json([
'status' => true,
'error' => null,
'url' => $url,
]);
}
private function respondError(Request $request, string $message, int $status, ?Gallery $gallery = null)
{
$format = $this->determineResponseFormat($request, $gallery);
if ($format === 'xml') {
$body = $this->buildXmlResponse('fail', null, $message);
return response($body, $status)->header('Content-Type', 'application/xml');
}
return response()->json([
'status' => false,
'error' => $message,
'url' => null,
], $status);
}
private function determineResponseFormat(Request $request, ?Gallery $gallery = null): string
{
$candidate = Str::of((string) $request->input('response_format'))->lower()->value();
if (in_array($candidate, ['json', 'xml'], true)) {
return $candidate;
}
$accept = Str::of((string) $request->header('Accept'))->lower()->value();
if (str_contains($accept, 'application/xml') || str_contains($accept, 'text/xml')) {
return 'xml';
}
if ($gallery && in_array($gallery->sparkbooth_response_format, ['json', 'xml'], true)) {
return $gallery->sparkbooth_response_format;
}
return 'json';
}
private function buildXmlResponse(string $status, ?string $url = null, ?string $errorMessage = null): string
{
if ($status === 'ok') {
$urlAttr = $url ? ' url="'.e($url).'"' : '';
return '<?xml version="1.0" encoding="UTF-8"?><rsp status="ok"'.$urlAttr.' />';
}
$errorAttr = $errorMessage ? '<err msg="'.e($errorMessage).'" />' : '';
return '<?xml version="1.0" encoding="UTF-8"?><rsp status="fail">'.$errorAttr.'</rsp>';
}
}

View File

@@ -26,6 +26,9 @@ class Gallery extends Model
'upload_enabled',
'upload_token_hash',
'upload_token_expires_at',
'sparkbooth_username',
'sparkbooth_password',
'sparkbooth_response_format',
];
protected function casts(): array
@@ -39,6 +42,8 @@ class Gallery extends Model
'access_duration_minutes' => 'int',
'upload_enabled' => 'bool',
'upload_token_expires_at' => 'datetime',
'sparkbooth_password' => 'encrypted',
'sparkbooth_response_format' => 'string',
];
}
@@ -60,4 +65,13 @@ class Gallery extends Model
return $token;
}
public function regenerateSparkboothPassword(): string
{
$password = \Illuminate\Support\Str::random(24);
$this->sparkbooth_password = $password;
$this->save();
return $password;
}
}

View File

@@ -35,6 +35,9 @@ class GalleryFactory extends Factory
'upload_enabled' => false,
'upload_token_hash' => null,
'upload_token_expires_at' => null,
'sparkbooth_username' => 'spark-'.$this->faker->regexify('[a-z0-9]{6}'),
'sparkbooth_password' => 'pw-'.$this->faker->regexify('[A-Za-z0-9]{10}'),
'sparkbooth_response_format' => 'json',
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('galleries', function (Blueprint $table): void {
$table->string('sparkbooth_username')->nullable()->after('upload_token_expires_at');
$table->text('sparkbooth_password')->nullable()->after('sparkbooth_username');
$table->string('sparkbooth_response_format', 10)->default('json')->after('sparkbooth_password');
$table->unique('sparkbooth_username');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('galleries', function (Blueprint $table): void {
$table->dropUnique(['sparkbooth_username']);
$table->dropColumn([
'sparkbooth_username',
'sparkbooth_password',
'sparkbooth_response_format',
]);
});
}
};

View File

@@ -33,6 +33,9 @@ class GallerySeeder extends Seeder
'upload_enabled' => false,
'upload_token_hash' => null,
'upload_token_expires_at' => null,
'sparkbooth_username' => null,
'sparkbooth_password' => null,
'sparkbooth_response_format' => 'json',
]
);
}

View File

@@ -6,15 +6,44 @@
<div class="grid gap-4 md:grid-cols-2">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Upload Token</p>
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $upload_token }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Trage diesen Token in Sparkbooth unter Upload Secret“ ein.</p>
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Benutzername</p>
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $sparkbooth_username ?? '—' }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen im Sparkbooth Custom Upload Dialog unter „Username“.</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Passwort</p>
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $sparkbooth_password ?? '—' }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen unter „Password“.</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Galerie-Link</p>
<p class="mt-1 break-all font-semibold text-gray-900 dark:text-white">{{ $gallery_url }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Slug: {{ $gallery['slug'] }}, Pfad: storage/{{ $gallery['images_path'] }}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Antwortformat</p>
<p class="mt-1 font-semibold text-gray-900 dark:text-white">{{ strtoupper($response_format ?? 'JSON') }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Muss mit „JSON Response“ oder „XML Response“ in Sparkbooth übereinstimmen.</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Legacy Upload Token</p>
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $upload_token }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Optionales Secret (Feld „token“) für ältere Integrationen.</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Hinweise</p>
<ul class="mt-2 list-disc space-y-1 pl-4 text-xs text-gray-600 dark:text-gray-300">
<li>Uploader „Custom Upload“ wählen.</li>
<li>Username &amp; Password wie oben eintragen.</li>
<li>Falls JSON Response aktiviert ist, hier ebenfalls JSON wählen (gleiches gilt für XML).</li>
</ul>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">

View File

@@ -15,15 +15,44 @@
<div class="mt-4 grid gap-4 md:grid-cols-2">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Upload Token</p>
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $result['upload_token'] }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Trage diesen Token in Sparkbooth unter „Upload Secret“ ein.</p>
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Benutzername</p>
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $result['sparkbooth_username'] }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen in Sparkbooth Settings Upload Custom Upload Username.</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Passwort</p>
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $result['sparkbooth_password'] }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Eintragen in Sparkbooth unter „Password“.</p>
</div>
</div>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Galerie-Link</p>
<p class="mt-1 break-all font-semibold text-gray-900 dark:text-white">{{ $result['gallery_url'] }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Slug: {{ $result['gallery']['slug'] }}, Pfad: storage/{{ $result['gallery']['images_path'] }}</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Standard-Antwortformat</p>
<p class="mt-1 font-semibold text-gray-900 dark:text-white">{{ strtoupper($result['response_format']) }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Muss mit der Auswahl „JSON Response“ oder „XML Response“ in Sparkbooth übereinstimmen.</p>
</div>
</div>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Legacy Upload Token</p>
<p class="mt-1 break-all font-mono text-sm text-gray-900 dark:text-white">{{ $result['upload_token'] }}</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-300">Optional: Für bestehende Integrationen nutzbar (Feld „token“).</p>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Hinweise</p>
<ul class="mt-2 list-disc space-y-1 pl-4 text-xs text-gray-600 dark:text-gray-300">
<li>Uploader: „Custom Upload“ wählen.</li>
<li>URL: {{ $result['upload_url'] }}</li>
<li>Username/Password eintragen, optional Message.</li>
</ul>
</div>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2">
@@ -41,14 +70,30 @@
</div>
</div>
<div class="mt-6">
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Beispiel (Custom Upload)</p>
<div class="mt-6 space-y-4">
<div>
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Sparkbooth Beispiel (Custom Upload)</p>
<pre class="mt-2 rounded-xl border border-gray-200 bg-gray-900 p-4 text-xs text-gray-100 dark:border-white/10">
POST {{ $result['upload_url'] }}
Content-Type: multipart/form-data
token={{ $result['upload_token'] }}
file=@your-photo.jpg
curl -X POST {{ $result['upload_url'] }} \
-F "media=@your-photo.jpg" \
-F "username={{ $result['sparkbooth_username'] }}" \
-F "password={{ $result['sparkbooth_password'] }}" \
-F "name=Guest" \
-F "message=Thanks!"
</pre>
</div>
<div>
<p class="text-xs uppercase tracking-[0.3em] text-gray-500 dark:text-gray-300">Erwartete Server Responses</p>
<div class="grid gap-4 md:grid-cols-2">
<pre class="rounded-xl border border-gray-200 bg-gray-900 p-4 text-xs text-gray-100 dark:border-white/10">{
"status": true,
"error": null,
"url": "{{ $result['gallery_url'] }}"
}</pre>
<pre class="rounded-xl border border-gray-200 bg-gray-900 p-4 text-xs text-gray-100 dark:border-white/10">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;rsp status="ok" url="{{ $result['gallery_url'] }}" /&gt;</pre>
</div>
</div>
</div>
</div>
@endif

View File

@@ -0,0 +1,117 @@
<?php
namespace Tests\Feature;
use App\Models\Gallery;
use App\Models\Image;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class SparkboothUploadTest extends TestCase
{
use RefreshDatabase;
public function test_json_upload_with_credentials_succeeds(): void
{
Storage::fake('public');
$gallery = Gallery::factory()->create([
'upload_enabled' => true,
'images_path' => 'uploads/test',
'sparkbooth_username' => 'spark-user',
'sparkbooth_password' => 'secret-123',
'sparkbooth_response_format' => 'json',
]);
$file = UploadedFile::fake()->image('photo.jpg', 800, 600);
$response = $this->postJson(route('api.sparkbooth.upload'), [
'username' => 'spark-user',
'password' => 'secret-123',
'media' => $file,
'name' => 'Guest',
'message' => 'Hallo',
]);
$response->assertOk()
->assertJson([
'status' => true,
'error' => null,
])
->assertJsonStructure(['url']);
$image = Image::first();
$this->assertNotNull($image);
$this->assertSame($gallery->id, $image->gallery_id);
$this->assertStringStartsWith('uploads/test/sparkbooth_', $image->path);
Storage::disk('public')->assertExists($image->path);
}
public function test_xml_response_for_invalid_credentials(): void
{
Storage::fake('public');
Gallery::factory()->create([
'upload_enabled' => true,
'images_path' => 'uploads/test',
'sparkbooth_username' => 'spark-user',
'sparkbooth_password' => 'secret-123',
'sparkbooth_response_format' => 'xml',
]);
$file = UploadedFile::fake()->image('photo.jpg', 800, 600);
$response = $this->post(route('api.sparkbooth.upload'), [
'username' => 'spark-user',
'password' => 'wrong',
'media' => $file,
'response_format' => 'xml',
]);
$response->assertStatus(403);
$response->assertHeader('Content-Type', 'application/xml');
$this->assertStringContainsString('<rsp status="fail">', $response->getContent());
}
public function test_base64_upload_with_token_succeeds(): void
{
Storage::fake('public');
$gallery = Gallery::factory()->create([
'upload_enabled' => true,
'images_path' => 'uploads/base64',
'sparkbooth_response_format' => 'json',
]);
$token = 'tokentest123';
$gallery->setUploadToken($token);
$gallery->save();
$fake = UploadedFile::fake()->image('inline.png', 20, 20);
$binary = base64_encode(file_get_contents($fake->getRealPath()));
$dataUri = 'data:image/png;base64,'.$binary;
$response = $this->postJson(route('api.sparkbooth.upload'), [
'token' => $token,
'media' => $dataUri,
'filename' => 'custom.png',
]);
$response->assertOk()
->assertJson([
'status' => true,
'error' => null,
])
->assertJsonStructure(['url']);
Storage::disk('public')->assertExists('uploads/base64/custom.png');
$this->assertDatabaseHas('images', [
'gallery_id' => $gallery->id,
'path' => 'uploads/base64/custom.png',
]);
}
}