Skip to content

Commit f98e9e4

Browse files
fix some issues with errors handler and add change pw page
1 parent 0695eb9 commit f98e9e4

3 files changed

Lines changed: 142 additions & 23 deletions

File tree

nebula/frontend/app.py

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

392392
async 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 theres 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 controllers /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)
540542
async 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+
550552
async 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")
10991150
async 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
21932243
async def run_scenarios(role, user):
21942244
"""

nebula/frontend/templates/layout.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ <h1 class="logo">
234234
{{ request.session.get("role") }}</a></li>
235235
{% if request.session.get("role", None) == "admin" %}
236236
<li><a href="{{ url_for('nebula_admin') }}">Admin Dashboard</a></li>
237+
{% else %}
238+
<li><a href="{{ url_for('nebula_settings') }}">Settings</a></li>
237239
{% endif %}
238240
<li><a href="{{ url_for('nebula_logout') }}">Logout</a></li>
239241
</ul>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{% extends "layout.html" %}
2+
{% block body %}
3+
{{ super() }}
4+
5+
<section id="home" class="home">
6+
<div class="container" style="text-align: center">
7+
<h1 class="logo" style="text-align: center">Settings</h1>
8+
<p style="text-align: center" class="fst-italic">Change your password</p>
9+
</div>
10+
</section>
11+
12+
<!-- Modal -->
13+
<div class="modal fade" id="user-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
14+
aria-hidden="true">
15+
<div class="modal-dialog modal-lg">
16+
<div class="modal-content">
17+
<div class="modal-header">
18+
<h5 class="modal-title" id="user-modal-title"></h5>
19+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
20+
</div>
21+
<div class="modal-body" id="user-modal-content"></div>
22+
<div class="modal-footer">
23+
<button type="button" class="btn btn-dark" data-bs-dismiss="modal">Close</button>
24+
</div>
25+
</div>
26+
</div>
27+
</div>
28+
29+
<section id="user-section" class="base">
30+
<div class="container">
31+
<div class="row p-3 justify-content-center">
32+
<div class="col-lg-6">
33+
<h3>Change Password</h3>
34+
<form action="/platform/user/change_password" method="post">
35+
<div class="form-group">
36+
<label for="current_password">Current Password</label>
37+
<input type="password" class="form-control" name="current_password" id="current_password" placeholder="Enter current password" required>
38+
</div>
39+
<div class="form-group mt-3">
40+
<label for="new_password">New Password</label>
41+
<input type="password" class="form-control" name="new_password" id="new_password" placeholder="Enter new password" required>
42+
</div>
43+
<div class="form-group mt-3">
44+
<button type="submit" class="btn btn-dark">Change Password</button>
45+
</div>
46+
</form>
47+
</div>
48+
</div>
49+
</div>
50+
</section>
51+
52+
{% if success %}
53+
<script>
54+
document.addEventListener('DOMContentLoaded', function() {
55+
showAlert('success', '{{ success }}');
56+
});
57+
</script>
58+
{% endif %}
59+
{% if error %}
60+
<script>
61+
document.addEventListener('DOMContentLoaded', function() {
62+
showAlert('danger', '{{ error }}');
63+
});
64+
</script>
65+
{% endif %}
66+
67+
{% endblock %}

0 commit comments

Comments
 (0)