Add mock options in frontend and removed from environment variables
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,3 +1 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
MOCK_AVCONNECT: bool = os.environ.get("MOCK_AVCONNECT", "").lower() in ("1", "true", "yes")
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 ───────────────────────────────────────────── -->
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user