fix(push): fix VAPID key format and add regenerate endpoint + UI button
This commit is contained in:
@@ -3,9 +3,9 @@ import logging
|
||||
|
||||
from quart import Blueprint, jsonify, request
|
||||
|
||||
from fabledassistant.auth import login_required, get_current_user_id
|
||||
from fabledassistant.auth import login_required, get_current_user_id, admin_required
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.services.push import delete_subscription, save_subscription, vapid_enabled
|
||||
from fabledassistant.services.push import delete_subscription, regenerate_vapid_keys, save_subscription, vapid_enabled
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,3 +42,13 @@ async def unsubscribe():
|
||||
return jsonify({"error": "endpoint is required"}), 400
|
||||
await delete_subscription(uid, endpoint)
|
||||
return "", 204
|
||||
|
||||
|
||||
@push_bp.route("/reset-vapid", methods=["POST"])
|
||||
@admin_required
|
||||
async def reset_vapid():
|
||||
"""Regenerate VAPID keys and clear all push subscriptions."""
|
||||
ok = await regenerate_vapid_keys()
|
||||
if ok:
|
||||
return jsonify({"publicKey": Config.VAPID_PUBLIC_KEY}), 200
|
||||
return jsonify({"error": "Key regeneration failed"}), 500
|
||||
|
||||
@@ -62,11 +62,14 @@ def ensure_vapid_keys() -> None:
|
||||
v = Vapid01()
|
||||
v.generate_keys()
|
||||
|
||||
private_pem = v.private_key.private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
# pywebpush expects the private key as a base64url-encoded DER blob
|
||||
# (passed to Vapid.from_string → from_der), NOT a PEM string.
|
||||
private_der = v.private_key.private_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
serialization.NoEncryption(),
|
||||
).decode()
|
||||
)
|
||||
private_b64 = base64.urlsafe_b64encode(private_der).rstrip(b"=").decode()
|
||||
|
||||
pub_bytes = v.public_key.public_bytes(
|
||||
serialization.Encoding.X962,
|
||||
@@ -76,15 +79,40 @@ def ensure_vapid_keys() -> None:
|
||||
|
||||
# Persist so they survive container restarts.
|
||||
_VAPID_KEYS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
_VAPID_KEYS_FILE.write_text(json.dumps({"private_key": private_pem, "public_key": public_b64}))
|
||||
_VAPID_KEYS_FILE.write_text(json.dumps({"private_key": private_b64, "public_key": public_b64}))
|
||||
|
||||
Config.VAPID_PRIVATE_KEY = private_pem
|
||||
Config.VAPID_PRIVATE_KEY = private_b64
|
||||
Config.VAPID_PUBLIC_KEY = public_b64
|
||||
logger.info("Generated new VAPID keys and saved to %s", _VAPID_KEYS_FILE)
|
||||
except Exception:
|
||||
logger.warning("Failed to generate VAPID keys — push notifications will be unavailable", exc_info=True)
|
||||
|
||||
|
||||
async def regenerate_vapid_keys() -> bool:
|
||||
"""Delete existing VAPID keys, clear all push subscriptions, and generate a fresh pair.
|
||||
|
||||
All existing browser subscriptions are invalidated when keys rotate, so they
|
||||
must be cleared — users will need to re-enable notifications.
|
||||
"""
|
||||
if _VAPID_KEYS_FILE.exists():
|
||||
_VAPID_KEYS_FILE.unlink()
|
||||
Config.VAPID_PRIVATE_KEY = ""
|
||||
Config.VAPID_PUBLIC_KEY = ""
|
||||
|
||||
# Clear all push subscriptions — they are bound to the old public key.
|
||||
async with async_session() as session:
|
||||
await session.execute(delete(PushSubscription))
|
||||
await session.commit()
|
||||
|
||||
ensure_vapid_keys()
|
||||
enabled = vapid_enabled()
|
||||
if enabled:
|
||||
logger.info("VAPID keys regenerated successfully")
|
||||
else:
|
||||
logger.error("VAPID key regeneration failed")
|
||||
return enabled
|
||||
|
||||
|
||||
async def save_subscription(user_id: int, subscription_json: dict, user_agent: str | None = None) -> PushSubscription:
|
||||
"""Upsert a push subscription by endpoint."""
|
||||
endpoint = subscription_json.get("endpoint", "")
|
||||
|
||||
Reference in New Issue
Block a user