sparkbooth anbindung gefixt
This commit is contained in:
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 & 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">
|
||||
|
||||
@@ -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"><?xml version="1.0" encoding="UTF-8"?>
|
||||
<rsp status="ok" url="{{ $result['gallery_url'] }}" /></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
117
tests/Feature/SparkboothUploadTest.php
Normal file
117
tests/Feature/SparkboothUploadTest.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user