Merge pull request #5 from ai5590/codex/connect-ui-client-to-server-for-authentication-8njihj

Add AuthService, WS client and key-vault; implement session-based auth flow and update auth UI/pages
This commit is contained in:
ai5590 2026-03-30 02:25:59 +03:00 committed by GitHub
commit 089146a137
3 changed files with 69 additions and 7 deletions

View File

@ -1,7 +1,7 @@
import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260327192619'; import { navigate, getRoute, PRE_AUTH_PAGES } from './router.js?v=20260327192619';
import { renderToolbar } from './components/toolbar.js?v=20260327192619'; import { renderToolbar } from './components/toolbar.js?v=20260327192619';
import { renderPageLabel } from './components/page-label.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 startView from './pages/start-view.js?v=20260327192619';
import * as entrySettingsView from './pages/entry-settings-view.js?v=20260327192619'; import * as entrySettingsView from './pages/entry-settings-view.js?v=20260327192619';
@ -113,6 +113,21 @@ function 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
}
}
async function init() {
await tryAutoLogin();
if (!window.location.hash) { if (!window.location.hash) {
navigate(state.session.isAuthorized ? 'profile-view' : 'start-view'); navigate(state.session.isAuthorized ? 'profile-view' : 'start-view');
} else { } else {
@ -120,3 +135,6 @@ if (!window.location.hash) {
} }
window.addEventListener('hashchange', renderApp); window.addEventListener('hashchange', renderApp);
}
init();

View File

@ -75,6 +75,7 @@ export function render({ navigate }) {
try { try {
await authService.reconnect(state.entrySettings.shineServer); await authService.reconnect(state.entrySettings.shineServer);
const result = await authService.createSessionForExistingUser(state.loginDraft.login, state.loginDraft.password); const result = await authService.createSessionForExistingUser(state.loginDraft.login, state.loginDraft.password);
await authService.persistSessionMaterial(state.loginDraft.login, result.sessionMaterial);
authorizeSession(result); authorizeSession(result);
await refreshSessions(); await refreshSessions();
setAuthInfo('Успешный вход выполнен.'); setAuthInfo('Успешный вход выполнен.');

View File

@ -4,10 +4,11 @@ import {
exportEd25519PublicKeyB64, exportEd25519PublicKeyB64,
exportPkcs8B64, exportPkcs8B64,
generateEd25519Pair, generateEd25519Pair,
importPkcs8Ed25519,
randomBase64, randomBase64,
signBase64, signBase64,
} from './crypto-utils.js?v=20260327192619'; } 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'; const BCH_SUFFIX = '001';
@ -158,6 +159,48 @@ export class AuthService {
await saveSessionMaterial(login, sessionMaterial); 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() { async listSessions() {
const response = await this.ws.request('ListSessions', {}); const response = await this.ws.request('ListSessions', {});
if (response.status !== 200) throw opError('ListSessions', response); if (response.status !== 200) throw opError('ListSessions', response);