rank < (int) setting('min_staff_rank', 7)) { abort(403, 'Forbidden'); } } private function handleApiError(string $action, \Exception $e): JsonResponse { Log::error("[FurniEditor] {$action} failed", [ 'error' => $e->getMessage(), 'user_id' => Auth::id(), ]); return response()->json(['error' => "{$action} mislukt"], 500); } private function formatItemData(\stdClass $item): array { return [ 'id' => $item->id, 'spriteId' => $item->sprite_id, 'itemName' => $item->item_name, 'publicName' => $item->public_name, 'type' => $item->type, 'width' => $item->width, 'length' => $item->length, 'stackHeight' => $item->stack_height, 'allowStack' => (bool) $item->allow_stack, 'allowWalk' => (bool) $item->allow_walk, 'allowSit' => (bool) $item->allow_sit, 'allowLay' => (bool) $item->allow_lay, 'allowGift' => (bool) ($item->allow_gift ?? false), 'allowTrade' => (bool) ($item->allow_trade ?? false), 'allowRecycle' => (bool) ($item->allow_recycle ?? false), 'allowMarketplaceSell' => (bool) ($item->allow_marketplace_sell ?? false), 'allowInventoryStack' => (bool) ($item->allow_inventory_stack ?? true), 'interactionType' => $item->interaction_type, 'interactionModesCount' => $item->interaction_modes_count, 'vendingIds' => $item->vending_ids ?? '', 'multiheight' => $item->multiheight ?? '', 'customparams' => $item->customparams ?? '', 'effectIdMale' => $item->effect_id_male ?? 0, 'effectIdFemale' => $item->effect_id_female ?? 0, 'clothingOnWalk' => $item->clothing_on_walk ?? '', ]; } private function formatCatalogItemData(\stdClass $r): array { return [ 'id' => $r->id, 'catalogName' => $r->catalog_name, 'costCredits' => $r->cost_credits, 'costPoints' => $r->cost_points, 'pointsType' => $r->points_type, 'pageId' => $r->page_id, 'pageName' => $r->page_id ? DB::table('catalog_pages')->where('id', $r->page_id)->value('caption') ?? '' : '', ]; } public function search(Request $request): JsonResponse { $this->checkAdmin(); try { $q = mb_substr(strip_tags((string) $request->input('q', '')), 0, 100); $type = $request->input('type', ''); $page = max(1, (int) $request->input('page', 1)); $limit = min(50, max(1, (int) $request->input('limit', 20))); $query = DB::table('items_base'); if ($q !== '' && $q !== '0') { $query->where(function ($w) use ($q) { $w->where('item_name', 'like', "%{$q}%") ->orWhere('public_name', 'like', "%{$q}%") ->orWhere('id', '=', $q); }); } if ($type) { $query->where('type', $type); } $total = $query->count(); $items = $query ->orderBy('id') ->skip(($page - 1) * $limit) ->take($limit) ->get() ->map(fn ($r) => [ 'id' => $r->id, 'spriteId' => $r->sprite_id, 'itemName' => $r->item_name, 'publicName' => $r->public_name, 'type' => $r->type, 'width' => $r->width, 'length' => $r->length, 'stackHeight' => $r->stack_height, 'allowStack' => (bool) $r->allow_stack, 'allowWalk' => (bool) $r->allow_walk, 'allowSit' => (bool) $r->allow_sit, 'allowLay' => (bool) $r->allow_lay, 'interactionType' => $r->interaction_type, 'interactionModesCount' => $r->interaction_modes_count, ]); return response()->json([ 'items' => $items, 'total' => $total, 'page' => $page, ]); } catch (\Exception $e) { return $this->handleApiError('Zoeken', $e); } } public function detail(Request $request): JsonResponse { $this->checkAdmin(); try { $id = (int) $request->input('id'); if ($id === 0) { return response()->json(['error' => 'Missing id'], 400); } $item = DB::table('items_base')->where('id', $id)->first(); if (! $item) { return response()->json(['error' => 'Item not found'], 404); } $catalog = DB::table('catalog_items') ->whereRaw('FIND_IN_SET(?, item_ids)', [$id]) ->get() ->map(fn ($r) => $this->formatCatalogItemData($r)); return response()->json([ 'item' => array_merge($this->formatItemData($item), [ 'description' => '', 'usageCount' => 0, ]), 'catalogItems' => $catalog, 'furniDataEntry' => null, ]); } catch (\Exception $e) { return $this->handleApiError('Item laden', $e); } } public function update(Request $request): JsonResponse { $this->checkAdmin(); try { $id = (int) $request->input('id'); if ($id === 0) { return response()->json(['error' => 'Missing id'], 400); } $data = $request->json()->all(); $update = $this->buildUpdateData($data); if ($update === []) { return response()->json(['error' => 'No fields to update'], 400); } DB::table('items_base')->where('id', $id)->update($update); Log::info('[Audit] FurniEditor update', [ 'user_id' => Auth::id(), 'item_id' => $id, 'fields' => array_keys($update), ]); return response()->json(['success' => true]); } catch (\Exception $e) { return $this->handleApiError('Actie', $e); } } public function create(Request $request): JsonResponse { $this->checkAdmin(); try { $data = $request->json()->all(); $id = DB::table('items_base')->insertGetId($this->buildInsertData($data)); Log::info('[Audit] FurniEditor create', [ 'user_id' => Auth::id(), 'new_id' => $id, ]); return response()->json(['id' => $id]); } catch (\Exception $e) { return $this->handleApiError('Actie', $e); } } public function delete(Request $request): JsonResponse { $this->checkAdmin(); try { $id = (int) $request->input('id'); if ($id === 0) { return response()->json(['error' => 'Missing id'], 400); } DB::table('items_base')->where('id', $id)->delete(); Log::warning('[Audit] FurniEditor delete', [ 'user_id' => Auth::id(), 'deleted_id' => $id, ]); return response()->json(['success' => true]); } catch (\Exception $e) { return $this->handleApiError('Actie', $e); } } public function interactions(Request $request): JsonResponse { $this->checkAdmin(); try { $rows = DB::table('items_base') ->select('interaction_type') ->groupBy('interaction_type') ->orderBy('interaction_type') ->pluck('interaction_type'); return response()->json(['interactions' => $rows]); } catch (\Exception $e) { return $this->handleApiError('Actie', $e); } } public function bySprite(Request $request): JsonResponse { $this->checkAdmin(); try { $spriteId = (int) $request->input('spriteId'); if ($spriteId === 0) { return response()->json(['error' => 'Missing spriteId'], 400); } $item = DB::table('items_base')->where('sprite_id', $spriteId)->first(); if (! $item) { return response()->json(['error' => 'Item not found'], 404); } return response()->json(['id' => $item->id]); } catch (\Exception $e) { return $this->handleApiError('Actie', $e); } } /** * @param array $data * @return array */ private function buildUpdateData(array $data): array { $map = [ 'itemName' => ['item_name', 'string', 100], 'publicName' => ['public_name', 'string', 100], 'spriteId' => ['sprite_id', 'integer', 0], 'width' => ['width', 'integer', 0], 'length' => ['length', 'integer', 0], 'stackHeight' => ['stack_height', 'integer', 0], 'allowStack' => ['allow_stack', 'boolean', 0], 'allowWalk' => ['allow_walk', 'boolean', 0], 'allowSit' => ['allow_sit', 'boolean', 0], 'allowLay' => ['allow_lay', 'boolean', 0], 'allowGift' => ['allow_gift', 'boolean', 0], 'allowTrade' => ['allow_trade', 'boolean', 0], 'allowRecycle' => ['allow_recycle', 'boolean', 0], 'allowMarketplaceSell' => ['allow_marketplace_sell', 'boolean', 0], 'allowInventoryStack' => ['allow_inventory_stack', 'boolean', 0], 'interactionType' => ['interaction_type', 'string', 50], 'interactionModesCount' => ['interaction_modes_count', 'integer', 0], 'vendingIds' => ['vending_ids', 'string', 255], 'multiheight' => ['multiheight', 'string', 255], 'customparams' => ['customparams', 'string', 255], 'effectIdMale' => ['effect_id_male', 'integer', 0], 'effectIdFemale' => ['effect_id_female', 'integer', 0], 'clothingOnWalk' => ['clothing_on_walk', 'string', 255], ]; $update = []; foreach ($map as $key => [$col, $type, $maxLen]) { if (! array_key_exists($key, $data)) { continue; } $update[$col] = $this->castValue($data[$key], $type, $maxLen); } return $update; } /** * @param array $data * @return array */ private function buildInsertData(array $data): array { return [ 'sprite_id' => $data['spriteId'] ?? 0, 'item_name' => $data['itemName'] ?? '', 'public_name' => $data['publicName'] ?? '', 'type' => $data['type'] ?? 's', 'width' => $data['width'] ?? 1, 'length' => $data['length'] ?? 1, 'stack_height' => $data['stackHeight'] ?? 0, 'allow_stack' => $data['allowStack'] ?? false, 'allow_walk' => $data['allowWalk'] ?? false, 'allow_sit' => $data['allowSit'] ?? false, 'allow_lay' => $data['allowLay'] ?? false, 'allow_gift' => $data['allowGift'] ?? true, 'allow_trade' => $data['allowTrade'] ?? true, 'allow_recycle' => $data['allowRecycle'] ?? false, 'allow_marketplace_sell' => $data['allowMarketplaceSell'] ?? false, 'allow_inventory_stack' => $data['allowInventoryStack'] ?? true, 'interaction_type' => $data['interactionType'] ?? 'default', 'interaction_modes_count' => $data['interactionModesCount'] ?? 1, ]; } private function castValue(mixed $value, string $type, int $maxLen = 0): mixed { return match ($type) { 'string' => mb_substr(strip_tags((string) $value), 0, $maxLen), 'integer' => (int) $value, 'boolean' => (bool) $value, default => $value, }; } }