DRY pass #3: unlinking a device by id or by its own token is one tail (#5373)

revoke_self and revoke_device each loaded the caller's DeviceToken, deleted
it, committed and audited DEVICE_UNLINKED; _unlink_device(which) is that,
owner-scoped as before, with each route keeping only how it names the row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 15:13:39 -04:00
co-authored by Claude Opus 5.5
parent 498b23789c
commit 31d8b1e65b
+9 -18
View File
@@ -659,21 +659,9 @@ async def revoke_own_device():
# using" is meaningless rather than merely unauthorized. The web app
# revokes by id.
return json_error("no device token was presented", 400)
async with session_scope() as db:
row = await db.scalar(
select(DeviceToken).where(
DeviceToken.token_hash == hash_token(token),
# Owner-scoped like every other device route. The hash already pins
# a single row; the guarantee shouldn't rest on one column.
DeviceToken.user_id == g.user_id,
)
)
if row is None:
return not_found()
await db.delete(row)
await db.commit()
await audit.record(audit.DEVICE_UNLINKED, user_id=g.user_id, email=await _email_of(db), detail=row.name)
return jsonify({"ok": True})
# Owner-scoped like every other device route. The hash already pins a single
# row; the guarantee shouldn't rest on one column.
return await _unlink_device(DeviceToken.token_hash == hash_token(token))
@bp.delete("/devices/<device_id>")
@@ -682,10 +670,13 @@ async def revoke_device(device_id: str):
did = parse_uuid(device_id)
if did is None:
return not_found()
return await _unlink_device(DeviceToken.id == did)
async def _unlink_device(which):
"""Delete the caller's device token matching `which`, and say so in the audit log."""
async with session_scope() as db:
row = await db.scalar(
select(DeviceToken).where(DeviceToken.id == did, DeviceToken.user_id == g.user_id)
)
row = await db.scalar(select(DeviceToken).where(which, DeviceToken.user_id == g.user_id))
if row is None:
return not_found()
await db.delete(row)