@@ -386,7 +386,7 @@ async def custom_http_exception_handler(request: Request, exc: StarletteHTTPExce
386386 return templates .TemplateResponse ("405.html" , context , status_code = exc .status_code )
387387 elif exc .status_code == status .HTTP_413_REQUEST_ENTITY_TOO_LARGE :
388388 return templates .TemplateResponse ("413.html" , context , status_code = exc .status_code )
389- return await request . app . default_exception_handler ( request , exc )
389+ return JSONResponse ({ "detail" : exc . detail , "status_code" : exc . status_code }, status_code = exc . status_code )
390390
391391
392392async def retry_with_backoff (func , * args , max_retries = 5 , initial_delay = 1 ):
@@ -468,7 +468,8 @@ async def _post():
468468 if response .status == 200 :
469469 return await response .json ()
470470 else :
471- raise HTTPException (status_code = response .status , detail = "Error posting data" )
471+ detail = await response .text ()
472+ raise HTTPException (status_code = response .status , detail = detail )
472473
473474 return await retry_with_backoff (_post )
474475
@@ -511,31 +512,32 @@ async def frontend_discover_vpn(session: dict = Depends(get_session)):
511512 to the internal controller, then returns the JSON result back to the client.
512513 Requires the user to be logged in.
513514 """
514-
515+
515516 # 1) Enforce authentication
516517 if "user" not in session :
517- # If there’ s no user in session, return HTTP 401 Unauthorized
518+ # If there' s no user in session, return HTTP 401 Unauthorized
518519 raise HTTPException (status_code = 401 , detail = "Login required" )
519-
520+
520521 # 2) Build the controller URL (using host/port from settings)
521522 url = f"http://{ settings .controller_host } :{ settings .controller_port } /discover-vpn"
522-
523+
523524 try :
524- # 3) Call the controller’ s /discover-vpn endpoint
525+ # 3) Call the controller' s /discover-vpn endpoint
525526 data = await controller_get (url )
526-
527+
527528 # 4) Return whatever JSON the controller gave us
528529 return JSONResponse (content = data )
529-
530+
530531 except HTTPException as e :
531532 # 5) If the controller itself raised an HTTPException, propagate it as-is
532533 raise e
533-
534+
534535 except Exception as e :
535536 # 6) For any other error, log it and return a generic 500 response
536537 logging .exception (f"Error proxying discover-vpn: { e } " )
537538 raise HTTPException (status_code = 500 , detail = "Error discovering VPN devices" )
538-
539+
540+
539541@app .get ("/platform/api/physical/state/{ip}" , response_class = JSONResponse )
540542async def proxy_physical_state (ip : str ):
541543 """
@@ -545,22 +547,23 @@ async def proxy_physical_state(ip: str):
545547 """
546548 url = f"http://{ settings .controller_host } :{ settings .controller_port } /physical/state/{ ip } "
547549 return await controller_get (url )
548-
549-
550+
551+
550552async def physical_nodes_available (ips : list [str ]) -> bool :
551553 """
552554 Return True only if *every* ip answered with {"running": false}.
553555 Any error or timeout counts as *not available*.
554556 """
555- tasks = [controller_get (
556- f"http://{ settings .controller_host } :{ settings .controller_port } /physical/state/{ ip } "
557- ) for ip in ips ]
557+ tasks = [
558+ controller_get (f"http://{ settings .controller_host } :{ settings .controller_port } /physical/state/{ ip } " )
559+ for ip in ips
560+ ]
558561 states = await asyncio .gather (* tasks , return_exceptions = True )
559-
562+
560563 for st in states :
561564 if not isinstance (st , dict ):
562- return False
563- if st .get ("running" ) is True :
565+ return False
566+ if st .get ("running" ) is True :
564567 return False
565568 return True
566569
@@ -1095,6 +1098,54 @@ async def nebula_admin(request: Request, session: dict = Depends(get_session)):
10951098 raise HTTPException (status_code = status .HTTP_401_UNAUTHORIZED )
10961099
10971100
1101+ @app .get ("/platform/settings" , response_class = HTMLResponse )
1102+ async def nebula_settings (request : Request , session : dict = Depends (get_session )):
1103+ """
1104+ Render the settings interface for authenticated users.
1105+ Enable to change the password of the user.
1106+ """
1107+ if "user" in session :
1108+ return templates .TemplateResponse ("settings.html" , {"request" : request })
1109+ else :
1110+ raise HTTPException (status_code = status .HTTP_401_UNAUTHORIZED )
1111+
1112+
1113+ @app .post ("/platform/user/change_password" )
1114+ async def nebula_change_password (
1115+ request : Request ,
1116+ session : dict = Depends (get_session ),
1117+ current_password : str = Form (...),
1118+ new_password : str = Form (...),
1119+ ):
1120+ """
1121+ Allow an authenticated user to change their own password by verifying the current password first.
1122+ """
1123+ if "user" not in session :
1124+ return RedirectResponse (url = "/platform/login" , status_code = status .HTTP_302_FOUND )
1125+
1126+ username = session ["user" ]
1127+ # Verify current password
1128+ try :
1129+ user_data = await verify_user (username , current_password )
1130+ except HTTPException as e :
1131+ if e .status_code == 401 :
1132+ return templates .TemplateResponse (
1133+ "settings.html" ,
1134+ {"request" : request , "error" : "Current password is incorrect." },
1135+ )
1136+ else :
1137+ return templates .TemplateResponse (
1138+ "settings.html" ,
1139+ {"request" : request , "error" : f"Error: { e .detail } " },
1140+ )
1141+ # Update password (keep role unchanged)
1142+ await update_user (username , new_password , user_data ["role" ])
1143+ return templates .TemplateResponse (
1144+ "settings.html" ,
1145+ {"request" : request , "success" : "Password changed successfully." },
1146+ )
1147+
1148+
10981149@app .post ("/platform/dashboard/{scenario_name}/save_note" )
10991150async def save_note_for_scenario (scenario_name : str , request : Request , session : dict = Depends (get_session )):
11001151 """
@@ -2161,7 +2212,7 @@ async def run_scenario(scenario_data: dict, role: str, user: str) -> None:
21612212
21622213 # PHYSICAL ➜ wait until all nodes are idle
21632214 is_physical = scenario_data .get ("deployment" ) == "physical"
2164- phys_ips = scenario_data .get ("physical_ips" , [])
2215+ phys_ips = scenario_data .get ("physical_ips" , [])
21652216
21662217 if is_physical :
21672218 wait_start = asyncio .get_event_loop ().time ()
@@ -2182,13 +2233,12 @@ async def run_scenario(scenario_data: dict, role: str, user: str) -> None:
21822233
21832234 # Register for synchronization
21842235 user_data .nodes_registration [scenario_name ] = {
2185- "n_nodes" : scenario_data ["n_nodes" ],
2186- "nodes" : set (),
2236+ "n_nodes" : scenario_data ["n_nodes" ],
2237+ "nodes" : set (),
21872238 "condition" : asyncio .Condition (),
21882239 }
21892240
21902241
2191-
21922242# Deploy the list of scenarios
21932243async def run_scenarios (role , user ):
21942244 """
0 commit comments