Harden WO web UI and fix JS-context XSS in both dashboards

- Use json.dumps for onclick URLs to prevent JS string breakout
- Add .lower() to WO render_badge color lookup matching PO pattern
- Add pagination to get_comments query
- Cap get_work_orders to 500 results matching PO pattern
This commit is contained in:
Adam Moussa 2026-05-12 15:03:05 -04:00
parent 4ba5c37f47
commit 57bcec5a2e
2 changed files with 17 additions and 10 deletions

View file

@ -5,6 +5,7 @@ Serves a simple HTML dashboard for viewing purchase orders.
Accessed via Lambda Function URL.
"""
import json
import os
from decimal import Decimal
from html import escape as esc
@ -195,7 +196,7 @@ def render_po_list(purchase_orders):
processed = esc((po.get("processed_at") or "")[:16])
rows += f"""
<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onclick="window.location='/po?id={po_number}'">
<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onclick="window.location={esc(json.dumps(f'/po?id={po.get("po_number", "")}'), quote=True)}">
<td style="padding:12px;font-weight:500;color:#3b82f6;">{po_number}</td>
<td style="padding:12px;">{supplier}</td>
<td style="padding:12px;font-weight:500;">{site_code}</td>

View file

@ -5,6 +5,7 @@ Serves a simple HTML dashboard for viewing work orders and comments.
Accessed via Lambda Function URL.
"""
import json
import os
from html import escape as esc
@ -16,7 +17,7 @@ WORK_ORDERS_TABLE = os.environ.get("WORK_ORDERS_TABLE", "WorkOrders")
COMMENTS_TABLE = os.environ.get("COMMENTS_TABLE", "WorkOrderComments")
def get_work_orders():
def get_work_orders(limit=500):
table = dynamodb.Table(WORK_ORDERS_TABLE)
items = []
response = table.scan()
@ -25,16 +26,21 @@ def get_work_orders():
response = table.scan(ExclusiveStartKey=response["LastEvaluatedKey"])
items.extend(response.get("Items", []))
items.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
return items
return items[:limit]
def get_comments(work_order_id):
table = dynamodb.Table(COMMENTS_TABLE)
response = table.query(
KeyConditionExpression="work_order_id = :woid",
ExpressionAttributeValues={":woid": work_order_id},
)
items = response.get("Items", [])
items = []
kwargs = {
"KeyConditionExpression": "work_order_id = :woid",
"ExpressionAttributeValues": {":woid": work_order_id},
}
response = table.query(**kwargs)
items.extend(response.get("Items", []))
while "LastEvaluatedKey" in response:
response = table.query(**kwargs, ExclusiveStartKey=response["LastEvaluatedKey"])
items.extend(response.get("Items", []))
items.sort(key=lambda x: x.get("created_at", ""), reverse=True)
return items
@ -42,7 +48,7 @@ def get_comments(work_order_id):
def render_badge(value, color_map):
if not value:
value = "unknown"
color = color_map.get(value, "#9ca3af")
color = color_map.get(value.lower(), "#9ca3af")
label = esc(value.replace("_", " ").title())
return f'<span style="background:{color};color:#fff;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:500;">{label}</span>'
@ -168,7 +174,7 @@ def render_work_orders_list(work_orders):
updated = esc((wo.get("updated_at") or "")[:16])
rows += f"""
<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onclick="window.location='/wo?id={wo_id}'">
<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer;" onclick="window.location={esc(json.dumps(f'/wo?id={wo.get("work_order_id", "")}'), quote=True)}">
<td style="padding:12px;font-weight:500;color:#3b82f6;">{wo_id}</td>
<td style="padding:12px;max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{desc}</td>
<td style="padding:12px;">{site}</td>