Auto-reset device_token_ack when a reprovisioned frame is reclaimed
Build and push server image / test (push) Successful in 21s
Build and push server image / build-and-push (push) Successful in 1m58s
Build and push server image / deploy (push) Successful in 53s

Once set, device_token_ack permanently locks out id-only requests
(auth.require_device) -- fine for a device that still has its token,
but a reprovisioned device has wiped its own token locally and had no
way back in short of a manual DB edit. The device's captive portal
always redirects the phone to /claim?device_id=... after
(re)provisioning, so reopen the handshake window there instead,
scoped to a logged-in owner/linked user of that frame.
This commit is contained in:
2026-07-24 15:30:45 -04:00
parent 569bf733e9
commit 82f60ed428
2 changed files with 99 additions and 0 deletions
+17
View File
@@ -310,6 +310,23 @@ def _render_claim(request: Request, db: Session, device_id: str, error: str | No
frame.owner_user_id == user.id or db.get(UserFrame, (user.id, frame.id)) is not None
):
status, pending_yours = "claimed_yours", False
if frame.device_token_ack:
# The device's captive portal redirects here on EVERY
# (re)provisioning cycle (see wifi_provisioning.c) -- if the
# physical frame was reset/reprovisioned, it no longer has
# the access token this frame row already acknowledged, and
# auth.require_device permanently locks out an id-only
# request once device_token_ack is set (device_id alone,
# unlike the token, isn't secret -- it's shown on the
# frame's own screen/QR). Reopening that handshake window
# here is what "give it a minute to connect" below actually
# depends on: it's safe because landing on this branch
# already requires knowing the device_id (physical/local
# access to the frame) AND being logged in as an owner/
# linked user of it.
frame.device_token_ack = False
db.commit()
logger.info("Frame #%d's device token handshake reopened (re-provisioned)", frame.id)
else:
status, pending_yours = "claimed", False
return templates.TemplateResponse(
+82
View File
@@ -0,0 +1,82 @@
"""A physical frame's device_token_ack becomes permanently sticky once
set (see app/auth.py's require_device) -- if the device is reset/
reprovisioned, it loses its access token and gets locked out with a 401
forever unless something reopens that handshake window. The device's
own captive portal redirects the phone's browser to /claim?device_id=
on every (re)provisioning cycle (see firmware/main/wifi_provisioning.c),
so pages.py's _render_claim uses that as the hook to auto-reset the ack
flag for a logged-in owner/linked user -- see routers/pages.py."""
from __future__ import annotations
from app.models import Frame
from .conftest import link_user, login, make_user
def test_claim_page_resets_device_token_ack_for_owner(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
frame = db_session.get(Frame, 1)
frame.device_id = "a0f262865898"
frame.device_token_ack = True
db_session.commit()
resp = client.get(f"/claim?device_id={frame.device_id}")
assert resp.status_code == 200
db_session.refresh(frame)
assert frame.device_token_ack is False
def test_claim_page_resets_device_token_ack_for_linked_user(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
bob = make_user(db_session, "bob")
frame = db_session.get(Frame, 1)
link_user(db_session, bob, frame)
frame.device_id = "a0f262865898"
frame.device_token_ack = True
db_session.commit()
client.cookies.clear()
login(client, "bob")
resp = client.get(f"/claim?device_id={frame.device_id}")
assert resp.status_code == 200
db_session.refresh(frame)
assert frame.device_token_ack is False
def test_claim_page_leaves_ack_alone_when_already_false(client, db_session):
client.post("/setup", data={"username": "alice", "password": "hunter22"})
frame = db_session.get(Frame, 1)
frame.device_id = "a0f262865898"
db_session.commit()
assert frame.device_token_ack is False
resp = client.get(f"/claim?device_id={frame.device_id}")
assert resp.status_code == 200
db_session.refresh(frame)
assert frame.device_token_ack is False
def test_claim_page_does_not_reset_ack_for_unrelated_user(client, db_session):
"""A user with no relationship to the frame lands on the "claimed"
(not "claimed_yours") branch -- must not be able to reopen another
household's device handshake just by knowing its device_id."""
client.post("/setup", data={"username": "alice", "password": "hunter22"})
frame = db_session.get(Frame, 1)
frame.device_id = "a0f262865898"
frame.device_token_ack = True
db_session.commit()
make_user(db_session, "mallory")
client.cookies.clear()
login(client, "mallory")
resp = client.get(f"/claim?device_id={frame.device_id}")
assert resp.status_code == 200
db_session.refresh(frame)
assert frame.device_token_ack is True