feat: add internal alpha frontend shell

This commit is contained in:
lussierj committed 2026-08-10 17:26:07 +00:00
1 parent eeb3c7634e
commit b741fd799b
10 files changed
+1982 -15

No files matched your search

+61 -14
View File
@@ -1,7 +1,7 @@
from datetime import datetime, timedelta, timezone
from html import escape
from typing import Any, Callable
from urllib.parse import quote
from urllib.parse import quote, urlsplit
import jwt
import psycopg
@@ -633,20 +633,60 @@ def reset_password(
def protected_page(path: str) -> bool:
exact = {"/dashboard", "/repairs", "/locations", "/system-health", "/accounts"}
exact = {
"/dashboard",
"/repairs",
"/locations",
"/system-health",
"/accounts",
"/monitoring/map",
"/operations/repairs",
"/assets/miners",
}
prefixes = ("/miners/", "/agent/", "/tools/")
return path in exact or path.startswith(prefixes)
def safe_next_path(value: str | None) -> str:
"""Return a safe local application destination for login redirects."""
fallback = "/dashboard"
if not value or value != value.strip():
return fallback
if any(ord(character) < 32 or ord(character) == 127 for character in value):
return fallback
try:
target = urlsplit(value)
except ValueError:
return fallback
path = target.path
if (
target.scheme
or target.netloc
or target.fragment
or not path.startswith("/")
or path.startswith("//")
or "\\" in path
or "%" in path
or any(segment in {".", ".."} for segment in path.split("/"))
or not protected_page(path)
):
return fallback
return value
def required_page_permission(path: str) -> str:
if path == "/accounts":
return "users.manage"
if path == "/system-health":
return "system_health.view"
if path == "/repairs":
if path in {"/repairs", "/operations/repairs"}:
return "repairs.view"
if path == "/locations":
if path in {"/locations", "/monitoring/map"}:
return "locations.view"
if path == "/assets/miners":
return "miners.view"
if path.startswith("/agent/"):
return "agents.view"
if path.startswith("/miners/"):
@@ -781,16 +821,19 @@ def install_identity_middleware(app) -> None:
if path == "/login":
user = request_user_or_none(request)
if user:
next_path = request.query_params.get("next") or "/dashboard"
if not next_path.startswith("/") or next_path.startswith("//"):
next_path = "/dashboard"
return RedirectResponse(next_path, status_code=303)
return RedirectResponse(
safe_next_path(request.query_params.get("next")),
status_code=303,
)
return await call_next(request)
if protected_page(path):
user = request_user_or_none(request)
if not user:
destination = "/login?next=" + quote(path, safe="/")
requested_path = path
if request.url.query:
requested_path += "?" + request.url.query
destination = "/login?next=" + quote(requested_path, safe="")
response = RedirectResponse(destination, status_code=303)
clear_session_cookie(response, request)
return response
@@ -853,7 +896,8 @@ button:disabled { opacity: .6; cursor: wait; }
<body>
<header><div class="brand">MinerOps</div></header>
<main>
<section class="login-panel" id="panel">
<section class="login-panel" id="panel"
data-next="__MINEROPS_SAFE_NEXT_PATH__">
<div class="panel-head"><h1 id="title">Sign in</h1><p class="subtitle" id="subtitle">Use your MinerOps account to continue.</p></div>
<form id="authForm">
<div class="field setup-only"><label for="displayName">Display name</label><input id="displayName" autocomplete="name" maxlength="120"></div>
@@ -875,8 +919,7 @@ button:disabled { opacity: .6; cursor: wait; }
let setupRequired = false;
function destination() {
const value = new URLSearchParams(location.search).get("next") || "/dashboard";
return value.startsWith("/") && !value.startsWith("//") ? value : "/dashboard";
return panel.dataset.next || "/dashboard";
}
function sessionHint() {
@@ -1063,8 +1106,12 @@ dialog::backdrop { background: rgba(18, 27, 35, .48); }
@router.get("/login", response_class=HTMLResponse)
def login_page():
return LOGIN_HTML
def login_page(request: Request):
next_path = escape(
safe_next_path(request.query_params.get("next")),
quote=True,
)
return LOGIN_HTML.replace("__MINEROPS_SAFE_NEXT_PATH__", next_path)
@router.get("/accounts", response_class=HTMLResponse)