First commit

This commit is contained in:
Ettore
2026-05-06 01:51:22 +02:00
commit 78fca8ebc2
56 changed files with 2584 additions and 0 deletions

189
src/routers/gates.py Normal file
View File

@@ -0,0 +1,189 @@
import json
from datetime import datetime
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 models import Credential, Gate as GateModel, Status
from core.schemas import GateCreate, GateResponse
from services.gates import call_open_gate
router = APIRouter(tags=["gates"])
# ── 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")
credential = Credential(
username=cred_db.username,
password=decrypt_secret(cred_db.password_enc),
)
credential.sessionid = cred_db.session_id
gate = GateModel(id=gate_db.avconnect_macro_id, name=gate_db.name, status=Status.ENABLED)
ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else None)
ua = request.headers.get("User-Agent")
success, error_msg, new_sid = call_open_gate(gate, credential)
db.add(GateAccessLog(
timestamp=datetime.utcnow(),
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:
raise HTTPException(502, error_msg or "Gate operation failed")
return {"success": True, "gate": gate_db.name}
# ── User-facing gate routes ───────────────────────────────────────────────────
@router.get("/api/gates", response_model=list[GateResponse])
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")
credential = Credential(
username=cred_db.username,
password=decrypt_secret(cred_db.password_enc),
)
credential.sessionid = cred_db.session_id
gate_status = Status.ENABLED if gate_db.status == "enabled" else Status.DISABLED
gate = GateModel(id=gate_db.avconnect_macro_id, name=gate_db.name, status=gate_status)
ip = request.headers.get("X-Forwarded-For", 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")
success, error_msg, new_sid = call_open_gate(gate, credential)
db.add(GateAccessLog(
timestamp=datetime.utcnow(),
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 new_sid and new_sid != cred_db.session_id:
cred_db.session_id = new_sid
db.commit()
if not success:
raise HTTPException(502, error_msg or "Gate operation failed")
return {"success": True, "gate": gate_db.name}