Auto-reset device_token_ack when a reprovisioned frame is reclaimed
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:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user