301 lines
13 KiB
Markdown
301 lines
13 KiB
Markdown

|
||
|
||
# Lagomare Gates
|
||
|
||
A web-based gate access management and control system. Authorized users can remotely open physical gates via a mobile-friendly PWA. An admin dashboard provides full management of gates, access codes, and users.
|
||
|
||
## Features
|
||
|
||
- **Keypass authentication** — users authenticate with an access code; each keypass can have a per-gate allowlist, an optional expiration date, and an optional time/day-of-week schedule
|
||
- **Keypass schedules** — restrict a keypass to specific days of the week and/or a time window (e.g. Monday–Friday 08:00–18:00, server local time)
|
||
- **Remote gate control** — integrates with [AVConnect](https://www.avconnect.it) and [Shelly Cloud](https://shelly.cloud) to trigger gate macros/relays
|
||
- **Gate icons** — each gate can be assigned any UTF-8 character or emoji as its icon, displayed on the user app button and keypass selector
|
||
- **Role-based admin panel** — two roles (`admin`, `manager`) with different permission levels
|
||
- **Two-factor authentication (TOTP)** — admins can enable app-based 2FA (Google Authenticator, Authy, etc.) on their account
|
||
- **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
|
||
|
||
| Layer | Technology |
|
||
|---|---|
|
||
| Backend | FastAPI + Uvicorn |
|
||
| ORM | SQLAlchemy |
|
||
| Database | SQLite |
|
||
| Auth | JWT (HS256) + bcrypt |
|
||
| 2FA | TOTP (RFC 6238) via pyotp |
|
||
| Credential storage | Fernet symmetric encryption |
|
||
| Gate integration | AVConnect HTTP API / Shelly Cloud API |
|
||
| Notifications | Telegram Bot API |
|
||
| QR generation | qrcode + Pillow |
|
||
| Frontend | Vanilla JS PWA |
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
src/
|
||
├── main.py # App entry point, startup, static file serving
|
||
├── core/
|
||
│ ├── auth.py # JWT creation/verification, password hashing
|
||
│ ├── config.py # Settings loaded from environment variables
|
||
│ ├── database.py # SQLAlchemy models and DB initialization
|
||
│ ├── dependencies.py # FastAPI dependency injection (auth guards)
|
||
│ └── schemas.py # Pydantic request/response schemas
|
||
├── 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 + QR code generation
|
||
│ ├── admins.py # Admin user management
|
||
│ ├── credentials.py # AVConnect credential management
|
||
│ ├── stats.py # Access log / statistics (paginated, filtered)
|
||
│ └── telegram.py # Telegram notification configuration
|
||
├── services/
|
||
│ ├── avconnect.py # AVConnect API client
|
||
│ ├── gates.py # Gate open orchestration
|
||
| |── shelly.py # Shelly Cloud API client
|
||
│ └── 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)
|
||
```
|
||
|
||
## API Endpoints
|
||
|
||
### Authentication
|
||
|
||
| Method | Endpoint | Description |
|
||
|---|---|---|
|
||
| POST | `/api/auth/admin` | Admin login — returns JWT |
|
||
| POST | `/api/auth/keypass` | Keypass login — returns JWT |
|
||
|
||
### User (keypass token required)
|
||
|
||
| Method | Endpoint | Description |
|
||
|---|---|---|
|
||
| GET | `/api/gates` | List gates accessible to the authenticated keypass |
|
||
| POST | `/api/gates/{gate_id}/open` | Open a gate |
|
||
|
||
### Admin — Gates (manager+)
|
||
|
||
| Method | Endpoint | Description |
|
||
|---|---|---|
|
||
| GET | `/api/admin/gates` | List all gates |
|
||
| POST | `/api/admin/gates` | Create a gate *(admin only)* |
|
||
| PUT | `/api/admin/gates/{gate_id}` | Update a gate *(admin only)* |
|
||
| DELETE | `/api/admin/gates/{gate_id}` | Delete a gate *(admin only)* |
|
||
| POST | `/api/admin/gates/{gate_id}/open` | Manually open a gate |
|
||
|
||
### Admin — Keypasses (manager+)
|
||
|
||
| Method | Endpoint | Description |
|
||
|---|---|---|
|
||
| GET | `/api/admin/keypasses` | List all keypasses |
|
||
| POST | `/api/admin/keypasses` | Create a keypass |
|
||
| PATCH | `/api/admin/keypasses/{kp_id}` | Update a keypass (description, expiry, gates, schedule) |
|
||
| 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)
|
||
|
||
| Method | Endpoint | Description |
|
||
|---|---|---|
|
||
| GET | `/api/admin/admins` | List admin users |
|
||
| POST | `/api/admin/admins` | Create an admin user |
|
||
| DELETE | `/api/admin/admins/{username}` | Delete an admin user |
|
||
| PATCH | `/api/admin/admins/{username}/password` | Change password |
|
||
| POST | `/api/admin/admins/{username}/totp/setup` | Generate a new TOTP secret and return provisioning URI + QR |
|
||
| POST | `/api/admin/admins/{username}/totp/enable` | Verify a TOTP code and activate 2FA |
|
||
| DELETE | `/api/admin/admins/{username}/totp` | Disable 2FA and discard the secret |
|
||
|
||
### Admin — AVConnect Credentials (admin only)
|
||
|
||
| Method | Endpoint | Description |
|
||
|---|---|---|
|
||
| GET | `/api/admin/credentials/avconnect` | View stored AVConnect credentials |
|
||
| PUT | `/api/admin/credentials/avconnect` | Create or update AVConnect credentials |
|
||
| GET | `/api/admin/credentials/shelly` | View stored Shelly Cloud credentials |
|
||
| PUT | `/api/admin/credentials/shelly` | Create or update Shelly Cloud 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` | 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
|
||
|
||
All settings are read from environment variables (centralised in `src/core/config.py`).
|
||
|
||
### Security
|
||
|
||
| Variable | Default | Description |
|
||
|---|---|---|
|
||
| `SECRET_KEY` | *(required)* | JWT signing key and Fernet encryption key. The application will refuse to start if this is not set. Use a long random string (`openssl rand -hex 32`). |
|
||
|
||
### Admin seed account
|
||
|
||
| Variable | Default | Description |
|
||
|---|---|---|
|
||
| `ADMIN_USERNAME` | `admin` | Username for the initial admin account created on first run. |
|
||
| `ADMIN_PASSWORD` | *(none)* | Password for the initial admin account. If unset, no seed account is created. Minimum 12 characters. |
|
||
|
||
### Server
|
||
|
||
| Variable | Default | Description |
|
||
|---|---|---|
|
||
| `APP_PORT` | `8000` | HTTP port the server listens on. |
|
||
|
||
### Database
|
||
|
||
| Variable | Default | Description |
|
||
|---|---|---|
|
||
| `DATABASE_URL` | `sqlite:///data/gates.db` | SQLAlchemy database URL. |
|
||
|
||
### Map (home location)
|
||
|
||
| Variable | Default | Description |
|
||
|---|---|---|
|
||
| `HOME_LAT` | *(none)* | WGS-84 latitude of the home/property marker shown on the frontend map. |
|
||
| `HOME_LON` | *(none)* | WGS-84 longitude of the home/property marker. |
|
||
| `HOME_NAME` | `Home` | Display name for the home marker popup. |
|
||
|
||
If `HOME_LAT` and `HOME_LON` are both set, the map is always visible. If only gates have coordinates (set per-gate in the admin panel), the map is shown only when at least one gate has a location.
|
||
|
||
### Network / reverse proxy
|
||
|
||
| Variable | Default | Description |
|
||
|---|---|---|
|
||
| `CORS_ORIGINS` | *(empty — no cross-origin requests)* | Comma-separated list of allowed CORS origins, e.g. `https://gates.example.com`. |
|
||
| `TRUSTED_PROXY_IPS` | `127.0.0.1` | Comma-separated list of reverse-proxy IPs whose `X-Forwarded-For` header is trusted for client IP resolution. |
|
||
|
||
### Logging
|
||
|
||
| Variable | Default | Description |
|
||
|---|---|---|
|
||
| `LOG_LEVEL` | `INFO` | Logging verbosity. One of `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. |
|
||
| `LOG_FILE` | `/var/log/lagomaregates.log` | Path to the rotating log file (10 MB, 5 backups). Set to an empty string to disable file logging. |
|
||
|
||
## Running with Docker Compose
|
||
|
||
```bash
|
||
# Copy and edit the compose file to set your SECRET_KEY
|
||
docker compose up -d
|
||
```
|
||
|
||
The default `docker-compose.yml` starts the service on port `8000`. Set a strong `SECRET_KEY` and, optionally, `ADMIN_USERNAME` / `ADMIN_PASSWORD` before deploying.
|
||
|
||
The `./data` directory is mounted into the container so the SQLite database persists across restarts.
|
||
|
||
## Running Locally
|
||
|
||
```bash
|
||
python -m venv venv
|
||
source venv/bin/activate
|
||
pip install -r requirements.txt
|
||
|
||
export SECRET_KEY="$(openssl rand -hex 32)"
|
||
export ADMIN_USERNAME="admin"
|
||
export ADMIN_PASSWORD="changeme-at-least-12"
|
||
|
||
uvicorn src.main:app --port 8000
|
||
```
|
||
|
||
The application is then available at:
|
||
|
||
- **User interface** — `http://localhost:8000/`
|
||
- **Admin dashboard** — `http://localhost:8000/admin`
|
||
|
||
## AVConnect Integration
|
||
|
||
Gates are controlled through one of two supported API providers.
|
||
|
||
### AVConnect
|
||
|
||
Each gate is mapped to an AVConnect *macro ID*. When a gate open request is received, the service:
|
||
|
||
1. Authenticates with AVConnect using the stored credentials (session is cached in the database)
|
||
2. Executes the configured macro for the gate
|
||
|
||
Credentials (password) are stored encrypted in the database using Fernet symmetric encryption derived from `SECRET_KEY`.
|
||
|
||
### Shelly Cloud
|
||
|
||
Each gate is mapped to a Shelly *device ID*. The service calls the Shelly Cloud API with the stored auth key to activate the device's relay.
|
||
|
||
The Shelly server URI and auth key are stored encrypted. Configure them under **Admin → Credentials → Shelly Cloud**.
|
||
|
||
**Mock mode** — when enabled via the admin dashboard, gate open requests always succeed without contacting any external API. Useful for testing.
|
||
|
||
## Keypass Schedules
|
||
|
||
In addition to an expiry date and a per-gate allowlist, each keypass can be restricted to a configurable time window:
|
||
|
||
- **Days of the week** — select any combination of Mon–Sun. If no day is checked the restriction applies on any day.
|
||
- **Time window** — a `From` / `To` time in 24-hour format (server local time). Both values must be provided together.
|
||
|
||
When a keypass with a schedule is used outside its allowed window the API returns **HTTP 403** (`Keypass is not valid today` / `Keypass is not valid at this time`) and the gate will not open. The restriction is enforced in `require_keypass` in `src/core/dependencies.py`.
|
||
|
||
The schedule is stored as a JSON object in the `schedule` column of the `keypasses` table:
|
||
|
||
```json
|
||
{ "days": [0, 1, 2, 3, 4], "time_start": "08:00", "time_end": "18:00" }
|
||
```
|
||
|
||
`days` uses Python's `weekday()` convention: 0 = Monday, 6 = Sunday. Any absent key is treated as unrestricted (e.g. omitting `days` means any day is allowed).
|
||
|
||
## 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://<your-domain>/?k=<KEYPASS_CODE>
|
||
```
|
||
|
||
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.
|
||
|
||
## Two-Factor Authentication (TOTP)
|
||
|
||
Each admin account can independently enable TOTP-based two-factor authentication:
|
||
|
||
1. Open **Admin → Admin Users** and click **Enable 2FA** on your own row
|
||
2. Scan the QR code with an authenticator app (Google Authenticator, Authy, 1Password, etc.)
|
||
3. Enter the 6-digit code to confirm — 2FA is only activated after a successful verification
|
||
4. On subsequent logins, after entering your password you will be prompted for the current TOTP code
|
||
|
||
To disable, click **Disable 2FA** on your row and confirm.
|
||
|
||
> Only the account owner can enable or disable their own 2FA. TOTP secrets are stored Fernet-encrypted in the database.
|
||
|
||
## Roles
|
||
|
||
| Role | Permissions |
|
||
|---|---|
|
||
| `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 |
|