diff --git a/README.md b/README.md index 846d4f3..52160c8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,10 @@ A web-based gate access management and control system. Authorized users can remo - **Keypass authentication** — users authenticate with an access code; each keypass can have a per-gate allowlist and an optional expiration date - **Remote gate control** — integrates with [AVConnect](https://www.avconnect.it) to trigger gate macros - **Role-based admin panel** — two roles (`admin`, `manager`) with different permission levels -- **Access audit log** — every open attempt is logged with timestamp, keypass, gate, IP, and result +- **Access audit log** — every open attempt is logged with timestamp, keypass, gate, IP, and result; filterable and paginated +- **Keypass QR codes** — generate a scannable QR code for each keypass; scanning opens the PWA and logs in automatically +- **Keypass code options** — choose character set (alphanumeric, alpha, numeric, or a 4-word passphrase) and length when auto-generating codes +- **Telegram notifications** — optional push notifications to a Telegram group whenever a gate is opened - **Progressive Web App** — installable on mobile devices with offline caching ## Tech Stack @@ -22,6 +25,8 @@ A web-based gate access management and control system. Authorized users can remo | Auth | JWT (HS256) + bcrypt | | Credential storage | Fernet symmetric encryption | | Gate integration | AVConnect HTTP API | +| Notifications | Telegram Bot API | +| QR generation | qrcode + Pillow | | Frontend | Vanilla JS PWA | ## Project Structure @@ -38,13 +43,15 @@ src/ ├── routers/ │ ├── auth.py # POST /api/auth/admin, POST /api/auth/keypass │ ├── gates.py # User-facing gate list and open endpoints -│ ├── keypasses.py # Admin keypass CRUD +│ ├── keypasses.py # Admin keypass CRUD + QR code generation │ ├── admins.py # Admin user management │ ├── credentials.py # AVConnect credential management -│ └── stats.py # Access log / statistics +│ ├── stats.py # Access log / statistics (paginated, filtered) +│ └── telegram.py # Telegram notification configuration ├── services/ │ ├── avconnect.py # AVConnect session management and macro execution -│ └── gates.py # Gate open orchestration +│ ├── gates.py # Gate open orchestration +│ └── telegram.py # Telegram Bot API client └── static/ # Frontend PWA (index.html, admin.html, JS, CSS) data/ └── gates.db # SQLite database (auto-created on first run) @@ -84,6 +91,7 @@ data/ | POST | `/api/admin/keypasses` | Create a keypass | | PATCH | `/api/admin/keypasses/{kp_id}` | Update a keypass | | DELETE | `/api/admin/keypasses/{kp_id}` | Revoke a keypass | +| GET | `/api/admin/keypasses/{kp_id}/qr` | Download QR code PNG for a keypass | ### Admin — Users (admin only) @@ -100,12 +108,24 @@ data/ |---|---|---| | GET | `/api/admin/credentials` | View stored credentials | | PUT | `/api/admin/credentials` | Create or update credentials | +| GET | `/api/admin/credentials/mock` | Get mock mode status | +| PUT | `/api/admin/credentials/mock` | Enable or disable mock mode | ### Admin — Statistics (manager+) | Method | Endpoint | Description | |---|---|---| -| GET | `/api/admin/stats` | Retrieve the last 500 access log entries | +| GET | `/api/admin/stats` | Paginated, filtered access log | + +Query parameters: `gate_id`, `keypass_code` (partial match), `success` (bool), `date_from`, `date_to`, `page` (default 1), `page_size` (default 50, max 200). + +### Admin — Telegram Notifications (admin only) + +| Method | Endpoint | Description | +|---|---|---| +| GET | `/api/admin/telegram` | Get current Telegram configuration | +| PUT | `/api/admin/telegram` | Save bot token and chat ID | +| POST | `/api/admin/telegram/test` | Send a test message | ## Configuration @@ -189,9 +209,32 @@ Gates are controlled through the AVConnect platform. Each gate is mapped to an A Credentials (password) are stored encrypted in the database using Fernet symmetric encryption derived from `SECRET_KEY`. +**Mock mode** — when enabled via the admin dashboard, gate open requests always succeed without contacting AVConnect. Useful for testing. + +## Keypass QR Codes + +Each active keypass has a **QR** button in the admin panel. Clicking it generates a PNG QR code that encodes the URL: + +``` +https:///?k= +``` + +Scanning the code with a phone opens the web app and logs in automatically. If the keypass is expired or revoked, the user is shown an error on the login screen. + +## Telegram Notifications + +Configure a Telegram bot to receive a message in a group or chat every time a gate is opened: + +1. Create a bot via [@BotFather](https://t.me/BotFather) and copy the token +2. Add the bot to your group and obtain the chat ID (e.g. using [@userinfobot](https://t.me/userinfobot)) +3. Open **Admin → Notifications**, enter the token and chat ID, and click **Save** +4. Use **Send test message** to verify the setup + +Notifications are sent in a background thread and never block the gate open response. Failures are logged as warnings and do not affect gate operation. + ## Roles | Role | Permissions | |---|---| -| `admin` | Full access — all endpoints including gate/user/credential management | -| `manager` | Gate open, keypass management, statistics — cannot manage admin users, AVConnect credentials, or create/delete gates | +| `admin` | Full access — all endpoints including gate/user/credential/notification management | +| `manager` | Gate open, keypass management, statistics — cannot manage admin users, AVConnect credentials, gate configuration, or Telegram settings | diff --git a/src/static/app.js b/src/static/app.js index 10ba896..a41ae6e 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -215,15 +215,18 @@ document.getElementById("logout-btn").addEventListener("click", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: k.toUpperCase() }), }) - .then(res => res.ok ? res.json() : Promise.reject()) + .then(res => res.ok ? res.json() : res.json().then(j => Promise.reject(j.detail || "Invalid keypass"))) .then(data => { saveToken(data.token); showGatesView(); loadGates(); }) - .catch(() => { + .catch(msg => { clearToken(); showLogin(); + const errEl = document.getElementById("login-error"); + errEl.textContent = typeof msg === "string" ? msg : "QR code login failed"; + errEl.classList.remove("hidden"); }); return; } diff --git a/src/static/sw.js b/src/static/sw.js index 3726602..ab34d42 100644 --- a/src/static/sw.js +++ b/src/static/sw.js @@ -1,6 +1,6 @@ /* Service worker - Lagomare Gates */ const CACHE = "lagomare-gates-v1"; -const PRECACHE = ["/", "/static/style.css", "/static/app.js", "/static/logo.svg", "/static/mobile_icon.png", "/manifest.json"]; +const PRECACHE = ["/static/style.css", "/static/app.js", "/static/logo.svg", "/static/mobile_icon.png", "/manifest.json"]; self.addEventListener("install", event => { event.waitUntil( @@ -20,6 +20,10 @@ self.addEventListener("fetch", event => { // Let API calls always go to the network if (event.request.url.includes("/api/")) return; + // Navigation requests (page loads, QR code opens) must always hit the network + // so query parameters like ?k=CODE are preserved for app.js + if (event.request.mode === "navigate") return; + event.respondWith( caches.match(event.request).then(cached => cached || fetch(event.request)) );