Feature::class, 'issue' => Issue::class, 'issue_task' => IssueTask::class, 'issue_comment' => IssueComment::class, 'project' => Project::class, 'phase' => Phase::class, 'layer' => Layer::class, ]; /** Upload a file (multipart) and attach it to a parent record. Idempotent by uuid. */ public function upload(Request $request) { $data = $request->validate([ 'uuid' => ['required', 'uuid'], 'parent_entity' => ['required', Rule::in(array_keys($this->map))], 'parent_id' => ['required', 'integer'], 'file' => ['required', 'file', 'max:20480'], // 20 MB 'category' => ['nullable', 'in:image,document,other'], 'description' => ['nullable', 'string'], ]); // Idempotency: same uuid already uploaded → return it. if ($existing = Media::where('uuid', $data['uuid'])->first()) { return response()->json(['status' => 'duplicate', 'media' => $this->payload($existing)]); } $parent = $this->map[$data['parent_entity']]::find($data['parent_id']); if (! $parent) { return response()->json(['status' => 'error', 'error' => 'parent not found'], 422); } $user = $request->user(); $project = $this->projectOf($data['parent_entity'], $parent); abort_unless($this->canAccess($user, $project) && $user->can('upload media'), 403); $file = $request->file('file'); $path = $file->store("media/{$data['parent_entity']}/{$parent->id}", 'public'); $mime = $file->getClientMimeType(); $media = $parent->media()->create([ 'uuid' => $data['uuid'], 'name' => $file->getClientOriginalName(), 'file_path' => $path, 'file_type' => $mime, 'file_extension' => $file->getClientOriginalExtension(), 'file_size' => $file->getSize(), 'category' => $data['category'] ?? (Str::startsWith($mime, 'image/') ? 'image' : 'document'), 'description' => $data['description'] ?? null, 'uploaded_by' => $user->id, 'client_updated_at' => $request->input('client_updated_at'), ]); return response()->json(['status' => 'applied', 'media' => $this->payload($media)]); } private function projectOf(string $entity, $parent): ?Project { return match ($entity) { 'project' => $parent, 'phase' => $parent->project, 'layer' => $parent->phase?->project, 'feature' => $parent->layer?->phase?->project, 'issue' => $parent->project, 'issue_task' => $parent->issue?->project, 'issue_comment' => $parent->issue?->project, default => null, }; } private function canAccess(User $user, ?Project $project): bool { if (! $project) { return false; } return $user->can('manage all') || $project->users()->where('user_id', $user->id)->exists(); } private function payload(Media $m): array { return [ 'id' => $m->id, 'uuid' => $m->uuid, 'url' => $m->url, 'name' => $m->name, 'file_type' => $m->file_type, 'category' => $m->category, 'updated_at' => $m->updated_at?->toIso8601String(), ]; } }