From d803e2d7f645f6020f7814ade132fe0815e2f351 Mon Sep 17 00:00:00 2001 From: Ettore <=> Date: Fri, 8 May 2026 20:16:04 +0200 Subject: [PATCH] Add mock options in frontend and removed from environment variables --- .env.example | 3 --- README.md | 1 - docker-compose.yml | 1 - src/core/config.py | 2 -- src/core/database.py | 1 + src/routers/credentials.py | 34 ++++++++++++++++++++++++++++++++++ src/routers/gates.py | 4 ++++ src/services/gates.py | 9 ++++----- src/static/admin.html | 10 ++++++++++ src/static/admin.js | 14 ++++++++++++++ 10 files changed, 67 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index a0f1fb5..12a7088 100644 --- a/.env.example +++ b/.env.example @@ -5,9 +5,6 @@ ADMIN_PASSWORD=changeme123 # JWT signing secret generate with: python -c "import secrets; print(secrets.token_hex(32))" SECRET_KEY=replace-with-a-random-64-char-hex-string -# Set to true to skip real AVConnect calls (for testing) -# MOCK_AVCONNECT=true - # Port configuration APP_PORT=8000 diff --git a/README.md b/README.md index 800dc14..6471394 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,6 @@ All settings are read from environment variables. | `ADMIN_PASSWORD` | *(none)* | Password for the initial admin account. If unset, no seed account is created. | | `APP_PORT` | `8000` | HTTP port the server listens on | | `DATABASE_URL` | `sqlite:///data/gates.db` | SQLAlchemy database URL | -| `MOCK_AVCONNECT` | `false` | Set to `true` to skip real AVConnect calls (always returns success — useful for development) | ## Running with Docker Compose diff --git a/docker-compose.yml b/docker-compose.yml index fd0052e..4fb9ec2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,5 @@ services: - ADMIN_USERNAME=admin - ADMIN_PASSWORD=changeme - SECRET_KEY=supersecretkey - - MOCK_AVCONNECT=false - APP_PORT=8000 restart: unless-stopped \ No newline at end of file diff --git a/src/core/config.py b/src/core/config.py index d6af125..21b405d 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -1,3 +1 @@ import os - -MOCK_AVCONNECT: bool = os.environ.get("MOCK_AVCONNECT", "").lower() in ("1", "true", "yes") diff --git a/src/core/database.py b/src/core/database.py index f2ca88c..8507b01 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -41,6 +41,7 @@ class ApiCredential(Base): username: Mapped[str] = mapped_column(String, nullable=False) password_enc: Mapped[str] = mapped_column(String, nullable=False) # Fernet-encrypted session_id: Mapped[Optional[str]] = mapped_column(String, nullable=True) + mock_avconnect: Mapped[bool] = mapped_column(Boolean, default=False) class Keypass(Base): diff --git a/src/routers/credentials.py b/src/routers/credentials.py index d6312a4..ad5aa3f 100644 --- a/src/routers/credentials.py +++ b/src/routers/credentials.py @@ -1,6 +1,7 @@ from typing import Optional from fastapi import APIRouter, Depends +from pydantic import BaseModel from sqlalchemy.orm import Session from core.auth import encrypt_secret @@ -38,3 +39,36 @@ async def upsert_credential( db.commit() db.refresh(cred) return CredentialRead(id=cred.id, username=cred.username) + + +# ── Mock AVConnect setting ───────────────────────────────────────────────────── + +class MockSettingResponse(BaseModel): + enabled: bool + + +class MockSettingRequest(BaseModel): + enabled: bool + + +@router.get("/mock", response_model=MockSettingResponse) +async def get_mock_setting( + db: Session = Depends(get_db), _: dict = Depends(require_admin) +): + cred: Optional[ApiCredential] = db.query(ApiCredential).first() + return MockSettingResponse(enabled=bool(cred.mock_avconnect) if cred else False) + + +@router.put("/mock", response_model=MockSettingResponse) +async def set_mock_setting( + req: MockSettingRequest, + db: Session = Depends(get_db), + _: dict = Depends(require_admin), +): + cred: Optional[ApiCredential] = db.query(ApiCredential).first() + if not cred: + from fastapi import HTTPException + raise HTTPException(503, "AVConnect credentials not configured") + cred.mock_avconnect = req.enabled + db.commit() + return MockSettingResponse(enabled=req.enabled) diff --git a/src/routers/gates.py b/src/routers/gates.py index ffcdb7b..0b616ac 100644 --- a/src/routers/gates.py +++ b/src/routers/gates.py @@ -88,11 +88,13 @@ async def admin_open_gate( ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else None) ua = request.headers.get("User-Agent") + mock = bool(cred_db.mock_avconnect) success, error_msg, new_sid = call_open_gate( gate_db.avconnect_macro_id, cred_db.username, decrypt_secret(cred_db.password_enc), cred_db.session_id, + mock=mock, ) db.add(GateAccessLog( @@ -154,11 +156,13 @@ async def open_gate( if allowed is not None and gate_id not in allowed: raise HTTPException(403, "This keypass does not have access to this gate") + mock = bool(cred_db.mock_avconnect) success, error_msg, new_sid = call_open_gate( gate_db.avconnect_macro_id, cred_db.username, decrypt_secret(cred_db.password_enc), cred_db.session_id, + mock=mock, ) db.add(GateAccessLog( diff --git a/src/services/gates.py b/src/services/gates.py index af6d995..43a7544 100644 --- a/src/services/gates.py +++ b/src/services/gates.py @@ -8,12 +8,11 @@ def call_open_gate( username: str, password: str, session_id: Optional[str] = None, + mock: bool = False, ) -> tuple[bool, Optional[str], Optional[str]]: - """Attempt to open a gate. Returns (success, error_msg, new_session_id). - Respects the MOCK_AVCONNECT environment variable. - """ - from core.config import MOCK_AVCONNECT - if MOCK_AVCONNECT: + """Attempt to open a gate. Returns (success, error_msg, new_session_id).""" + if mock: + return True, None, None return True, None, None try: api = AVConnectAPI(username, password, session_id) diff --git a/src/static/admin.html b/src/static/admin.html index da80f94..1bf8ee8 100644 --- a/src/static/admin.html +++ b/src/static/admin.html @@ -188,6 +188,16 @@ +

Mock Mode

+
+

+ When enabled, gate open requests always succeed without contacting AVConnect. +

+ +
diff --git a/src/static/admin.js b/src/static/admin.js index 02a3d34..d179168 100644 --- a/src/static/admin.js +++ b/src/static/admin.js @@ -447,8 +447,22 @@ async function loadCredentials() { document.getElementById("cred-username").value = list[0].username; } } catch { /* no creds yet */ } + try { + const { enabled } = await api("GET", "/api/admin/credentials/mock"); + document.getElementById("mock-toggle").checked = enabled; + } catch { /* ignore */ } } +document.getElementById("mock-toggle").addEventListener("change", async e => { + try { + await api("PUT", "/api/admin/credentials/mock", { enabled: e.target.checked }); + showToast(e.target.checked ? "Mock mode enabled" : "Mock mode disabled"); + } catch (err) { + showToast(err.message, true); + e.target.checked = !e.target.checked; // revert + } +}); + document.getElementById("credentials-form").addEventListener("submit", async e => { e.preventDefault(); const username = document.getElementById("cred-username").value.trim();