Skip to content

Commit c16394e

Browse files
committed
Fix newly registered NFC card recognition
1 parent 5b346f5 commit c16394e

7 files changed

Lines changed: 159 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable TapAuth changes are documented here.
44

5+
## Unreleased
6+
7+
- Fixed newly registered cards appearing unregistered during the NFC cooldown window.
8+
- Added a post-write persistence check and registration-flow regression tests.
9+
510
## 1.0.0 — 2026-07-22
611

712
- Renamed the project to TapAuth.

app.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -294,15 +294,31 @@ def register_from_tap():
294294
latest = valid_latest_tap(uid, tap_counter)
295295
if not latest:
296296
return jsonify({"error": "This tap has expired. Tap your card again."}), 409
297-
if get_user_by_nfc(uid):
298-
return jsonify({"error": "This card is already registered."}), 409
299297

300298
required = ("student_no", "firstname", "lastname", "course")
301299
missing = [field for field in required if not str(data.get(field) or "").strip()]
302300
if missing:
303301
return jsonify({"error": "Complete all required student details."}), 400
304302

305303
try:
304+
existing_user = get_user_by_nfc(uid)
305+
if existing_user:
306+
nfc_reader.cache_registered_user(uid, {
307+
**existing_user,
308+
"checked_in": is_user_checked_in(uid),
309+
})
310+
return jsonify({
311+
"success": True,
312+
"message": "This card is already registered.",
313+
"user": {
314+
"firstname": existing_user.get("firstname"),
315+
"fullname": existing_user.get("fullname"),
316+
"student_no": existing_user.get("student_no"),
317+
"course": existing_user.get("course"),
318+
"checked_in": is_user_checked_in(uid),
319+
},
320+
})
321+
306322
user_record = create_user(
307323
student_no=str(data["student_no"]),
308324
lastname=str(data["lastname"]),
@@ -313,17 +329,19 @@ def register_from_tap():
313329
room="AIRHUB",
314330
nfc_code=uid,
315331
)
332+
user_response = {
333+
"firstname": user_record.get("firstname"),
334+
"fullname": user_record.get("fullname"),
335+
"student_no": user_record.get("student_no"),
336+
"course": user_record.get("course"),
337+
"checked_in": False,
338+
}
339+
nfc_reader.cache_registered_user(uid, user_response)
316340
enqueue_sync("user", user_record)
317341
return jsonify({
318342
"success": True,
319343
"message": "Registration complete.",
320-
"user": {
321-
"firstname": user_record.get("firstname"),
322-
"fullname": user_record.get("fullname"),
323-
"student_no": user_record.get("student_no"),
324-
"course": user_record.get("course"),
325-
"checked_in": False,
326-
},
344+
"user": user_response,
327345
})
328346
except Exception as exc:
329347
app.logger.exception("Tap registration failed")

database.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ def create_user(student_no, lastname, firstname, middlename, course, project_typ
103103
),
104104
)
105105
conn.commit()
106-
return get_user_by_nfc(nfc_code)
106+
user = get_user_by_nfc(nfc_code)
107+
if not user:
108+
raise RuntimeError("The student record could not be read after registration.")
109+
return user
107110
finally:
108111
close(cursor, conn)
109112

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="description" content="TapAuth NFC attendance and reservations for the School of Engineering.">
77
<title>TapAuth - School of Engineering</title>
88
<link rel="icon" href="data:,">
9-
<link rel="stylesheet" href="styles.css?v=20260722-11">
9+
<link rel="stylesheet" href="styles.css?v=20260725-1">
1010
</head>
1111
<body data-runtime="preview">
1212
<main class="airhub-shell">
@@ -253,6 +253,6 @@ <h2>3D Printing Reservation</h2>
253253
</div>
254254
</section>
255255
</main>
256-
<script src="script.js?v=20260722-11" defer></script>
256+
<script src="script.js?v=20260725-1" defer></script>
257257
</body>
258258
</html>

scanner.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ def update_user_state(self, uid, checked_in):
8888
if isinstance(user, dict):
8989
user["checked_in"] = bool(checked_in)
9090

91+
def cache_registered_user(self, uid, user):
92+
"""Replace a just-registered card's stale unknown-card cache."""
93+
with self._lock:
94+
if self._last_uid != uid:
95+
return False
96+
safe_user = {
97+
"firstname": user.get("firstname"),
98+
"fullname": user.get("fullname"),
99+
"student_no": user.get("student_no"),
100+
"course": user.get("course"),
101+
"checked_in": bool(user.get("checked_in", False)),
102+
}
103+
self._last_payload = {"user": safe_user}
104+
self._last_message = f"{safe_user.get('fullname') or 'Student'} · ID registered"
105+
self._cooldown.pop(uid, None)
106+
return True
107+
91108
def _set_error(self, message):
92109
with self._lock:
93110
self._last_error = message

script.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,10 @@
449449
currentTap.user = data.user;
450450
currentTap.registered = true;
451451
openTapDialog(currentTap);
452+
setTapBanner("Registration complete", "Your school ID is now linked to your student profile.", {
453+
kind: "success",
454+
timeout: 5000
455+
});
452456
} catch (error) {
453457
registrationCodeMessage.textContent = error.message;
454458
} finally {

tests/test_registration_flow.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import unittest
2+
from unittest.mock import patch
3+
4+
import app as app_module
5+
from scanner import NFCStandbyReader
6+
7+
8+
class RegistrationFlowTests(unittest.TestCase):
9+
def test_reader_cache_becomes_registered_immediately(self):
10+
reader = NFCStandbyReader(on_tap=lambda _uid: {"message": "School ID detected"})
11+
reader._last_uid = "CARD-1"
12+
reader._last_payload = None
13+
reader._cooldown["CARD-1"] = 1
14+
15+
updated = reader.cache_registered_user("CARD-1", {
16+
"firstname": "Alex",
17+
"fullname": "ALEX SANTOS",
18+
"student_no": "2026-00001",
19+
"course": "BS COMPUTER ENGINEERING",
20+
})
21+
22+
self.assertTrue(updated)
23+
self.assertEqual(reader.latest_tap(-1)["user"]["student_no"], "2026-00001")
24+
self.assertNotIn("CARD-1", reader._cooldown)
25+
26+
@patch.object(app_module, "enqueue_sync")
27+
@patch.object(app_module.nfc_reader, "cache_registered_user")
28+
@patch.object(app_module, "create_user")
29+
@patch.object(app_module, "get_user_by_nfc", return_value=None)
30+
@patch.object(app_module, "valid_latest_tap", return_value={"uid": "CARD-1", "tap_counter": 7})
31+
def test_registration_response_is_immediately_usable(
32+
self,
33+
_valid_tap,
34+
_get_user,
35+
create_user,
36+
cache_registered_user,
37+
enqueue_sync,
38+
):
39+
create_user.return_value = {
40+
"id": 12,
41+
"firstname": "ALEX",
42+
"fullname": "ALEX SANTOS",
43+
"student_no": "2026-00001",
44+
"course": "BS COMPUTER ENGINEERING",
45+
"nfc_code": "CARD-1",
46+
}
47+
client = app_module.app.test_client()
48+
49+
response = client.post("/register_from_tap", json={
50+
"uid": "CARD-1",
51+
"tap_counter": 7,
52+
"firstname": "Alex",
53+
"lastname": "Santos",
54+
"student_no": "2026-00001",
55+
"course": "BS Computer Engineering",
56+
})
57+
58+
self.assertEqual(response.status_code, 200)
59+
self.assertTrue(response.get_json()["success"])
60+
self.assertEqual(response.get_json()["user"]["student_no"], "2026-00001")
61+
cache_registered_user.assert_called_once()
62+
enqueue_sync.assert_called_once()
63+
64+
@patch.object(app_module, "is_user_checked_in", return_value=False)
65+
@patch.object(app_module.nfc_reader, "cache_registered_user")
66+
@patch.object(app_module, "get_user_by_nfc")
67+
@patch.object(app_module, "valid_latest_tap", return_value={"uid": "CARD-1", "tap_counter": 8})
68+
def test_registration_retry_recovers_existing_card(
69+
self,
70+
_valid_tap,
71+
get_user,
72+
cache_registered_user,
73+
_checked_in,
74+
):
75+
get_user.return_value = {
76+
"id": 12,
77+
"firstname": "ALEX",
78+
"fullname": "ALEX SANTOS",
79+
"student_no": "2026-00001",
80+
"course": "BS COMPUTER ENGINEERING",
81+
"nfc_code": "CARD-1",
82+
}
83+
client = app_module.app.test_client()
84+
85+
response = client.post("/register_from_tap", json={
86+
"uid": "CARD-1",
87+
"tap_counter": 8,
88+
"firstname": "Alex",
89+
"lastname": "Santos",
90+
"student_no": "2026-00001",
91+
"course": "BS Computer Engineering",
92+
})
93+
94+
self.assertEqual(response.status_code, 200)
95+
self.assertEqual(response.get_json()["message"], "This card is already registered.")
96+
cache_registered_user.assert_called_once()
97+
98+
99+
if __name__ == "__main__":
100+
unittest.main()

0 commit comments

Comments
 (0)