"""Web Push HTTP endpoints. GET /push/vapid-public-key — publiczny klucz VAPID dla klienta JS POST /push/subscribe — zapisz subskrypcję w DB POST /push/unsubscribe — usuń subskrypcję POST /push/test — self-push (test własnego subskrybenta) POST /push/pending-url — (iOS PWA) zapis URL z notificationclick GET /push/pending-url — odczytaj i wyczyść pending URL """ import logging from datetime import datetime from flask import jsonify, request from flask_login import login_required, current_user from database import SessionLocal, PushSubscription from extensions import limiter from . import bp from .push_service import ( send_push, set_pending_url, pop_pending_url, _vapid_public_key, ) logger = logging.getLogger(__name__) def exempt_from_csrf(app): """Exempt push routes from CSRF protection (JS fetch from SW + frontend).""" csrf = app.extensions.get('csrf') if csrf: csrf.exempt(bp) @bp.route('/vapid-public-key', methods=['GET']) @login_required @limiter.exempt def vapid_public_key(): key = _vapid_public_key() if not key: return jsonify({'error': 'VAPID not configured'}), 503 return jsonify({'key': key}) @bp.route('/subscribe', methods=['POST']) @login_required @limiter.limit('30 per minute') def subscribe(): payload = request.get_json(silent=True) or {} endpoint = (payload.get('endpoint') or '').strip() keys = payload.get('keys') or {} p256dh = (keys.get('p256dh') or '').strip() auth = (keys.get('auth') or '').strip() if not endpoint or not p256dh or not auth: return jsonify({'error': 'endpoint, p256dh i auth wymagane'}), 400 user_agent = (request.headers.get('User-Agent') or '')[:500] db = SessionLocal() try: existing = db.query(PushSubscription).filter_by(endpoint=endpoint).first() if existing: existing.user_id = current_user.id existing.p256dh = p256dh existing.auth = auth existing.user_agent = user_agent existing.last_used_at = datetime.now() else: sub = PushSubscription( user_id=current_user.id, endpoint=endpoint, p256dh=p256dh, auth=auth, user_agent=user_agent, ) db.add(sub) db.commit() return jsonify({'ok': True}) except Exception as e: db.rollback() logger.error("push subscribe error: %s", e) return jsonify({'error': 'Błąd zapisu subskrypcji'}), 500 finally: db.close() @bp.route('/unsubscribe', methods=['POST']) @login_required @limiter.limit('30 per minute') def unsubscribe(): payload = request.get_json(silent=True) or {} endpoint = (payload.get('endpoint') or '').strip() if not endpoint: return jsonify({'error': 'endpoint wymagany'}), 400 db = SessionLocal() try: sub = db.query(PushSubscription).filter_by( endpoint=endpoint, user_id=current_user.id ).first() if sub: db.delete(sub) db.commit() return jsonify({'ok': True}) except Exception as e: db.rollback() logger.error("push unsubscribe error: %s", e) return jsonify({'error': 'Błąd'}), 500 finally: db.close() @bp.route('/test', methods=['POST']) @login_required @limiter.limit('5 per minute') def test_push(): stats = send_push( user_id=current_user.id, title='🔔 Powiadomienia działają', body='To testowe powiadomienie. Zobaczysz takie okienko kiedy ktoś napisze do Ciebie wiadomość.', url='/wiadomosci', tag='push-test', ) return jsonify({'ok': True, **stats}) @bp.route('/pending-url', methods=['POST']) @login_required @limiter.limit('60 per minute') def pending_url_set(): payload = request.get_json(silent=True) or {} url = (payload.get('url') or '').strip() if not url or not url.startswith('/'): return jsonify({'error': 'URL must be same-origin path'}), 400 set_pending_url(current_user.id, url) return jsonify({'ok': True}) @bp.route('/pending-url', methods=['GET']) @login_required @limiter.exempt def pending_url_get(): url = pop_pending_url(current_user.id) return jsonify({'url': url}) @bp.route('/pending-url/clear', methods=['POST']) @login_required @limiter.exempt def pending_url_clear(): pop_pending_url(current_user.id) return jsonify({'ok': True})