diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 026b76b..8da6510 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,47 +1,62 @@ -# TapAuth architecture +# How TapAuth works -TapAuth is a local-first NFC attendance and reservation kiosk designed for a Raspberry Pi. A private on-device registry keeps card identity available without MySQL; MySQL stores the complete operational records, and Firebase Realtime Database is an optional, retryable remote copy. +TapAuth is local-first because the Raspberry Pi is the one device that should remain useful even when another service is unavailable. -## Runtime flow +## A tap, from card to record + +When a card is tapped, the ACR122U reader sends its UID to the Flask application. TapAuth normalizes that UID so raw reader bytes and common text formats resolve to one stable value. + +The application checks the private on-device registry first: ```text -ACR122U NFC reader - | - v -Raspberry Pi / Flask ----> MySQL - | \----> private on-device NFC registry - | | - | +---- durable Firebase retry queue - v -Firebase Realtime Database ----> public-safe activity page +ACR122U reader + | + v +UID normalization + | + v +On-device registry + | + +-- known card --> check in, check out, or appointment + | + +-- unknown card --> short-lived registration session ``` -The reader runs continuously and reconnects after hardware interruptions. A card tap creates a short-lived session. Card identity is checked against the durable local registry first, so an assigned card remains recognized through service restarts and MySQL outages. Registry records reconcile back to MySQL when it becomes available. +I thought about relying only on MySQL here, but that makes a registered card appear unknown whenever the connection has a temporary problem. The registry avoids that. A successful database lookup or registration refreshes the local copy, and a background worker reconciles locally saved students when MySQL returns. + +## Where each kind of data belongs + +| Data | Primary location | Other copies | +| --- | --- | --- | +| Card identity fallback | `data/registered_cards.json` | Reconciled with MySQL | +| Students | MySQL `users` | Private Firebase copy without NFC UID | +| Attendance | MySQL `logs` | Public-safe Firebase event copy | +| Reservations | MySQL `reservations` | Private Firebase copy | +| 3D model files | Pi-local `uploads/models` | Never copied to Firebase | -## Data ownership +The local registry uses atomic replacement, keeps a backup copy, and applies restricted file permissions when supported. Its directory is ignored by Git. -- `users`: private student profile and card association in MySQL; mirrored to private Firebase data without the NFC UID. -- `data/registered_cards.json`: private Pi-local identity fallback with restricted file permissions, atomic replacement, and a backup copy; ignored by Git. -- `logs`: complete local attendance record; mirrored to the public path with event and timing fields only. -- `reservations`: requester and request details in MySQL; mirrored to private Firebase data without NFC UIDs or local file paths. -- `uploads/models`: Pi-local 3D files; ignored by Git and never copied to Realtime Database. +## When something is offline -Firebase paths live below the configurable `tapauth` root. Database rules allow public reads only for `tapauth/logs`; browsers cannot write any path. +- **MySQL unavailable:** known cards are still recognized and new registrations can be saved locally. Local identities are queued for reconciliation. +- **Firebase unavailable:** normal local operations continue. Failed remote writes remain in `firebase_sync_queue` for retry. +- **NFC reader disconnected:** the scanner keeps retrying instead of terminating the service. +- **Browser refreshed:** the server remains the source of truth for real NFC sessions. -## Availability model +MySQL still owns complete attendance and reservation operations. The registry specifically removes MySQL as a single point of failure for card recognition and registration. -Attendance and reservations complete against local MySQL even when Firebase is unavailable. Failed cloud writes enter `firebase_sync_queue`, and the background worker retries them without blocking the kiosk. +## Privacy boundary -## Security boundary +Oh! This part matters: the public activity feed never needs a student's card UID or full profile. -- Student registration must match the latest NFC UID and tap counter and expires after 120 seconds. -- The management dashboard requires `TAPAUTH_ADMIN_CODE` and is intended for a trusted local network. -- Public API responses remove NFC UIDs and model storage paths. -- `.env`, database exports, uploaded models, Firebase secrets, and service-account files are excluded from Git. -- The Firebase browser key identifies the public web application; it is not an administrator credential. +- Public responses contain sanitized event and timing fields. +- NFC UIDs and model-file paths are removed from public Firebase records. +- Registration must match the latest physical tap and expires after 120 seconds. +- The local management page requires `TAPAUTH_ADMIN_CODE`. +- `.env`, registry data, uploads, exports, and server credentials stay outside Git. -For an internet-facing deployment, place the Flask service behind HTTPS, network authentication, and a reverse proxy rather than exposing port 5000 directly. +If Flask is exposed beyond a trusted local network, place it behind HTTPS, authentication, and a reverse proxy. -## Extension points +## Main extension points -Database access, Firebase synchronization, NFC reading, and presentation are separated into modules. A future hosted API, email provider, or object-storage adapter can be added without changing the kiosk interaction model. +The reader, database, Firebase adapter, local registry, and interface are separate modules. A hosted API, email service, or object-storage adapter can be added without rewriting the NFC interaction itself. diff --git a/CHANGELOG.md b/CHANGELOG.md index a32aabb..6e60e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,37 @@ # Changelog -All notable TapAuth changes are documented here. +This file records the changes I would want to know about before updating an installed TapAuth kiosk. -## Unreleased +## 1.1.0 — 2026-07-29 + +I kept running into one uncomfortable edge case: a student could finish registration, tap again, and still look unregistered if MySQL was slow or unavailable. This release makes card recognition local-first. + +### Changed + +- Added a durable on-device NFC registry with atomic writes and backup recovery. +- Allowed student registration and recognition to continue without an active MySQL connection. +- Added automatic reconciliation between the local registry and MySQL. +- Normalized ACR122U UIDs across raw bytes and common text formats. +- Added legacy-record self-healing, database retries, and a recent-registration cache. +- Expanded Recent Logs to the latest 25 records with a responsive **See more** control. +- Added the server dependencies needed by registration tests in CI. +- Reworked the public documentation around the actual project story and runtime flow. + +### Fixed -- Added a durable, atomic on-device NFC registry that recognizes assigned cards without MySQL and reconciles automatically when MySQL returns. -- Canonicalized ACR122U UIDs across raw bytes and common text formats, with legacy-record self-healing, database retries, and a recent-registration fallback cache. -- Expanded Recent Logs to the latest 25 records with responsive row fitting and a See more control. - Fixed newly registered cards appearing unregistered during the NFC cooldown window. -- Added a post-write persistence check and registration-flow regression tests. -- Added the server dependencies required by registration tests to CI. +- Fixed database outages being reported as though a card had never been registered. +- Added post-write persistence checks and regression coverage for offline registration. ## 1.0.0 — 2026-07-22 +This was the first public TapAuth release: the point where the AIRHub NFC prototype became a reusable Raspberry Pi project. + - Renamed the project to TapAuth. -- Added short-lived tap-bound student registration without a shared student code. -- Added local and Firebase-backed student, attendance, and reservation data. +- Added tap-bound registration without a shared student registration code. +- Added NFC attendance, 3D printing requests, and teacher appointments. +- Added MySQL storage and Firebase-backed private/public data boundaries. - Added secure local 3D model storage and reservation queue positions. -- Added the student and reservation management dashboard. -- Added Raspberry Pi startup, reader recovery, update tooling, CI, and open-source documentation. +- Added student and reservation management. +- Added Raspberry Pi startup, reader recovery, update tools, CI, and community files. - Removed student identity from the publicly readable Firebase activity feed. -- Added automated privacy boundary tests and complete GitHub community templates. -- Removed obsolete machine-specific recovery notes and the deprecated Firestore-named sync alias. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a42cbbc..3a0592c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,13 @@ # Contributing to TapAuth -Thanks for helping improve TapAuth. +TapAuth is a personal project, but I am happy to review focused improvements that keep the kiosk dependable and understandable. -1. Fork the repository and create a focused branch. -2. Keep the kiosk lightweight and usable at 1366×700. -3. Never commit `.env`, database exports, NFC identifiers, or Firebase server credentials. -4. Run the checks documented in the README. -5. Explain hardware, schema, and UI behavior changes in the pull request. +1. Fork the repository and create one branch for one clear change. +2. Keep the interface lightweight and usable at 1366 × 700. +3. Never commit `.env`, student records, database exports, NFC UIDs, or server credentials. +4. Run the checks listed in the README. +5. Explain what changed, why it matters at the kiosk, and how you verified it. -For NFC changes, include the reader model and Raspberry Pi OS version used for testing. For database changes, make `schema.sql` safe to run against an existing installation. +For NFC changes, mention the reader model and Raspberry Pi OS version you tested. For database changes, keep `schema.sql` safe to run on an existing installation. + +Oh! If a change adds a dependency, please explain why the Pi needs it. Keeping the runtime small is part of the project. diff --git a/README.md b/README.md index aae9574..ce3d8b8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TapAuth — NFC access and reservations +# TapAuth ![TapAuth repository cover](docs/assets/github-cover.png) @@ -7,50 +7,55 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Raspberry Pi](https://img.shields.io/badge/Raspberry%20Pi-ready-C51A4A.svg)](scripts/setup_raspberry_pi.sh) -TapAuth is an open-source, Raspberry Pi-ready NFC attendance and reservation kiosk. It pairs an ACR122U reader with a lightweight Flask app, keeps working from local MySQL when the internet is unavailable, and mirrors students and attendance logs to Firebase Realtime Database. +TapAuth is my Raspberry Pi NFC attendance and reservation system for the Asia Pacific College School of Engineering AIRHub. -Built for the Asia Pacific College School of Engineering AIRHub by [@devkyato](https://github.com/devkyato). +I started this as a straightforward tap-in and tap-out kiosk. Then I thought about what happens when the internet drops, MySQL restarts, or a newly registered card is tapped again immediately. That changed the project: the Pi now recognizes cards from its own durable registry first, keeps normal kiosk interactions fast, and reconciles data with MySQL and Firebase when those services are available. -## Try the interface locally +## What it does -The repository includes a dependency-free preview that uses browser storage and simulated NFC taps: +- Detects whether a registered NFC card should check in or check out. +- Lets an unknown card register through a short, tap-bound session. +- Keeps registered cards recognizable without depending on MySQL for every tap. +- Supports 3D printing requests and teacher appointments. +- Shows the latest 25 activity records, with a compact **See more** view. +- Provides a private student and reservation management page. +- Mirrors safe activity data to Firebase without exposing NFC identifiers. +- Runs without a frontend build step: Flask, Python, HTML, CSS, and JavaScript. -```bash -python -m http.server 4173 -``` +## The part I wanted to get right -Open `http://127.0.0.1:4173`. The Flask, MySQL, NFC, and Firebase services are used only by the Raspberry Pi runtime. +Oh! On card registration, saving a student once is not enough if the next lookup depends entirely on a database connection. TapAuth stores a private copy at `data/registered_cards.json` on the Pi. The write is atomic, a backup is kept, and the directory is ignored by Git. -## Highlights +The tap flow is: -- NFC tap-in and tap-out with automatic state detection -- Fast registration directly after an unknown card is tapped—no shared student code -- 3D printing and teacher appointment request flows with Pi-local model storage -- Durable on-device NFC registry with MySQL reconciliation and a Firebase retry queue -- Private student directory and code-protected management dashboard -- Firebase Realtime Database live copy and public-safe attendance feed -- Automatic Raspberry Pi startup, kiosk mode, reader reconnection, and GitHub updates -- Plain HTML, CSS, JavaScript, and Python; no frontend build step +```text +NFC card + | + v +Local card registry ---- recognized immediately + | + +---- MySQL available? ---- sync student and attendance data + | + +---- Firebase available? - copy approved private/public records +``` -## How it works +MySQL remains the main operational database. The local registry is the recognition fallback, not a public student database. Firebase is an optional remote copy and public-safe activity source. -```text -ACR122U card tap - │ - ▼ -Raspberry Pi + Flask ───► On-device NFC registry - │ │ - ├────────────────────► Local MySQL - │ └── offline-safe retry queue - ▼ -Firebase Realtime Database ───► hosted/public activity view +## Try the interface + +The browser preview uses simulated taps and local browser storage: + +```bash +python -m http.server 4173 ``` -Unknown cards receive a short-lived registration session tied to that exact physical tap. Registered cards can check in/out or open the appointment flow. Student NFC identifiers never appear in public Firebase records or the admin API. +Open `http://127.0.0.1:4173`. + +For the real NFC flow, run the Flask application on a Raspberry Pi with an ACR122U reader. -## Quick start on Raspberry Pi +## Install on a Raspberry Pi -Requirements: Raspberry Pi OS, an ACR122U USB NFC reader, internet for first-time setup, and a Firebase Realtime Database if cloud sync is wanted. +You need Raspberry Pi OS, an ACR122U USB NFC reader, and internet access for the first installation. Firebase is optional. ```bash git clone https://github.com/devkyato/TapAuth.git @@ -60,27 +65,28 @@ nano .env bash scripts/setup_raspberry_pi.sh ``` -The setup script installs MariaDB, Python dependencies, libnfc, udev permissions, the `airhub.service` systemd unit, boot-time Git updates, and Chromium kiosk startup. +The setup script installs MariaDB, Python dependencies, libnfc, reader permissions, the `airhub.service` systemd unit, and Chromium kiosk startup. -Open: +After setup: - Kiosk: `http://127.0.0.1:5000/` - Student management: `http://127.0.0.1:5000/admin` -- Health and reader status: `http://127.0.0.1:5000/system_status` +- System status: `http://127.0.0.1:5000/system_status` -## Environment +## Configuration -Start from [.env.example](.env.example). At minimum, set a strong MySQL password and admin code: +Copy [.env.example](.env.example) to `.env`. At minimum, replace these values: ```env AIRHUB_DB_USER=airhub_app AIRHUB_DB_PASSWORD=replace-with-a-strong-password AIRHUB_DB_NAME=airhub_db TAPAUTH_ADMIN_CODE=replace-with-a-private-admin-code -TAPAUTH_REGISTRY_PATH= ``` -To enable Firebase copying: +`TAPAUTH_REGISTRY_PATH` may be left blank to use `data/registered_cards.json`. + +To enable Firebase: ```env AIRHUB_FIREBASE_ENABLED=true @@ -91,14 +97,11 @@ AIRHUB_FIREBASE_PROJECT_ID=your-project-id AIRHUB_FIREBASE_ROOT=tapauth ``` -The Firebase browser `apiKey` is a public project identifier, not an admin credential. Keep the Realtime Database secret, service-account files, `.env`, and MySQL password out of Git. +The Firebase browser `apiKey` identifies the web app; it is not an administrator secret. Never commit `.env`, database secrets, service-account files, student records, or NFC UIDs. -## Firebase setup +## Firebase copy -1. Create a Firebase project and Realtime Database. -2. Copy your web app configuration into `hosting/firebase-config.js`. -3. Configure the server-side values in the Raspberry Pi `.env`. -4. Deploy the included rules and hosting files: +I treated Firebase as a synchronized view, not as a requirement for tapping a card. This keeps the kiosk usable on the local network even when cloud access is interrupted. ```bash npm install -g firebase-tools @@ -107,28 +110,28 @@ firebase use your-project-id firebase deploy --only database,hosting ``` -Student and reservation data is private under `tapauth/users` and `tapauth/reservations`. Only event and timing fields in the public-safe `tapauth/logs` feed are readable from the hosted page. Administrators can inspect the complete database in the Firebase Console or use the Pi-local `/admin` dashboard. - -To copy all existing MySQL students and logs into Firebase: +To copy existing MySQL records: ```bash source .venv/bin/activate python scripts/sync_realtime_db.py ``` -## Updating a Raspberry Pi +Private users and reservations live below `tapauth/users` and `tapauth/reservations`. Only sanitized event and timing fields under `tapauth/logs` are publicly readable. + +## Update an installed Pi ```bash -cd /home/mako-airhub/TapAuth -git pull +cd ~/TapAuth +git pull --ff-only origin main bash scripts/update_pi_from_github.sh sudo systemctl restart airhub.service sudo systemctl status airhub.service ``` -For a non-default deployment branch, set `TAPAUTH_GIT_BRANCH` in `.env`. +The update script also reconciles MySQL students with the local card registry. If MySQL is temporarily unavailable, the existing local registry stays usable. -## NFC troubleshooting +## If the reader is not responding ```bash bash scripts/diagnose_nfc.sh @@ -136,31 +139,33 @@ sudo systemctl restart airhub.service journalctl -u airhub.service -f ``` -The service continuously retries a disconnected reader. `pcscd` is disabled during setup because it commonly claims the ACR122U before libnfc. +TapAuth retries disconnected readers automatically. The setup disables `pcscd` because it can claim the ACR122U before libnfc. -## Project map +## Project guide -```text -app.py Flask API, tap-session safeguards, admin routes -scanner.py ACR122U standby reader and reconnect loop -nfc_utils.py stable UID normalization across reader formats -local_registry.py durable MySQL-independent card recognition -database.py MySQL students, logs, and sync queue -firebase_adapter.py Realtime Database writer -index.html / script.js kiosk and reservation experience -templates/admin.html student management dashboard -hosting/ Firebase-hosted public activity view -scripts/ Pi setup, updates, diagnostics, backup, migration -schema.sql idempotent local database schema -tests/ privacy and Firebase boundary checks -``` +| Path | Purpose | +| --- | --- | +| `app.py` | Flask API, tap sessions, registration, attendance, and admin routes | +| `scanner.py` | ACR122U reader loop and reconnection | +| `nfc_utils.py` | Stable UID normalization | +| `local_registry.py` | Durable card-recognition fallback | +| `database.py` | MySQL students, logs, reservations, and sync queue | +| `firebase_adapter.py` | Firebase Realtime Database writer | +| `index.html`, `script.js` | Kiosk and reservation interface | +| `templates/admin.html` | Local management page | +| `hosting/` | Firebase-hosted public activity page | +| `scripts/` | Setup, update, sync, diagnostics, backup, and migration | +| `tests/` | Registration, privacy, UID, registry, and activity tests | + +For a deeper technical reference, see [ARCHITECTURE.md](ARCHITECTURE.md). -## Quality checks +## Check everything ```bash python -m compileall -q . node --check script.js node --check hosting/app.js +node --check hosting/firebase-config.js python -m json.tool firebase.json python -m json.tool database.rules.json python -m unittest discover -s tests -v @@ -168,8 +173,8 @@ python -m unittest discover -s tests -v ## Contributing -Focused issues and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md), get help through [SUPPORT.md](SUPPORT.md), and report vulnerabilities privately according to [SECURITY.md](SECURITY.md). +This is a personal project, but focused issues and pull requests are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md), use [SUPPORT.md](SUPPORT.md) for diagnostics, and report security problems privately through [SECURITY.md](SECURITY.md). ## License -[MIT](LICENSE) +TapAuth is available under the [MIT License](LICENSE). diff --git a/SUPPORT.md b/SUPPORT.md index b4b90eb..946dfa8 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,10 +1,12 @@ -# TapAuth support +# Getting TapAuth unstuck -Before opening an issue: +I would check the system in this order: -1. Run `bash scripts/diagnose_nfc.sh` for reader problems. -2. Check `sudo systemctl status airhub.service` and `journalctl -u airhub.service -n 100`. -3. Open `/system_status` on the Raspberry Pi. -4. Run `python scripts/diagnose_firebase.py` for sync problems. +1. Open `/system_status` on the Raspberry Pi. +2. Run `sudo systemctl status airhub.service`. +3. Read the latest service output with `journalctl -u airhub.service -n 100`. +4. For reader problems, run `bash scripts/diagnose_nfc.sh`. +5. For Firebase problems, run `python scripts/diagnose_firebase.py`. +6. Restart cleanly with `sudo systemctl restart airhub.service`. -When filing a public issue, share error messages and versions, but remove student information, NFC UIDs, passwords, tokens, and `.env` values. +If you open a public issue, include the error, Raspberry Pi OS version, Python version, and reader model. Remove student information, NFC UIDs, passwords, tokens, and `.env` values first. diff --git a/config.py b/config.py index ab1fbd7..33c2638 100644 --- a/config.py +++ b/config.py @@ -20,6 +20,7 @@ APP_CONFIG = { "name": "TapAuth", + "version": "1.1.0", "environment": os.getenv("AIRHUB_ENV", "local"), "active_storage": os.getenv("AIRHUB_STORAGE", "mysql"), "firebase_ready": True, diff --git a/docs/RELEASE_1.1.0.md b/docs/RELEASE_1.1.0.md new file mode 100644 index 0000000..319ceb3 --- /dev/null +++ b/docs/RELEASE_1.1.0.md @@ -0,0 +1,31 @@ +# TapAuth 1.1.0 — The local-first registration release + +I thought registration was finished once a student record reached MySQL. Real usage showed the missing part: the same card still has to be recognized immediately, even during a slow query, a database restart, or an internet outage. + +So, this release gives the Raspberry Pi its own durable card registry. + +A registration is written locally first, with atomic replacement and a backup copy. TapAuth can recognize that card after a service restart without asking MySQL on every tap. When MySQL is available, the local record is reconciled automatically. Firebase remains an optional synchronized copy rather than a requirement for using the kiosk. + +This release also: + +- normalizes NFC UIDs from different ACR122U representations; +- repairs compatible legacy card records during lookup; +- distinguishes an unavailable database from a genuinely unknown card; +- keeps the latest 25 activity entries available behind **See more**; +- adds regression tests for local persistence, backup recovery, and offline registration; +- refreshes the project documentation in a more direct, personal voice. + +## Updating a Raspberry Pi + +```bash +cd ~/TapAuth +git pull --ff-only origin main +bash scripts/update_pi_from_github.sh +sudo systemctl restart airhub.service +``` + +The update script imports existing MySQL students into the local registry. If MySQL is temporarily unavailable during the update, the registry already on the Pi remains intact. + +## Verification + +TapAuth 1.1.0 is checked with Python compilation, JavaScript syntax checks, Firebase JSON validation, shell syntax checks, and the complete unit-test suite.