feat: add internal alpha frontend shell
This commit is contained in:
1 parent
eeb3c7634e
commit
b741fd799b
10 files changed
+1982
-15
No files matched your search
+61
-14
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user