mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 00:01:58 -06:00
Fixed Remote Access Login Issues to Borealis WebUI
This commit is contained in:
@@ -7,25 +7,43 @@ export default function Login({ onLogin }) {
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const sha512 = async (text) => {
|
const sha512 = async (text) => {
|
||||||
|
try {
|
||||||
|
if (window.crypto && window.crypto.subtle && window.isSecureContext) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const data = encoder.encode(text);
|
const data = encoder.encode(text);
|
||||||
const hashBuffer = await crypto.subtle.digest("SHA-512", data);
|
const hashBuffer = await window.crypto.subtle.digest("SHA-512", data);
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
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) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const hash = await sha512(password);
|
const hash = await sha512(password);
|
||||||
|
const body = hash
|
||||||
|
? { username, password_sha512: hash }
|
||||||
|
: { username, password };
|
||||||
const resp = await fetch("/api/auth/login", {
|
const resp = await fetch("/api/auth/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
body: JSON.stringify({ username, password_sha512: hash })
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!resp.ok) throw new Error(data?.error || `HTTP ${resp.status}`);
|
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("");
|
setError("");
|
||||||
onLogin({ username: data.username, role: data.role });
|
onLogin({ username: data.username, role: data.role });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -9,10 +9,16 @@ export default defineConfig({
|
|||||||
open: true,
|
open: true,
|
||||||
host: true,
|
host: true,
|
||||||
strictPort: 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: {
|
proxy: {
|
||||||
'/api': 'http://localhost:5000',
|
// Ensure cookies/headers are forwarded correctly to Flask
|
||||||
'/socket.io': { target:'ws://localhost:5000', ws:true }
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:5000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/socket.io': { target:'ws://127.0.0.1:5000', ws:true, changeOrigin: true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import requests
|
|||||||
from flask import Flask, request, jsonify, Response, send_from_directory, make_response, session
|
from flask import Flask, request, jsonify, Response, send_from_directory, make_response, session
|
||||||
from flask_socketio import SocketIO, emit, join_room
|
from flask_socketio import SocketIO, emit, join_room
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import os # To Read Production ReactJS Server Folder
|
import os # To Read Production ReactJS Server Folder
|
||||||
@@ -32,12 +34,33 @@ app = Flask(
|
|||||||
static_url_path=''
|
static_url_path=''
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enable CORS on All Routes (allow credentials for dev UI)
|
# Respect reverse proxy headers for scheme/host so cookies and redirects behave
|
||||||
CORS(app, supports_credentials=True)
|
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)
|
# Basic secret key for session cookies (can be overridden via env)
|
||||||
app.secret_key = os.environ.get('BOREALIS_SECRET', 'borealis-dev-secret')
|
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(
|
socketio = SocketIO(
|
||||||
app,
|
app,
|
||||||
cors_allowed_origins="*",
|
cors_allowed_origins="*",
|
||||||
@@ -136,12 +159,25 @@ def _user_row_to_dict(row):
|
|||||||
|
|
||||||
|
|
||||||
def _current_user():
|
def _current_user():
|
||||||
|
# Prefer server-side session if present
|
||||||
u = session.get('username')
|
u = session.get('username')
|
||||||
role = session.get('role')
|
role = session.get('role')
|
||||||
if not u:
|
if u:
|
||||||
return None
|
|
||||||
return {"username": u, "role": role or "User"}
|
return {"username": u, "role": role or "User"}
|
||||||
|
|
||||||
|
# Otherwise allow token-based auth (Authorization: Bearer <token> 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():
|
def _require_login():
|
||||||
user = _current_user()
|
user = _current_user()
|
||||||
@@ -159,6 +195,30 @@ def _require_admin():
|
|||||||
return None
|
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"])
|
@app.route("/api/auth/login", methods=["POST"])
|
||||||
def api_login():
|
def api_login():
|
||||||
payload = request.get_json(silent=True) or {}
|
payload = request.get_json(silent=True) or {}
|
||||||
@@ -193,7 +253,16 @@ def api_login():
|
|||||||
# set session cookie
|
# set session cookie
|
||||||
session['username'] = row[1]
|
session['username'] = row[1]
|
||||||
session['role'] = role
|
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:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@@ -201,7 +270,10 @@ def api_login():
|
|||||||
@app.route("/api/auth/logout", methods=["POST"]) # simple logout
|
@app.route("/api/auth/logout", methods=["POST"]) # simple logout
|
||||||
def api_logout():
|
def api_logout():
|
||||||
session.clear()
|
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
|
@app.route("/api/auth/me", methods=["GET"]) # whoami
|
||||||
|
|||||||
Reference in New Issue
Block a user