diff --git a/Data/Server/WebUI/src/Login.jsx b/Data/Server/WebUI/src/Login.jsx index c9f85a8..0d015fc 100644 --- a/Data/Server/WebUI/src/Login.jsx +++ b/Data/Server/WebUI/src/Login.jsx @@ -7,25 +7,43 @@ export default function Login({ onLogin }) { const [error, setError] = useState(""); const sha512 = async (text) => { - const encoder = new TextEncoder(); - const data = encoder.encode(text); - const hashBuffer = await crypto.subtle.digest("SHA-512", data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); + try { + if (window.crypto && window.crypto.subtle && window.isSecureContext) { + const encoder = new TextEncoder(); + const data = encoder.encode(text); + const hashBuffer = await window.crypto.subtle.digest("SHA-512", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); + } + } catch (_) { + // fall through to return null + } + // Not a secure context or subtle crypto unavailable + return null; }; const handleSubmit = async (e) => { e.preventDefault(); try { const hash = await sha512(password); + const body = hash + ? { username, password_sha512: hash } + : { username, password }; const resp = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", - body: JSON.stringify({ username, password_sha512: hash }) + body: JSON.stringify(body) }); const data = await resp.json(); if (!resp.ok) throw new Error(data?.error || `HTTP ${resp.status}`); + // Persist token via cookie as a proxy-friendly fallback + if (data?.token) { + try { + // Set cookie for current host; SameSite=Lax for dev + document.cookie = `borealis_auth=${data.token}; Path=/; SameSite=Lax`; + } catch (_) {} + } setError(""); onLogin({ username: data.username, role: data.role }); } catch (err) { diff --git a/Data/Server/WebUI/vite.config.mts b/Data/Server/WebUI/vite.config.mts index 025a5c1..326bf09 100644 --- a/Data/Server/WebUI/vite.config.mts +++ b/Data/Server/WebUI/vite.config.mts @@ -9,10 +9,16 @@ export default defineConfig({ open: true, host: true, strictPort: true, - allowedHosts: ['localhost','127.0.0.1','borealis.bunny-lab.io'], + // Allow LAN/IP access during dev (so other devices can reach Vite) + // If you want to restrict, replace `true` with an explicit allowlist. + allowedHosts: true, proxy: { - '/api': 'http://localhost:5000', - '/socket.io': { target:'ws://localhost:5000', ws:true } + // Ensure cookies/headers are forwarded correctly to Flask + '/api': { + target: 'http://127.0.0.1:5000', + changeOrigin: true, + }, + '/socket.io': { target:'ws://127.0.0.1:5000', ws:true, changeOrigin: true } } }, build: { diff --git a/Data/Server/server.py b/Data/Server/server.py index 394beab..4213b32 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -8,6 +8,8 @@ import requests from flask import Flask, request, jsonify, Response, send_from_directory, make_response, session from flask_socketio import SocketIO, emit, join_room from flask_cors import CORS +from werkzeug.middleware.proxy_fix import ProxyFix +from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired import time import os # To Read Production ReactJS Server Folder @@ -32,12 +34,33 @@ app = Flask( static_url_path='' ) -# Enable CORS on All Routes (allow credentials for dev UI) -CORS(app, supports_credentials=True) +# Respect reverse proxy headers for scheme/host so cookies and redirects behave +app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) + +# Enable CORS on All Routes (allow credentials). Optionally lock down via env. +_cors_origins = os.environ.get('BOREALIS_CORS_ORIGINS') # e.g. "https://ui.example.com,https://admin.example.com" +if _cors_origins: + origins = [o.strip() for o in _cors_origins.split(',') if o.strip()] + CORS(app, supports_credentials=True, origins=origins) +else: + CORS(app, supports_credentials=True) # Basic secret key for session cookies (can be overridden via env) app.secret_key = os.environ.get('BOREALIS_SECRET', 'borealis-dev-secret') +# Session cookie policy (tunable for dev/prod/reverse proxy) +# Defaults keep dev working; override via env in production/proxy scenarios. +app.config.update( + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE=os.environ.get('BOREALIS_COOKIE_SAMESITE', 'Lax'), # set to 'None' when UI/API are on different sites + SESSION_COOKIE_SECURE=(os.environ.get('BOREALIS_COOKIE_SECURE', '0').lower() in ('1', 'true', 'yes')), +) + +# Optionally pin cookie domain if served under a fixed hostname (leave unset for host-only/IP dev) +_cookie_domain = os.environ.get('BOREALIS_COOKIE_DOMAIN') # e.g. ".example.com" or "borealis.bunny-lab.io" +if _cookie_domain: + app.config['SESSION_COOKIE_DOMAIN'] = _cookie_domain + socketio = SocketIO( app, cors_allowed_origins="*", @@ -136,11 +159,24 @@ def _user_row_to_dict(row): def _current_user(): + # Prefer server-side session if present u = session.get('username') role = session.get('role') - if not u: - return None - return {"username": u, "role": role or "User"} + if u: + return {"username": u, "role": role or "User"} + + # Otherwise allow token-based auth (Authorization: Bearer or borealis_auth cookie) + token = None + auth = request.headers.get('Authorization') or '' + if auth.lower().startswith('bearer '): + token = auth.split(' ', 1)[1].strip() + if not token: + token = request.cookies.get('borealis_auth') + if token: + user = _verify_token(token) + if user: + return user + return None def _require_login(): @@ -159,6 +195,30 @@ def _require_admin(): return None +# --------------------------------------------- +# Token helpers (for dev/proxy-friendly auth) +# --------------------------------------------- +def _token_serializer(): + secret = app.secret_key or 'borealis-dev-secret' + return URLSafeTimedSerializer(secret, salt='borealis-auth') + + +def _make_token(username: str, role: str) -> str: + s = _token_serializer() + payload = {"u": username, "r": role or 'User', "ts": _now_ts()} + return s.dumps(payload) + + +def _verify_token(token: str): + try: + s = _token_serializer() + max_age = int(os.environ.get('BOREALIS_TOKEN_TTL_SECONDS', 60*60*24*30)) # 30 days + data = s.loads(token, max_age=max_age) + return {"username": data.get('u'), "role": data.get('r') or 'User'} + except (BadSignature, SignatureExpired, Exception): + return None + + @app.route("/api/auth/login", methods=["POST"]) def api_login(): payload = request.get_json(silent=True) or {} @@ -193,7 +253,16 @@ def api_login(): # set session cookie session['username'] = row[1] session['role'] = role - return jsonify({"status": "ok", "username": row[1], "role": role}) + + # also issue a signed bearer token and set a dev-friendly cookie + token = _make_token(row[1], role) + resp = jsonify({"status": "ok", "username": row[1], "role": role, "token": token}) + # mirror session cookie flags for the token cookie + samesite = app.config.get('SESSION_COOKIE_SAMESITE', 'Lax') + secure = bool(app.config.get('SESSION_COOKIE_SECURE', False)) + domain = app.config.get('SESSION_COOKIE_DOMAIN', None) + resp.set_cookie('borealis_auth', token, httponly=False, samesite=samesite, secure=secure, domain=domain, path='/') + return resp except Exception as e: return jsonify({"error": str(e)}), 500 @@ -201,7 +270,10 @@ def api_login(): @app.route("/api/auth/logout", methods=["POST"]) # simple logout def api_logout(): session.clear() - return jsonify({"status": "ok"}) + resp = jsonify({"status": "ok"}) + # Clear token cookie as well + resp.set_cookie('borealis_auth', '', expires=0, path='/') + return resp @app.route("/api/auth/me", methods=["GET"]) # whoami