Jul 8 - Implement QR code per facility

This commit is contained in:
2026-07-08 13:06:52 -04:00
parent d0e72af188
commit ff3c76c0d8
11 changed files with 581 additions and 4 deletions
+20 -3
View File
@@ -197,10 +197,13 @@ users: id, username (unique, indexed), full_name, email (unique, indexed),
### Facility / Area
```
facilities: id, name, address, contact_person, contact_phone, active, project_id (FK)
facilities: id, name, address, contact_person, contact_phone, active, project_id (FK),
public_token VARCHAR(48) unique ← Phase 34 (QR landing page)
areas: id, facility_id (FK), name, area_type
```
**`public_token`** (Phase 34): unguessable per-facility token encoded in the facility's QR code. The QR points at `/f/<public_token>` — a **login-free** occupant summary page. `Facility.generate_public_token()` / `ensure_public_token()` mint one on demand; new facilities get one at creation, existing rows were backfilled by phase34. Rotating the token (regenerating it) invalidates any printed QR — intentional, for when a code is compromised.
**`area_type` choices:** `restroom`, `lobby`, `hallway`, `office`, `kitchen`, `storage`, `floor`, `outdoor`, `other`
### Project / CustomerAssignment
@@ -397,7 +400,8 @@ contract_notification_recipients:
|---|---|---|
| `auth` | `/auth` | `/login`, `/logout`, `/profile`, `/users/*`, `/notification-matrix` |
| `dashboard` | `/` | `GET /`, `/facility-trend` (AJAX) |
| `facilities` | `/facilities` | CRUD + area management |
| `facilities` | `/facilities` | CRUD + area management + QR code (`/<id>/qr` printable page, `/<id>/qr.png` image — staff only, customers 403) |
| `public` | `/f` | **No login.** `GET /<token>` occupant facility summary; `POST /<token>/report` occupant issue report (rate-limited `5/hour`, honeypot). Resolves ACTIVE facility by `public_token` or 404. |
| `projects` | `/projects` | CRUD + customer assignment management + notification-recipient add/remove (`/<id>/notify-recipients/add`, `/notify-recipients/<rid>/remove` — admin only) |
| `customers` | `/customers` | list, invite, set-password, manage, import CSV |
| `inspections` | `/inspections` | list, start, execute, view, PDF export, flag-issue, save-draft (AJAX), flag-followup, reinspect, upload-photo (AJAX) |
@@ -709,7 +713,8 @@ phase1_projects_roles → phase6_features → phase7_mobile_api → phase8_notif
→ phase30_device_registry
→ phase31_device_registry
→ phase32_device_token_columns
→ phase33_contract_recipients ← HEAD
→ phase33_contract_recipients
→ phase34_facility_qr ← HEAD
```
### phase21_performance_indexes
@@ -794,6 +799,17 @@ flask db upgrade
sudo systemctl restart gunicorn
```
### phase34_facility_qr
Revision id `phase34_facility_qr` (file `phase34_facility_public_token.py`). Adds `facilities.public_token VARCHAR(48)` (unguessable, unique) and **backfills a token for every existing facility** in the migration body, then creates the `uq_facility_public_token` unique index. Backs the public QR landing pages (see §5 `Facility.public_token` and the Public Facility QR section in §7). Uses `INFORMATION_SCHEMA` checks — safe to re-run.
**Deploy order:**
```bash
pip install qrcode # new dependency (Pillow already present)
flask db upgrade # adds + backfills public_token
sudo systemctl restart gunicorn
```
**Deploy order for phases 2432:**
```bash
flask db upgrade
@@ -1122,6 +1138,7 @@ timeout = 30
| 71 | **`ProxyFix` must wrap `app.wsgi_app` in `create_app()`** | Behind Nginx, `remote_addr` is `127.0.0.1` for every request without it, collapsing all Flask-Limiter keys into one bucket (global instead of per-client rate limiting). `x_for=1` trusts exactly one proxy hop. See §19. |
| 72 | **Device registration has exactly ONE implementation — `register_device()` in `app/api/auth.py` → `api_device_tokens`** | A second `POST /api/v1/devices/register` (`app/api/devices.py` + `DeviceRegistration` model) was removed July 2026. It was shadowed by the `api_auth` route at routing time and queried the dropped `device_registrations` table. Do not reintroduce a competing device model or duplicate register route. |
| 73 | **Per-contract recipients are dispatched ONLY inside `notify_by_matrix()` — never add a parallel path** | `_notify_contract_recipients()` runs after role + global-custom-email routing and shares the `notified` / `sent_emails` dedup sets. Any new event that should reach contract recipients must go through `notify_by_matrix()` (passing `facility_id`, or an `issue_id`/`inspection_id` that resolves to one). Bypassing it means contract recipients are silently skipped and dedup breaks. Commit stays the caller's responsibility. |
| 74 | **The `public` blueprint (`/f/*`) is login-free — keep it occupant-safe** | Pages are addressed by unguessable `public_token` (never facility id), 404 on inactive/unknown facilities, and expose only a quality rating, last-inspected date, and open-issue COUNT — **never** issue descriptions, inspector names, per-item scores, or any other facility's data. The `report` POST must stay CSRF-protected (Flask-WTF form), rate-limited, and honeypot-guarded; public-reported issues are created with `reported_by=NULL`, `severity='medium'`, and routed through `notify_by_matrix('issue_created', facility_id=...)`. Do not add fields that leak internal detail, and do not reuse `render_template('base.html')` here — the public page is a standalone template with no authenticated nav. |
---