From b2ac816bc290643462469e63b548f5cf70b19437 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Thu, 9 Jul 2026 09:58:43 -0400 Subject: [PATCH] Jul 9 - Update to allow customer to manage their facilities' QR codes --- CLAUDE.md | 4 ++- app/routes/facilities.py | 58 ++++++++++++++++++------------ app/templates/facilities/list.html | 4 +-- app/templates/facilities/qr.html | 2 +- app/templates/facilities/view.html | 2 +- docs/manual_updates_customer.md | 16 +++++++++ 6 files changed, 58 insertions(+), 28 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0950f93..ec839b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -407,6 +407,8 @@ Management (`/scheduled-inspections/new|edit|delete`) is `@project_manager_requi | Notification Matrix | ✅ only | ❌ | ❌ | ❌ | ❌ | | Customers | ✅ | ✅ | ❌ | ❌ | ❌ | | Facilities | ✅ | ✅ | ✅ | read | scoped | +| Facility QR (view/print) | ✅ | ✅ | ✅ | ✅ | scoped | +| Facility QR (regenerate) | ✅ | ✅ | ❌ | ❌ | scoped | | Contracts | ✅ | ✅ | ✅ | read | scoped | | Templates | ✅ | ✅ | ❌ | ❌ | ❌ | | Inspections (execute) | ✅ | ✅ | ✅ | ✅ | read | @@ -438,7 +440,7 @@ Management (`/scheduled-inspections/new|edit|delete`) is `@project_manager_requi |---|---|---| | `auth` | `/auth` | `/login`, `/logout`, `/profile`, `/users/*`, `/notification-matrix` | | `dashboard` | `/` | `GET /`, `/facility-trend` (AJAX) | -| `facilities` | `/facilities` | CRUD + area management + QR code: `//qr` printable page, `//qr.png` image, `POST //qr/regenerate` (admin/director — invalidates old printed code), `/qr/print-all[?contract_id=]` bulk sheet. All QR routes are staff-only (customers 403); inspectors scoped to contracted facilities. | +| `facilities` | `/facilities` | CRUD + area management + QR code: `//qr` printable page, `//qr.png` image, `POST //qr/regenerate` (invalidates old printed code), `/qr/print-all[?contract_id=]` bulk sheet. **Customers may use all QR actions (including regenerate) for their own assigned facilities**; inspectors/PM/admin/director for any. Scope enforced by `_facility_for_qr_or_403()` (customers) / `get_customer_scope` (print-all). Regenerate is limited to admin/director + scoped customer (PM/inspector excluded). | | `public` | `/f` | **No login.** `GET /` occupant facility summary; `POST //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 (`//notify-recipients/add`, `/notify-recipients//remove` — admin only) | | `customers` | `/customers` | list, invite, set-password, manage, import CSV | diff --git a/app/routes/facilities.py b/app/routes/facilities.py index 984d1d8..b70ef55 100644 --- a/app/routes/facilities.py +++ b/app/routes/facilities.py @@ -97,7 +97,9 @@ def view_facility(facility_id): return render_template('facilities/view.html', facility=facility, areas=areas) -# ── Public QR code (staff-only generation) ──────────────────────────────────── +# ── Public QR code ──────────────────────────────────────────────────────────── +# Staff (admin/director/pm/inspector) may access QR for any facility; customers +# may access QR only for facilities in their assigned scope. def _public_facility_url(facility): """Absolute URL the QR encodes — the login-free occupant summary page.""" @@ -108,15 +110,27 @@ def _public_facility_url(facility): token=facility.public_token, _external=True) -@bp.route('//qr.png') -@login_required -def facility_qr_png(facility_id): - """Return the facility's QR code as a PNG image (staff only).""" - if current_user.role == 'customer': - abort(403) +def _facility_for_qr_or_403(facility_id): + """Load a facility for a QR action, enforcing customer facility scope. + + Customers may only touch QR codes for facilities they are assigned to; all + other (staff) roles have unrestricted QR access. + """ facility = db.session.get(Facility, facility_id) if facility is None: abort(404) + if current_user.role == 'customer': + cids = get_customer_scope(current_user) or [] + if facility.id not in cids: + abort(403) + return facility + + +@bp.route('//qr.png') +@login_required +def facility_qr_png(facility_id): + """Return the facility's QR code as a PNG image.""" + facility = _facility_for_qr_or_403(facility_id) # Token may not exist for pre-phase34 rows viewed before any commit. created = not facility.public_token @@ -141,11 +155,7 @@ def facility_qr_png(facility_id): @login_required def facility_qr_page(facility_id): """Printable page: facility name + QR + public URL + posting instructions.""" - if current_user.role == 'customer': - abort(403) - facility = db.session.get(Facility, facility_id) - if facility is None: - abort(404) + facility = _facility_for_qr_or_403(facility_id) public_url = _public_facility_url(facility) db.session.commit() # persist token if it was just generated return render_template('facilities/qr.html', @@ -154,12 +164,15 @@ def facility_qr_page(facility_id): @bp.route('//qr/regenerate', methods=['POST']) @login_required -@supervisor_required def facility_qr_regenerate(facility_id): - """Mint a NEW public token, invalidating any previously printed QR code.""" - facility = db.session.get(Facility, facility_id) - if facility is None: - abort(404) + """Mint a NEW public token, invalidating any previously printed QR code. + + Allowed for admin/director, and for customers on their own assigned + facilities. Project managers and inspectors cannot regenerate. + """ + facility = _facility_for_qr_or_403(facility_id) + if current_user.role not in ('admin', 'director', 'customer'): + abort(403) facility.public_token = Facility.generate_public_token() db.session.commit() @@ -178,17 +191,18 @@ def facility_qr_regenerate(facility_id): def facility_qr_print_all(): """Printable sheet of QR codes for all facilities the user can see. - Optional ?contract_id= limits the sheet to one contract. Customers have - no QR access (403); inspectors are scoped to their contracted facilities. + Optional ?contract_id= limits the sheet to one contract. Inspectors are + scoped to their contracted facilities; customers to their assigned + facilities; managers see all active facilities. """ - if current_user.role == 'customer': - abort(403) - contract_id = request.args.get('contract_id', type=int) if current_user.role == 'inspector': fids = get_inspector_scope(current_user) or [] query = Facility.query.filter(Facility.id.in_(fids), Facility.active == True) + elif current_user.role == 'customer': + fids = get_customer_scope(current_user) or [] + query = Facility.query.filter(Facility.id.in_(fids), Facility.active == True) else: query = Facility.query.filter(Facility.active == True) diff --git a/app/templates/facilities/list.html b/app/templates/facilities/list.html index e6c6d79..e3ce050 100644 --- a/app/templates/facilities/list.html +++ b/app/templates/facilities/list.html @@ -8,12 +8,10 @@

Facilities

- {% if current_user.role != 'customer' %} + class="btn btn-outline-dark" title="Printable sheet of your facilities' QR codes"> Print All QR Codes - {% endif %} {% if current_user.role in ['admin', 'director'] %} Add Facility diff --git a/app/templates/facilities/qr.html b/app/templates/facilities/qr.html index 502363e..769ac59 100644 --- a/app/templates/facilities/qr.html +++ b/app/templates/facilities/qr.html @@ -17,7 +17,7 @@ Back to Facility
- {% if current_user.role in ['admin', 'director'] %} + {% if current_user.role in ['admin', 'director', 'customer'] %}
diff --git a/app/templates/facilities/view.html b/app/templates/facilities/view.html index 5f097e2..a006b5d 100644 --- a/app/templates/facilities/view.html +++ b/app/templates/facilities/view.html @@ -17,7 +17,7 @@ Scorecard {% endif %} - {% if current_user.role in ['admin', 'director', 'project_manager'] %} + {% if current_user.role in ['admin', 'director', 'project_manager', 'customer'] %} QR Code diff --git a/docs/manual_updates_customer.md b/docs/manual_updates_customer.md index c3875bc..95853b3 100644 --- a/docs/manual_updates_customer.md +++ b/docs/manual_updates_customer.md @@ -29,6 +29,22 @@ When you scan the code (or open the link), you'll see: > For privacy, the public page shows only summary information. It never displays > individual issue details, inspector names, or any other facility's data. +### Getting, printing, and regenerating the QR code (from your portal) + +You can produce the QR code yourself for any facility you're assigned to: + +- **One facility** — open the facility from your dashboard/facilities list and + click **QR Code**. This opens a printable page with the code and its link; + click **Print** to print it. +- **All your facilities at once** — on the **Facilities** page click **Print All + QR Codes** for a single sheet covering every facility you're assigned to. +- **Regenerate** — on a facility's QR page, click **Regenerate** to issue a new + code. Use this if a posted code is defaced or you want to retire an old one. + **Any previously printed code for that facility stops working**, so reprint and + repost after regenerating. + +*[Screenshot: facility QR page with Print and Regenerate buttons]* + ### Reporting a problem 1. On the facility status page, scroll to **Report a Problem**.