Skip to content

Web Push notifikace

Atrea User API podporuje Web Push notifikace pomocí VAPID (Voluntary Application Server Identification) standardu.

Přehled

Web Push umožňuje odesílat notifikace přímo do prohlížeče uživatele, i když aplikace není otevřena. Funguje na všech moderních prohlížečích (Chrome, Firefox, Edge, Safari 16.4+).

Architektura

Backend aplikace

    └── POST /api/internal/notifications (type s pushEnabled=true)


        PushService

    ┌───────────┴──────────────┐
    │  Načte subscripce       │
    │  uživatele z DB         │
    └───────────┬──────────────┘

    ┌───────────▼──────────────┐
    │  web-push library        │
    │  (VAPID podpis)         │
    └───────────┬──────────────┘

    ┌───────────▼──────────────┐
    │  Push server prohlížeče  │
    │  (FCM, Mozilla, Apple)   │
    └───────────┬──────────────┘

    ┌───────────▼──────────────┐
    │  Prohlížeč uživatele    │
    │  (Service Worker)       │
    └──────────────────────────┘

Konfigurace VAPID klíčů

Generování klíčů

bash
npx web-push generate-vapid-keys

Výstup:

Public Key:
BM8U3B-cXKbcl...

Private Key:
x7Vc3A2...

Konfigurace v config/production.yaml

yaml
push:
  vapidPublicKey: "BM8U3B-cXKbcl..."
  vapidPrivateKey: "x7Vc3A2..."
  vapidSubject: "mailto:admin@atrea.eu"

VAPID klíče

Generujte VAPID klíče jednou a uložte je bezpečně. Pokud klíče změníte, všechny existující subscripce přestanou fungovat a uživatelé se musí znovu přihlásit k odběru.


Frontend integrace

1. Získání VAPID public key

javascript
const response = await fetch('/api/profile/me/push/vapid-key', {
  headers: { 'Authorization': `Bearer ${token}` }
});
const { data: { vapidPublicKey } } = await response.json();

2. Registrace Service Workeru

javascript
// public/sw.js
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  
  event.waitUntil(
    self.registration.showNotification(data.title ?? 'Notifikace', {
      body: data.body,
      icon: data.icon ?? '/logo.svg',
      badge: '/badge.svg',
      data: data.url ? { url: data.url } : undefined,
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  if (event.notification.data?.url) {
    event.waitUntil(clients.openWindow(event.notification.data.url));
  }
});
javascript
// app.js
const registration = await navigator.serviceWorker.register('/sw.js');

3. Žádost o povolení a subscripce

javascript
async function subscribeToPush() {
  // Žádost o povolení notifikací
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return;

  // Konverze VAPID public key
  const vapidKey = urlBase64ToUint8Array(vapidPublicKey);

  // Vytvoření subscripce
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: vapidKey,
  });

  // Odeslání subscripce na server
  await fetch('/api/profile/me/push/subscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
    },
    body: JSON.stringify({
      subscription: subscription.toJSON(),
      deviceLabel: navigator.userAgent,
    }),
  });
}

// Pomocná funkce pro konverzi VAPID key
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const rawData = window.atob(base64);
  return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}

4. Odhlášení z odběru

javascript
async function unsubscribeFromPush() {
  const subscription = await registration.pushManager.getSubscription();
  if (!subscription) return;

  await fetch('/api/profile/me/push/unsubscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
    },
    body: JSON.stringify({ endpoint: subscription.endpoint }),
  });

  await subscription.unsubscribe();
}

API endpointy

GET /profile/me/push/vapid-key

Vrátí VAPID public key pro inicializaci.

bash
GET /api/profile/me/push/vapid-key
Authorization: Bearer <token>
json
{ "data": { "vapidPublicKey": "BM8U3B..." } }

POST /profile/me/push/subscribe

Registruje push subscripci pro přihlášeného uživatele.

bash
POST /api/profile/me/push/subscribe
Authorization: Bearer <token>
json
{
  "subscription": {
    "endpoint": "https://fcm.googleapis.com/fcm/send/...",
    "keys": {
      "p256dh": "BMutZ...",
      "auth": "Kd9..."
    }
  },
  "deviceLabel": "Chrome na macOS"
}

POST /profile/me/push/unsubscribe

Odregistruje subscripci.

bash
POST /api/profile/me/push/unsubscribe
Authorization: Bearer <token>
{ "endpoint": "https://fcm.googleapis.com/fcm/send/..." }

GET /profile/me/push/subscriptions

Vrátí seznam všech subscripcí uživatele.

bash
GET /api/profile/me/push/subscriptions
Authorization: Bearer <token>
json
{
  "data": [
    {
      "id": 12,
      "endpoint": "https://fcm.googleapis.com/fcm/send/...",
      "deviceLabel": "Chrome na macOS",
      "createdAt": "2024-01-20T10:00:00.000Z"
    }
  ]
}

Odesílání push ze serveru

Push notifikace se odesílají automaticky jako součást POST /internal/notifications, pokud:

  1. Typ má pushEnabled: true
  2. Uživatel má povolen push pro daný typ
  3. Uživatel má registrované push subscripce

Payload odeslaný do prohlížeče:

json
{
  "title": "Renderovaný předmět šablony",
  "body": "Krátký text notifikace",
  "icon": "/assets/img/atrea/logo.svg",
  "url": "https://app.atrea.eu/notifications"
}

Zpracování expirovaných subscripcí

Pokud push endpoint vrátí 410 Gone nebo 404, subscripce se automaticky smaže z databáze (uživatel odregistroval notifikace v prohlížeči).


Podpora prohlížečů

ProhlížečPodpora
Chrome (desktop + Android)
Firefox
Edge
Safari 16.4+ (macOS + iOS)
Opera
Samsung Internet

iOS Safari

Safari na iOS podporuje Web Push od verze 16.4 (iOS 16.4+). Web app musí být přidána na plochu (PWA) pro zobrazení notifikací na iOS.

Atrea User API — interní dokumentace