mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 15:41:58 -06:00
Fix static asset fallback and seed default admin
This commit is contained in:
@@ -66,6 +66,10 @@ def bootstrap() -> EngineRuntime:
|
||||
else:
|
||||
logger.info("migrations-skipped")
|
||||
|
||||
with sqlite_connection.connection_scope(settings.database_path) as conn:
|
||||
sqlite_migrations.ensure_default_admin(conn)
|
||||
logger.info("default-admin-ensured")
|
||||
|
||||
app = create_app(settings, db_factory=db_factory)
|
||||
services = build_service_container(settings, db_factory=db_factory, logger=logger.getChild("services"))
|
||||
app.extensions["engine_services"] = services
|
||||
|
||||
@@ -122,6 +122,10 @@ def _resolve_static_root(project_root: Path) -> Path:
|
||||
project_root / "Engine" / "web-interface",
|
||||
project_root / "Data" / "Engine" / "WebUI" / "build",
|
||||
project_root / "Data" / "Engine" / "WebUI",
|
||||
project_root / "Server" / "web-interface" / "build",
|
||||
project_root / "Server" / "web-interface",
|
||||
project_root / "Server" / "WebUI" / "build",
|
||||
project_root / "Server" / "WebUI",
|
||||
project_root / "Data" / "Server" / "web-interface" / "build",
|
||||
project_root / "Data" / "Server" / "web-interface",
|
||||
project_root / "Data" / "Server" / "WebUI" / "build",
|
||||
|
||||
@@ -9,7 +9,7 @@ from .connection import (
|
||||
connection_factory,
|
||||
connection_scope,
|
||||
)
|
||||
from .migrations import apply_all
|
||||
from .migrations import apply_all, ensure_default_admin
|
||||
|
||||
__all__ = [
|
||||
"SQLiteConnectionFactory",
|
||||
@@ -18,6 +18,7 @@ __all__ = [
|
||||
"connection_factory",
|
||||
"connection_scope",
|
||||
"apply_all",
|
||||
"ensure_default_admin",
|
||||
]
|
||||
|
||||
try: # pragma: no cover - optional dependency shim
|
||||
|
||||
@@ -15,6 +15,10 @@ from typing import List, Optional, Sequence, Tuple
|
||||
|
||||
|
||||
DEVICE_TABLE = "devices"
|
||||
_DEFAULT_ADMIN_USERNAME = "admin"
|
||||
_DEFAULT_ADMIN_PASSWORD_SHA512 = (
|
||||
"e6c83b282aeb2e022844595721cc00bbda47cb24537c1779f9bb84f04039e1676e6ba8573e588da1052510e3aa0a32a9e55879ae22b0c2d62136fc0a3e85f8bb"
|
||||
)
|
||||
|
||||
|
||||
def apply_all(conn: sqlite3.Connection) -> None:
|
||||
@@ -30,6 +34,8 @@ def apply_all(conn: sqlite3.Connection) -> None:
|
||||
_ensure_github_token_table(conn)
|
||||
_ensure_scheduled_jobs_table(conn)
|
||||
_ensure_scheduled_job_run_tables(conn)
|
||||
_ensure_users_table(conn)
|
||||
_ensure_default_admin(conn)
|
||||
|
||||
conn.commit()
|
||||
|
||||
@@ -504,4 +510,86 @@ def _normalized_guid(value: Optional[str]) -> str:
|
||||
return ""
|
||||
return str(value).strip()
|
||||
|
||||
__all__ = ["apply_all"]
|
||||
def _ensure_users_table(conn: sqlite3.Connection) -> None:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT,
|
||||
password_sha512 TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'Admin',
|
||||
last_login INTEGER,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER,
|
||||
mfa_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
mfa_secret TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
try:
|
||||
cur.execute("PRAGMA table_info(users)")
|
||||
columns = [row[1] for row in cur.fetchall()]
|
||||
if "mfa_enabled" not in columns:
|
||||
cur.execute("ALTER TABLE users ADD COLUMN mfa_enabled INTEGER NOT NULL DEFAULT 0")
|
||||
if "mfa_secret" not in columns:
|
||||
cur.execute("ALTER TABLE users ADD COLUMN mfa_secret TEXT")
|
||||
except sqlite3.Error:
|
||||
# Aligning the schema is best-effort; older deployments may lack ALTER
|
||||
# TABLE privileges but can continue using existing columns.
|
||||
pass
|
||||
|
||||
|
||||
def _ensure_default_admin(conn: sqlite3.Connection) -> None:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT COUNT(*) FROM users WHERE LOWER(role)='admin'")
|
||||
row = cur.fetchone()
|
||||
if row and (row[0] or 0):
|
||||
return
|
||||
|
||||
now = int(datetime.now(timezone.utc).timestamp())
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) FROM users WHERE LOWER(username)=LOWER(?)",
|
||||
(_DEFAULT_ADMIN_USERNAME,),
|
||||
)
|
||||
existing = cur.fetchone()
|
||||
if not existing or not (existing[0] or 0):
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO users (
|
||||
username, display_name, password_sha512, role,
|
||||
last_login, created_at, updated_at, mfa_enabled, mfa_secret
|
||||
) VALUES (?, ?, ?, 'Admin', 0, ?, ?, 0, NULL)
|
||||
""",
|
||||
(
|
||||
_DEFAULT_ADMIN_USERNAME,
|
||||
"Administrator",
|
||||
_DEFAULT_ADMIN_PASSWORD_SHA512,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE users
|
||||
SET role='Admin',
|
||||
updated_at=?
|
||||
WHERE LOWER(username)=LOWER(?)
|
||||
AND LOWER(role)!='admin'
|
||||
""",
|
||||
(now, _DEFAULT_ADMIN_USERNAME),
|
||||
)
|
||||
|
||||
|
||||
def ensure_default_admin(conn: sqlite3.Connection) -> None:
|
||||
"""Guarantee that at least one admin account exists."""
|
||||
|
||||
_ensure_users_table(conn)
|
||||
_ensure_default_admin(conn)
|
||||
conn.commit()
|
||||
|
||||
|
||||
__all__ = ["apply_all", "ensure_default_admin"]
|
||||
|
||||
@@ -63,6 +63,23 @@ def test_static_root_falls_back_to_legacy_source(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("BOREALIS_ROOT", raising=False)
|
||||
|
||||
|
||||
def test_static_root_considers_runtime_copy(tmp_path, monkeypatch):
|
||||
"""Runtime Server/WebUI copies should be considered when Data assets are missing."""
|
||||
|
||||
runtime_source = tmp_path / "Server" / "WebUI"
|
||||
runtime_source.mkdir(parents=True)
|
||||
(runtime_source / "index.html").write_text("runtime", encoding="utf-8")
|
||||
|
||||
monkeypatch.setenv("BOREALIS_ROOT", str(tmp_path))
|
||||
monkeypatch.delenv("BOREALIS_STATIC_ROOT", raising=False)
|
||||
|
||||
settings = load_environment()
|
||||
|
||||
assert settings.flask.static_root == runtime_source.resolve()
|
||||
|
||||
monkeypatch.delenv("BOREALIS_ROOT", raising=False)
|
||||
|
||||
|
||||
def test_resolve_project_root_defaults_to_repository(monkeypatch):
|
||||
"""The project root should resolve to the repository checkout."""
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import sqlite3
|
||||
import unittest
|
||||
|
||||
@@ -24,6 +25,56 @@ class MigrationTests(unittest.TestCase):
|
||||
self.assertIn("scheduled_jobs", tables)
|
||||
self.assertIn("scheduled_job_runs", tables)
|
||||
self.assertIn("github_token", tables)
|
||||
self.assertIn("users", tables)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT username, role, password_sha512 FROM users WHERE LOWER(username)=LOWER(?)",
|
||||
("admin",),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
self.assertIsNotNone(row)
|
||||
if row:
|
||||
self.assertEqual(row[0], "admin")
|
||||
self.assertEqual(row[1].lower(), "admin")
|
||||
self.assertEqual(row[2], hashlib.sha512(b"Password").hexdigest())
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_ensure_default_admin_promotes_existing_user(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT,
|
||||
password_sha512 TEXT,
|
||||
role TEXT,
|
||||
last_login INTEGER,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER,
|
||||
mfa_enabled INTEGER DEFAULT 0,
|
||||
mfa_secret TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO users (username, display_name, password_sha512, role) VALUES (?, ?, ?, ?)",
|
||||
("admin", "Custom", "hash", "user"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
migrations.ensure_default_admin(conn)
|
||||
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT role, password_sha512 FROM users WHERE LOWER(username)=LOWER(?)",
|
||||
("admin",),
|
||||
)
|
||||
role, password_hash = cursor.fetchone()
|
||||
self.assertEqual(role.lower(), "admin")
|
||||
self.assertEqual(password_hash, "hash")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user