diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 9d2ca6c..bfbe63f 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -1,7 +1,7 @@ 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 { state, togglePageLabel } from './state.js?v=20260327192619'; +import { authService, authorizeSession, refreshSessions, state, togglePageLabel } from './state.js?v=20260327192619'; import * as startView from './pages/start-view.js?v=20260327192619'; import * as entrySettingsView from './pages/entry-settings-view.js?v=20260327192619'; @@ -113,10 +113,28 @@ function renderApp() { } } -if (!window.location.hash) { - navigate(state.session.isAuthorized ? 'profile-view' : 'start-view'); -} else { - renderApp(); +async function tryAutoLogin() { + if (!state.session.login || !state.session.sessionId) return; + try { + await authService.reconnect(state.entrySettings.shineServer); + const resumed = await authService.resumeSession(state.session.login, state.session.sessionId); + authorizeSession(resumed); + await refreshSessions(); + } catch { + // silent fallback to auth screens + } } -window.addEventListener('hashchange', renderApp); +async function init() { + await tryAutoLogin(); + + if (!window.location.hash) { + navigate(state.session.isAuthorized ? 'profile-view' : 'start-view'); + } else { + renderApp(); + } + + window.addEventListener('hashchange', renderApp); +} + +init(); diff --git a/shine-UI/js/pages/login-password-view.js b/shine-UI/js/pages/login-password-view.js index c8c112e..adf79ca 100644 --- a/shine-UI/js/pages/login-password-view.js +++ b/shine-UI/js/pages/login-password-view.js @@ -75,6 +75,7 @@ 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('Успешный вход выполнен.'); diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index b4c702f..2908cb9 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -4,10 +4,11 @@ import { exportEd25519PublicKeyB64, exportPkcs8B64, generateEd25519Pair, + importPkcs8Ed25519, randomBase64, signBase64, } from './crypto-utils.js?v=20260327192619'; -import { saveEncryptedUserSecrets, saveSessionMaterial } from './key-vault.js?v=20260327192619'; +import { loadSessionMaterial, saveEncryptedUserSecrets, saveSessionMaterial } from './key-vault.js?v=20260327192619'; const BCH_SUFFIX = '001'; @@ -158,6 +159,48 @@ export class AuthService { await saveSessionMaterial(login, sessionMaterial); } + + async resumeSession(login, preferredSessionId = '') { + const cleanLogin = (login || '').trim(); + if (!cleanLogin) throw new Error('Нет login для авто-входа'); + + const sessionMaterial = await loadSessionMaterial(cleanLogin); + if (!sessionMaterial?.sessionId || !sessionMaterial?.sessionKey || !sessionMaterial?.sessionPrivPkcs8) { + throw new Error('На устройстве нет сохраненного ключа сессии'); + } + + const targetSessionId = preferredSessionId || sessionMaterial.sessionId; + const privateKey = await importPkcs8Ed25519(sessionMaterial.sessionPrivPkcs8); + + const challengeResp = await this.ws.request('SessionChallenge', { sessionId: targetSessionId }); + if (challengeResp.status !== 200) throw opError('SessionChallenge', challengeResp); + + const nonce = challengeResp?.payload?.nonce; + if (!nonce) throw new Error('SessionChallenge: не вернулся nonce'); + + const timeMs = Date.now(); + const preimage = `SESSION_LOGIN:${targetSessionId}:${timeMs}:${nonce}`; + const signatureB64 = await signBase64(privateKey, preimage); + + const loginResp = await this.ws.request('SessionLogin', { + sessionId: targetSessionId, + sessionKey: sessionMaterial.sessionKey, + timeMs, + signatureB64, + clientInfo: makeClientInfo(), + }); + if (loginResp.status !== 200) throw opError('SessionLogin', loginResp); + + const storagePwd = loginResp?.payload?.storagePwd; + if (!storagePwd) throw new Error('SessionLogin: не вернулся storagePwd'); + + return { + login: cleanLogin, + sessionId: targetSessionId, + storagePwd, + }; + } + async listSessions() { const response = await this.ws.request('ListSessions', {}); if (response.status !== 200) throw opError('ListSessions', response);