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