@@ -62,27 +62,28 @@ async def _get_all_worker_containers() -> list[dict[str, Any]]:
6262 is_android = sys_info .get ("device_type" ) == "android"
6363 worker_name = w .get ("name" , "worker" )
6464
65- # Docker containers (from Docker-based workers)
66- containers = _safe_json (w .get ("containers" , "[]" ))
67- for c in containers :
68- slug = c .get ("slug" , "" )
69- if slug :
70- result .append (
71- {
72- "slug" : slug ,
73- "name" : c .get ("name" , slug ),
74- "status" : c .get ("status" , "unknown" ),
75- "image" : c .get ("image" , "" ),
76- "cpu_percent" : c .get ("cpu_percent" , 0 ),
77- "memory_mb" : c .get ("memory_mb" , 0 ),
78- "category" : "" ,
79- "deployed_by" : worker_name ,
80- "_node" : worker_name ,
81- "_worker_id" : w .get ("id" ),
82- "_has_docker" : worker_has_docker ,
83- "_is_android" : False ,
84- }
85- )
65+ # Docker containers (from Docker-based workers only — skip for Android)
66+ if not is_android :
67+ containers = _safe_json (w .get ("containers" , "[]" ))
68+ for c in containers :
69+ slug = c .get ("slug" , "" )
70+ if slug :
71+ result .append (
72+ {
73+ "slug" : slug ,
74+ "name" : c .get ("name" , slug ),
75+ "status" : c .get ("status" , "unknown" ),
76+ "image" : c .get ("image" , "" ),
77+ "cpu_percent" : c .get ("cpu_percent" , 0 ),
78+ "memory_mb" : c .get ("memory_mb" , 0 ),
79+ "category" : "" ,
80+ "deployed_by" : worker_name ,
81+ "_node" : worker_name ,
82+ "_worker_id" : w .get ("id" ),
83+ "_has_docker" : worker_has_docker ,
84+ "_is_android" : False ,
85+ }
86+ )
8687
8788 # Android apps (from Android workers)
8889 if is_android :
@@ -309,6 +310,18 @@ def _require_owner(request: Request) -> dict[str, Any]:
309310 return user
310311
311312
313+ def _require_private_network (request : Request ) -> None :
314+ """Block requests from public IPs (for first-run setup)."""
315+ if not request .client or not request .client .host :
316+ return
317+ try :
318+ client_ip = ipaddress .ip_address (request .client .host )
319+ except ValueError :
320+ return
321+ if not (client_ip .is_loopback or client_ip .is_private ):
322+ raise HTTPException (status_code = 403 , detail = "First-run setup only allowed from private networks" )
323+
324+
312325# ---------------------------------------------------------------------------
313326# Auth routes
314327# ---------------------------------------------------------------------------
@@ -373,6 +386,8 @@ async def page_register(request: Request, error: str = ""):
373386 user = auth .get_current_user (request )
374387 if not user or user .get ("r" ) != "owner" :
375388 return RedirectResponse ("/login" , status_code = 303 )
389+ if is_first :
390+ _require_private_network (request )
376391
377392 return templates .TemplateResponse (
378393 request ,
@@ -404,6 +419,9 @@ async def do_register(
404419 if not user or user .get ("r" ) != "owner" :
405420 raise HTTPException (status_code = 403 , detail = "Only owners can add users" )
406421
422+ if is_first :
423+ _require_private_network (request )
424+
407425 if not re .match (r"^[a-zA-Z0-9_-]{3,32}$" , username ):
408426 return templates .TemplateResponse (
409427 request ,
@@ -773,7 +791,7 @@ class DeployRequest(BaseModel):
773791
774792@app .post ("/api/deploy/{slug}" )
775793async def api_deploy (request : Request , slug : str , body : DeployRequest , worker_id : int | None = None ) -> dict [str , str ]:
776- _require_writer (request )
794+ _require_owner (request )
777795 worker_id = await _resolve_worker_id (worker_id )
778796 svc = catalog .get_service (slug )
779797 if not svc :
@@ -794,6 +812,15 @@ async def api_deploy(request: Request, slug: str, body: DeployRequest, worker_id
794812 env [var ["key" ]] = str (default )
795813 env .update (body .env or {})
796814
815+ # Validate required env vars are not blank
816+ missing = [
817+ var .get ("label" , var ["key" ])
818+ for var in docker_conf .get ("env" , [])
819+ if var .get ("required" ) and not env .get (var ["key" ], "" ).strip ()
820+ ]
821+ if missing :
822+ raise HTTPException (status_code = 400 , detail = f"Missing required fields: { ', ' .join (missing )} " )
823+
797824 # Ports — key is "container_port/protocol" per Docker SDK
798825 ports : dict [str , int ] = {}
799826 for mapping in docker_conf .get ("ports" , []):
@@ -1651,17 +1678,7 @@ async def api_worker_command(request: Request, worker_id: int, body: WorkerComma
16511678 _require_writer (request )
16521679
16531680 worker = await database .get_worker (worker_id )
1654- if not worker :
1655- raise HTTPException (status_code = 404 , detail = "Worker not found" )
1656- if worker ["status" ] != "online" :
1657- raise HTTPException (status_code = 503 , detail = "Worker is offline" )
1658- if not worker ["url" ]:
1659- raise HTTPException (status_code = 503 , detail = "Worker URL not known" )
1660-
1661- url = worker ["url" ].rstrip ("/" )
1662- headers = {}
1663- if FLEET_API_KEY :
1664- headers ["Authorization" ] = f"Bearer { FLEET_API_KEY } "
1681+ url , headers = _get_verified_worker_url (worker )
16651682
16661683 try :
16671684 async with httpx .AsyncClient (timeout = 30 ) as client :
0 commit comments