05/22 Implement license version
This commit is contained in:
@@ -645,6 +645,90 @@ proxies; static files are served directly).
|
||||
|
||||
---
|
||||
|
||||
## 22. License Key System (added 2026-05-22)
|
||||
|
||||
### Overview
|
||||
|
||||
TechDesk ships with an offline RSA-signed license key system for commercial
|
||||
distribution as a self-hosted SaaS product. No phone-home is required — the
|
||||
key itself encodes tier and expiry, verified by a baked-in public key.
|
||||
|
||||
### Tiers
|
||||
|
||||
| Tier | Features |
|
||||
|---|---|
|
||||
| Community (no key) | Core ticketing, KB, user management, bulk actions, CSV export |
|
||||
| Business | Everything above + AI Chatbot, SLA tracking, weekly digest, ticket watchers, time tracking, satisfaction surveys |
|
||||
| Enterprise | Everything above + email ingestion (IMAP) |
|
||||
|
||||
### Key Format
|
||||
|
||||
```
|
||||
TDESK-<payload_b64>.<signature_b64>
|
||||
```
|
||||
|
||||
- `payload_b64` = `base64url(UTF-8 JSON bytes)`
|
||||
- `signature_b64` = `base64url(RSA-SHA256 signature of payload bytes)`
|
||||
- JSON payload: `{ customer, email, tier, issued_at, expires_at }`
|
||||
- RSA-2048 with PKCS1v15 padding and SHA-256
|
||||
|
||||
### License Server (`license_server/`)
|
||||
|
||||
Vendor-hosted internal Flask app. **Never expose to the public internet.**
|
||||
|
||||
- `generate_keys.py` — run once to produce `private_key.pem` and `public_key.pem`
|
||||
- `app.py` — password-protected UI; `GET /` lists issued keys, `POST /generate` creates a new key
|
||||
- `licenses.db` — SQLite log of all issued keys
|
||||
- Runs on `127.0.0.1:5001`; set `LICENSE_SERVER_PASSWORD` env var
|
||||
|
||||
**Rule:** `private_key.pem` must NEVER be committed to git or included in any customer build.
|
||||
After generating, copy `public_key.pem` contents into `PUBLIC_KEY_PEM` in `license_service.py`.
|
||||
|
||||
### TechDesk License Client (`app/services/license_service.py`)
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `validate_license(key_string)` | Verify RSA signature + expiry. Returns status dict. |
|
||||
| `get_status()` | Read cached `app.config['LICENSE_STATUS']`. Requires app context. |
|
||||
| `feature_enabled(feature_name)` | True if current tier grants access. Requires app context. |
|
||||
| `days_until_expiry()` | Days remaining, or None if invalid. |
|
||||
|
||||
**Status dict keys:** `valid`, `tier`, `customer`, `email`, `issued_at`, `expires_at`, `days_left`, `reason`
|
||||
|
||||
**Reason codes:** `no_key`, `no_public_key`, `expired`, `invalid_signature`, `invalid_format`
|
||||
|
||||
**Feature name constants:** `chatbot`, `sla`, `digest`, `watchers`, `time_tracking`, `surveys`, `email_ingestion`
|
||||
|
||||
### Integration Points
|
||||
|
||||
- **`config/config.py`** — `LICENSE_KEY = os.environ.get('LICENSE_KEY', '')`
|
||||
- **`app/__init__.py`** — validates on startup, stores in `app.config['LICENSE_STATUS']`; gates SLA/digest/email-ingestion APScheduler jobs by tier
|
||||
- **`inject_globals` context processor** — injects `license` dict into all templates
|
||||
- **`app/routes/admin.py`** — `GET /admin/license` shows status + feature checklist
|
||||
- **`app/routes/chatbot.py`** — `feature_enabled('chatbot')` gate; returns 403 JSON if unlicensed
|
||||
- **`app/routes/tickets.py`** — `feature_enabled('watchers')` on watch/unwatch; `feature_enabled('time_tracking')` on log-time
|
||||
- **`app/services/sla_service.py`** — `check_sla_breaches` returns early if `feature_enabled('sla')` is False
|
||||
- **`app/services/notification_service.py`** — `send_weekly_digest` returns early if `feature_enabled('digest')` is False
|
||||
- **`app/templates/base.html`** — admin-only warning banner when `license.valid` is False or expiry < 30 days; chatbot FAB hidden for Community tier
|
||||
- **`app/templates/tickets/detail.html`** — watcher card + time tracking card hidden for Community tier
|
||||
|
||||
### Behavior Rules
|
||||
|
||||
- **Soft failure always** — unlicensed app never crashes or hard-blocks; features degrade gracefully
|
||||
- **Warning banner is admin-only** — employees see no license-related UI
|
||||
- **Public key is safe to ship** — it can only verify, not forge signatures
|
||||
- **Startup log** — `[LICENSE]` prefix in app log shows tier, customer, expiry on every start
|
||||
|
||||
### Setup Steps for a New Customer Key
|
||||
|
||||
1. On your license server: run `python app.py`, log in, fill in customer details, click Generate
|
||||
2. Copy the full `TDESK-...` key from the UI
|
||||
3. Send the key to the customer
|
||||
4. Customer adds `LICENSE_KEY=TDESK-...` to their `.env` and restarts: `sudo systemctl restart gunicorn`
|
||||
5. Customer visits `Admin → License` to confirm activation
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-05-22*
|
||||
|
||||
## 18. Notification Architecture
|
||||
|
||||
Reference in New Issue
Block a user