Files
timemaster/backend/app/models/kiosk_device.py
T
patrick 981bde3dc1 feat(kiosk): Migration 0021 – Ed25519-Auth, Status-Enum, Heartbeat, IP-Whitelist
Migration 0021_kiosk_security (eingeklinkt zwischen 0020 und 0022):
- kiosk_devices: token_hash + is_active → status enum(pending/approved/revoked)
- kiosk_devices: public_key, key_algorithm, enrollment_token_hash/expires_at
- kiosk_devices: last_heartbeat_at, client_version, offline_queue_size
- kiosk_devices: current_user_id (DSGVO), ip_whitelist (CIDR)
- companies: kiosk_require_approval, kiosk_track_current_user, kiosk_heartbeat_interval_sec

Model KioskDevice: komplett überarbeitet (KioskDeviceStatus Enum)
Model Company: 3 neue Kiosk-Felder

Bestehende Geräte: status=revoked (müssen neu enrolled werden)
Existing servers: SQL manuell angewendet (Alembic skip bei inserted migrations)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:08:33 +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"),
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})>"