Files
timemaster/backend/app/models/kiosk_device.py
T
patrick e83a3fbbdd fix: agent-08 Kiosk-Härtung + 24h-Zeiteintrag-Bug
- fix: worked_minutes nutzt jetzt Sekunden statt Minuten für Overnight-Vergleich
  (end < start statt end <= start) – verhindert 24h-Anzeige bei Schnell-Stempel
  in derselben Minute (z.B. 23:34:46 → 23:34:48)
- fix: _check_arbzg() gleicher Sec-basierter Fix
- fix: KioskDeviceStatus Enum values_callable → kiosk list crasht nicht mehr
- feat: kiosk rotate-key CLI-Kommando (Status→pending, Re-Enrollment)
- feat: Kiosk-Settings in CompanyOut/CompanyUpdate Schema (require_approval,
  track_current_user, heartbeat_interval_sec)
- feat: Kiosk-Terminal-Einstellungsblock in CompanySettingsPage (🖥️)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 01:42:08 +02:00

77 lines
3.6 KiB
Python

import uuid
import enum
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
if TYPE_CHECKING:
from app.models.company import Company
from app.models.user import User
class KioskAuthMethod(str, enum.Enum):
PIN = "pin"
NFC = "nfc"
QR = "qr"
LIST = "list" # Mitarbeiter-Liste
class KioskDeviceStatus(str, enum.Enum):
PENDING = "pending" # Wartet auf Admin-Freigabe
APPROVED = "approved" # Aktiv, darf stempeln
REVOKED = "revoked" # Gesperrt / neu enrollen erforderlich
class KioskDevice(Base):
__tablename__ = "kiosk_devices"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"),
nullable=False, index=True
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
location: Mapped[str | None] = mapped_column(String(255))
# ── Ed25519-Auth (löst token_hash + is_active ab) ─────────────────────────
status: Mapped[KioskDeviceStatus] = mapped_column(
Enum(KioskDeviceStatus, name="kioskdevicestatus", values_callable=lambda x: [e.value for e in x]),
nullable=False,
default=KioskDeviceStatus.REVOKED,
)
public_key: Mapped[str | None] = mapped_column(Text) # Ed25519 PEM/OpenSSH
key_algorithm: Mapped[str] = mapped_column(String(20), nullable=False, default="ed25519")
# ── Enrollment (einmaliger Setup-Flow) ────────────────────────────────────
enrollment_token_hash: Mapped[str | None] = mapped_column(String(64))
enrollment_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
# ── Heartbeat & Liveness ──────────────────────────────────────────────────
last_heartbeat_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
client_version: Mapped[str | None] = mapped_column(String(50))
offline_queue_size: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
# ── DSGVO: aktuell eingestempelter User (per Firma deaktivierbar) ─────────
current_user_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
# ── Sicherheit ────────────────────────────────────────────────────────────
ip_whitelist: Mapped[str | None] = mapped_column(Text) # CIDR-Liste, z.B. "10.0.0.0/24,192.168.1.0/24"
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# ── Relationships ─────────────────────────────────────────────────────────
company: Mapped["Company"] = relationship("Company", lazy="noload")
current_user: Mapped["User | None"] = relationship(
"User", foreign_keys=[current_user_id], lazy="noload"
)
def __repr__(self) -> str:
return f"<KioskDevice {self.name} status={self.status.value} ({self.company_id})>"