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}