Implement package limit notification system
This commit is contained in:
@@ -2,10 +2,23 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\EventJoinToken;
|
||||
use App\Models\EventMediaAsset;
|
||||
use App\Models\Photo;
|
||||
use App\Services\Analytics\JoinTokenAnalyticsRecorder;
|
||||
use App\Services\EventJoinTokenService;
|
||||
use App\Services\Packages\PackageLimitEvaluator;
|
||||
use App\Services\Packages\PackageUsageTracker;
|
||||
use App\Services\Storage\EventStorageManager;
|
||||
use App\Support\ApiError;
|
||||
use App\Support\ImageHelper;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
@@ -13,16 +26,6 @@ use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Support\ImageHelper;
|
||||
use App\Services\Analytics\JoinTokenAnalyticsRecorder;
|
||||
use App\Services\EventJoinTokenService;
|
||||
use App\Services\Storage\EventStorageManager;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventJoinToken;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Models\Photo;
|
||||
use App\Models\EventMediaAsset;
|
||||
|
||||
class EventPublicController extends BaseController
|
||||
{
|
||||
@@ -32,8 +35,9 @@ class EventPublicController extends BaseController
|
||||
private readonly EventJoinTokenService $joinTokenService,
|
||||
private readonly EventStorageManager $eventStorageManager,
|
||||
private readonly JoinTokenAnalyticsRecorder $analyticsRecorder,
|
||||
) {
|
||||
}
|
||||
private readonly PackageLimitEvaluator $packageLimitEvaluator,
|
||||
private readonly PackageUsageTracker $packageUsageTracker,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return JsonResponse|array{0: object, 1: EventJoinToken}
|
||||
@@ -249,8 +253,7 @@ class EventPublicController extends BaseController
|
||||
array $context = [],
|
||||
?string $rawToken = null,
|
||||
?EventJoinToken $joinToken = null
|
||||
): JsonResponse
|
||||
{
|
||||
): JsonResponse {
|
||||
$failureLimit = max(1, (int) config('join_tokens.failure_limit', 10));
|
||||
$failureDecay = max(1, (int) config('join_tokens.failure_decay_minutes', 5));
|
||||
|
||||
@@ -393,22 +396,33 @@ class EventPublicController extends BaseController
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getLocalized($value, $locale, $default = '') {
|
||||
private function getLocalized($value, $locale, $default = '')
|
||||
{
|
||||
if (is_string($value) && json_decode($value) !== null) {
|
||||
$data = json_decode($value, true);
|
||||
|
||||
return $data[$locale] ?? $data['de'] ?? $default;
|
||||
}
|
||||
|
||||
return $value ?: $default;
|
||||
}
|
||||
|
||||
private function toPublicUrl(?string $path): ?string
|
||||
{
|
||||
if (! $path) return null;
|
||||
if (! $path) {
|
||||
return null;
|
||||
}
|
||||
// Already absolute URL
|
||||
if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) return $path;
|
||||
if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
|
||||
return $path;
|
||||
}
|
||||
// Already a public storage URL
|
||||
if (str_starts_with($path, '/storage/')) return $path;
|
||||
if (str_starts_with($path, 'storage/')) return '/' . $path;
|
||||
if (str_starts_with($path, '/storage/')) {
|
||||
return $path;
|
||||
}
|
||||
if (str_starts_with($path, 'storage/')) {
|
||||
return '/'.$path;
|
||||
}
|
||||
|
||||
// Common relative paths stored in DB (e.g. 'photos/...', 'thumbnails/...', 'events/...')
|
||||
if (str_starts_with($path, 'photos/') || str_starts_with($path, 'thumbnails/') || str_starts_with($path, 'events/')) {
|
||||
@@ -420,7 +434,8 @@ class EventPublicController extends BaseController
|
||||
$needle = '/storage/app/public/';
|
||||
if (str_contains($normalized, $needle)) {
|
||||
$rel = substr($normalized, strpos($normalized, $needle) + strlen($needle));
|
||||
return '/storage/' . ltrim($rel, '/');
|
||||
|
||||
return '/storage/'.ltrim($rel, '/');
|
||||
}
|
||||
|
||||
return $path; // fallback as-is
|
||||
@@ -768,6 +783,7 @@ class EventPublicController extends BaseController
|
||||
'disk' => $diskName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -952,7 +968,7 @@ class EventPublicController extends BaseController
|
||||
return $result;
|
||||
}
|
||||
|
||||
[$event] = $result;
|
||||
[$event, $joinToken] = $result;
|
||||
$eventId = $event->id;
|
||||
$locale = $request->query('locale', 'de');
|
||||
|
||||
@@ -965,7 +981,7 @@ class EventPublicController extends BaseController
|
||||
'emotions.id',
|
||||
'emotions.name',
|
||||
'emotions.icon as emoji',
|
||||
'emotions.description'
|
||||
'emotions.description',
|
||||
])
|
||||
->orderBy('emotions.sort_order')
|
||||
->get();
|
||||
@@ -973,13 +989,13 @@ class EventPublicController extends BaseController
|
||||
$payload = $rows->map(function ($r) use ($locale) {
|
||||
$nameData = json_decode($r->name, true);
|
||||
$name = $nameData[$locale] ?? $nameData['de'] ?? $r->name;
|
||||
|
||||
|
||||
$descriptionData = json_decode($r->description, true);
|
||||
$description = $descriptionData ? ($descriptionData[$locale] ?? $descriptionData['de'] ?? '') : '';
|
||||
|
||||
|
||||
return [
|
||||
'id' => (int) $r->id,
|
||||
'slug' => 'emotion-' . $r->id, // Generate slug from ID
|
||||
'slug' => 'emotion-'.$r->id, // Generate slug from ID
|
||||
'name' => $name,
|
||||
'emoji' => $r->emoji,
|
||||
'description' => $description,
|
||||
@@ -1005,7 +1021,7 @@ class EventPublicController extends BaseController
|
||||
return $result;
|
||||
}
|
||||
|
||||
[$event] = $result;
|
||||
[$event, $joinToken] = $result;
|
||||
$eventId = $event->id;
|
||||
|
||||
$query = DB::table('tasks')
|
||||
@@ -1018,7 +1034,7 @@ class EventPublicController extends BaseController
|
||||
'tasks.description',
|
||||
'tasks.example_text as instructions',
|
||||
'tasks.emotion_id',
|
||||
'tasks.sort_order'
|
||||
'tasks.sort_order',
|
||||
])
|
||||
->orderBy('event_task_collection.sort_order')
|
||||
->orderBy('tasks.sort_order')
|
||||
@@ -1033,8 +1049,10 @@ class EventPublicController extends BaseController
|
||||
$value = $r->$field;
|
||||
if (is_string($value) && json_decode($value) !== null) {
|
||||
$data = json_decode($value, true);
|
||||
|
||||
return $data[$locale] ?? $data['de'] ?? $default;
|
||||
}
|
||||
|
||||
return $value ?: $default;
|
||||
};
|
||||
|
||||
@@ -1051,7 +1069,7 @@ class EventPublicController extends BaseController
|
||||
$emotionName = $value ?: 'Unbekannte Emotion';
|
||||
}
|
||||
$emotion = [
|
||||
'slug' => 'emotion-' . $r->emotion_id, // Generate slug from ID
|
||||
'slug' => 'emotion-'.$r->emotion_id, // Generate slug from ID
|
||||
'name' => $emotionName,
|
||||
];
|
||||
}
|
||||
@@ -1092,7 +1110,7 @@ class EventPublicController extends BaseController
|
||||
return $result;
|
||||
}
|
||||
|
||||
[$event] = $result;
|
||||
[$event, $joinToken] = $result;
|
||||
$eventId = $event->id;
|
||||
|
||||
$deviceId = (string) $request->header('X-Device-Id', 'anon');
|
||||
@@ -1109,7 +1127,7 @@ class EventPublicController extends BaseController
|
||||
'photos.emotion_id',
|
||||
'photos.task_id',
|
||||
'photos.guest_name',
|
||||
'tasks.title as task_title'
|
||||
'tasks.title as task_title',
|
||||
])
|
||||
->where('photos.event_id', $eventId)
|
||||
->orderByDesc('photos.created_at')
|
||||
@@ -1126,14 +1144,14 @@ class EventPublicController extends BaseController
|
||||
$locale = $request->query('locale', 'de');
|
||||
|
||||
$rows = $query->get()->map(function ($r) use ($locale) {
|
||||
$r->file_path = $this->toPublicUrl((string)($r->file_path ?? ''));
|
||||
$r->thumbnail_path = $this->toPublicUrl((string)($r->thumbnail_path ?? ''));
|
||||
|
||||
$r->file_path = $this->toPublicUrl((string) ($r->file_path ?? ''));
|
||||
$r->thumbnail_path = $this->toPublicUrl((string) ($r->thumbnail_path ?? ''));
|
||||
|
||||
// Localize task title if present
|
||||
if ($r->task_title) {
|
||||
$r->task_title = $this->getLocalized($r->task_title, $locale, 'Unbenannte Aufgabe');
|
||||
}
|
||||
|
||||
|
||||
return $r;
|
||||
});
|
||||
$latestPhotoAt = DB::table('photos')->where('event_id', $eventId)->max('created_at');
|
||||
@@ -1146,10 +1164,11 @@ class EventPublicController extends BaseController
|
||||
if ($reqEtag && $reqEtag === $etag) {
|
||||
return response('', 304);
|
||||
}
|
||||
|
||||
return response()->json($payload)
|
||||
->header('Cache-Control', 'no-store')
|
||||
->header('ETag', $etag)
|
||||
->header('Last-Modified', (string)$latestPhotoAt);
|
||||
->header('Last-Modified', (string) $latestPhotoAt);
|
||||
}
|
||||
|
||||
public function photo(int $id)
|
||||
@@ -1166,7 +1185,7 @@ class EventPublicController extends BaseController
|
||||
'photos.emotion_id',
|
||||
'photos.task_id',
|
||||
'photos.created_at',
|
||||
'tasks.title as task_title'
|
||||
'tasks.title as task_title',
|
||||
])
|
||||
->where('photos.id', $id)
|
||||
->where('events.status', 'published')
|
||||
@@ -1174,15 +1193,15 @@ class EventPublicController extends BaseController
|
||||
if (! $row) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found or event not public']], 404);
|
||||
}
|
||||
$row->file_path = $this->toPublicUrl((string)($row->file_path ?? ''));
|
||||
$row->thumbnail_path = $this->toPublicUrl((string)($row->thumbnail_path ?? ''));
|
||||
|
||||
$row->file_path = $this->toPublicUrl((string) ($row->file_path ?? ''));
|
||||
$row->thumbnail_path = $this->toPublicUrl((string) ($row->thumbnail_path ?? ''));
|
||||
|
||||
// Localize task title if present
|
||||
$locale = request()->query('locale', 'de');
|
||||
if ($row->task_title) {
|
||||
$row->task_title = $this->getLocalized($row->task_title, $locale, 'Unbenannte Aufgabe');
|
||||
}
|
||||
|
||||
|
||||
return response()->json($row)->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
@@ -1207,6 +1226,7 @@ class EventPublicController extends BaseController
|
||||
$exists = DB::table('photo_likes')->where('photo_id', $id)->where('guest_name', $deviceId)->exists();
|
||||
if ($exists) {
|
||||
$count = (int) DB::table('photos')->where('id', $id)->value('likes_count');
|
||||
|
||||
return response()->json(['liked' => true, 'likes_count' => $count]);
|
||||
}
|
||||
|
||||
@@ -1241,9 +1261,42 @@ class EventPublicController extends BaseController
|
||||
return $result;
|
||||
}
|
||||
|
||||
[$event] = $result;
|
||||
[$event, $joinToken] = $result;
|
||||
$eventId = $event->id;
|
||||
|
||||
$eventModel = Event::with([
|
||||
'tenant',
|
||||
'eventPackage.package',
|
||||
'eventPackages.package',
|
||||
'storageAssignments.storageTarget',
|
||||
])->findOrFail($eventId);
|
||||
|
||||
$tenantModel = $eventModel->tenant;
|
||||
|
||||
if (! $tenantModel) {
|
||||
return ApiError::response(
|
||||
'event_not_found',
|
||||
'Event not accessible',
|
||||
'The selected event is no longer available.',
|
||||
Response::HTTP_NOT_FOUND,
|
||||
['scope' => 'photos', 'event_id' => $eventId]
|
||||
);
|
||||
}
|
||||
|
||||
$violation = $this->packageLimitEvaluator->assessPhotoUpload($tenantModel, $eventId, $eventModel);
|
||||
if ($violation !== null) {
|
||||
return ApiError::response(
|
||||
$violation['code'],
|
||||
$violation['title'],
|
||||
$violation['message'],
|
||||
$violation['status'],
|
||||
$violation['meta']
|
||||
);
|
||||
}
|
||||
|
||||
$eventPackage = $this->packageLimitEvaluator
|
||||
->resolveEventPackageForPhotoUpload($tenantModel, $eventId, $eventModel);
|
||||
|
||||
$deviceId = (string) $request->header('X-Device-Id', 'anon');
|
||||
$deviceId = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', $deviceId), 0, 64) ?: 'anon';
|
||||
|
||||
@@ -1263,7 +1316,19 @@ class EventPublicController extends BaseController
|
||||
Response::HTTP_TOO_MANY_REQUESTS
|
||||
);
|
||||
|
||||
return response()->json(['error' => ['code' => 'limit_reached', 'message' => 'Upload-Limit erreicht']], 429);
|
||||
return ApiError::response(
|
||||
'upload_device_limit',
|
||||
'Device upload limit reached',
|
||||
'This device cannot upload more photos for this event.',
|
||||
Response::HTTP_TOO_MANY_REQUESTS,
|
||||
[
|
||||
'scope' => 'photos',
|
||||
'event_id' => $eventId,
|
||||
'device_id' => $deviceId,
|
||||
'limit' => 50,
|
||||
'used' => $deviceCount,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
@@ -1294,7 +1359,7 @@ class EventPublicController extends BaseController
|
||||
'file_path' => $url,
|
||||
'thumbnail_path' => $thumbUrl,
|
||||
'likes_count' => 0,
|
||||
|
||||
|
||||
// Handle emotion_id: prefer explicit ID, fallback to slug lookup, then default
|
||||
'emotion_id' => $this->resolveEmotionId($validated, $eventId),
|
||||
'is_featured' => 0,
|
||||
@@ -1333,6 +1398,13 @@ class EventPublicController extends BaseController
|
||||
->where('id', $photoId)
|
||||
->update(['media_asset_id' => $asset->id]);
|
||||
|
||||
if ($eventPackage) {
|
||||
$previousUsed = (int) $eventPackage->used_photos;
|
||||
$eventPackage->increment('used_photos');
|
||||
$eventPackage->refresh();
|
||||
$this->packageUsageTracker->recordPhotoUsage($eventPackage, $previousUsed, 1);
|
||||
}
|
||||
|
||||
$response = response()->json([
|
||||
'id' => $photoId,
|
||||
'file_path' => $url,
|
||||
@@ -1378,7 +1450,7 @@ class EventPublicController extends BaseController
|
||||
->where('emotions.id', $id)
|
||||
->where('events.id', $eventId)
|
||||
->exists();
|
||||
|
||||
|
||||
if ($exists) {
|
||||
return $id;
|
||||
}
|
||||
@@ -1396,6 +1468,7 @@ class EventPublicController extends BaseController
|
||||
|
||||
return $defaultEmotion ?: 1; // Ultimate fallback to emotion ID 1 (assuming "Happy" exists)
|
||||
}
|
||||
|
||||
public function achievements(Request $request, string $identifier)
|
||||
{
|
||||
$result = $this->resolvePublishedEvent($request, $identifier, ['id']);
|
||||
@@ -1628,5 +1701,4 @@ class EventPublicController extends BaseController
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,37 +5,69 @@ namespace App\Http\Controllers\Api\Tenant;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Tenant\PhotoStoreRequest;
|
||||
use App\Http\Resources\Tenant\PhotoResource;
|
||||
use App\Models\Event;
|
||||
use App\Models\Photo;
|
||||
use App\Support\ImageHelper;
|
||||
use App\Services\Storage\EventStorageManager;
|
||||
use App\Jobs\ProcessPhotoSecurityScan;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventMediaAsset;
|
||||
use App\Models\Photo;
|
||||
use App\Services\Packages\PackageLimitEvaluator;
|
||||
use App\Services\Packages\PackageUsageTracker;
|
||||
use App\Services\Storage\EventStorageManager;
|
||||
use App\Support\ApiError;
|
||||
use App\Support\ImageHelper;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Models\EventMediaAsset;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class PhotoController extends Controller
|
||||
{
|
||||
public function __construct(private readonly EventStorageManager $eventStorageManager)
|
||||
{
|
||||
}
|
||||
public function __construct(
|
||||
private readonly EventStorageManager $eventStorageManager,
|
||||
private readonly PackageLimitEvaluator $packageLimitEvaluator,
|
||||
private readonly PackageUsageTracker $packageUsageTracker,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the event's photos.
|
||||
*/
|
||||
public function index(Request $request, string $eventSlug): AnonymousResourceCollection
|
||||
{
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
$event = Event::where('slug', $eventSlug)
|
||||
$event = Event::with([
|
||||
'tenant',
|
||||
'eventPackage.package',
|
||||
'eventPackages.package',
|
||||
'storageAssignments.storageTarget',
|
||||
])->where('slug', $eventSlug)
|
||||
->where('tenant_id', $tenantId)
|
||||
->firstOrFail();
|
||||
|
||||
$tenant = $event->tenant;
|
||||
|
||||
if ($tenant) {
|
||||
$violation = $this->packageLimitEvaluator->assessPhotoUpload($tenant, $event->id, $event);
|
||||
|
||||
if ($violation !== null) {
|
||||
return ApiError::response(
|
||||
$violation['code'],
|
||||
$violation['title'],
|
||||
$violation['message'],
|
||||
$violation['status'],
|
||||
$violation['meta']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$eventPackage = $tenant
|
||||
? $this->packageLimitEvaluator->resolveEventPackageForPhotoUpload($tenant, $event->id, $event)
|
||||
: null;
|
||||
|
||||
$previousUsedPhotos = $eventPackage?->used_photos ?? 0;
|
||||
|
||||
$query = Photo::where('event_id', $event->id)
|
||||
->with('event')->withCount('likes')
|
||||
->orderBy('created_at', 'desc');
|
||||
@@ -68,7 +100,7 @@ class PhotoController extends Controller
|
||||
$validated = $request->validated();
|
||||
$file = $request->file('photo');
|
||||
|
||||
if (!$file) {
|
||||
if (! $file) {
|
||||
throw ValidationException::withMessages([
|
||||
'photo' => 'No photo file uploaded.',
|
||||
]);
|
||||
@@ -76,7 +108,7 @@ class PhotoController extends Controller
|
||||
|
||||
// Validate file type and size
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
if (!in_array($file->getMimeType(), $allowedTypes)) {
|
||||
if (! in_array($file->getMimeType(), $allowedTypes)) {
|
||||
throw ValidationException::withMessages([
|
||||
'photo' => 'Only JPEG, PNG, and WebP images are allowed.',
|
||||
]);
|
||||
@@ -89,12 +121,11 @@ class PhotoController extends Controller
|
||||
}
|
||||
|
||||
// Determine storage target
|
||||
$event->load('storageAssignments.storageTarget');
|
||||
$disk = $this->eventStorageManager->getHotDiskForEvent($event);
|
||||
|
||||
// Generate unique filename
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
$filename = Str::uuid() . '.' . $extension;
|
||||
$filename = Str::uuid().'.'.$extension;
|
||||
$path = "events/{$eventSlug}/photos/{$filename}";
|
||||
|
||||
// Store original file
|
||||
@@ -160,6 +191,12 @@ class PhotoController extends Controller
|
||||
|
||||
ProcessPhotoSecurityScan::dispatch($photo->id);
|
||||
|
||||
if ($eventPackage) {
|
||||
$eventPackage->increment('used_photos');
|
||||
$eventPackage->refresh();
|
||||
$this->packageUsageTracker->recordPhotoUsage($eventPackage, $previousUsedPhotos, 1);
|
||||
}
|
||||
|
||||
$photo->load('event')->loadCount('likes');
|
||||
|
||||
return response()->json([
|
||||
@@ -283,7 +320,6 @@ class PhotoController extends Controller
|
||||
/**
|
||||
* Bulk approve photos (admin only)
|
||||
*/
|
||||
|
||||
public function feature(Request $request, string $eventSlug, Photo $photo): JsonResponse
|
||||
{
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
@@ -317,6 +353,7 @@ class PhotoController extends Controller
|
||||
|
||||
return response()->json(['message' => 'Photo removed from featured', 'data' => new PhotoResource($photo)]);
|
||||
}
|
||||
|
||||
public function bulkApprove(Request $request, string $eventSlug): JsonResponse
|
||||
{
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
@@ -436,7 +473,7 @@ class PhotoController extends Controller
|
||||
$totalPhotos = Photo::where('event_id', $event->id)->count();
|
||||
$pendingPhotos = Photo::where('event_id', $event->id)->where('status', 'pending')->count();
|
||||
$approvedPhotos = Photo::where('event_id', $event->id)->where('status', 'approved')->count();
|
||||
$totalLikes = DB::table('photo_likes')->whereIn('photo_id',
|
||||
$totalLikes = DB::table('photo_likes')->whereIn('photo_id',
|
||||
Photo::where('event_id', $event->id)->pluck('id')
|
||||
)->count();
|
||||
$totalStorage = Photo::where('event_id', $event->id)->sum('size');
|
||||
@@ -492,13 +529,13 @@ class PhotoController extends Controller
|
||||
|
||||
// Generate unique filename
|
||||
$extension = pathinfo($request->filename, PATHINFO_EXTENSION);
|
||||
$filename = Str::uuid() . '.' . $extension;
|
||||
$filename = Str::uuid().'.'.$extension;
|
||||
$path = "events/{$eventSlug}/pending/{$filename}";
|
||||
|
||||
// For local storage, return direct upload endpoint
|
||||
// For S3, use Storage::disk('s3')->temporaryUrl() or presigned URL
|
||||
$uploadUrl = url("/api/v1/tenant/events/{$eventSlug}/upload-direct");
|
||||
|
||||
|
||||
$fields = [
|
||||
'event_id' => $event->id,
|
||||
'filename' => $filename,
|
||||
@@ -605,9 +642,3 @@ class PhotoController extends Controller
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user