190 lines
6.3 KiB
Python
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}
|