Sep 16 - Optimize code, part 1

This commit is contained in:
2026-09-16 10:52:59 -04:00
parent 4212726611
commit 7626287344
22 changed files with 587 additions and 96 deletions
+52 -4
View File
@@ -92,7 +92,8 @@ QR_Code_Management/
│ ├── __init__.py
│ ├── helpers.py # Role decorators, QR generation, role/permission helpers
│ ├── geocoding.py # Haversine distance, Google Maps client, reverse geocode
── template_helpers.py # Context processors: get_employee_name, format_hours, etc.
── template_helpers.py # Context processors: get_employee_name, format_hours, etc.
│ └── excel_safety.py # Formula-injection guard for every Excel/CSV export (Set 23)
├── static/
│ ├── css/
@@ -121,6 +122,7 @@ QR_Code_Management/
├── migration_photo_verification_toggle.py # Adds photo_verification_enabled to qr_codes
├── migration_PM_permissions.py # One-time: user_project/location_permissions tables
├── migration_dynamic_qr_locations.py # One-time: qr_type column + qr_code_locations table
├── migration_attendance_indexes.py # attendance_data secondary indexes (online DDL, re-runnable)
└── optimize_time_attendance_db.py # Standalone DB index optimization script
```
@@ -193,6 +195,21 @@ STAFF_LEVEL_ROLES = ['staff', 'payroll', 'project_manager', 'accounting']
**`/register` is restricted to `@admin_required`** — public self-registration is disabled.
**Server-side role enforcement (Sept 2026, Set 23)** — the sidebar in `base_authenticated.html`
is the reference for who may use what; routes enforce the same rules:
- `roles_required(*roles)` decorator and `restrict_blueprint_to_roles(bp, roles)` (a blueprint
`before_request`) in `utils/helpers.py`. Anonymous → login redirect; wrong role → flash +
dashboard redirect, or JSON 401/403 for `/api/`, `X-Requested-With` or JSON callers.
- `PAYROLL_AREA_ROLES = ('admin','payroll','accounting')` → whole blueprints: `time_attendance`,
`legacy_attendance`, `employees`, `statistics`.
- `QR_MANAGEMENT_ROLES = ('admin','staff','payroll','accounting')` → QR create / edit / bulk import
(not project managers). QR toggle / activate / deactivate → `admin` only (the dashboard shows
those buttons to admins only).
- User management state changes (delete, reactivate, promote, demote, activate, deactivate,
permanently-delete) are **POST-only** — never re-add GET: CSRF validation only runs on POST.
- New routes in a gated blueprint are covered automatically; new routes elsewhere need an
explicit decorator.
---
## 5. Application Factory & Initialization Order
@@ -227,7 +244,10 @@ STAFF_LEVEL_ROLES = ['staff', 'payroll', 'project_manager', 'accounting']
**Critical notes:**
- `create_tables()` and `update_existing_qr_codes()` run inside `with app.app_context()` inside `create_app()` — they execute under gunicorn, not only under `__main__`.
- `PerformanceMonitor` is imported lazily inside `__main__` only — gunicorn workers never load it.
- `update_existing_qr_codes()` prefers `QR_BASE_URL` from `.env`. Falls back to `FLASK_HOST`/`FLASK_PORT`. Never uses `request.url_root`.
- `update_existing_qr_codes()` builds startup QR images from `QR_BASE_URL` only. When it is not set,
missing images are **not** generated at startup (the old `FLASK_HOST`/`FLASK_PORT` fallback wrote
`http://localhost:5000/...` into QR codes). Note: the QR routes in `routes/qr_codes.py` build URLs
from `request.url_root`, not `QR_BASE_URL` — there is no `_get_qr_base_url()` helper despite older notes.
---
@@ -503,7 +523,14 @@ Exception: `/qr/<url>/checkin` is CSRF-exempt (public, unauthenticated).
- **CSRF-exempt:** `auth.login`, `auth.register`, `qr_codes.qr_checkin`, `static`
### Rate Limiting
- 5 failed login attempts within 15 minutes → IP blocked 15 minutes (in-memory per worker)
- Failed logins are counted over 15 minutes per **IP + username** (blocked after 5) and per
**username from any IP** (blocked after 20) — `SecurityManager.is_auth_rate_limited(username)`.
Not a plain per-IP counter, so users behind one shared IP cannot lock each other out.
In-memory per worker. The check that runs is in `auth.login`; the middleware's
`request.endpoint in ['login', ...]` check never matches (endpoint is `auth.login`)
- Client IP = `request.remote_addr` after `ProxyFix(x_for=TRUSTED_PROXY_COUNT)` in `create_app()`.
**Never read the first `X-Forwarded-For` entry** — the client controls it (that bypassed the limiter).
Behind Cloudflare + Nginx set `TRUSTED_PROXY_COUNT=2`
- `SecurityManager.create_secure_session()` called on login success (clears failed-attempt counter)
### Session Security
@@ -870,7 +897,11 @@ PHOTO_VERIFICATION_DISTANCE_THRESHOLD # Miles (default 0.3)
VERIFICATION_PHOTO_MAX_SIZE # Bytes (default 5MB)
UPLOAD_FOLDER # Temp path (default /tmp)
DEFAULT_ADMIN_PASSWORD # CHANGE IN PRODUCTION
QR_BASE_URL # Public-facing domain for QR links — required behind reverse proxy
QR_BASE_URL # Public-facing domain — used for QR images generated at startup (none generated if unset)
TRUSTED_PROXY_COUNT # Reverse proxies in front of the app (default 1 = Nginx; 2 with Cloudflare; 0 = none)
MAX_UPLOAD_SIZE_MB # Whole-request cap, default 50 (Nginx client_max_body_size must be >= this)
# MAX_FORM_MEMORY_SIZE = VERIFICATION_PHOTO_MAX_SIZE + 1 MB (Flask 3.1 default 500 KB
# would 413 the base64 photo field) — set in config.py, not .env
THEME_NAME # Activates static/css/theme-{THEME_NAME}.css
SYNC_INTERVAL_MINUTES
SQLALCHEMY_ENGINE_OPTIONS_POOL_SIZE / POOL_TIMEOUT / POOL_RECYCLE / MAX_OVERFLOW
@@ -936,6 +967,7 @@ def run():
| `migration_photo_verification_toggle.py` | `qr_codes.photo_verification_enabled` TINYINT(1) DEFAULT 1 |
| `migration_dynamic_qr_locations.py` | `qr_type` column + `qr_code_locations` table |
| `migration_PM_permissions.py` | `user_project_permissions` + `user_location_permissions` tables |
| `migration_attendance_indexes.py` | `attendance_data` indexes: `idx_ad_date_time`, `idx_ad_emp_date_time`, `idx_ad_qr_emp_date`, `idx_ad_location_name`, `idx_ad_verif_status` — skips names/leading columns that already exist; online DDL with plain ALTER fallback (Set 23) |
---
@@ -1271,6 +1303,22 @@ it (the workers share no pub/sub).
| `static/js/qr_destination.js` | `disableFormImmediately()` replaced the Employee ID placeholder with "Checking location services..." and nothing restored it after location services were confirmed. The override is removed — the submit button already shows the checking state |
| `templates/qr_destination.html` | Placeholder is now "Enter your employee ID / Ingrese su ID de empleado" |
### Set 23 — Review Step 1: Security, Limits, Indexes (Sept 15, 2026)
| File | Fix |
|---|---|
| `routes/users.py`, `templates/users.html`, `static/js/users.js` | 7 user-management actions POST-only (were GET+POST, so a forged link could promote/delete — CSRF only checks POST). Promote/demote links → CSRF forms with a `data-confirm` listener. `users.js` (not loaded by any template) switched to POST |
| `utils/helpers.py`, `routes/time_attendance.py`, `employees.py`, `legacy_attendance.py`, `statistics.py`, `qr_codes.py` | `roles_required` / `restrict_blueprint_to_roles`; role rules in §4. Previously any logged-in user (incl. project managers) could import/export time attendance, delete employees, create/deactivate QR codes |
| `advanced_security_middleware.py`, `routes/auth.py`, `app.py`, `config.py` | Login limiter: real IP via `ProxyFix`, counters per IP+username and per username (§10). The first `X-Forwarded-For` entry was trusted, so rotating it gave unlimited attempts |
| `utils/excel_safety.py` + `routes/attendance_export.py`, `routes/time_attendance_export.py`, `legacy_attendance_service.py`, `routes/statistics.py` | Formula injection: `excel_hyperlink()` escapes `"` in HYPERLINK arguments; `neutralize_unexpected_formulas(wb)` before every export `wb.save` stores any formula not matching the app's own shapes (Google Maps HYPERLINK, SUM, SUMIF) as text; `csv_safe()` in the statistics CSV. **New formula types in exports must be added to `_ALLOWED_FORMULA_PATTERNS`, or they are written as text** |
| `routes/qr_codes.py` | Check-in refuses an employee ID whose base is not 14 ASCII digits (was: only counted digits, so `=HYPERLINK(...)` passed into attendance_data) |
| `location_logging.py` | `/api/location-debug-info` admin-only; no longer echoes request headers (incl. the session Cookie) |
| `app.py`, `config.py` | Startup QR images use `QR_BASE_URL` or are skipped (no more `localhost` QR codes); `MAX_FORM_MEMORY_SIZE` / `MAX_CONTENT_LENGTH`; `@app.errorhandler(413)` (bilingual JSON for `/qr/` + `/api/`, flash + same-host redirect otherwise) |
| `static/js/qr_destination.js` | `updateSubmitButton()` looked for `#submitCheckin`, but the button is `#submitButton`, so the post-clone submit path never showed "Processing" — now finds either, bilingual label, restores via `renderSubmitButton()` |
| `routes/attendance.py` | Report query selects `NULL AS verification_photo` (the page never used the base64 photos it loaded for up to 1,000 rows) |
| `tools/migration_attendance_indexes.py` | New migration — see §17 |
| — | Verified offline (67 checks): formula guard incl. openpyxl save/reload, role gates per role and for JSON callers, limiter behind ProxyFix (spoofed XFF, shared IP, per-username cap, reset on login), debug endpoint, check-in ID rule, config limits, POST-only routes, migration against a fake cursor (create / covered / re-run / fallback / missing column). Not run against MySQL, Nginx or a browser |
| — | Known, not changed: `templates/confirm_delete_qr.html` links (GET) to the POST-only `deactivate_qr_code`, so that button returns 405 |
---
## 21. Infrastructure & Deployment