Implement Web Push Notifications system

- Add push_notifications.py module for managing subscriptions and sending notifications
- Create Flask API endpoints for subscription management (/api/push/*)
- Integrate push sending with existing notification system (_create_notification)
- Create frontend JavaScript library (push-notifications.js) for client-side subscription
- Update base.html template with manifest link and Service Worker registration
- Add manifest.json and service-worker.js for PWA support
- Add pywebpush dependency to requirements.txt
- Create generate-vapid-keys.sh script for VAPID key generation
- Add comprehensive documentation (WEB_PUSH_NOTIFICATIONS.md)

Features:
- Subscribe/unsubscribe users from push notifications
- Store subscriptions in MongoDB with automatic indexing
- Send push to individual users or all admins
- Notification deduplication and versioning
- Service Worker handles offline caching and push events
- Full PWA support for mobile app installation
- VAPID authentication for secure push communication
This commit is contained in:
2026-04-19 13:59:00 +02:00
parent 3469264445
commit f98ad5d0ee
9 changed files with 1961 additions and 1 deletions
+442
View File
@@ -0,0 +1,442 @@
# Web Push Notifications für Inventarsystem
## Überblick
Web Push Notifications ermöglichen es, Benutzer über wichtige Ereignisse in Echtzeit zu benachrichtigen, auch wenn sie die Anwendung nicht aktiv nutzen. Diese Implementierung nutzt:
- **Service Workers** für Hintergrundprozesse und Offline-Unterstützung
- **Web Push API** für Benachrichtigungen auf Desktop und Mobil
- **MongoDB** zur Speicherung von Subscriptions
- **VAPID-Authentifizierung** für sichere Push-Kommunikation
## Architektur
```
┌─────────────────────────────────────────────────────┐
│ Browser / Client-Side │
├─────────────────────────────────────────────────────┤
│ • Service Worker (static/service-worker.js) │
│ • Push Notification Manager (js/push-notifications) │
│ • Web App Manifest (manifest.json) │
└─────────────────────────────────────────────────────┘
↕ (Push Subscriptions / Notifications)
┌─────────────────────────────────────────────────────┐
│ Server / Backend │
├─────────────────────────────────────────────────────┤
│ • Flask API Endpoints (/api/push/*) │
│ • Push Notification Manager (push_notifications.py)│
│ • MongoDB Collections (push_subscriptions) │
│ • VAPID Key Management │
└─────────────────────────────────────────────────────┘
↕ (VAPID-signed push messages)
┌─────────────────────────────────────────────────────┐
│ Push Service (Firebase, Web Push Service) │
├─────────────────────────────────────────────────────┤
│ • Stores subscriptions │
│ • Delivers push messages to browsers │
└─────────────────────────────────────────────────────┘
```
## Setup & Konfiguration
### 1. VAPID-Schlüssel generieren
VAPID-Schlüssel sind erforderlich für die Authentifizierung mit dem Push-Dienst:
```bash
cd /path/to/Inventarsystem
bash generate-vapid-keys.sh
```
Dies erzeugt ein Schlüsselpaar. **Speichern Sie den Private Key sicher!**
Beispielausgabe:
```
PUBLIC KEY (share with browsers):
BBxyz...xyz
PRIVATE KEY (keep secret!):
AAabc...abc
```
### 2. Umgebungsvariablen setzen
Speichern Sie die Schlüssel als Umgebungsvariablen:
```bash
export VAPID_PUBLIC_KEY="BBxyz...xyz"
export VAPID_PRIVATE_KEY="AAabc...abc"
export VAPID_SUBJECT="mailto:admin@inventarsystem.local"
```
Für Docker:
```bash
# In .env oder docker-compose.yml
VAPID_PUBLIC_KEY=BBxyz...xyz
VAPID_PRIVATE_KEY=AAabc...abc
VAPID_SUBJECT=mailto:admin@inventarsystem.local
```
### 3. Abhängigkeiten installieren
```bash
pip install -r requirements.txt
# oder
pip install pywebpush
```
### 4. MongoDB Collection initialisieren
Die `push_subscriptions` Collection wird automatisch erstellt beim ersten Speichern einer Subscription. Indizes werden automatisch erstellt durch:
```python
from Web.push_notifications import ensure_push_subscriptions_collection
ensure_push_subscriptions_collection()
```
## API Endpoints
### `POST /api/push/subscribe`
Speichert eine Notification Subscription des Benutzers.
**Request:**
```json
{
"subscription": {
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
"keys": {
"p256dh": "BCOA...",
"auth": "kXA..."
}
}
}
```
**Response:**
```json
{
"success": true,
"message": "Successfully subscribed to push notifications"
}
```
---
### `POST /api/push/unsubscribe`
Deaktiviert eine Notification Subscription.
**Request:**
```json
{
"endpoint": "https://fcm.googleapis.com/fcm/send/..."
}
```
**Response:**
```json
{
"success": true,
"message": "Successfully unsubscribed from push notifications"
}
```
---
### `GET /api/push/subscriptions`
Listet alle aktiven Subscriptions des aktuellen Benutzers auf.
**Response:**
```json
{
"success": true,
"count": 2,
"subscriptions": [
{
"id": "507f1f77bcf86cd799439011",
"endpoint": "https://...",
"created_at": "2026-04-10T14:30:00",
"last_used": "2026-04-10T15:45:00",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)..."
}
]
}
```
---
### `GET /api/push/vapid-key`
Ruft den öffentlichen VAPID-Schlüssel ab (erforderlich für Browser).
**Response:**
```json
{
"success": true,
"vapid_key": "BBxyz...xyz"
}
```
---
### `POST /api/push/test` (Admin only)
Sendet eine Test-Benachrichtigung.
**Request:**
```json
{
"target_user": "username" // optional, default: current user
}
```
**Response:**
```json
{
"success": true,
"message": "Test push sent to 2 subscription(s)"
}
```
## Frontend Integration
### Aktivierung in Settings
Fügen Sie einen Container in Ihre Einstellungsseite ein:
```html
<div id="push-notification-settings"></div>
<script src="{{ url_for('static', filename='js/push-notifications.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
showPushNotificationSettings();
});
</script>
```
Dies zeigt einen Button und Subscription-Status an.
### Manuelle Steuerung
```javascript
// Initialisieren
await pushNotificationManager.init();
// Benachrichtigungen aktivieren
await pushNotificationManager.subscribe();
// Benachrichtigungen deaktivieren
await pushNotificationManager.unsubscribe();
// Status prüfen
const isSubscribed = await pushNotificationManager.isSubscribed();
// Test-Benachrichtigung senden (Admin)
await pushNotificationManager.sendTestNotification();
```
## Backend Integration
### Benachrichtigungen versenden
```python
from Web import push_notifications as pn
# Benachrichtigung an einen Benutzer
pn.send_push_notification(
'username',
'Titel',
'Nachricht',
url='/my_borrowed_items',
reference={'item_id': '123', 'type': 'borrowing'}
)
# Benachrichtigung an alle Admins
pn.send_push_to_all_admins(
'Admin Alert',
'Wichtiges Ereignis',
url='/main_admin'
)
```
### Integration mit Notification-System
Benachrichtigungen werden automatisch über Push versendet, wenn erstellt:
```python
# In app.py
_create_notification(
db,
audience='user',
notif_type='borrowing_activated',
title='Ausleihung aktiviert',
message='Ihre geplante Ausleihung ist jetzt aktiv',
target_user='john_doe',
reference={'url': '/my_borrowed_items', 'item_id': '123'}
)
# → Schreibt in DB + sendet Push-Benachrichtigung
```
## MongoDB Schema
### Collection: `push_subscriptions`
```json
{
"_id": ObjectId("..."),
"Username": "john_doe",
"Endpoint": "https://fcm.googleapis.com/fcm/send/...",
"Keys": {
"p256dh": "BCOA...",
"auth": "kXA..."
},
"SubscriptionHash": "abc123def456...",
"IsActive": true,
"CreatedAt": ISODate("2026-04-10T14:30:00Z"),
"LastUsed": ISODate("2026-04-10T15:45:00Z"),
"UserAgent": "Mozilla/5.0..."
}
```
**Indizes:**
- `Username` - Schnelle Abfrage nach Benutzer
- `{Username: 1, IsActive: 1}` - Abfrage aktiver Subscriptions
- `CreatedAt` - Zeitbasierte Bereinigung
- `SubscriptionHash` - Eindeutigkeit, Duplikat-Verhinderung
## Service Worker
### Funktionen
Die Service Worker (`static/service-worker.js`) behandelt:
1. **Push Events** - Empfängt und zeigt Benachrichtigungen an
2. **Click Handler** - Öffnet relevante URLs bei Benachrichtigungs-Klick
3. **Offline Caching** - Speichert statische Assets offline
4. **Background Sync** - Synchronisiert Daten im Hintergrund
5. **Installation** - Installiert sich selbst und lädt Cache vor
### Push-Payload-Format
```json
{
"title": "Ausleihung aktiviert",
"body": "Ihre geplante Ausleihung ist jetzt aktiv",
"icon": "/static/img/logo-192x192.png",
"badge": "/static/img/badge-72x72.png",
"tag": "notification-borrowing_activated",
"url": "/my_borrowed_items",
"reference": {
"item_id": "123",
"type": "borrowing"
}
}
```
## Troubleshooting
### Benachrichtigungen werden nicht empfangen
1. **VAPID-Schlüssel nicht gesetzt**
```bash
# Überprüfen
curl http://localhost:5000/api/push/vapid-key
# Sollte VAPID_PUBLIC_KEY zurückgeben, nicht "nicht konfiguriert"
```
2. **Service Worker nicht registriert**
- Browser DevTools → Application → Service Workers
- Sollte "activated and running" anzeigen
- Überprüfen Sie Console auf Fehler
3. **Notification Permission verweigert**
- Browser-Einstellungen überprüfen
- Site-Benachrichtigungsberechtigungen zurücksetzen
- `chrome://settings/content/notifications` (Chrome)
4. **No active subscriptions**
```bash
# MongoDB überprüfen
db.push_subscriptions.find({Username: "username"})
# Sollte aktive Subscriptions anzeigen
```
### Debug-Befehle
```bash
# Alle Subscriptions anzeigen
docker exec inventarsystem-mongodb mongosh --eval \
'db.push_subscriptions.find({}).pretty()' Inventarsystem
# Test-Push senden
curl -X POST http://localhost:5000/api/push/test \
-H "Content-Type: application/json" \
-c cookies.txt -b cookies.txt
# Logs überprüfen
docker logs inventarsystem-app | grep -i "push\|notification"
```
## Browser-Unterstützung
| Browser | Desktop | Mobile | Service Worker | Push API |
|---------|---------|--------|-----------------|----------|
| Chrome | ✅ | ✅ | ✅ | ✅ |
| Firefox | ✅ | ✅ | ✅ | ✅ |
| Safari | ⚠️ | ✅ | ⚠️ | ⚠️ |
| Edge | ✅ | ✅ | ✅ | ✅ |
**Safari-Hinweis:** Verwendet Web Push über APNs mit Einschränkungen.
## Best Practices
### 1. Notification Häufigkeit
- Nicht mehr als 1 Benachrichtigung pro Minute pro Benutzer
- Sammelns Sie verwandte Events
### 2. Payload-Größe
- Halten Sie Nachrichten kurz (<100 Zeichen)
- Verwenden Sie `reference` für Kontext, nicht `body`
### 3. Sicherheit
- **Private Key**: Niemals in Code commiten!
- **Secrets**: Nur als Umgebungsvariablen speichern
- **Validate**: Server-seitig alle Subscription-Daten validieren
### 4. Datenschutz
- Dokumentieren Sie Push-Sammlung (DSGVO)
- Bieten Sie einfache Opt-out-Möglichkeit
- Speichern Sie keine PII in Push-Nachrichten
### 5. Wartung
Stale Subscriptions automatisch bereinigen:
```python
# In Scheduler oder Cron-Job
from Web.push_notifications import cleanup_inactive_subscriptions
cleanup_inactive_subscriptions() # Entfernt inaktive Subs älter als 30 Tage
```
## Performance-Tipps
1. **Batch-Sends**: Senden Sie mehrere Pushes in einem Query
2. **Async**: Verwenden Sie Background Tasks für Push-Sending
3. **Redis**: Nutzen Sie Cache für häufig angeforderte VAPID-Keys
4. **Indexes**: MongoDB-Indexes auf `Username`, `IsActive` sollten vorhanden sein
## Zukünftige Erweiterungen
- [ ] Action Buttons in Benachrichtigungen (Approve/Deny)
- [ ] Benachrichtigungs-Kategorien und Gruppierung
- [ ] Rich-Media-Unterstützung (Bilder, Video)
- [ ] Scheduled Notifications
- [ ] Analytics & Delivery Tracking
---
**Version:** 1.0 (Inventarsystem v0.5+)
**Letzte Aktualisierung:** April 2026
+202
View File
@@ -32,6 +32,7 @@ import user as us
import items as it
import ausleihung as au
import audit_log as al
import push_notifications as pn
import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from bson.objectid import ObjectId
@@ -745,8 +746,30 @@ def _create_notification(db, *, audience, notif_type, title, message, target_use
if audience == 'user' and target_user:
_bump_notification_version(f'user:{target_user}')
# Send push notification to user
try:
pn.send_push_notification(
target_user,
title,
message,
url=reference.get('url', '/') if reference else '/',
reference=reference,
tag=f'notification-{notif_type}'
)
except Exception as e:
app.logger.warning(f'Failed to send push notification to {target_user}: {e}')
elif audience == 'admin':
_bump_notification_version('admin')
# Send push notification to all admins
try:
pn.send_push_to_all_admins(
title,
message,
url=reference.get('url', '/') if reference else '/',
reference=reference
)
except Exception as e:
app.logger.warning(f'Failed to send push to admins: {e}')
return True
@@ -10740,3 +10763,182 @@ def get_optimal_image_quality(img, target_size_kb=80):
return quality
return best_quality
# ============================================================================
# PUSH NOTIFICATION API ENDPOINTS
# ============================================================================
@app.route('/api/push/subscribe', methods=['POST'])
def subscribe_to_push():
"""
Subscribe a user to push notifications
Expects JSON payload:
{
'subscription': {
'endpoint': '...',
'keys': {'p256dh': '...', 'auth': '...'}
}
}
"""
if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
try:
data = request.get_json() or {}
subscription = data.get('subscription')
if not subscription or not subscription.get('endpoint'):
return jsonify({'success': False, 'error': 'Invalid subscription'}), 400
username = session['username']
success = pn.save_push_subscription(username, subscription)
if success:
app.logger.info(f'Push subscription saved for {username}')
return jsonify({
'success': True,
'message': 'Successfully subscribed to push notifications'
})
else:
return jsonify({'success': False, 'error': 'Failed to save subscription'}), 500
except Exception as e:
app.logger.error(f'Error subscribing to push: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/push/unsubscribe', methods=['POST'])
def unsubscribe_from_push():
"""
Unsubscribe a user from push notifications
Expects JSON payload:
{
'endpoint': '...'
}
"""
if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
try:
data = request.get_json() or {}
endpoint = data.get('endpoint')
if not endpoint:
return jsonify({'success': False, 'error': 'Missing endpoint'}), 400
username = session['username']
success = pn.remove_push_subscription(username, endpoint)
if success:
app.logger.info(f'Push subscription removed for {username}')
return jsonify({
'success': True,
'message': 'Successfully unsubscribed from push notifications'
})
else:
return jsonify({'success': False, 'error': 'Subscription not found'}), 404
except Exception as e:
app.logger.error(f'Error unsubscribing from push: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/push/subscriptions', methods=['GET'])
def get_push_subscriptions():
"""
Get all push subscriptions for the current user
"""
if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
try:
username = session['username']
subscriptions = pn.get_user_subscriptions(username)
# Convert ObjectIds to strings for JSON serialization
subs_data = []
for sub in subscriptions:
subs_data.append({
'id': str(sub['_id']),
'endpoint': sub.get('Endpoint', ''),
'created_at': sub.get('CreatedAt', '').isoformat() if sub.get('CreatedAt') else '',
'last_used': sub.get('LastUsed', '').isoformat() if sub.get('LastUsed') else '',
'user_agent': sub.get('UserAgent', ''),
})
return jsonify({
'success': True,
'subscriptions': subs_data,
'count': len(subs_data)
})
except Exception as e:
app.logger.error(f'Error getting push subscriptions: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/push/vapid-key', methods=['GET'])
def get_vapid_key():
"""
Get the VAPID public key for push notifications
Used by the service worker to communicate with push service
"""
try:
vapid_key = pn.VAPID_PUBLIC_KEY
if not vapid_key:
app.logger.warning('VAPID_PUBLIC_KEY not configured')
return jsonify({
'success': False,
'error': 'Push notifications not configured on server'
}), 501
return jsonify({
'success': True,
'vapid_key': vapid_key
})
except Exception as e:
app.logger.error(f'Error getting VAPID key: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/push/test', methods=['POST'])
def test_push_notification():
"""
Send a test push notification (admin only)
"""
if 'username' not in session:
return jsonify({'success': False, 'error': 'Not authenticated'}), 401
if not us.is_admin(session['username']):
return jsonify({'success': False, 'error': 'Admin access required'}), 403
try:
data = request.get_json() or {}
target_user = data.get('target_user', session['username'])
sent = pn.send_push_notification(
target_user,
'Test Benachrichtigung',
'Dies ist eine Test-Benachrichtigung von Inventarsystem',
url='/',
tag='test-notification'
)
if sent > 0:
return jsonify({
'success': True,
'message': f'Test push sent to {sent} subscription(s)'
})
else:
return jsonify({
'success': False,
'error': 'No active subscriptions for user'
}), 404
except Exception as e:
app.logger.error(f'Error sending test push: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
+421
View File
@@ -0,0 +1,421 @@
"""
Push Notification Management System
Handles Web Push notifications for users via Service Workers
"""
import os
import json
import datetime
from pymongo import MongoClient
from bson import ObjectId
import requests
import hashlib
import logging
import settings as cfg
logger = logging.getLogger(__name__)
# VAPID keys for push notifications (should be in environment variables)
VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY', '')
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY', '')
VAPID_SUBJECT = os.getenv('VAPID_SUBJECT', f'mailto:admin@{os.getenv("SERVER_NAME", "localhost")}')
# Push service endpoint (typically Firebase or Web Push Service)
PUSH_SERVICE_URL = 'https://fcm.googleapis.com/fcm/send' # Firebase Cloud Messaging
FCM_API_KEY = os.getenv('FCM_API_KEY', '') # Firebase API key
def get_push_subscriptions_collection(db=None):
"""Get MongoDB push subscriptions collection"""
if db is None:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
return db['push_subscriptions']
def get_user_subscriptions(username):
"""
Get all active push subscriptions for a user
Args:
username (str): Username
Returns:
list: List of subscription documents
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
subscriptions = list(subs_col.find({
'Username': username,
'IsActive': True
}))
client.close()
return subscriptions
except Exception as e:
logger.error(f'Error getting push subscriptions for {username}: {e}')
return []
def save_push_subscription(username, subscription_obj):
"""
Save a new push subscription for a user
Args:
username (str): Username
subscription_obj (dict): Subscription object from Service Worker
{
'endpoint': 'https://...',
'keys': {
'p256dh': '...',
'auth': '...'
}
}
Returns:
bool: Success status
"""
try:
if not subscription_obj.get('endpoint'):
logger.warning(f'Invalid subscription object for {username}')
return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
# Create unique hash of subscription to avoid duplicates
sub_hash = hashlib.md5(
f"{username}:{subscription_obj['endpoint']}".encode()
).hexdigest()
# Check if subscription already exists
existing = subs_col.find_one({
'Username': username,
'SubscriptionHash': sub_hash
})
if existing:
# Update last used time
subs_col.update_one(
{'_id': existing['_id']},
{'$set': {
'LastUsed': datetime.datetime.now(),
'IsActive': True
}}
)
logger.info(f'Updated existing subscription for {username}')
client.close()
return True
# Save new subscription
subscription_doc = {
'Username': username,
'Endpoint': subscription_obj['endpoint'],
'Keys': subscription_obj.get('keys', {}),
'SubscriptionHash': sub_hash,
'IsActive': True,
'CreatedAt': datetime.datetime.now(),
'LastUsed': datetime.datetime.now(),
'UserAgent': subscription_obj.get('userAgent', ''),
}
subs_col.insert_one(subscription_doc)
logger.info(f'Saved new push subscription for {username}')
client.close()
return True
except Exception as e:
logger.error(f'Error saving push subscription for {username}: {e}')
return False
def remove_push_subscription(username, endpoint):
"""
Remove a push subscription
Args:
username (str): Username
endpoint (str): Subscription endpoint URL
Returns:
bool: Success status
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
result = subs_col.update_one(
{
'Username': username,
'Endpoint': endpoint
},
{'$set': {'IsActive': False}}
)
client.close()
return result.modified_count > 0
except Exception as e:
logger.error(f'Error removing push subscription for {username}: {e}')
return False
def send_push_notification(username, title, body, icon=None, url='/', reference=None, tag='notification'):
"""
Send a push notification to all user's subscriptions
Args:
username (str): Target username
title (str): Notification title
body (str): Notification body
icon (str, optional): Icon URL
url (str, optional): URL to open on click
reference (dict, optional): Reference data (item_id, etc)
tag (str, optional): Notification tag for grouping
Returns:
int: Number of successfully sent notifications
"""
try:
subscriptions = get_user_subscriptions(username)
if not subscriptions:
logger.debug(f'No active push subscriptions for {username}')
return 0
sent_count = 0
for subscription in subscriptions:
success = _send_to_subscription(
subscription,
title,
body,
icon,
url,
reference,
tag
)
if success:
sent_count += 1
else:
# Mark subscription as inactive if send fails
_mark_subscription_inactive(subscription['_id'])
logger.info(f'Sent push notification to {username}: {sent_count}/{len(subscriptions)} subscriptions')
return sent_count
except Exception as e:
logger.error(f'Error sending push notification to {username}: {e}')
return 0
def _send_to_subscription(subscription, title, body, icon, url, reference, tag):
"""Send push notification to a specific subscription"""
try:
payload = {
'title': title,
'body': body,
'icon': icon or '/static/img/logo-192x192.png',
'badge': '/static/img/badge-72x72.png',
'tag': tag,
'url': url,
'reference': reference or {},
}
# If using Firebase Cloud Messaging
if FCM_API_KEY and subscription.get('Endpoint', '').startswith('https://fcm.'):
return _send_fcm_notification(subscription, payload)
# Otherwise use standard Web Push Protocol
return _send_web_push_notification(subscription, payload)
except Exception as e:
logger.error(f'Error sending to subscription {subscription.get("_id")}: {e}')
return False
def _send_fcm_notification(subscription, payload):
"""Send notification via Firebase Cloud Messaging"""
try:
if not FCM_API_KEY:
logger.warning('FCM_API_KEY not configured')
return False
fcm_payload = {
'to': subscription['Endpoint'],
'notification': {
'title': payload['title'],
'body': payload['body'],
'icon': payload['icon'],
'badge': payload['badge'],
'tag': payload['tag'],
'click_action': payload['url'],
},
'data': {
'url': payload['url'],
'type': payload.get('type', 'info'),
}
}
headers = {
'Authorization': f'key={FCM_API_KEY}',
'Content-Type': 'application/json'
}
response = requests.post(
'https://fcm.googleapis.com/fcm/send',
json=fcm_payload,
headers=headers,
timeout=10
)
return response.status_code == 200
except Exception as e:
logger.error(f'FCM notification error: {e}')
return False
def _send_web_push_notification(subscription, payload):
"""Send notification using standard Web Push Protocol"""
try:
# This requires pywebpush library
from pywebpush import webpush
webpush(
subscription_info={
'endpoint': subscription['Endpoint'],
'keys': subscription.get('Keys', {})
},
data=json.dumps(payload),
vapid_private_key=VAPID_PRIVATE_KEY,
vapid_claims={'sub': VAPID_SUBJECT},
timeout=10
)
return True
except ImportError:
logger.warning('pywebpush not installed, install with: pip install pywebpush')
return False
except Exception as e:
logger.error(f'Web push error: {e}')
return False
def _mark_subscription_inactive(subscription_id):
"""Mark a subscription as inactive (e.g., after failed send)"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
subs_col.update_one(
{'_id': ObjectId(subscription_id)},
{'$set': {'IsActive': False}}
)
client.close()
except Exception as e:
logger.error(f'Error marking subscription inactive: {e}')
def send_push_to_all_admins(title, body, icon=None, url='/', reference=None):
"""
Send a push notification to all admin users
Args:
title (str): Notification title
body (str): Notification body
icon (str, optional): Icon URL
url (str, optional): URL to open on click
reference (dict, optional): Reference data
Returns:
int: Total notifications sent
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
users_col = db['users']
# Get all admin users
admin_users = list(users_col.find(
{'IsAdmin': True},
{'Username': 1}
))
client.close()
total_sent = 0
for admin_doc in admin_users:
sent = send_push_notification(
admin_doc['Username'],
title,
body,
icon,
url,
reference,
tag='admin-notification'
)
total_sent += sent
logger.info(f'Sent push to all admins: {total_sent} notifications')
return total_sent
except Exception as e:
logger.error(f'Error sending push to admins: {e}')
return 0
def cleanup_inactive_subscriptions():
"""
Remove inactive subscriptions older than 30 days
Run this periodically as a maintenance task
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
cutoff_date = datetime.datetime.now() - datetime.timedelta(days=30)
result = subs_col.delete_many({
'IsActive': False,
'LastUsed': {'$lt': cutoff_date}
})
client.close()
logger.info(f'Cleaned up {result.deleted_count} inactive subscriptions')
return result.deleted_count
except Exception as e:
logger.error(f'Error cleaning up subscriptions: {e}')
return 0
# Database collection schema
def ensure_push_subscriptions_collection():
"""Ensure the push_subscriptions collection exists with proper indexes"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
subs_col = get_push_subscriptions_collection(db)
# Create indexes
subs_col.create_index('Username')
subs_col.create_index([('Username', 1), ('IsActive', 1)])
subs_col.create_index([('CreatedAt', 1)]) # TTL-like usage
subs_col.create_index('SubscriptionHash', unique=True)
logger.info('Push subscriptions collection indexes created')
client.close()
except Exception as e:
logger.error(f'Error ensuring push subscriptions collection: {e}')
+470
View File
@@ -0,0 +1,470 @@
/**
* Push Notification Management
* Handles subscription, unsubscription, and UI updates for web push notifications
*/
class PushNotificationManager {
constructor() {
this.serviceWorkerRegistration = null;
this.vapidKey = null;
this.isSupported = this.checkSupport();
}
/**
* Check if push notifications are supported
*/
checkSupport() {
return (
'serviceWorker' in navigator &&
'PushManager' in window &&
'Notification' in window
);
}
/**
* Initialize push notification system
* Must be called after page load
*/
async init() {
if (!this.isSupported) {
console.log('Push notifications not supported in this browser');
return false;
}
try {
// Get service worker registration
const registrations = await navigator.serviceWorker.getRegistrations();
this.serviceWorkerRegistration = registrations.find(
reg => reg.scope === window.location.origin + '/'
);
if (!this.serviceWorkerRegistration) {
console.warn('Service Worker not found, retrying...');
// Try registering if not already done
try {
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/static/service-worker.js');
} catch (err) {
console.error('Failed to register Service Worker:', err);
return false;
}
}
// Fetch VAPID public key from server
const keyResponse = await fetch('/api/push/vapid-key');
if (keyResponse.ok) {
const keyData = await keyResponse.json();
this.vapidKey = keyData.vapid_key;
} else {
console.warn('Failed to fetch VAPID key');
return false;
}
return true;
} catch (error) {
console.error('Failed to initialize push notifications:', error);
return false;
}
}
/**
* Request notification permission from user
*/
async requestPermission() {
if (!this.isSupported) {
console.warn('Push notifications not supported');
return false;
}
if (Notification.permission === 'granted') {
console.log('Push notifications already permitted');
return true;
}
if (Notification.permission === 'denied') {
console.warn('Push notifications have been denied by user');
return false;
}
try {
const permission = await Notification.requestPermission();
return permission === 'granted';
} catch (error) {
console.error('Error requesting notification permission:', error);
return false;
}
}
/**
* Subscribe to push notifications
*/
async subscribe() {
if (!this.isSupported) {
console.error('Push notifications not supported');
return false;
}
if (!this.serviceWorkerRegistration) {
console.error('Service Worker not initialized');
return false;
}
if (!this.vapidKey) {
console.error('VAPID key not available');
return false;
}
try {
// Check if already subscribed
const existingSubscription = await this.serviceWorkerRegistration.pushManager.getSubscription();
if (existingSubscription) {
console.log('Already subscribed to push notifications');
return await this.saveSubscriptionToServer(existingSubscription);
}
// Request permission if needed
const hasPermission = await this.requestPermission();
if (!hasPermission) {
console.warn('User denied notification permission');
return false;
}
// Subscribe to push service
const subscription = await this.serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(this.vapidKey)
});
// Save subscription to server
return await this.saveSubscriptionToServer(subscription);
} catch (error) {
console.error('Failed to subscribe to push notifications:', error);
return false;
}
}
/**
* Unsubscribe from push notifications
*/
async unsubscribe() {
if (!this.serviceWorkerRegistration) {
console.error('Service Worker not initialized');
return false;
}
try {
const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription();
if (!subscription) {
console.warn('No active push subscription');
return false;
}
// Remove subscription on server
const success = await this.removeSubscriptionFromServer(subscription);
// Unsubscribe from push service
if (success) {
await subscription.unsubscribe();
return true;
}
return false;
} catch (error) {
console.error('Failed to unsubscribe from push notifications:', error);
return false;
}
}
/**
* Check if user is subscribed to push notifications
*/
async isSubscribed() {
if (!this.serviceWorkerRegistration) {
return false;
}
try {
const subscription = await this.serviceWorkerRegistration.pushManager.getSubscription();
return subscription !== null;
} catch (error) {
console.error('Failed to check subscription status:', error);
return false;
}
}
/**
* Save subscription to server
*/
async saveSubscriptionToServer(subscription) {
try {
const response = await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
subscription: subscription.toJSON()
})
});
if (response.ok) {
const data = await response.json();
console.log('Subscription saved to server:', data.message);
return true;
} else {
const error = await response.json();
console.error('Failed to save subscription:', error.error);
return false;
}
} catch (error) {
console.error('Error communicating with server:', error);
return false;
}
}
/**
* Remove subscription from server
*/
async removeSubscriptionFromServer(subscription) {
try {
const response = await fetch('/api/push/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: subscription.endpoint
})
});
if (response.ok) {
console.log('Subscription removed from server');
return true;
} else {
const error = await response.json();
console.error('Failed to remove subscription:', error.error);
return false;
}
} catch (error) {
console.error('Error communicating with server:', error);
return false;
}
}
/**
* Get all subscriptions for current user
*/
async getSubscriptions() {
try {
const response = await fetch('/api/push/subscriptions');
if (response.ok) {
const data = await response.json();
return data.subscriptions || [];
}
return [];
} catch (error) {
console.error('Error fetching subscriptions:', error);
return [];
}
}
/**
* Send test notification (admin only)
*/
async sendTestNotification(targetUser = null) {
try {
const response = await fetch('/api/push/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
target_user: targetUser
})
});
if (response.ok) {
const data = await response.json();
console.log('Test notification sent:', data.message);
return true;
} else {
const error = await response.json();
console.error('Failed to send test notification:', error.error);
return false;
}
} catch (error) {
console.error('Error sending test notification:', error);
return false;
}
}
/**
* Convert VAPID key from base64 string to Uint8Array
* Required for subscribing to push service
*/
urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
}
// Create global instance
const pushNotificationManager = new PushNotificationManager();
/**
* Show notification subscription UI (typically in settings)
*/
function showPushNotificationSettings() {
const container = document.getElementById('push-notification-settings');
if (!container) return;
if (!pushNotificationManager.isSupported) {
container.innerHTML = '<p class="text-muted">Push-Benachrichtigungen werden in diesem Browser nicht unterstützt.</p>';
return;
}
const html = `
<div class="push-notification-settings">
<h5>Push-Benachrichtigungen</h5>
<p>Erhalten Sie Benachrichtigungen für wichtige Ereignisse direkt auf Ihrem Gerät.</p>
<div id="push-status" style="margin: 15px 0;"></div>
<button id="toggle-push-btn" class="btn btn-primary" style="margin-bottom: 10px;">
Benachrichtigungen aktivieren
</button>
<div id="subscriptions-list" style="margin-top: 15px;"></div>
</div>
`;
container.innerHTML = html;
// Set up button handler
const toggleBtn = document.getElementById('toggle-push-btn');
pushNotificationManager.init().then(() => {
updatePushStatus();
});
toggleBtn.addEventListener('click', togglePushNotifications);
}
/**
* Update push notification status display
*/
async function updatePushStatus() {
const statusDiv = document.getElementById('push-status');
const toggleBtn = document.getElementById('toggle-push-btn');
if (!statusDiv || !toggleBtn) return;
const isSubscribed = await pushNotificationManager.isSubscribed();
const permission = Notification.permission;
let statusHtml = '<div class="alert alert-info">';
if (isSubscribed) {
statusHtml += '<strong>✓ Aktiv:</strong> Sie erhalten Push-Benachrichtigungen.';
toggleBtn.textContent = 'Benachrichtigungen deaktivieren';
toggleBtn.classList.remove('btn-primary');
toggleBtn.classList.add('btn-danger');
} else if (permission === 'denied') {
statusHtml += '<strong>✗ Blockiert:</strong> Benachrichtigungen wurden abgelehnt. Bitte überprüfen Sie Ihre Browser-Einstellungen.';
toggleBtn.disabled = true;
toggleBtn.textContent = 'Benachrichtigungen deaktiviert';
} else {
statusHtml += '<strong>○ Inaktiv:</strong> Sie erhalten derzeit keine Push-Benachrichtigungen.';
toggleBtn.textContent = 'Benachrichtigungen aktivieren';
toggleBtn.classList.add('btn-primary');
toggleBtn.classList.remove('btn-danger');
}
statusHtml += '</div>';
statusDiv.innerHTML = statusHtml;
// Show subscriptions list
const subscriptionsList = document.getElementById('subscriptions-list');
if (subscriptionsList && isSubscribed) {
const subs = await pushNotificationManager.getSubscriptions();
if (subs.length > 0) {
let subsHtml = '<h6>Aktive Abos:</h6><ul class="list-group">';
subs.forEach(sub => {
const endpoint = new URL(sub.endpoint);
subsHtml += `<li class="list-group-item"><small>${endpoint.hostname}</small><br><small class="text-muted">${sub.created_at}</small></li>`;
});
subsHtml += '</ul>';
subscriptionsList.innerHTML = subsHtml;
}
}
}
/**
* Toggle push notifications on/off
*/
async function togglePushNotifications() {
const isSubscribed = await pushNotificationManager.isSubscribed();
if (isSubscribed) {
// Unsubscribe
const success = await pushNotificationManager.unsubscribe();
if (success) {
showAlert('Benachrichtigungen deaktiviert', 'success');
} else {
showAlert('Fehler beim Deaktivieren der Benachrichtigungen', 'danger');
}
} else {
// Subscribe
const success = await pushNotificationManager.subscribe();
if (success) {
showAlert('Benachrichtigungen aktiviert!', 'success');
} else {
showAlert('Fehler beim Aktivieren der Benachrichtigungen', 'danger');
}
}
// Update status display
updatePushStatus();
}
/**
* Show an alert message
*/
function showAlert(message, type = 'info') {
// Try to show toast or alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Find a good place to show the alert
const container = document.querySelector('.container-fluid') || document.body;
const firstElement = container.firstChild;
if (firstElement) {
container.insertBefore(alertDiv, firstElement);
} else {
container.appendChild(alertDiv);
}
// Auto-dismiss after 5 seconds
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
// Auto-initialize on page load
document.addEventListener('DOMContentLoaded', async () => {
await pushNotificationManager.init();
});
+134
View File
@@ -0,0 +1,134 @@
{
"name": "Inventarsystem",
"short_name": "Inventar",
"description": "Verwaltungssystem für Schulinventare mit Ausleihmanagement",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#007bff",
"dir": "ltr",
"lang": "de-DE",
"prefer_related_applications": false,
"related_applications": [],
"icons": [
{
"src": "/static/img/logo-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-192x192-maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/img/logo-512x512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots": [
{
"src": "/static/img/screenshot-540x720.png",
"sizes": "540x720",
"type": "image/png",
"form_factor": "narrow"
},
{
"src": "/static/img/screenshot-1280x720.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
],
"categories": ["education", "productivity"],
"shortcuts": [
{
"name": "Meine Ausleihungen",
"short_name": "Ausleihungen",
"description": "Zeige meine aktuellen Ausleihungen",
"url": "/my_borrowed_items",
"icons": [
{
"src": "/static/img/shortcut-96x96.png",
"sizes": "96x96"
}
]
},
{
"name": "Kalender",
"short_name": "Kalender",
"description": "Kalender für Ausleihplanungen",
"url": "/terminplan",
"icons": [
{
"src": "/static/img/shortcut-96x96.png",
"sizes": "96x96"
}
]
}
],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [
{
"name": "media",
"accept": ["image/*", "video/*"]
}
]
}
}
}
+185
View File
@@ -0,0 +1,185 @@
/**
* Service Worker for Web Push Notifications
* Handles incoming push events and displays notifications
*/
const CACHE_NAME = 'inventarsystem-v1';
const ASSETS_TO_CACHE = [
'/',
'/static/css/styles.css',
'/static/js/scripts.js',
'/offline.html'
];
// Install event - cache assets
self.addEventListener('install', (event) => {
console.log('[Service Worker] Installing...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS_TO_CACHE).catch((err) => {
console.warn('[Service Worker] Cache add error:', err);
// Don't fail the installation
return Promise.resolve();
});
})
);
self.skipWaiting();
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('[Service Worker] Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
self.clients.claim();
});
// Push event - handle incoming push notifications
self.addEventListener('push', (event) => {
console.log('[Service Worker] Push notification received');
let notificationData = {
title: 'Inventarsystem',
body: 'Neue Benachrichtigung',
icon: '/static/img/logo-192x192.png',
badge: '/static/img/badge-72x72.png',
tag: 'inventarsystem-notification',
requireInteraction: false,
};
// Parse the push event data if available
if (event.data) {
try {
const data = event.data.json();
notificationData = {
title: data.title || notificationData.title,
body: data.message || data.body || notificationData.body,
icon: data.icon || notificationData.icon,
badge: data.badge || notificationData.badge,
tag: data.tag || notificationData.tag,
requireInteraction: data.requireInteraction || false,
data: {
url: data.url || '/',
reference: data.reference || null,
type: data.type || 'info',
},
};
console.log('[Service Worker] Push data parsed:', notificationData);
} catch (err) {
console.error('[Service Worker] Failed to parse push data:', err);
notificationData.body = event.data.text();
}
}
event.waitUntil(
self.registration.showNotification(notificationData.title, {
body: notificationData.body,
icon: notificationData.icon,
badge: notificationData.badge,
tag: notificationData.tag,
requireInteraction: notificationData.requireInteraction,
data: notificationData.data,
actions: [
{
action: 'open',
title: 'Öffnen',
icon: '/static/img/open-icon.png'
},
{
action: 'close',
title: 'Schließen',
icon: '/static/img/close-icon.png'
}
],
})
);
});
// Notification click event - handle user clicks on notifications
self.addEventListener('notificationclick', (event) => {
console.log('[Service Worker] Notification clicked:', event.action);
event.notification.close();
const urlToOpen = event.notification.data.url || '/';
event.waitUntil(
clients.matchAll({
type: 'window',
includeUncontrolled: true
}).then((windowClients) => {
// Check if the app is already open
for (let i = 0; i < windowClients.length; i++) {
const client = windowClients[i];
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// If not open, open a new window
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// Notification close event - track when users dismiss notifications
self.addEventListener('notificationclose', (event) => {
console.log('[Service Worker] Notification closed');
// Could send analytics here
});
// Background fetch event (optional, for large data syncs)
self.addEventListener('sync', (event) => {
console.log('[Service Worker] Background sync triggered:', event.tag);
if (event.tag === 'sync-notifications') {
event.waitUntil(
fetch('/api/notifications/sync').then((response) => {
console.log('[Service Worker] Notifications synced');
})
);
}
});
// Fetch event - serve cached content when offline
self.addEventListener('fetch', (event) => {
// Skip non-GET requests
if (event.request.method !== 'GET') {
return;
}
// Skip API calls - always use network
if (event.request.url.includes('/api/')) {
return;
}
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
return fetch(event.request).then((response) => {
// Cache successful responses
if (response && response.status === 200) {
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
}
return response;
}).catch(() => {
// Return offline page
return caches.match('/offline.html');
});
})
);
});
+24
View File
@@ -24,6 +24,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css', v=ASSET_VERSION) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/planned_appointments.css', v=ASSET_VERSION) }}">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<script>
(function () {
@@ -1752,5 +1753,28 @@
<!-- Mobile compatibility scripts -->
<script src="{{ url_for('static', filename='js/mobile_compatibility.js') }}"></script>
<script src="{{ url_for('static', filename='js/ios_fixes.js') }}"></script>
<!-- Service Worker Registration for Push Notifications -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Register service worker for push notifications and offline support
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('{{ url_for("static", filename="service-worker.js") }}')
.then(function(registration) {
console.log('Service Worker registered successfully:', registration);
// Check if we have permission for push notifications
if ('Notification' in window && Notification.permission === 'granted') {
console.log('Push notifications already enabled');
}
})
.catch(function(error) {
console.warn('Service Worker registration failed:', error);
});
} else {
console.log('Service Workers not supported in this browser');
}
});
</script>
</body>
</html>
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# VAPID Key Generation Script for Web Push Notifications
# Generates VAPID (Voluntary Application Server Identification) keys required for push notifications
echo "==========================================="
echo "VAPID Key Generation for Inventarsystem"
echo "==========================================="
echo ""
# Check if Python is available
if ! command -v python3 &> /dev/null; then
echo "❌ Error: Python 3 is required but not installed."
exit 1
fi
# Check if pywebpush is installed
echo "Checking for pywebpush..."
python3 -c "import pywebpush" 2>/dev/null
if [ $? -ne 0 ]; then
echo "Installing pywebpush..."
pip3 install pywebpush
if [ $? -ne 0 ]; then
echo "❌ Error: Failed to install pywebpush"
echo "Please install manually: pip3 install pywebpush"
exit 1
fi
fi
echo "✓ pywebpush is installed"
echo ""
# Generate VAPID keys
echo "Generating VAPID keys..."
python3 << 'EOF'
from pywebpush import generate_keys
try:
keys = generate_keys()
print("✓ VAPID Keys generated successfully!")
print("")
print("PUBLIC KEY (share with browsers):")
print(keys['public_key'])
print("")
print("PRIVATE KEY (keep secret!):")
print(keys['private_key'])
print("")
print("==========================================="
print("Add these to your environment variables:")
print("==========================================="
print("")
print("export VAPID_PUBLIC_KEY='" + keys['public_key'] + "'")
print("export VAPID_PRIVATE_KEY='" + keys['private_key'] + "'")
print("export VAPID_SUBJECT='mailto:admin@yourdomain.com'")
print("")
print("Or add to your .env file:")
print("")
print("VAPID_PUBLIC_KEY=" + keys['public_key'])
print("VAPID_PRIVATE_KEY=" + keys['private_key'])
print("VAPID_SUBJECT=mailto:admin@yourdomain.com")
print("")
print("⚠️ IMPORTANT: Keep the PRIVATE KEY secret!")
print("==========================================="
except Exception as e:
print(f"❌ Error generating VAPID keys: {e}")
exit(1)
EOF
if [ $? -eq 0 ]; then
echo ""
echo "✓ Next steps:"
echo "1. Copy the PUBLIC KEY to your browser-side code"
echo "2. Set the PRIVATE KEY in your server environment"
echo "3. Update config.json with your email address"
echo "4. Test with: curl http://localhost:5000/api/push/vapid-key"
else
echo "❌ Error generating keys"
exit 1
fi
+2 -1
View File
@@ -12,4 +12,5 @@ redis
reportlab
python-barcode
openpyxl
cryptography
cryptography
pywebpush