feat: fix inventory service SQL injection, add slot population, and live frontend updates

- Replace f-string SQL interpolation with parameterized ANY(:ids) queries
- Populate slot column from IntValues[231735296] (Decal Slot key)
- Add startup migration to add container_id/slot columns to existing DB
- Extract createInventorySlot() for reuse by initial load and live deltas
- Add updateInventoryLive() handler for WebSocket inventory_delta messages
- Add inventory_delta case to browser WebSocket message dispatcher

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-28 15:51:20 +00:00
parent 749652d534
commit f145e6e131
2 changed files with 201 additions and 148 deletions

View file

@ -358,7 +358,19 @@ async def startup():
# Create tables if they don't exist
Base.metadata.create_all(engine)
# Migrate: add container_id and slot columns if missing (added for live inventory)
from sqlalchemy import inspect as sa_inspect
inspector = sa_inspect(engine)
existing_columns = {c['name'] for c in inspector.get_columns('items')}
with engine.begin() as conn:
if 'container_id' not in existing_columns:
conn.execute(sa.text("ALTER TABLE items ADD COLUMN container_id BIGINT DEFAULT 0"))
logger.info("Migration: added container_id column to items table")
if 'slot' not in existing_columns:
conn.execute(sa.text("ALTER TABLE items ADD COLUMN slot INTEGER DEFAULT -1"))
logger.info("Migration: added slot column to items table")
# Create performance indexes
create_indexes(engine)
@ -1407,6 +1419,7 @@ async def process_inventory(inventory: InventoryItem):
# Container/position tracking
container_id=item_data.get('ContainerId', 0),
slot=int(item_data.get('IntValues', {}).get('231735296', item_data.get('IntValues', {}).get(231735296, -1))),
# Item state
bonded=basic['bonded'],
@ -1561,13 +1574,13 @@ async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
)
if existing:
id_list = ','.join(str(row['id']) for row in existing)
await database.execute(f"DELETE FROM item_raw_data WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_combat_stats WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_requirements WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_enhancements WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_ratings WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_spells WHERE item_id IN ({id_list})")
db_ids = [row['id'] for row in existing]
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
'item_enhancements', 'item_ratings', 'item_spells'):
await database.execute(
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
{"ids": db_ids}
)
await database.execute(
"DELETE FROM items WHERE character_name = :character_name AND item_id = :item_id",
{"character_name": character_name, "item_id": item_game_id}
@ -1597,6 +1610,7 @@ async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
# Container/position tracking
container_id=item.get('ContainerId', 0),
slot=int(item.get('IntValues', {}).get('231735296', item.get('IntValues', {}).get(231735296, -1))),
# Item state
bonded=basic['bonded'],
@ -1736,15 +1750,15 @@ async def delete_inventory_item(character_name: str, item_id: int):
)
if existing:
id_list = ','.join(str(row['id']) for row in existing)
db_ids = [row['id'] for row in existing]
# Delete from all related tables first
await database.execute(f"DELETE FROM item_raw_data WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_combat_stats WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_requirements WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_enhancements WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_ratings WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_spells WHERE item_id IN ({id_list})")
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
'item_enhancements', 'item_ratings', 'item_spells'):
await database.execute(
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
{"ids": db_ids}
)
# Delete from main items table
await database.execute(