14-04-2026
То что дела ай и то во что надо влить изменнеия
This commit is contained in:
parent
62e55dbaec
commit
cfc92beec0
@ -1,20 +1,39 @@
|
||||
self.addEventListener('install', () => self.skipWaiting());
|
||||
self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
|
||||
|
||||
async function broadcastToClients(payload) {
|
||||
const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
||||
clients.forEach((client) => {
|
||||
client.postMessage({
|
||||
type: 'SHINE_WEB_PUSH_EVENT',
|
||||
payload,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
let body = 'Новое сообщение SHiNE';
|
||||
let rawText = '';
|
||||
try {
|
||||
if (event.data) {
|
||||
const text = event.data.text();
|
||||
body = text || body;
|
||||
rawText = text || '';
|
||||
body = rawText || body;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
event.waitUntil(self.registration.showNotification('SHiNE: входящее сообщение', {
|
||||
event.waitUntil(Promise.all([
|
||||
self.registration.showNotification('SHiNE: входящее сообщение', {
|
||||
body,
|
||||
tag: 'shine-direct-message',
|
||||
renotify: true,
|
||||
}));
|
||||
}),
|
||||
broadcastToClients({
|
||||
body,
|
||||
rawText,
|
||||
receivedAt: Date.now(),
|
||||
}),
|
||||
]));
|
||||
});
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
<div id="modal-root"></div>
|
||||
<script>
|
||||
// Public VAPID key for Web Push (Base64URL)
|
||||
window.__SHINE_WEBPUSH_VAPID_PUBLIC_KEY__ = '';
|
||||
window.__SHINE_WEBPUSH_VAPID_PUBLIC_KEY__ = 'BOdoWZndZRaNe9kyUFsJ5-xEfFABXNKennAKg15Z7ycAwUIQ7yDV_sIWWYJCwJriN4g9oU-CyJPrn1U6lfxuDbI';
|
||||
</script>
|
||||
<script>
|
||||
(function attachAppWithBuildHash() {
|
||||
|
||||
@ -4,6 +4,7 @@ import { captureClientError, setClientErrorTransport } from './services/client-e
|
||||
import { initPwaPush } from './services/pwa-push-service.js';
|
||||
import {
|
||||
authService,
|
||||
addAppLogEntry,
|
||||
authorizeSession,
|
||||
isSessionInvalidError,
|
||||
refreshSessions,
|
||||
@ -36,6 +37,7 @@ import * as deviceCameraView from './pages/device-camera-view.js';
|
||||
import * as showKeysView from './pages/show-keys-view.js';
|
||||
import * as deviceSessionView from './pages/device-session-view.js';
|
||||
import * as languageView from './pages/language-view.js';
|
||||
import * as appLogView from './pages/app-log-view.js';
|
||||
import * as messagesList from './pages/messages-list.js';
|
||||
import * as contactSearchView from './pages/contact-search-view.js';
|
||||
import * as chatView from './pages/chat-view.js';
|
||||
@ -68,6 +70,7 @@ const routes = {
|
||||
'show-keys-view': showKeysView,
|
||||
'device-session-view': deviceSessionView,
|
||||
'language-view': languageView,
|
||||
'app-log-view': appLogView,
|
||||
'messages-list': messagesList,
|
||||
'contact-search-view': contactSearchView,
|
||||
'chat-view': chatView,
|
||||
@ -100,6 +103,18 @@ function showGlobalErrorAlert(title, details = {}) {
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
const pageId = getRoute().pageId || '';
|
||||
addAppLogEntry({
|
||||
level: 'error',
|
||||
source: 'global_error',
|
||||
message: event.message || 'Global JS error',
|
||||
details: {
|
||||
pageId,
|
||||
sourceUrl: event.filename || '',
|
||||
line: event.lineno,
|
||||
column: event.colno,
|
||||
stack: event.error?.stack || '',
|
||||
},
|
||||
});
|
||||
captureClientError({
|
||||
kind: 'global_error',
|
||||
message: event.message || 'Global JS error',
|
||||
@ -125,6 +140,16 @@ window.addEventListener('error', (event) => {
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const reason = event.reason;
|
||||
const pageId = getRoute().pageId || '';
|
||||
addAppLogEntry({
|
||||
level: 'error',
|
||||
source: 'unhandled_rejection',
|
||||
message: reason?.message || String(reason || 'Unhandled promise rejection'),
|
||||
details: {
|
||||
pageId,
|
||||
reasonType: reason?.constructor?.name || typeof reason,
|
||||
stack: reason?.stack || '',
|
||||
},
|
||||
});
|
||||
captureClientError({
|
||||
kind: 'unhandled_rejection',
|
||||
message: reason?.message || String(reason || 'Unhandled promise rejection'),
|
||||
@ -200,10 +225,30 @@ async function tryAutoLogin() {
|
||||
}
|
||||
|
||||
async function init() {
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
source: 'app',
|
||||
message: 'Инициализация UI запущена',
|
||||
});
|
||||
|
||||
setSessionResetHandler(() => {
|
||||
navigate('start-view');
|
||||
});
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
const data = event?.data || {};
|
||||
if (data.type !== 'SHINE_WEB_PUSH_EVENT') return;
|
||||
const payload = data.payload || {};
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
source: 'web-push',
|
||||
message: 'Получено push-событие в service worker',
|
||||
details: payload,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
authService.onEvent('SessionRevoked', async () => {
|
||||
await terminateCurrentSession({ infoMessage: 'Сессия закрыта с другого устройства.' });
|
||||
});
|
||||
@ -226,18 +271,38 @@ async function init() {
|
||||
}
|
||||
}
|
||||
const added = addIncomingMessage(fromLogin, text, messageId);
|
||||
if (added) {
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
source: 'incoming-dm',
|
||||
message: `Входящее сообщение от ${fromLogin}`,
|
||||
details: { messageId, text },
|
||||
});
|
||||
}
|
||||
if (added && Notification.permission === 'granted') {
|
||||
try {
|
||||
new Notification(`Сообщение от ${fromLogin}`, { body: text || '' });
|
||||
} catch {}
|
||||
}
|
||||
if (eventId) {
|
||||
try { await authService.ackIncomingMessage(eventId, messageId); } catch {}
|
||||
try {
|
||||
await authService.ackIncomingMessage(eventId, messageId);
|
||||
} catch (error) {
|
||||
addAppLogEntry({
|
||||
level: 'warn',
|
||||
source: 'incoming-dm',
|
||||
message: 'Не удалось отправить ACK на входящее сообщение',
|
||||
details: { eventId, messageId, error: error?.message || 'unknown' },
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
await tryAutoLogin();
|
||||
if (state.session.isAuthorized) {
|
||||
await initPwaPush({ authService });
|
||||
await initPwaPush({
|
||||
authService,
|
||||
onLog: (entry) => addAppLogEntry(entry),
|
||||
});
|
||||
window.setInterval(async () => {
|
||||
if (!state.session.isAuthorized) return;
|
||||
try {
|
||||
|
||||
96
shine-UI/js/pages/app-log-view.js
Normal file
96
shine-UI/js/pages/app-log-view.js
Normal file
@ -0,0 +1,96 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { clearAppLogEntries, getAppLogEntries } from '../state.js';
|
||||
|
||||
export const pageMeta = { id: 'app-log-view', title: 'Лог приложения' };
|
||||
|
||||
function formatTime(ts) {
|
||||
try {
|
||||
return new Date(ts).toLocaleTimeString('ru-RU');
|
||||
} catch {
|
||||
return String(ts || '');
|
||||
}
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Лог приложения',
|
||||
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
||||
}),
|
||||
);
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'card row';
|
||||
controls.style.justifyContent = 'space-between';
|
||||
controls.style.gap = '8px';
|
||||
controls.innerHTML = `
|
||||
<button class="ghost-btn" type="button" data-action="refresh">Обновить</button>
|
||||
<button class="ghost-btn" type="button" data-action="clear">Очистить</button>
|
||||
`;
|
||||
|
||||
const status = document.createElement('div');
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Загрузка лога...';
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'stack';
|
||||
|
||||
function renderEntries() {
|
||||
const entries = getAppLogEntries();
|
||||
list.innerHTML = '';
|
||||
|
||||
if (!entries.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'card meta-muted';
|
||||
empty.textContent = 'Лог пока пуст.';
|
||||
list.append(empty);
|
||||
status.className = 'status-line is-available';
|
||||
status.textContent = 'Записей: 0';
|
||||
return;
|
||||
}
|
||||
|
||||
entries.slice().reverse().forEach((entry) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'card stack';
|
||||
const level = String(entry.level || 'info').toUpperCase();
|
||||
const source = String(entry.source || 'ui');
|
||||
const details = String(entry.details || '').trim();
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.className = 'meta-muted';
|
||||
head.textContent = `[${formatTime(entry.ts)}] ${level} ${source}`;
|
||||
|
||||
const message = document.createElement('div');
|
||||
message.textContent = entry.message || '';
|
||||
|
||||
row.append(head, message);
|
||||
|
||||
if (details) {
|
||||
const detailsNode = document.createElement('pre');
|
||||
detailsNode.className = 'meta-muted';
|
||||
detailsNode.style.whiteSpace = 'pre-wrap';
|
||||
detailsNode.style.margin = '0';
|
||||
detailsNode.textContent = details;
|
||||
row.append(detailsNode);
|
||||
}
|
||||
|
||||
list.append(row);
|
||||
});
|
||||
|
||||
status.className = 'status-line is-available';
|
||||
status.textContent = `Записей: ${entries.length}`;
|
||||
}
|
||||
|
||||
controls.querySelector('[data-action="refresh"]').addEventListener('click', renderEntries);
|
||||
controls.querySelector('[data-action="clear"]').addEventListener('click', () => {
|
||||
clearAppLogEntries();
|
||||
renderEntries();
|
||||
});
|
||||
|
||||
screen.append(controls, status, list);
|
||||
renderEntries();
|
||||
return screen;
|
||||
}
|
||||
@ -45,20 +45,20 @@ export function render({ navigate }) {
|
||||
async function loadList() {
|
||||
try {
|
||||
const relations = await loadCurrentRelations();
|
||||
const follows = relations.outFollows || [];
|
||||
const contacts = relations.outContacts || [];
|
||||
list.innerHTML = '';
|
||||
|
||||
if (!follows.length) {
|
||||
if (!contacts.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'card meta-muted';
|
||||
empty.textContent = 'Ваш список контактов пока пуст';
|
||||
list.append(empty);
|
||||
status.className = 'status-line is-available';
|
||||
status.textContent = 'Нет подписок на пользователей.';
|
||||
status.textContent = 'Нет контактов.';
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = follows.map((login) => {
|
||||
const rows = contacts.map((login) => {
|
||||
const preview = directMessages.find((item) => item.id.toLowerCase() === login.toLowerCase());
|
||||
const chat = getChatMessages(login);
|
||||
const lastChat = chat[chat.length - 1];
|
||||
|
||||
@ -19,11 +19,13 @@ export function render({ navigate }) {
|
||||
<button class="text-btn" type="button" id="settings-device">Устройства</button>
|
||||
<button class="text-btn" type="button" id="settings-servers">Настройки серверов</button>
|
||||
<button class="text-btn" type="button" id="settings-language">Язык / Language</button>
|
||||
<button class="text-btn" type="button" id="settings-app-log">Лог приложения</button>
|
||||
`;
|
||||
|
||||
card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view'));
|
||||
card.querySelector('#settings-servers').addEventListener('click', () => navigate('server-settings-view'));
|
||||
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
|
||||
card.querySelector('#settings-app-log').addEventListener('click', () => navigate('app-log-view'));
|
||||
|
||||
screen.append(card);
|
||||
return screen;
|
||||
|
||||
@ -62,7 +62,8 @@ export function resolveToolbarActive(pageId) {
|
||||
pageId === 'device-camera-view' ||
|
||||
pageId === 'show-keys-view' ||
|
||||
pageId === 'device-session-view' ||
|
||||
pageId === 'language-view'
|
||||
pageId === 'language-view' ||
|
||||
pageId === 'app-log-view'
|
||||
) {
|
||||
return 'profile-view';
|
||||
}
|
||||
|
||||
@ -11,34 +11,100 @@ function urlBase64ToUint8Array(base64String) {
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
export async function initPwaPush({ authService }) {
|
||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
||||
export async function initPwaPush({ authService, onLog = null }) {
|
||||
const log = (entry) => {
|
||||
if (typeof onLog === 'function') onLog(entry);
|
||||
};
|
||||
|
||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||||
log({
|
||||
level: 'warn',
|
||||
source: 'web-push',
|
||||
message: 'Web Push недоступен: нет serviceWorker или PushManager',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const vapidPublicKey = window.__SHINE_WEBPUSH_VAPID_PUBLIC_KEY__ || '';
|
||||
if (!vapidPublicKey) return;
|
||||
if (!vapidPublicKey) {
|
||||
log({
|
||||
level: 'warn',
|
||||
source: 'web-push',
|
||||
message: 'Web Push отключен: не задан публичный VAPID ключ',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js');
|
||||
log({
|
||||
level: 'info',
|
||||
source: 'web-push',
|
||||
message: 'Service Worker зарегистрирован',
|
||||
details: { scope: registration.scope },
|
||||
});
|
||||
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== 'granted') return;
|
||||
if (permission !== 'granted') {
|
||||
log({
|
||||
level: 'warn',
|
||||
source: 'web-push',
|
||||
message: `Разрешение на уведомления: ${permission}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
log({
|
||||
level: 'info',
|
||||
source: 'web-push',
|
||||
message: 'Разрешение на уведомления получено',
|
||||
});
|
||||
|
||||
let sub = await registration.pushManager.getSubscription();
|
||||
let isNewSubscription = false;
|
||||
if (!sub) {
|
||||
sub = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
|
||||
});
|
||||
isNewSubscription = true;
|
||||
}
|
||||
log({
|
||||
level: 'info',
|
||||
source: 'web-push',
|
||||
message: isNewSubscription ? 'Создана новая push-подписка' : 'Найдена существующая push-подписка',
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(sub);
|
||||
if (localStorage.getItem(LS_KEY) === serialized) return;
|
||||
const prevSerialized = localStorage.getItem(LS_KEY);
|
||||
if (prevSerialized === serialized) {
|
||||
log({
|
||||
level: 'info',
|
||||
source: 'web-push',
|
||||
message: 'Push-подписка не изменилась, отправка на сервер не требуется',
|
||||
});
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(LS_KEY, serialized);
|
||||
|
||||
const json = sub.toJSON();
|
||||
const endpoint = json.endpoint || '';
|
||||
const p256dhKey = json.keys?.p256dh || '';
|
||||
const authKey = json.keys?.auth || '';
|
||||
if (!endpoint || !p256dhKey || !authKey) return;
|
||||
if (!endpoint || !p256dhKey || !authKey) {
|
||||
log({
|
||||
level: 'warn',
|
||||
source: 'web-push',
|
||||
message: 'Подписка неполная: endpoint/p256dh/auth отсутствуют',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
log({
|
||||
level: 'info',
|
||||
source: 'web-push',
|
||||
message: 'Push-токен получен, отправка на сервер',
|
||||
details: { endpoint },
|
||||
});
|
||||
|
||||
await authService.upsertPushToken({
|
||||
endpoint,
|
||||
@ -47,7 +113,17 @@ export async function initPwaPush({ authService }) {
|
||||
platform: 'web',
|
||||
userAgent: navigator.userAgent || '',
|
||||
});
|
||||
} catch {
|
||||
// silent for MVP
|
||||
log({
|
||||
level: 'info',
|
||||
source: 'web-push',
|
||||
message: 'Push-подписка успешно отправлена на сервер',
|
||||
});
|
||||
} catch (error) {
|
||||
log({
|
||||
level: 'error',
|
||||
source: 'web-push',
|
||||
message: 'Ошибка инициализации Web Push',
|
||||
details: error?.message || 'unknown',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { clearClientAuthData } from './services/key-vault.js';
|
||||
|
||||
const clone = (value) => JSON.parse(JSON.stringify(value));
|
||||
const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1';
|
||||
const MAX_APP_LOG_ENTRIES = 500;
|
||||
const INVALID_SESSION_CODES = new Set([
|
||||
'NOT_AUTHENTICATED',
|
||||
'SESSION_NOT_FOUND',
|
||||
@ -60,6 +61,7 @@ function createInitialState({ withStoredSession = true } = {}) {
|
||||
return {
|
||||
chats: clone(chatMessages),
|
||||
contacts: [],
|
||||
appLog: [],
|
||||
incomingDedup: {},
|
||||
notificationsTab: 'replies',
|
||||
pageLabelCollapsed: false,
|
||||
@ -155,6 +157,49 @@ export function setContacts(list) {
|
||||
state.contacts = Array.isArray(list) ? [...list] : [];
|
||||
}
|
||||
|
||||
function toText(value) {
|
||||
if (typeof value === 'string') return value;
|
||||
if (value == null) return '';
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function addAppLogEntry({
|
||||
level = 'info',
|
||||
source = 'ui',
|
||||
message = '',
|
||||
details = '',
|
||||
} = {}) {
|
||||
const cleanMessage = String(message || '').trim();
|
||||
if (!cleanMessage) return;
|
||||
const cleanLevel = String(level || 'info').trim().toLowerCase();
|
||||
const normalizedLevel = (cleanLevel === 'error' || cleanLevel === 'warn') ? cleanLevel : 'info';
|
||||
|
||||
state.appLog.push({
|
||||
id: `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`,
|
||||
ts: Date.now(),
|
||||
level: normalizedLevel,
|
||||
source: String(source || 'ui').trim() || 'ui',
|
||||
message: cleanMessage,
|
||||
details: toText(details),
|
||||
});
|
||||
|
||||
if (state.appLog.length > MAX_APP_LOG_ENTRIES) {
|
||||
state.appLog.splice(0, state.appLog.length - MAX_APP_LOG_ENTRIES);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAppLogEntries() {
|
||||
return [...state.appLog];
|
||||
}
|
||||
|
||||
export function clearAppLogEntries() {
|
||||
state.appLog = [];
|
||||
}
|
||||
|
||||
export function togglePageLabel() {
|
||||
state.pageLabelCollapsed = !state.pageLabelCollapsed;
|
||||
}
|
||||
|
||||
@ -64,20 +64,20 @@ public class Net_SendDirectMessage_Handler implements JsonMessageHandler {
|
||||
try {
|
||||
publicKey32 = Ed25519Util.keyFromBase64(fromUser.getDeviceKey());
|
||||
} catch (Exception e) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNPROCESSABLE, "BAD_DEVICE_KEY", "Некорректный deviceKey отправителя");
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный deviceKey отправителя");
|
||||
}
|
||||
if (!Ed25519Util.verify(packet.signedBody, packet.signature64, publicKey32)) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNPROCESSABLE, "BAD_SIGNATURE", "Подпись не прошла проверку");
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_SIGNATURE", "Подпись не прошла проверку");
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (Math.abs(now - packet.timeMs) > REPLAY_TTL_MS) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNPROCESSABLE, "BAD_TIME_WINDOW", "Время сообщения вышло за окно 15 минут");
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_TIME_WINDOW", "Время сообщения вышло за окно 15 минут");
|
||||
}
|
||||
|
||||
boolean replayOk = SignedDmReplayDAO.getInstance().registerUnique(packet.fromLogin, packet.timeMs, packet.nonce, now);
|
||||
if (!replayOk) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNPROCESSABLE, "REPLAY", "Повторное сообщение заблокировано");
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "REPLAY", "Повторное сообщение заблокировано");
|
||||
}
|
||||
|
||||
String messageId = NetIdGenerator.eventId("msg");
|
||||
|
||||
@ -8,10 +8,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import utils.config.AppConfig;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
|
||||
public final class WebPushSender {
|
||||
private static final Logger log = LoggerFactory.getLogger(WebPushSender.class);
|
||||
@ -41,8 +39,7 @@ public final class WebPushSender {
|
||||
endpoint,
|
||||
new Subscription.Keys(p256dhKey, authKey)
|
||||
);
|
||||
byte[] payloadBytes = Base64.getDecoder().decode(payloadB64);
|
||||
Notification notification = new Notification(subscription, payloadBytes);
|
||||
Notification notification = new Notification(subscription, payloadB64);
|
||||
var response = service().send(notification);
|
||||
int code = response.getStatusLine().getStatusCode();
|
||||
return code >= 200 && code < 300;
|
||||
|
||||
@ -14,6 +14,6 @@ server.info.origin=
|
||||
server.info.extraInfo=
|
||||
|
||||
# Web Push (VAPID)
|
||||
webpush.vapid.public=
|
||||
webpush.vapid.private=
|
||||
webpush.vapid.public=BOdoWZndZRaNe9kyUFsJ5-xEfFABXNKennAKg15Z7ycAwUIQ7yDV_sIWWYJCwJriN4g9oU-CyJPrn1U6lfxuDbI
|
||||
webpush.vapid.private=3hCt7XxTvLzuoxinjT5QcKRQEBnGZHXn8ZilU31RPNE
|
||||
webpush.vapid.subject=mailto:admin@shine.local
|
||||
|
||||
Loading…
Reference in New Issue
Block a user