release: BUG fixes batch 1-6, full bug scan docs, Ubuntu/Linux dev
- Backend: auth refresh reuse, schedule/retry/sync/agent/files fixes - Frontend: push WS, files ETag browse, vite build assets - Docs: audit/changelog, production deploy gate, bug scan registry B7-77 - Deploy: deploy-production.sh, prune assets, gate logs
This commit is contained in:
+62
-32
@@ -36,14 +36,20 @@ CONFIG_JSON = CONFIG_DIR / "config.json"
|
||||
STATE_FILE = ROOT_DIR / ".install_state.json"
|
||||
|
||||
|
||||
def _is_installed() -> bool:
|
||||
return ENV_FILE.exists() or INSTALL_LOCK.exists()
|
||||
def _has_env() -> bool:
|
||||
"""Python backend configured (.env written at install step 3)."""
|
||||
return ENV_FILE.exists()
|
||||
|
||||
|
||||
def _is_locked() -> bool:
|
||||
return INSTALL_LOCK.exists()
|
||||
|
||||
|
||||
def _is_installed() -> bool:
|
||||
"""Install wizard fully complete — same as locked (backward-compatible for /status)."""
|
||||
return _is_locked()
|
||||
|
||||
|
||||
# ── Pydantic Models ──
|
||||
|
||||
class InitDbRequest(BaseModel):
|
||||
@@ -176,7 +182,6 @@ def _write_config_json(req: InitDbRequest, api_key: str, site_url: str) -> None:
|
||||
"db_port": req.db_port,
|
||||
"db_name": req.db_name,
|
||||
"db_user": req.db_user,
|
||||
"db_pass": req.db_pass,
|
||||
"app_name": "Nexus",
|
||||
"app_version": "6.0.0",
|
||||
"session_lifetime": 28800,
|
||||
@@ -292,6 +297,19 @@ def _configure_guardian(install_dir: str, api_port: str) -> list[str]:
|
||||
return results
|
||||
|
||||
|
||||
def _finalize_install_lock() -> None:
|
||||
"""Write install lock marker and remove wizard state file."""
|
||||
write_utf8_lf(
|
||||
INSTALL_LOCK,
|
||||
f"Nexus installation locked at {datetime.now(timezone.utc).isoformat()}\n",
|
||||
)
|
||||
if STATE_FILE.exists():
|
||||
try:
|
||||
STATE_FILE.unlink()
|
||||
except OSError as e:
|
||||
logger.warning("Failed to remove install state file: %s", e)
|
||||
|
||||
|
||||
# ── Endpoints ──
|
||||
|
||||
@router.get("/status")
|
||||
@@ -300,20 +318,35 @@ async def install_status():
|
||||
return {
|
||||
"installed": _is_installed(),
|
||||
"locked": _is_locked(),
|
||||
"configured": _has_env(),
|
||||
}
|
||||
|
||||
|
||||
def _reject_if_installed():
|
||||
"""Block install wizard after .env exists (except GET /status)."""
|
||||
if _is_installed():
|
||||
def _reject_if_locked():
|
||||
"""Block install wizard mutations after step 5 lock."""
|
||||
if _is_locked():
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="系统已安装,安装向导已关闭。仅可查询 /api/install/status",
|
||||
detail="安装向导已锁定。仅可查询 /api/install/status",
|
||||
)
|
||||
|
||||
|
||||
# Backward-compatible alias
|
||||
_reject_post_install_wizard = _reject_if_installed
|
||||
def _reject_if_env_exists():
|
||||
"""Block repeat database init (step 3) once .env exists."""
|
||||
if _has_env():
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="系统已初始化,无法重复执行数据库安装。请继续创建管理员或前往登录。",
|
||||
)
|
||||
|
||||
|
||||
def _reject_if_installed():
|
||||
"""Backward-compatible name — means wizard locked, not merely .env present."""
|
||||
_reject_if_locked()
|
||||
|
||||
|
||||
# Steps 2–5 (until lock): allow while .env exists
|
||||
_reject_post_install_wizard = _reject_if_locked
|
||||
|
||||
|
||||
@router.get("/env-check")
|
||||
@@ -402,7 +435,8 @@ async def env_check():
|
||||
@router.post("/init-db")
|
||||
async def init_db(req: InitDbRequest):
|
||||
"""Step 3: Connect to MySQL, create tables, write configs."""
|
||||
_reject_if_installed()
|
||||
_reject_if_locked()
|
||||
_reject_if_env_exists()
|
||||
|
||||
db_url = _make_db_url(req)
|
||||
redis_url = _build_redis_url(req)
|
||||
@@ -538,9 +572,9 @@ async def init_db(req: InitDbRequest):
|
||||
@router.post("/create-admin")
|
||||
async def create_admin(req: CreateAdminRequest):
|
||||
"""Step 4: Create admin account and set brand name."""
|
||||
_reject_if_installed()
|
||||
if _is_locked():
|
||||
raise HTTPException(400, "系统已锁定")
|
||||
_reject_if_locked()
|
||||
if not _has_env():
|
||||
raise HTTPException(400, "请先完成步骤3:数据库初始化")
|
||||
|
||||
if len(req.admin_password) < 6:
|
||||
raise HTTPException(400, "密码长度至少6位")
|
||||
@@ -561,9 +595,9 @@ async def create_admin(req: CreateAdminRequest):
|
||||
# Insert admin
|
||||
await conn.execute(
|
||||
text(
|
||||
"INSERT INTO admins (username, password_hash, email) "
|
||||
"VALUES (:u, :h, :e) "
|
||||
"ON DUPLICATE KEY UPDATE password_hash = :h2, email = :e2"
|
||||
"INSERT INTO admins (username, password_hash, email, is_active) "
|
||||
"VALUES (:u, :h, :e, 1) "
|
||||
"ON DUPLICATE KEY UPDATE password_hash = :h2, email = :e2, is_active = 1"
|
||||
),
|
||||
{
|
||||
"u": req.admin_username,
|
||||
@@ -624,7 +658,9 @@ async def create_admin(req: CreateAdminRequest):
|
||||
state["system_name"] = req.system_name
|
||||
write_utf8_lf(STATE_FILE, json.dumps(state, ensure_ascii=False) + "\n")
|
||||
|
||||
return {"success": True}
|
||||
# Atomically lock after admin creation (closes unauthenticated install API window)
|
||||
_finalize_install_lock()
|
||||
return {"success": True, "locked": True}
|
||||
|
||||
|
||||
@router.post("/lock")
|
||||
@@ -635,7 +671,10 @@ async def lock_install():
|
||||
This prevents unauthenticated callers from locking the installer
|
||||
before the legitimate admin has finished setup.
|
||||
"""
|
||||
_reject_if_installed()
|
||||
if _is_locked():
|
||||
return {"success": True, "already_locked": True}
|
||||
if not _has_env():
|
||||
raise HTTPException(400, "请先完成步骤3:数据库初始化")
|
||||
|
||||
# Verify at least one active admin account exists before allowing lock
|
||||
try:
|
||||
@@ -656,21 +695,12 @@ async def lock_install():
|
||||
except Exception as e:
|
||||
logger.warning("Failed to verify admin account before locking: %s", e)
|
||||
# If DB is temporarily unreachable, conservatively reject lock
|
||||
raise HTTPException(status_code=503, detail="数据库暂时不可用,无法验证管理员账户,请稍后重试。")
|
||||
|
||||
# Create lock marker file
|
||||
write_utf8_lf(
|
||||
INSTALL_LOCK,
|
||||
f"Nexus installation locked at {datetime.now(timezone.utc).isoformat()}\n",
|
||||
)
|
||||
|
||||
# Clean up state file
|
||||
if STATE_FILE.exists():
|
||||
try:
|
||||
STATE_FILE.unlink()
|
||||
except OSError as e:
|
||||
logger.warning("Failed to remove install state file: %s", e)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="数据库暂时不可用,无法验证管理员账户,请稍后重试。",
|
||||
) from e
|
||||
|
||||
_finalize_install_lock()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user