Enable guest photo deletion and ownership flags
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-05 22:05:10 +01:00
parent c6aaf859f5
commit 18b4f36fcf
10 changed files with 455 additions and 14 deletions

View File

@@ -2848,7 +2848,8 @@ class EventPublicController extends BaseController
[$locale] = $this->resolveGuestLocale($request, $event);
$fallbacks = $this->localeFallbackChain($locale, $event->default_locale ?? null);
$deviceId = (string) $request->header('X-Device-Id', 'anon');
$deviceId = $this->normalizeGuestIdentifier((string) $request->header('X-Device-Id', ''));
$deviceId = $deviceId !== '' ? $deviceId : 'anon';
$filter = $request->query('filter');
$since = $request->query('since');
@@ -2863,6 +2864,7 @@ class EventPublicController extends BaseController
'photos.emotion_id',
'photos.task_id',
'photos.guest_name',
'photos.created_by_device_id',
'photos.created_at',
'photos.ingest_source',
'tasks.title as task_title',
@@ -2880,13 +2882,16 @@ class EventPublicController extends BaseController
if ($filter === 'photobooth') {
$query->whereIn('photos.ingest_source', [Photo::SOURCE_PHOTOBOOTH, Photo::SOURCE_SPARKBOOTH]);
} elseif ($filter === 'myphotos' && $deviceId !== 'anon') {
$query->where('guest_name', $deviceId);
$query->where(function ($inner) use ($deviceId) {
$inner->where('created_by_device_id', $deviceId)
->orWhere('guest_name', $deviceId);
});
}
if ($since) {
$query->where('photos.created_at', '>', $since);
}
$rows = $query->get()->map(function ($r) use ($fallbacks, $token) {
$rows = $query->get()->map(function ($r) use ($fallbacks, $token, $deviceId) {
$r->file_path = $this->makeSignedGalleryAssetUrlForId($token, (int) $r->id, 'full')
?? $this->resolveSignedFallbackUrl((string) ($r->file_path ?? ''));
$r->thumbnail_path = $this->makeSignedGalleryAssetUrlForId($token, (int) $r->id, 'thumbnail')
@@ -2912,6 +2917,10 @@ class EventPublicController extends BaseController
$r->emotion = $emotion;
$r->ingest_source = $r->ingest_source ?? Photo::SOURCE_UNKNOWN;
$createdBy = $r->created_by_device_id ? $this->normalizeGuestIdentifier((string) $r->created_by_device_id) : '';
$r->is_mine = $deviceId !== 'anon'
&& $deviceId !== ''
&& (($createdBy !== '' && $createdBy === $deviceId) || ($createdBy === '' && (string) $r->guest_name === $deviceId));
return $r;
});
@@ -3052,6 +3061,111 @@ class EventPublicController extends BaseController
return response()->json(['liked' => false, 'likes_count' => $count]);
}
public function destroyPhoto(Request $request, string $token, Photo $photo): JsonResponse
{
$result = $this->resolvePublishedEvent($request, $token, ['id']);
if ($result instanceof JsonResponse) {
return $result;
}
[$event] = $result;
$deviceId = $this->resolveDeviceIdentifier($request);
if ($deviceId === 'anonymous') {
return ApiError::response(
'photo_delete_forbidden',
'Delete Not Allowed',
'This photo cannot be deleted from this device.',
Response::HTTP_FORBIDDEN,
['photo_id' => $photo->id]
);
}
if ($photo->event_id !== (int) $event->id) {
return ApiError::response(
'photo_not_found',
'Photo Not Found',
'Photo not found or event not public.',
Response::HTTP_NOT_FOUND,
['photo_id' => $photo->id]
);
}
$ownerId = $photo->created_by_device_id
? $this->normalizeGuestIdentifier((string) $photo->created_by_device_id)
: '';
$guestName = is_string($photo->guest_name) ? $photo->guest_name : '';
$isOwner = $ownerId !== ''
? $ownerId === $deviceId
: ($guestName !== '' && $guestName === $deviceId);
if (! $isOwner) {
return ApiError::response(
'photo_delete_forbidden',
'Delete Not Allowed',
'This photo cannot be deleted from this device.',
Response::HTTP_FORBIDDEN,
['photo_id' => $photo->id]
);
}
$eventModel = Event::with(['eventPackage.package'])->find((int) $event->id);
$assets = EventMediaAsset::where('photo_id', $photo->id)->get();
foreach ($assets as $asset) {
if (! is_string($asset->path) || $asset->path === '') {
continue;
}
try {
Storage::disk($asset->disk)->delete($asset->path);
} catch (\Throwable $e) {
Log::warning('Failed to delete guest photo asset from storage', [
'asset_id' => $asset->id,
'disk' => $asset->disk,
'path' => $asset->path,
'error' => $e->getMessage(),
]);
}
}
if ($assets->isEmpty() && $eventModel) {
$fallbackDisk = $this->eventStorageManager->getHotDiskForEvent($eventModel);
$paths = array_values(array_filter([
is_string($photo->path ?? null) ? $photo->path : null,
is_string($photo->thumbnail_path ?? null) ? $photo->thumbnail_path : null,
is_string($photo->file_path ?? null) ? $photo->file_path : null,
]));
if (! empty($paths)) {
Storage::disk($fallbackDisk)->delete($paths);
}
}
DB::transaction(function () use ($photo, $assets) {
$photo->likes()->delete();
PhotoShareLink::where('photo_id', $photo->id)->delete();
if ($assets->isNotEmpty()) {
EventMediaAsset::whereIn('id', $assets->pluck('id'))->delete();
}
$photo->delete();
});
$eventPackage = $eventModel?->eventPackage;
if ($eventPackage && $eventPackage->package) {
$previousUsed = (int) $eventPackage->used_photos;
if ($previousUsed > 0) {
$eventPackage->decrement('used_photos');
$eventPackage->refresh();
$this->packageUsageTracker->recordPhotoUsage($eventPackage, $previousUsed, -1);
}
}
return response()->json([
'message' => 'Photo deleted successfully',
'photo_id' => $photo->id,
]);
}
public function upload(Request $request, string $token)
{
$result = $this->resolvePublishedEvent($request, $token, ['id']);