Poprawki bezpieczenstwa: maile, kredyty, token strumienia, lokalizacja, spotkania, zgloszenia
Deploy Polska Lokalnie / deploy (push) Successful in 13s

- Imie, tytul artykulu i powod odrzucenia oraz {{name}} w kampaniach
  escapowane w HTML maili; limit dlugosci imienia.
- Kredyty promowania zuzywane i dopisywane warunkowym UPDATE, ITN blokuje
  wiersz zamowienia; zakup i kredyt tylko na aktywny plan/pakiet.
- Strumien powiadomien na krotkim (60 s) tokenie ?ticket= pobieranym przez
  POST /api/notifications/stream-token; token sesji nie jest przyjmowany z adresu.
- Przyblizona lokalizacja jako srodek kratki 300 m zamiast odwracalnego
  przesuniecia liczonego z id.
- Propozycja spotkania respektuje blokade i moderacje, bez e-maila w nazwie.
- Zglaszajacy dostaje samo potwierdzenie; zgloszenia tylko opublikowanych
  ogloszen; nie zapisujemy rozmowy przyslanej przez przegladarke.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 14:36:48 +02:00
parent 0d149467e9
commit b850e4c092
25 changed files with 403 additions and 103 deletions
+40 -9
View File
@@ -107,7 +107,8 @@ export function NotificationsProvider({ children }: { children: ReactNode }) {
.catch(() => { /* cicho - brak sieci/tokenu */ });
}, []);
// Ładowanie listy + realtime (SSE) gdy użytkownik zalogowany. Token w query param (EventSource nie ustawia nagłówków).
// Ładowanie listy + realtime (SSE) gdy użytkownik zalogowany. EventSource nie ustawia nagłówków,
// więc w adresie idzie krótki (60 s) token samego strumienia - nie token sesji, który trafiałby do logów.
useEffect(() => {
const token = getToken();
if (!user || !token) {
@@ -118,22 +119,52 @@ export function NotificationsProvider({ children }: { children: ReactNode }) {
refresh();
const source = new EventSource(`/api/notifications/stream?access_token=${encodeURIComponent(token)}`);
source.addEventListener('notification', (event) => {
let disposed = false;
let source: EventSource | null = null;
let reconnectId: number | undefined;
const connect = async () => {
try {
const incoming = JSON.parse((event as MessageEvent).data) as ServerNotification;
setNotifications((current) => (current.some((item) => item.id === incoming.id) ? current : [incoming, ...current]));
const { token: ticket } = await apiFetch<{ token: string }>('/notifications/stream-token', { method: 'POST' });
if (disposed) {
return;
}
source = new EventSource(`/api/notifications/stream?ticket=${encodeURIComponent(ticket)}`);
source.addEventListener('notification', (event) => {
try {
const incoming = JSON.parse((event as MessageEvent).data) as ServerNotification;
setNotifications((current) => (current.some((item) => item.id === incoming.id) ? current : [incoming, ...current]));
} catch {
/* ignoruj nieparsowalne zdarzenie */
}
});
// Token strumienia wygasa po minucie, więc samoczynne wznowienie EventSource pod tym samym
// adresem by nie zadziałało - po zerwaniu łączymy się od nowa z nowym tokenem.
source.onerror = () => {
source?.close();
scheduleReconnect();
};
} catch {
/* ignoruj nieparsowalne zdarzenie */
scheduleReconnect();
}
});
// onerror: EventSource sam ponawia połączenie.
};
const scheduleReconnect = () => {
if (!disposed) {
window.clearTimeout(reconnectId);
reconnectId = window.setTimeout(() => { void connect(); }, 5000);
}
};
void connect();
// Zapasowy polling na wypadek zerwanego SSE.
const pollId = window.setInterval(refresh, 60000);
return () => {
source.close();
disposed = true;
source?.close();
window.clearTimeout(reconnectId);
window.clearInterval(pollId);
};
}, [user, refresh]);