Add mock options in frontend and removed from environment variables

This commit is contained in:
Ettore
2026-05-08 20:16:04 +02:00
parent bd5403b2d3
commit d803e2d7f6
10 changed files with 67 additions and 12 deletions

View File

@@ -5,9 +5,6 @@ ADMIN_PASSWORD=changeme123
# JWT signing secret generate with: python -c "import secrets; print(secrets.token_hex(32))" # JWT signing secret generate with: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=replace-with-a-random-64-char-hex-string 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 # Port configuration
APP_PORT=8000 APP_PORT=8000

View File

@@ -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. | | `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 | | `APP_PORT` | `8000` | HTTP port the server listens on |
| `DATABASE_URL` | `sqlite:///data/gates.db` | SQLAlchemy database URL | | `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 ## Running with Docker Compose

View File

@@ -11,6 +11,5 @@ services:
- ADMIN_USERNAME=admin - ADMIN_USERNAME=admin
- ADMIN_PASSWORD=changeme - ADMIN_PASSWORD=changeme
- SECRET_KEY=supersecretkey - SECRET_KEY=supersecretkey
- MOCK_AVCONNECT=false
- APP_PORT=8000 - APP_PORT=8000
restart: unless-stopped restart: unless-stopped

View File

@@ -1,3 +1 @@
import os import os
MOCK_AVCONNECT: bool = os.environ.get("MOCK_AVCONNECT", "").lower() in ("1", "true", "yes")

View File

@@ -41,6 +41,7 @@ class ApiCredential(Base):
username: Mapped[str] = mapped_column(String, nullable=False) username: Mapped[str] = mapped_column(String, nullable=False)
password_enc: Mapped[str] = mapped_column(String, nullable=False) # Fernet-encrypted password_enc: Mapped[str] = mapped_column(String, nullable=False) # Fernet-encrypted
session_id: Mapped[Optional[str]] = mapped_column(String, nullable=True) session_id: Mapped[Optional[str]] = mapped_column(String, nullable=True)
mock_avconnect: Mapped[bool] = mapped_column(Boolean, default=False)
class Keypass(Base): class Keypass(Base):

View File

@@ -1,6 +1,7 @@
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from core.auth import encrypt_secret from core.auth import encrypt_secret
@@ -38,3 +39,36 @@ async def upsert_credential(
db.commit() db.commit()
db.refresh(cred) db.refresh(cred)
return CredentialRead(id=cred.id, username=cred.username) 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)

View File

@@ -88,11 +88,13 @@ async def admin_open_gate(
ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else None) ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else None)
ua = request.headers.get("User-Agent") ua = request.headers.get("User-Agent")
mock = bool(cred_db.mock_avconnect)
success, error_msg, new_sid = call_open_gate( success, error_msg, new_sid = call_open_gate(
gate_db.avconnect_macro_id, gate_db.avconnect_macro_id,
cred_db.username, cred_db.username,
decrypt_secret(cred_db.password_enc), decrypt_secret(cred_db.password_enc),
cred_db.session_id, cred_db.session_id,
mock=mock,
) )
db.add(GateAccessLog( db.add(GateAccessLog(
@@ -154,11 +156,13 @@ async def open_gate(
if allowed is not None and gate_id not in allowed: if allowed is not None and gate_id not in allowed:
raise HTTPException(403, "This keypass does not have access to this gate") 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( success, error_msg, new_sid = call_open_gate(
gate_db.avconnect_macro_id, gate_db.avconnect_macro_id,
cred_db.username, cred_db.username,
decrypt_secret(cred_db.password_enc), decrypt_secret(cred_db.password_enc),
cred_db.session_id, cred_db.session_id,
mock=mock,
) )
db.add(GateAccessLog( db.add(GateAccessLog(

View File

@@ -8,12 +8,11 @@ def call_open_gate(
username: str, username: str,
password: str, password: str,
session_id: Optional[str] = None, session_id: Optional[str] = None,
mock: bool = False,
) -> tuple[bool, Optional[str], Optional[str]]: ) -> tuple[bool, Optional[str], Optional[str]]:
"""Attempt to open a gate. Returns (success, error_msg, new_session_id). """Attempt to open a gate. Returns (success, error_msg, new_session_id)."""
Respects the MOCK_AVCONNECT environment variable. if mock:
""" return True, None, None
from core.config import MOCK_AVCONNECT
if MOCK_AVCONNECT:
return True, None, None return True, None, None
try: try:
api = AVConnectAPI(username, password, session_id) api = AVConnectAPI(username, password, session_id)

View File

@@ -188,6 +188,16 @@
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
</form> </form>
</div> </div>
<h3 style="margin:1.5rem 0 1rem">Mock Mode</h3>
<div class="card" style="max-width:440px">
<p style="color:var(--text-muted);font-size:.9rem;margin-bottom:1rem">
When enabled, gate open requests always succeed without contacting AVConnect.
</p>
<label style="display:flex;align-items:center;gap:.75rem;cursor:pointer;margin:0">
<input type="checkbox" id="mock-toggle" style="width:1.1rem;height:1.1rem;flex-shrink:0;cursor:pointer" />
<span style="font-weight:600">Enable mock AVConnect</span>
</label>
</div>
</div> </div>
<!-- ── Statistics pane ───────────────────────────────────────────── --> <!-- ── Statistics pane ───────────────────────────────────────────── -->

View File

@@ -447,8 +447,22 @@ async function loadCredentials() {
document.getElementById("cred-username").value = list[0].username; document.getElementById("cred-username").value = list[0].username;
} }
} catch { /* no creds yet */ } } 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 => { document.getElementById("credentials-form").addEventListener("submit", async e => {
e.preventDefault(); e.preventDefault();
const username = document.getElementById("cred-username").value.trim(); const username = document.getElementById("cred-username").value.trim();