diff --git a/backend/app/routers/kiosk.py b/backend/app/routers/kiosk.py index cd52dc0..03d87e6 100644 --- a/backend/app/routers/kiosk.py +++ b/backend/app/routers/kiosk.py @@ -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) diff --git a/backend/app/schemas/kiosk_auth.py b/backend/app/schemas/kiosk_auth.py index 19609ef..e92819f 100644 --- a/backend/app/schemas/kiosk_auth.py +++ b/backend/app/schemas/kiosk_auth.py @@ -37,3 +37,27 @@ class KioskUserListEntry(BaseModel): class KioskUserListResponse(BaseModel): users: list[KioskUserListEntry] + + +class KioskStampRequest(BaseModel): + """Body für Stempel-Aktionen. session_token identifiziert den eingeloggten + Mitarbeiter (aus /auth/pin|nfc|qr|list), das Gerät ist bereits über die + Ed25519-Signatur (verify_kiosk_request) authentifiziert.""" + session_token: str = Field(..., min_length=1) + note: str | None = None + + +class KioskStampResponse(BaseModel): + entry_id: uuid.UUID + status: str # "stamped_in" | "stamped_out" | "break_started" | "break_ended" + warnings: list[str] = [] + + +class KioskStampStatusRequest(BaseModel): + session_token: str = Field(..., min_length=1) + + +class KioskStampStatusResponse(BaseModel): + stamped_in: bool + on_break: bool + start_time: str | None = None