diff --git a/deploy_shine-ui.sh b/deploy_shine-ui.sh index 65f3654..53b08f6 100755 --- a/deploy_shine-ui.sh +++ b/deploy_shine-ui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -SRC_DIR="/home/player/docker/shine-UI" +SRC_DIR="shine-UI" REMOTE_HOST="root@194.87.0.247" REMOTE_DIR="/home/user/docker/caddyFile/sites/shine-UI" BUILD_VERSION="$(date -u +%Y%m%d%H%M%S)" diff --git a/shine-UI/index.html b/shine-UI/index.html index 02c30d0..8057d1e 100644 --- a/shine-UI/index.html +++ b/shine-UI/index.html @@ -4,9 +4,9 @@ Shine UI Demo - - - + + +
@@ -15,6 +15,6 @@
- + diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index bfbe63f..5dfaad7 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -1,37 +1,46 @@ -import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260327192619'; -import { renderToolbar } from './components/toolbar.js?v=20260327192619'; -import { renderPageLabel } from './components/page-label.js?v=20260327192619'; -import { authService, authorizeSession, refreshSessions, state, togglePageLabel } from './state.js?v=20260327192619'; +import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260330001044'; +import { renderToolbar } from './components/toolbar.js?v=20260330001044'; +import { renderPageLabel } from './components/page-label.js?v=20260330001044'; +import { + authService, + authorizeSession, + isSessionInvalidError, + refreshSessions, + setSessionResetHandler, + state, + terminateCurrentSession, + togglePageLabel, +} from './state.js?v=20260330001044'; -import * as startView from './pages/start-view.js?v=20260327192619'; -import * as entrySettingsView from './pages/entry-settings-view.js?v=20260327192619'; -import * as registerView from './pages/register-view.js?v=20260327192619'; -import * as registrationPaymentView from './pages/registration-payment-view.js?v=20260327192619'; -import * as registrationKeysView from './pages/registration-keys-view.js?v=20260327192619'; -import * as topupView from './pages/topup-view.js?v=20260327192619'; -import * as loginView from './pages/login-view.js?v=20260327192619'; -import * as loginCameraView from './pages/login-camera-view.js?v=20260327192619'; -import * as loginPasswordView from './pages/login-password-view.js?v=20260327192619'; -import * as keyStorageView from './pages/key-storage-view.js?v=20260327192619'; +import * as startView from './pages/start-view.js?v=20260330001044'; +import * as entrySettingsView from './pages/entry-settings-view.js?v=20260330001044'; +import * as registerView from './pages/register-view.js?v=20260330001044'; +import * as registrationPaymentView from './pages/registration-payment-view.js?v=20260330001044'; +import * as registrationKeysView from './pages/registration-keys-view.js?v=20260330001044'; +import * as topupView from './pages/topup-view.js?v=20260330001044'; +import * as loginView from './pages/login-view.js?v=20260330001044'; +import * as loginCameraView from './pages/login-camera-view.js?v=20260330001044'; +import * as loginPasswordView from './pages/login-password-view.js?v=20260330001044'; +import * as keyStorageView from './pages/key-storage-view.js?v=20260330001044'; -import * as profileView from './pages/profile-view.js?v=20260327192619'; -import * as walletView from './pages/wallet-view.js?v=20260327192619'; -import * as settingsView from './pages/settings-view.js?v=20260327192619'; -import * as serverSettingsView from './pages/server-settings-view.js?v=20260327192619'; -import * as deviceView from './pages/device-view.js?v=20260327192619'; -import * as connectDeviceView from './pages/connect-device-view.js?v=20260327192619'; -import * as deviceQrView from './pages/device-qr-view.js?v=20260327192619'; -import * as deviceCameraView from './pages/device-camera-view.js?v=20260327192619'; -import * as showKeysView from './pages/show-keys-view.js?v=20260327192619'; -import * as deviceSessionView from './pages/device-session-view.js?v=20260327192619'; -import * as languageView from './pages/language-view.js?v=20260327192619'; -import * as messagesList from './pages/messages-list.js?v=20260327192619'; -import * as contactSearchView from './pages/contact-search-view.js?v=20260327192619'; -import * as chatView from './pages/chat-view.js?v=20260327192619'; -import * as channelsList from './pages/channels-list.js?v=20260327192619'; -import * as channelView from './pages/channel-view.js?v=20260327192619'; -import * as networkView from './pages/network-view.js?v=20260327192619'; -import * as notificationsView from './pages/notifications-view.js?v=20260327192619'; +import * as profileView from './pages/profile-view.js?v=20260330001044'; +import * as walletView from './pages/wallet-view.js?v=20260330001044'; +import * as settingsView from './pages/settings-view.js?v=20260330001044'; +import * as serverSettingsView from './pages/server-settings-view.js?v=20260330001044'; +import * as deviceView from './pages/device-view.js?v=20260330001044'; +import * as connectDeviceView from './pages/connect-device-view.js?v=20260330001044'; +import * as deviceQrView from './pages/device-qr-view.js?v=20260330001044'; +import * as deviceCameraView from './pages/device-camera-view.js?v=20260330001044'; +import * as showKeysView from './pages/show-keys-view.js?v=20260330001044'; +import * as deviceSessionView from './pages/device-session-view.js?v=20260330001044'; +import * as languageView from './pages/language-view.js?v=20260330001044'; +import * as messagesList from './pages/messages-list.js?v=20260330001044'; +import * as contactSearchView from './pages/contact-search-view.js?v=20260330001044'; +import * as chatView from './pages/chat-view.js?v=20260330001044'; +import * as channelsList from './pages/channels-list.js?v=20260330001044'; +import * as channelView from './pages/channel-view.js?v=20260330001044'; +import * as networkView from './pages/network-view.js?v=20260330001044'; +import * as notificationsView from './pages/notifications-view.js?v=20260330001044'; const routes = { 'start-view': startView, @@ -120,12 +129,19 @@ async function tryAutoLogin() { const resumed = await authService.resumeSession(state.session.login, state.session.sessionId); authorizeSession(resumed); await refreshSessions(); - } catch { - // silent fallback to auth screens + } catch (error) { + if (isSessionInvalidError(error)) { + await terminateCurrentSession({ + infoMessage: 'Сессия на этом устройстве уже завершена. Выполните вход заново.', + }); + } } } async function init() { + setSessionResetHandler(() => { + navigate('start-view'); + }); await tryAutoLogin(); if (!window.location.hash) { diff --git a/shine-UI/js/components/toolbar.js b/shine-UI/js/components/toolbar.js index ec04845..7aa2dda 100644 --- a/shine-UI/js/components/toolbar.js +++ b/shine-UI/js/components/toolbar.js @@ -1,4 +1,4 @@ -import { resolveToolbarActive } from '../router.js?v=20260327192619'; +import { resolveToolbarActive } from '../router.js?v=20260330001044'; const ITEMS = [ { pageId: 'messages-list', label: 'Личные сообщения', icon: '💬' }, diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js index 1235efb..6a7ca1c 100644 --- a/shine-UI/js/pages/channel-view.js +++ b/shine-UI/js/pages/channel-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { channelPosts, channels } from '../mock-data.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { channelPosts, channels } from '../mock-data.js?v=20260330001044'; export const pageMeta = { id: 'channel-view', title: 'Канал' }; diff --git a/shine-UI/js/pages/channels-list.js b/shine-UI/js/pages/channels-list.js index 5f440b5..c8768a0 100644 --- a/shine-UI/js/pages/channels-list.js +++ b/shine-UI/js/pages/channels-list.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { channels } from '../mock-data.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { channels } from '../mock-data.js?v=20260330001044'; export const pageMeta = { id: 'channels-list', title: 'Каналы' }; diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index ed55b16..5f77b7b 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { directMessages } from '../mock-data.js?v=20260327192619'; -import { addChatMessage, getChatMessages } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { directMessages } from '../mock-data.js?v=20260330001044'; +import { addChatMessage, getChatMessages } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'chat-view', title: 'Чат' }; diff --git a/shine-UI/js/pages/connect-device-view.js b/shine-UI/js/pages/connect-device-view.js index 4096559..d560ca2 100644 --- a/shine-UI/js/pages/connect-device-view.js +++ b/shine-UI/js/pages/connect-device-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'connect-device-view', title: 'Подключить устройство' }; diff --git a/shine-UI/js/pages/contact-search-view.js b/shine-UI/js/pages/contact-search-view.js index e15f07f..5b1f695 100644 --- a/shine-UI/js/pages/contact-search-view.js +++ b/shine-UI/js/pages/contact-search-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { contactDirectory, directMessages } from '../mock-data.js?v=20260327192619'; -import { ensureChat } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { contactDirectory, directMessages } from '../mock-data.js?v=20260330001044'; +import { ensureChat } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'contact-search-view', title: 'Поиск контактов' }; diff --git a/shine-UI/js/pages/device-camera-view.js b/shine-UI/js/pages/device-camera-view.js index 0fbb87e..57a7f76 100644 --- a/shine-UI/js/pages/device-camera-view.js +++ b/shine-UI/js/pages/device-camera-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; export const pageMeta = { id: 'device-camera-view', title: 'Подключить через камеру' }; diff --git a/shine-UI/js/pages/device-qr-view.js b/shine-UI/js/pages/device-qr-view.js index 0d71173..f054ac1 100644 --- a/shine-UI/js/pages/device-qr-view.js +++ b/shine-UI/js/pages/device-qr-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { profile } from '../mock-data.js?v=20260327192619'; -import { state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { profile } from '../mock-data.js?v=20260330001044'; +import { state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'device-qr-view', title: 'Показать QR-код' }; diff --git a/shine-UI/js/pages/device-session-view.js b/shine-UI/js/pages/device-session-view.js index 6348c27..ff6d51c 100644 --- a/shine-UI/js/pages/device-session-view.js +++ b/shine-UI/js/pages/device-session-view.js @@ -1,5 +1,12 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { authService, refreshSessions, setAuthError, state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { + authService, + isSessionInvalidError, + refreshSessions, + setAuthError, + state, + terminateCurrentSession, +} from '../state.js?v=20260330001044'; export const pageMeta = { id: 'device-session-view', title: 'Сеанс устройства' }; @@ -51,11 +58,39 @@ export function render({ navigate, route }) { actionBtn.textContent = 'Завершить сеанс'; actionBtn.addEventListener('click', async () => { + const isCurrentSession = session.sessionId === state.session.sessionId; + const confirmed = window.confirm( + isCurrentSession ? 'Хотите завершить текущую сессию?' : 'Хотите завершить этот сеанс?', + ); + if (!confirmed) return; + try { await authService.closeSession(session.sessionId); + } catch (error) { + if (!isSessionInvalidError(error)) { + setAuthError(error.message); + window.alert(error.message); + return; + } + } + + if (isCurrentSession) { + await terminateCurrentSession({ + infoMessage: 'Текущая сессия завершена, данные на устройстве очищены.', + }); + return; + } + + try { await refreshSessions(); navigate('device-view'); } catch (error) { + if (isSessionInvalidError(error)) { + await terminateCurrentSession({ + infoMessage: 'Сессия на этом устройстве уже завершена. Выполните вход заново.', + }); + return; + } setAuthError(error.message); window.alert(error.message); } diff --git a/shine-UI/js/pages/device-view.js b/shine-UI/js/pages/device-view.js index 6cc9636..9d82ffe 100644 --- a/shine-UI/js/pages/device-view.js +++ b/shine-UI/js/pages/device-view.js @@ -1,11 +1,13 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; import { + authService, + isSessionInvalidError, refreshSessions, setAuthError, setAuthInfo, state, terminateCurrentSession, -} from '../state.js?v=20260327192619'; +} from '../state.js?v=20260330001044'; export const pageMeta = { id: 'device-view', title: 'Устройства' }; @@ -83,9 +85,23 @@ export function render({ navigate }) { endCurrentSessionBtn.className = 'text-btn'; endCurrentSessionBtn.type = 'button'; endCurrentSessionBtn.textContent = 'Завершить текущую сессию'; - endCurrentSessionBtn.addEventListener('click', () => { - terminateCurrentSession(); - navigate('start-view'); + endCurrentSessionBtn.addEventListener('click', async () => { + const confirmed = window.confirm('Хотите завершить текущую сессию?'); + if (!confirmed) return; + + try { + await authService.closeSession(state.session.sessionId); + } catch (error) { + if (!isSessionInvalidError(error)) { + setAuthError(error.message); + window.alert(error.message); + return; + } + } + + await terminateCurrentSession({ + infoMessage: 'Текущая сессия завершена, данные на устройстве очищены.', + }); }); currentMenu.append(endCurrentSessionBtn); @@ -113,6 +129,12 @@ export function render({ navigate }) { buildList(); setAuthInfo('Список сессий обновлён.'); } catch (error) { + if (isSessionInvalidError(error)) { + await terminateCurrentSession({ + infoMessage: 'Сессия на этом устройстве уже завершена. Выполните вход заново.', + }); + return; + } setAuthError(error.message); window.alert(error.message); } diff --git a/shine-UI/js/pages/entry-settings-view.js b/shine-UI/js/pages/entry-settings-view.js index 6942022..ce5394e 100644 --- a/shine-UI/js/pages/entry-settings-view.js +++ b/shine-UI/js/pages/entry-settings-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false }; diff --git a/shine-UI/js/pages/key-storage-view.js b/shine-UI/js/pages/key-storage-view.js index 6c5e8a7..784b2e8 100644 --- a/shine-UI/js/pages/key-storage-view.js +++ b/shine-UI/js/pages/key-storage-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { authorizeSession, state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { authorizeSession, state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'key-storage-view', title: 'Какие ключи сохранить', showAppChrome: false }; diff --git a/shine-UI/js/pages/language-view.js b/shine-UI/js/pages/language-view.js index 8aa2c8a..cdc53cf 100644 --- a/shine-UI/js/pages/language-view.js +++ b/shine-UI/js/pages/language-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'language-view', title: 'Язык' }; diff --git a/shine-UI/js/pages/login-camera-view.js b/shine-UI/js/pages/login-camera-view.js index 043a747..ba09bae 100644 --- a/shine-UI/js/pages/login-camera-view.js +++ b/shine-UI/js/pages/login-camera-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; export const pageMeta = { id: 'login-camera-view', title: 'Войти по камере', showAppChrome: false }; diff --git a/shine-UI/js/pages/login-password-view.js b/shine-UI/js/pages/login-password-view.js index adf79ca..3af3451 100644 --- a/shine-UI/js/pages/login-password-view.js +++ b/shine-UI/js/pages/login-password-view.js @@ -1,14 +1,11 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; import { authService, - authorizeSession, clearAuthMessages, - refreshSessions, setAuthBusy, setAuthError, - setAuthInfo, state, -} from '../state.js?v=20260327192619'; +} from '../state.js?v=20260330001044'; export const pageMeta = { id: 'login-password-view', title: 'Войти по логину', showAppChrome: false }; @@ -75,11 +72,14 @@ export function render({ navigate }) { try { await authService.reconnect(state.entrySettings.shineServer); const result = await authService.createSessionForExistingUser(state.loginDraft.login, state.loginDraft.password); - await authService.persistSessionMaterial(state.loginDraft.login, result.sessionMaterial); - authorizeSession(result); - await refreshSessions(); - setAuthInfo('Успешный вход выполнен.'); - navigate('profile-view'); + state.registrationDraft.flowType = 'login'; + state.registrationDraft.login = result.login; + state.registrationDraft.password = state.loginDraft.password; + state.registrationDraft.sessionId = result.sessionId; + state.registrationDraft.storagePwd = result.storagePwd; + state.registrationDraft.pendingKeyBundle = result.keyBundle; + state.registrationDraft.pendingSessionMaterial = result.sessionMaterial; + navigate('registration-keys-view'); } catch (error) { setAuthError(error.message); window.alert(error.message); diff --git a/shine-UI/js/pages/login-view.js b/shine-UI/js/pages/login-view.js index cb87553..e152d41 100644 --- a/shine-UI/js/pages/login-view.js +++ b/shine-UI/js/pages/login-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; export const pageMeta = { id: 'login-view', title: 'Войти', showAppChrome: false }; diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js index c19fb33..837a7dc 100644 --- a/shine-UI/js/pages/messages-list.js +++ b/shine-UI/js/pages/messages-list.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { directMessages } from '../mock-data.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { directMessages } from '../mock-data.js?v=20260330001044'; export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' }; diff --git a/shine-UI/js/pages/network-view.js b/shine-UI/js/pages/network-view.js index d26e90a..0418844 100644 --- a/shine-UI/js/pages/network-view.js +++ b/shine-UI/js/pages/network-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { networkGraph } from '../mock-data.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { networkGraph } from '../mock-data.js?v=20260330001044'; export const pageMeta = { id: 'network-view', title: 'Связи' }; diff --git a/shine-UI/js/pages/notifications-view.js b/shine-UI/js/pages/notifications-view.js index 6c24159..33cee7c 100644 --- a/shine-UI/js/pages/notifications-view.js +++ b/shine-UI/js/pages/notifications-view.js @@ -1,6 +1,6 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { notifications } from '../mock-data.js?v=20260327192619'; -import { state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { notifications } from '../mock-data.js?v=20260330001044'; +import { state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'notifications-view', title: 'Уведомления' }; diff --git a/shine-UI/js/pages/profile-view.js b/shine-UI/js/pages/profile-view.js index b50f613..6fc256f 100644 --- a/shine-UI/js/pages/profile-view.js +++ b/shine-UI/js/pages/profile-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { profile } from '../mock-data.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { profile } from '../mock-data.js?v=20260330001044'; export const pageMeta = { id: 'profile-view', title: 'Профиль' }; diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index 64cfa70..a252081 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { authService, state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { authService, clearAuthMessages, state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; diff --git a/shine-UI/js/pages/registration-keys-view.js b/shine-UI/js/pages/registration-keys-view.js index efe9314..b55f1eb 100644 --- a/shine-UI/js/pages/registration-keys-view.js +++ b/shine-UI/js/pages/registration-keys-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; import { authService, authorizeSession, @@ -6,7 +6,7 @@ import { setAuthError, setAuthInfo, state, -} from '../state.js?v=20260327192619'; +} from '../state.js?v=20260330001044'; export const pageMeta = { id: 'registration-keys-view', title: 'Сохранение ключей', showAppChrome: false }; @@ -14,6 +14,7 @@ export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; + const isLoginFlow = state.registrationDraft.flowType === 'login'; const normalizedLogin = (state.registrationDraft.login || '').trim(); const displayLogin = normalizedLogin || '@new.user'; @@ -22,7 +23,9 @@ export function render({ navigate }) { const title = document.createElement('p'); title.className = 'auth-copy'; - title.textContent = `Отлично, логин ${displayLogin} зарегистрирован.`; + title.textContent = isLoginFlow + ? `Вход выполнен для логина ${displayLogin}.` + : `Отлично, логин ${displayLogin} зарегистрирован.`; const question = document.createElement('p'); question.className = 'auth-copy'; @@ -43,17 +46,17 @@ export function render({ navigate }) { const rootRow = document.createElement('label'); rootRow.className = 'checkbox-row'; - rootRow.innerHTML = ` root key`; + rootRow.append(rootToggle, document.createTextNode('root key')); const blockchainRow = document.createElement('label'); blockchainRow.className = 'checkbox-row'; - blockchainRow.innerHTML = ` blockchain key`; + blockchainRow.append(blockchainToggle, document.createTextNode('blockchain.key')); const deviceRow = document.createElement('label'); deviceRow.className = 'checkbox-row'; deviceRow.append(deviceToggle, document.createTextNode('device key (всегда)')); - card.append(title, question, rootRow, deviceRow, blockchainRow); + card.append(title, question, rootRow, blockchainRow, deviceRow); const actions = document.createElement('div'); actions.className = 'auth-footer-actions'; @@ -91,13 +94,30 @@ export function render({ navigate }) { state.registrationDraft.pendingSessionMaterial, ); + if (!state.keyStorage.saveRoot && state.registrationDraft.pendingKeyBundle) { + state.registrationDraft.pendingKeyBundle.rootPair = null; + } + if (!state.keyStorage.saveBlockchain && state.registrationDraft.pendingKeyBundle) { + state.registrationDraft.pendingKeyBundle.blockchainPair = null; + } + authorizeSession({ login: state.registrationDraft.login, sessionId: state.registrationDraft.sessionId, storagePwd: state.registrationDraft.storagePwd, }); + + state.loginDraft.login = state.registrationDraft.login; + state.loginDraft.password = ''; + state.registrationDraft.flowType = ''; + state.registrationDraft.password = ''; + state.registrationDraft.storagePwd = ''; + state.registrationDraft.sessionId = ''; + state.registrationDraft.pendingKeyBundle = null; + state.registrationDraft.pendingSessionMaterial = null; + await refreshSessions(); - setAuthInfo('Ключи сохранены, регистрация завершена.'); + setAuthInfo(isLoginFlow ? 'Ключи сохранены, вход завершён.' : 'Ключи сохранены, регистрация завершена.'); navigate('profile-view'); } catch (error) { setAuthError(error.message); diff --git a/shine-UI/js/pages/registration-payment-view.js b/shine-UI/js/pages/registration-payment-view.js index bd70c98..bb4a3ac 100644 --- a/shine-UI/js/pages/registration-payment-view.js +++ b/shine-UI/js/pages/registration-payment-view.js @@ -1,11 +1,11 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; import { authService, refreshRegistrationBalance, setAuthError, setAuthInfo, state, -} from '../state.js?v=20260327192619'; +} from '../state.js?v=20260330001044'; export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false }; @@ -79,6 +79,7 @@ export function render({ navigate }) { await authService.reconnect(state.entrySettings.shineServer); const result = await authService.registerUser(state.registrationDraft.login, state.registrationDraft.password); + state.registrationDraft.flowType = 'registration'; state.registrationDraft.sessionId = result.sessionId; state.registrationDraft.storagePwd = result.storagePwd; state.registrationDraft.pendingKeyBundle = result.keyBundle; diff --git a/shine-UI/js/pages/server-settings-view.js b/shine-UI/js/pages/server-settings-view.js index 3bc5070..0101e34 100644 --- a/shine-UI/js/pages/server-settings-view.js +++ b/shine-UI/js/pages/server-settings-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { checkServerAvailability, saveEntrySettings, state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' }; diff --git a/shine-UI/js/pages/settings-view.js b/shine-UI/js/pages/settings-view.js index 700475b..41f59e2 100644 --- a/shine-UI/js/pages/settings-view.js +++ b/shine-UI/js/pages/settings-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; export const pageMeta = { id: 'settings-view', title: 'Настройки' }; diff --git a/shine-UI/js/pages/show-keys-view.js b/shine-UI/js/pages/show-keys-view.js index 3b1e5fc..fd9fb16 100644 --- a/shine-UI/js/pages/show-keys-view.js +++ b/shine-UI/js/pages/show-keys-view.js @@ -1,32 +1,25 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { state } from '../state.js?v=20260330001044'; +import { loadEncryptedUserSecrets } from '../services/key-vault.js?v=20260330001044'; export const pageMeta = { id: 'show-keys-view', title: 'Показать ключи' }; -function randomKey(length = 44) { - const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789'; - let result = ''; - for (let i = 0; i < length; i += 1) { - result += chars[Math.floor(Math.random() * chars.length)]; - } - return result; -} - export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; - const keys = { - root: randomKey(), - blockchain: randomKey(), - device: randomKey(), - }; - const visible = { root: false, blockchain: false, device: false, }; + const keys = { + root: '', + blockchain: '', + device: '', + }; + screen.append( renderHeader({ title: 'Показать ключи', @@ -37,6 +30,11 @@ export function render({ navigate }) { const card = document.createElement('div'); card.className = 'card stack'; + const status = document.createElement('p'); + status.className = 'meta-muted'; + status.textContent = 'Загружаем сохранённые ключи...'; + card.append(status); + const renderField = (id, label) => { const row = document.createElement('div'); row.className = 'key-card stack'; @@ -50,79 +48,80 @@ export function render({ navigate }) { return row; }; - card.append(renderField('root', 'root key'), renderField('blockchain', 'blockchain key'), renderField('device', 'device key')); + card.append( + renderField('root', 'root key'), + renderField('blockchain', 'blockchain.key'), + renderField('device', 'device key'), + ); + + const setMissingState = (id) => { + const valueEl = card.querySelector(`[data-value="${id}"]`); + const btnEl = card.querySelector(`[data-toggle="${id}"]`); + valueEl.textContent = 'нет данных'; + btnEl.disabled = true; + btnEl.textContent = 'Нет'; + }; const updateField = (id) => { const valueEl = card.querySelector(`[data-value="${id}"]`); const btnEl = card.querySelector(`[data-toggle="${id}"]`); + if (!keys[id]) { + setMissingState(id); + return; + } valueEl.textContent = visible[id] ? keys[id] : '*****'; + btnEl.disabled = false; btnEl.textContent = visible[id] ? 'Скрыть' : 'Показать'; }; card.querySelectorAll('[data-toggle]').forEach((button) => { button.addEventListener('click', () => { const { toggle } = button.dataset; + if (!keys[toggle]) return; visible[toggle] = !visible[toggle]; updateField(toggle); }); }); + ['root', 'blockchain', 'device'].forEach((id) => updateField(id)); + const actions = document.createElement('div'); actions.className = 'auth-footer-actions'; - actions.innerHTML = ` - - - `; - const confirmModal = document.createElement('div'); - confirmModal.className = 'modal-shell'; - confirmModal.hidden = true; - confirmModal.innerHTML = ` - - - `; + const closeButton = document.createElement('button'); + closeButton.className = 'ghost-btn'; + closeButton.type = 'button'; + closeButton.textContent = 'Назад'; + closeButton.addEventListener('click', () => navigate('device-view')); + actions.append(closeButton); - const openModal = () => { - confirmModal.hidden = false; - confirmModal.querySelector('.modal-dialog').focus(); - }; + (async () => { + try { + if (!state.session.login || !state.session.storagePwdInMemory) { + throw new Error('Нет активной сессии для чтения ключей'); + } - const closeModal = () => { - confirmModal.hidden = true; - }; + const savedKeys = await loadEncryptedUserSecrets( + state.session.login, + state.session.storagePwdInMemory, + ); - actions.querySelector('#save-keys').addEventListener('click', openModal); - actions.querySelector('#cancel-keys').addEventListener('click', () => navigate('device-view')); + keys.root = savedKeys.rootKey || ''; + keys.blockchain = savedKeys.blockchainKey || ''; + keys.device = savedKeys.deviceKey || ''; - confirmModal.querySelector('#confirm-keys-ok').addEventListener('click', () => { - keys.root = randomKey(); - keys.blockchain = randomKey(); - keys.device = randomKey(); - updateField('root'); - updateField('blockchain'); - updateField('device'); - closeModal(); - }); - - confirmModal.addEventListener('click', (event) => { - const target = event.target; - if (target instanceof HTMLElement && target.dataset.close === 'true') { - closeModal(); + if (keys.root || keys.blockchain || keys.device) { + status.textContent = 'Показаны только ключи, сохранённые на этом устройстве.'; + } else { + status.textContent = 'На этом устройстве нет сохранённых ключей.'; + } + } catch (error) { + status.textContent = 'На этом устройстве нет сохранённых ключей.'; } - }); - confirmModal.addEventListener('keydown', (event) => { - if (event.key === 'Escape') { - closeModal(); - } - }); + ['root', 'blockchain', 'device'].forEach((id) => updateField(id)); + })(); - screen.append(card, actions, confirmModal); + screen.append(card, actions); return screen; } diff --git a/shine-UI/js/pages/start-view.js b/shine-UI/js/pages/start-view.js index eb2c0c4..15f9f22 100644 --- a/shine-UI/js/pages/start-view.js +++ b/shine-UI/js/pages/start-view.js @@ -1,4 +1,4 @@ -import { clearStartHint, state } from '../state.js?v=20260327192619'; +import { clearStartHint, state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'start-view', title: 'Старт', showAppChrome: false }; diff --git a/shine-UI/js/pages/topup-view.js b/shine-UI/js/pages/topup-view.js index 3ae049d..7dd9f74 100644 --- a/shine-UI/js/pages/topup-view.js +++ b/shine-UI/js/pages/topup-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { state } from '../state.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { state } from '../state.js?v=20260330001044'; export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false }; diff --git a/shine-UI/js/pages/wallet-view.js b/shine-UI/js/pages/wallet-view.js index ba1bad6..ab9b0a7 100644 --- a/shine-UI/js/pages/wallet-view.js +++ b/shine-UI/js/pages/wallet-view.js @@ -1,5 +1,5 @@ -import { renderHeader } from '../components/header.js?v=20260327192619'; -import { wallet } from '../mock-data.js?v=20260327192619'; +import { renderHeader } from '../components/header.js?v=20260330001044'; +import { wallet } from '../mock-data.js?v=20260330001044'; export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' }; diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index 2908cb9..93d477f 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -1,4 +1,4 @@ -import { WsJsonClient } from './ws-client.js?v=20260327192619'; +import { WsJsonClient } from './ws-client.js?v=20260330001044'; import { deriveEd25519FromPassword, exportEd25519PublicKeyB64, @@ -7,8 +7,8 @@ import { importPkcs8Ed25519, randomBase64, signBase64, -} from './crypto-utils.js?v=20260327192619'; -import { loadSessionMaterial, saveEncryptedUserSecrets, saveSessionMaterial } from './key-vault.js?v=20260327192619'; +} from './crypto-utils.js?v=20260330001044'; +import { loadSessionMaterial, saveEncryptedUserSecrets, saveSessionMaterial } from './key-vault.js?v=20260330001044'; const BCH_SUFFIX = '001'; @@ -25,7 +25,11 @@ function normalizeServerUrl(url) { function opError(op, response) { const message = response?.payload?.message || response?.message || 'Неизвестная ошибка сервера'; const code = response?.payload?.code || response?.code || 'UNKNOWN'; - return new Error(`${op}: ${message} (${code})`); + const error = new Error(`${op}: ${message} (${code})`); + error.op = op; + error.code = code; + error.status = response?.status || 0; + return error; } function makeClientInfo() { @@ -145,7 +149,8 @@ export class AuthService { if (!user.exists) throw new Error('Пользователь не найден'); const keyBundle = await this.derivePasswordKeyBundle(password); - return this.createAuthSession(cleanLogin, keyBundle); + const session = await this.createAuthSession(cleanLogin, keyBundle); + return { ...session, keyBundle }; } async persistSelectedKeys(login, storagePwd, keyBundle, saveOptions = { saveRoot: true, saveBlockchain: true }) { diff --git a/shine-UI/js/services/key-vault.js b/shine-UI/js/services/key-vault.js index 0447814..e988a28 100644 --- a/shine-UI/js/services/key-vault.js +++ b/shine-UI/js/services/key-vault.js @@ -1,7 +1,7 @@ import { decryptJsonWithStoragePwd, encryptJsonWithStoragePwd, -} from './crypto-utils.js?v=20260327192619'; +} from './crypto-utils.js?v=20260330001044'; const DB_NAME = 'shine-ui-auth'; const DB_VERSION = 1; @@ -76,3 +76,12 @@ export async function saveSessionMaterial(login, material) { export async function loadSessionMaterial(login) { return get(STORE_SESSIONS, login); } + +export async function clearClientAuthData() { + await new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(DB_NAME); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error || new Error('Не удалось очистить IndexedDB')); + request.onblocked = () => reject(new Error('Очистка IndexedDB заблокирована открытыми соединениями')); + }); +} diff --git a/shine-UI/js/state.js b/shine-UI/js/state.js index c905e60..46dd425 100644 --- a/shine-UI/js/state.js +++ b/shine-UI/js/state.js @@ -1,8 +1,15 @@ -import { chatMessages, wallet } from './mock-data.js?v=20260327192619'; -import { AuthService } from './services/auth-service.js?v=20260327192619'; +import { chatMessages, wallet } from './mock-data.js?v=20260330001044'; +import { AuthService } from './services/auth-service.js?v=20260330001044'; +import { clearClientAuthData } from './services/key-vault.js?v=20260330001044'; const clone = (value) => JSON.parse(JSON.stringify(value)); const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1'; +const INVALID_SESSION_CODES = new Set([ + 'NOT_AUTHENTICATED', + 'SESSION_NOT_FOUND', + 'SESSION_KEY_NOT_ACTUAL', + 'SESSION_OF_ANOTHER_USER', +]); function loadStoredSession() { try { @@ -30,68 +37,73 @@ function clearStoredSession() { } } -const storedSession = loadStoredSession(); - -export const state = { - chats: clone(chatMessages), - notificationsTab: 'replies', - pageLabelCollapsed: false, - session: { - isAuthorized: false, - login: storedSession?.login || '', - sessionId: storedSession?.sessionId || '', - storagePwdInMemory: '', - }, - startHint: '', - entrySettings: { - language: 'ru', - solanaServer: 'https://api.mainnet-beta.solana.com', - shineServer: 'wss://shineup.me/ws', - arweaveServer: 'https://arweave.net', - statuses: { - solanaServer: 'idle', - shineServer: 'idle', - arweaveServer: 'idle', +function createInitialState({ withStoredSession = true } = {}) { + const storedSession = withStoredSession ? loadStoredSession() : null; + return { + chats: clone(chatMessages), + notificationsTab: 'replies', + pageLabelCollapsed: false, + session: { + isAuthorized: false, + login: storedSession?.login || '', + sessionId: storedSession?.sessionId || '', + storagePwdInMemory: '', }, - }, - registrationDraft: { - login: '', - password: '', - sessionId: '', - storagePwd: '', - pendingKeyBundle: null, - pendingSessionMaterial: null, - }, - loginDraft: { - login: storedSession?.login || '', - password: '', - }, - registrationPayment: { - walletAddress: wallet.publicAddress, - balanceSOL: '0.0068', - }, - keyStorage: { - rootKey: 'Ключ root хранится в зашифрованном виде', - blockchainKey: 'Ключ blockchain хранится в зашифрованном виде', - deviceKey: 'Ключ device хранится в зашифрованном виде', - saveRoot: true, - saveBlockchain: true, - saveDevice: true, - }, - deviceConnect: { - root: true, - blockchain: true, - device: true, - }, - authUi: { - busy: false, - error: '', - info: '', - }, - sessions: [], -}; + startHint: '', + entrySettings: { + language: 'ru', + solanaServer: 'https://api.mainnet-beta.solana.com', + shineServer: 'wss://shineup.me/ws', + arweaveServer: 'https://arweave.net', + statuses: { + solanaServer: 'idle', + shineServer: 'idle', + arweaveServer: 'idle', + }, + }, + registrationDraft: { + flowType: '', + login: '', + password: '', + sessionId: '', + storagePwd: '', + pendingKeyBundle: null, + pendingSessionMaterial: null, + }, + loginDraft: { + login: storedSession?.login || '', + password: '', + }, + registrationPayment: { + walletAddress: wallet.publicAddress, + balanceSOL: '0.0068', + }, + keyStorage: { + rootKey: 'Ключ root хранится в зашифрованном виде', + blockchainKey: 'Ключ blockchain хранится в зашифрованном виде', + deviceKey: 'Ключ device хранится в зашифрованном виде', + saveRoot: false, + saveBlockchain: true, + saveDevice: true, + }, + deviceConnect: { + root: true, + blockchain: true, + device: true, + }, + authUi: { + busy: false, + error: '', + info: '', + }, + sessions: [], + }; +} + +export const state = createInitialState(); export const authService = new AuthService(state.entrySettings.shineServer); +let onSessionReset = null; export function getChatMessages(chatId) { if (!state.chats[chatId]) { @@ -170,19 +182,49 @@ export function authorizeSession({ login, sessionId, storagePwd }) { state.startHint = ''; } +export function setSessionResetHandler(handler) { + onSessionReset = typeof handler === 'function' ? handler : null; +} + +export function isSessionInvalidError(error) { + return INVALID_SESSION_CODES.has(error?.code); +} + export async function refreshSessions() { state.sessions = await authService.listSessions(); return state.sessions; } -export function terminateCurrentSession() { - state.session.isAuthorized = false; - state.session.login = ''; - state.session.sessionId = ''; - state.session.storagePwdInMemory = ''; - state.sessions = []; +function resetStateForSignedOut() { + const next = createInitialState({ withStoredSession: false }); + state.chats = next.chats; + state.notificationsTab = next.notificationsTab; + state.session = next.session; + state.startHint = next.startHint; + state.registrationDraft = next.registrationDraft; + state.loginDraft = next.loginDraft; + state.registrationPayment = next.registrationPayment; + state.keyStorage = next.keyStorage; + state.deviceConnect = next.deviceConnect; + state.authUi = next.authUi; + state.sessions = next.sessions; +} + +export async function terminateCurrentSession({ infoMessage = '' } = {}) { clearStoredSession(); - state.startHint = ''; + resetStateForSignedOut(); + authService.close(); + try { + await clearClientAuthData(); + } catch { + // ignore cleanup errors in prototype mode + } + if (infoMessage) { + state.startHint = infoMessage; + } + if (onSessionReset) { + onSessionReset(); + } } export function refreshRegistrationBalance() {