feat(kiosk): Stempel-Endpunkte für Kiosk-Sessions (fehlten bisher komplett)
Login (PIN/NFC/QR/Liste) lieferte einen session_token, aber es gab keinen
Endpunkt der ihn tatsächlich zum Stempeln nutzt (X-Kiosk-Session-Token war
nur als Konzept in kiosk_session_service dokumentiert, nirgends verdrahtet).
Neu: POST /kiosk/stamp/{in,out,break-start,break-end,status}. Gerät wird
weiterhin per Ed25519 verifiziert (verify_kiosk_request), zusätzlich validiert
_user_from_session() dass der session_token zu genau diesem Gerät gehört und
der User noch aktiv/in der richtigen Firma ist. Reuse von time_service
(stamp_in/out/break_start/break_end/get_today), source=KIOSK.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
This commit is contained in:
@@ -24,12 +24,19 @@ from app.schemas.kiosk_auth import (
|
||||
KioskPinLoginRequest,
|
||||
KioskQrLoginRequest,
|
||||
KioskSessionResponse,
|
||||
KioskStampRequest,
|
||||
KioskStampResponse,
|
||||
KioskStampStatusRequest,
|
||||
KioskStampStatusResponse,
|
||||
KioskUserListEntry,
|
||||
KioskUserListResponse,
|
||||
)
|
||||
from app.schemas.time_entry import StampInRequest
|
||||
from app.models.time_entry import EntrySource
|
||||
from app.services.kiosk_auth_service import _display_name, kiosk_auth_service
|
||||
from app.services.kiosk_service import kiosk_service
|
||||
from app.services.kiosk_session_service import SESSION_TTL_SECONDS, kiosk_session_service
|
||||
from app.services.time_service import time_service
|
||||
|
||||
router = APIRouter(prefix="/kiosk", tags=["Kiosk"])
|
||||
|
||||
@@ -255,6 +262,90 @@ async def kiosk_logout(
|
||||
return {"message": "Abgemeldet."}
|
||||
|
||||
|
||||
# ── Stempel-Aktionen (signiertes Gerät + eingeloggte Kiosk-Session) ──────────
|
||||
|
||||
async def _user_from_session(
|
||||
session_token: str, device: KioskDevice, db: AsyncSession
|
||||
) -> User:
|
||||
"""Lädt den User zur Kiosk-Session, prüft dass Session zu diesem Gerät gehört."""
|
||||
session = await kiosk_session_service.require_session(session_token)
|
||||
if session["device_id"] != str(device.id):
|
||||
raise HTTPException(status_code=401, detail="Session gehört zu einem anderen Gerät.")
|
||||
user = await db.get(User, _uuid.UUID(session["user_id"]))
|
||||
if user is None or not user.is_active or user.company_id != device.company_id:
|
||||
raise HTTPException(status_code=401, detail="Mitarbeiter nicht mehr aktiv.")
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/stamp/in", response_model=KioskStampResponse)
|
||||
async def kiosk_stamp_in(
|
||||
data: KioskStampRequest,
|
||||
device: KioskDevice = Depends(verify_kiosk_request),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user = await _user_from_session(data.session_token, device, db)
|
||||
entry, warnings = await time_service.stamp_in(
|
||||
user, StampInRequest(source=EntrySource.KIOSK, note=data.note), db
|
||||
)
|
||||
await db.commit()
|
||||
return KioskStampResponse(entry_id=entry.id, status="stamped_in", warnings=warnings)
|
||||
|
||||
|
||||
@router.post("/stamp/out", response_model=KioskStampResponse)
|
||||
async def kiosk_stamp_out(
|
||||
data: KioskStampRequest,
|
||||
device: KioskDevice = Depends(verify_kiosk_request),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user = await _user_from_session(data.session_token, device, db)
|
||||
entry, warnings = await time_service.stamp_out(user, data.note, db)
|
||||
await db.commit()
|
||||
return KioskStampResponse(entry_id=entry.id, status="stamped_out", warnings=warnings)
|
||||
|
||||
|
||||
@router.post("/stamp/break-start", response_model=KioskStampResponse)
|
||||
async def kiosk_break_start(
|
||||
data: KioskStampRequest,
|
||||
device: KioskDevice = Depends(verify_kiosk_request),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user = await _user_from_session(data.session_token, device, db)
|
||||
entry = await time_service.break_start(user, db)
|
||||
await db.commit()
|
||||
return KioskStampResponse(entry_id=entry.id, status="break_started")
|
||||
|
||||
|
||||
@router.post("/stamp/break-end", response_model=KioskStampResponse)
|
||||
async def kiosk_break_end(
|
||||
data: KioskStampRequest,
|
||||
device: KioskDevice = Depends(verify_kiosk_request),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user = await _user_from_session(data.session_token, device, db)
|
||||
entry = await time_service.break_end(user, db)
|
||||
await db.commit()
|
||||
return KioskStampResponse(entry_id=entry.id, status="break_ended")
|
||||
|
||||
|
||||
@router.post("/stamp/status", response_model=KioskStampStatusResponse)
|
||||
async def kiosk_stamp_status(
|
||||
data: KioskStampStatusRequest,
|
||||
device: KioskDevice = Depends(verify_kiosk_request),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Aktueller Stempel-Status des eingeloggten Mitarbeiters (für Button-Zustand)."""
|
||||
user = await _user_from_session(data.session_token, device, db)
|
||||
entries = await time_service.get_today(user, db)
|
||||
open_entry = next((e for e in entries if e.end_time is None), None)
|
||||
if open_entry is None:
|
||||
return KioskStampStatusResponse(stamped_in=False, on_break=False)
|
||||
return KioskStampStatusResponse(
|
||||
stamped_in=True,
|
||||
on_break=open_entry.break_start is not None,
|
||||
start_time=open_entry.start_time.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
# ── Kiosk-Endpunkte (signierte Requests via Ed25519) ──────────────────────────
|
||||
|
||||
@router.post("/heartbeat", response_model=HeartbeatResponse)
|
||||
|
||||
Reference in New Issue
Block a user