Files
lagomareGates/src/routers/gates.py
2026-05-09 17:52:59 +02:00

190 lines
6.3 KiB
Python

import json
import logging
from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from core.auth import decrypt_secret
from core.database import ApiCredential, GateAccessLog, GateDB, Keypass, get_db
from core.dependencies import require_admin, require_manager, require_keypass
from core.schemas import GateCreate, GatePublicResponse, GateResponse
from services.gates import call_open_gate
router = APIRouter(tags=["gates"])
logger = logging.getLogger(__name__)
# ── Admin: gate CRUD ──────────────────────────────────────────────────────────
@router.get("/api/admin/gates", response_model=list[GateResponse])
async def list_gates_admin(
db: Session = Depends(get_db), _: dict = Depends(require_manager)
):
return db.query(GateDB).order_by(GateDB.id).all()
@router.post("/api/admin/gates", response_model=GateResponse, status_code=201)
async def create_gate(
req: GateCreate,
db: Session = Depends(get_db),
_: dict = Depends(require_admin),
):
if req.gate_type not in ("car", "pedestrian"):
raise HTTPException(400, "gate_type must be 'car' or 'pedestrian'")
gate = GateDB(**req.model_dump())
db.add(gate)
db.commit()
db.refresh(gate)
return gate
@router.put("/api/admin/gates/{gate_id}", response_model=GateResponse)
async def update_gate(
gate_id: int,
req: GateCreate,
db: Session = Depends(get_db),
_: dict = Depends(require_admin),
):
gate: Optional[GateDB] = db.query(GateDB).filter(GateDB.id == gate_id).first()
if not gate:
raise HTTPException(404, "Gate not found")
for k, v in req.model_dump().items():
setattr(gate, k, v)
db.commit()
db.refresh(gate)
return gate
@router.delete("/api/admin/gates/{gate_id}", status_code=204)
async def delete_gate(
gate_id: int,
db: Session = Depends(get_db),
_: dict = Depends(require_admin),
):
gate: Optional[GateDB] = db.query(GateDB).filter(GateDB.id == gate_id).first()
if not gate:
raise HTTPException(404, "Gate not found")
db.delete(gate)
db.commit()
@router.post("/api/admin/gates/{gate_id}/open")
async def admin_open_gate(
gate_id: int,
request: Request,
db: Session = Depends(get_db),
caller: dict = Depends(require_manager),
):
gate_db: Optional[GateDB] = db.query(GateDB).filter(GateDB.id == gate_id).first()
if not gate_db:
raise HTTPException(404, "Gate not found")
if gate_db.status != "enabled":
raise HTTPException(409, "Gate is disabled")
cred_db: Optional[ApiCredential] = db.query(ApiCredential).first()
if not cred_db:
raise HTTPException(503, "AVConnect credentials not configured")
ip = 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(
timestamp=datetime.now(timezone.utc),
keypass_id=0,
keypass_code=f"[{caller['sub']}]",
gate_id=gate_db.id,
gate_name=gate_db.name,
ip_address=ip,
user_agent=ua,
success=success,
error=error_msg,
))
if new_sid and new_sid != cred_db.session_id:
cred_db.session_id = new_sid
db.commit()
if not success:
logger.error("Gate open failed: gate_id=%d caller=%r error=%r", gate_db.id, caller["sub"], error_msg)
raise HTTPException(502, error_msg or "Gate operation failed")
logger.info("Gate opened by admin: gate_id=%d gate=%r caller=%r ip=%r", gate_db.id, gate_db.name, caller["sub"], ip)
return {"success": True, "gate": gate_db.name}
# ── User-facing gate routes ───────────────────────────────────────────────────
@router.get("/api/gates", response_model=list[GatePublicResponse])
async def list_gates(
db: Session = Depends(get_db), _kp: Keypass = Depends(require_keypass)
):
allowed = json.loads(_kp.allowed_gates) if _kp.allowed_gates else None
q = db.query(GateDB).filter(GateDB.status == "enabled")
if allowed is not None:
q = q.filter(GateDB.id.in_(allowed))
return q.order_by(GateDB.id).all()
@router.post("/api/gates/{gate_id}/open")
async def open_gate(
gate_id: int,
request: Request,
db: Session = Depends(get_db),
_kp: Keypass = Depends(require_keypass),
):
gate_db: Optional[GateDB] = db.query(GateDB).filter(
GateDB.id == gate_id, GateDB.status == "enabled"
).first()
if not gate_db:
raise HTTPException(404, "Gate not found or disabled")
cred_db: Optional[ApiCredential] = db.query(ApiCredential).first()
if not cred_db:
raise HTTPException(503, "AVConnect credentials not configured")
ip = request.client.host if request.client else None
ua = request.headers.get("User-Agent")
allowed = json.loads(_kp.allowed_gates) if _kp.allowed_gates else None
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(
timestamp=datetime.now(timezone.utc),
keypass_id=_kp.id,
keypass_code=_kp.code,
gate_id=gate_db.id,
gate_name=gate_db.name,
ip_address=ip,
user_agent=ua,
success=success,
error=error_msg,
))
if not success:
logger.error("Gate open failed: gate_id=%d keypass_id=%d error=%r", gate_db.id, _kp.id, error_msg)
raise HTTPException(502, error_msg or "Gate operation failed")
logger.info("Gate opened by keypass: gate_id=%d gate=%r keypass_id=%d ip=%r", gate_db.id, gate_db.name, _kp.id, ip)
return {"success": True, "gate": gate_db.name}