"""Registered devices (shop tablets). Only a registered device may offer PIN sign-in.""" from __future__ import annotations import uuid from typing import Any from psycopg.rows import dict_row from .sessions import new_token, token_hash NAME_MAX = 80 def register(conn: Any, key: bytes, *, name: str, user_id: Any) -> tuple[str, uuid.UUID]: """Insert a device; returns (cookie token, device id).""" token = new_token() row = conn.execute( "INSERT INTO devices (name, token_hash, registered_by, registered_at, last_seen_at)" " VALUES (%s, %s, %s, now(), now()) RETURNING id", (name, token_hash(key, token), user_id), ).fetchone() return token, row[0] def find(conn: Any, key: bytes, token: str | None) -> dict[str, Any] | None: """The unrevoked device for a cookie token, or None.""" if not token: return None with conn.cursor(row_factory=dict_row) as cur: row: dict[str, Any] | None = cur.execute( "SELECT id, name, registered_at, last_seen_at FROM devices WHERE token_hash = %s AND revoked_at IS NULL", (token_hash(key, token),), ).fetchone() return row def touch(conn: Any, device_id: Any) -> None: conn.execute( "UPDATE devices SET last_seen_at = now() WHERE id = %s" " AND (last_seen_at IS NULL OR last_seen_at < now() - interval '1 minute')", (device_id,), ) def get(conn: Any, device_id: Any) -> dict[str, Any] | None: with conn.cursor(row_factory=dict_row) as cur: row: dict[str, Any] | None = cur.execute( "SELECT id, name, revoked_at FROM devices WHERE id = %s", (device_id,) ).fetchone() return row def revoke(conn: Any, device_id: Any, by: Any) -> bool: row = conn.execute( "UPDATE devices SET revoked_at = now(), revoked_by = %s WHERE id = %s AND revoked_at IS NULL RETURNING id", (by, device_id), ).fetchone() return row is not None def public(row: dict[str, Any] | None) -> dict[str, Any] | None: return None if row is None else {"id": str(row["id"]), "name": row["name"]}